/* ═══════════════════════════════════════════════════════════════
   site.css — 全站公共样式
   暖纸底 · 飞行胶囊顶栏 · 纸色流转 · 颗粒质感 · 滚动进度
   用法：<link rel="stylesheet" href="/public/assets/site.css?v=44">
   页面专属样式写在各自 <style> 里，只保留差异部分。
   ═══════════════════════════════════════════════════════════════ */
/* 颜色一律走 tokens.css 的变量，这里不写死色值。
   只剩两处例外，都是「本来就是绝对色」的地方：
     · 四季纸色（.season-*）—— 一年四季的固定纸样，不是主题衍生色；
     · @media print 的灰阶板 —— 打印就是白纸黑字，与屏幕主题无关。
   高光/玻璃这类半透明白，统一用 color-mix(in srgb, var(--card) N%, transparent) 现算。 */


/* ---------- 设计变量 ----------
   所有配色统一在 tokens.css 里维护（前台 / 后台 / 登录页共用同一份），
   这里只负责把它拉进来。@import 必须排在所有规则之前。 */
@import url("/public/assets/tokens.css?v=26");

/* ---------- 基础 ---------- */
/* 手机上点链接/卡片时那层灰闪（WebKit 的点击高亮）全站不要，统一压掉。 */
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* scrollbar-gutter 常驻（batch 18）：预留滚动条那 15px，让它出现或消失都不改变
   可视宽度。全站实测受益（本机 Chrome 1280x900，数值是 CLS 分数）：
     /links  0.23 -> 0.06      /photos 0.28 -> 0.07
     /        0.08 -> 0.07      /archive 0.14 -> 0.14
   前两个页面原来那笔位移里，横向半项主因是：数据注入前页面不够长没有滚动条，
   注入后滚动条出现，可视宽度 1280 -> 1265，居中的 .container 于是整体横移 7px
   （CLS previousRect 记为 x:260 -> x:253），再加上整页页脚被推出视口。
   /home 和 /archive 基本没改善（0.08->0.07、0.14->0.14）。archive 的位移元素
   已核实是 .stats-after 与 .heatmap-section（previousRect 的 x 恒为 277/259，
   只有 y 在动 —— 纯纵向，本条治不到）；首页那 9 条只数了个数，
   没逐条定性，不在此处替它们下结论。

   ⚠️ 这条推翻了第 17 轮写在 post.html 里的判断，那句话说「只在文章页加，
   因为其余 7 页都有纵向滚动条」。那句是错的：「现在有几页有滚动条」取决于当页
   数据多长，而数据是异步来的 —— 加载中的那一瞬 links/photos/首页都可能短到
   没有滚动条。留着它会误导下一个人，所以连同理由一起搬到这里。

   代价与边界：
   - 桌面端每页永久少 15px 可视宽度。这是换来「不横移」的定价，实测可接受。
   - 移动端是覆盖式滚动条，实测 390px 下 clientWidth 仍是 390、不占宽度、
     横向溢出 0 —— 手机无代价。
   - 它只治横向位移。/about 当时 0.1391 一分没降（那页 .container 的 x 始终不变），
     那是「静态一行简介被替换成 257px 长文」的另一病 —— 那条已由 batch 19
     改成服务端出页注入解决，实测 0。归档页的纵向位移仍是旧的 0.1374。 */
html { scroll-behavior: smooth; scrollbar-gutter: stable; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1rem;
  /* 行高接上 --lead（batch 10 抽出来却一直没接的那根线）。
     值就是 1.75，接之前 body 实测 lineHeight=28px、接之后仍是 28px ——
     零视觉变化，只是让「正文行高」这件事有一个可以改的地方，
     而不是散在 body、.prose、后台各写一份字面量。
     ⚠️ 带 fallback：--lead/--measure 来自 tokens.css，而 tokens.css 是
        site.css 里 @import 进来的。@import 一旦失败（缓存/代理/半路断），
        没有 fallback 的话这两个属性会整条失效 —— 行高掉回 normal、
        容器宽度变成 none，全站版式当场散架。写死值时不存在这个风险，
        接线就不许把兜底一起接掉。 */
  line-height: var(--lead, 1.75);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  transition: background 1.2s var(--ease);
}

/* 选中文本。原来写死 color: var(--ink)：在暖纸对（浅主色上必须用墨，6.05），
   但青竹/海盐蓝的 --honey 本身是深色，墨字压上去只有 2.26 / 2.28 ——
   选中一行字反而看不清。--on-honey 按主题给（暖纸=墨，另两套=白）。 */
::selection { background: var(--honey); color: var(--on-honey); }
a { color: var(--plum); }
img { max-width: 100%; }

/* 阅读栏宽接上 --measure：同样是逐位相同（760px）的接线。
   post.html 的 --read、archive/index 那些「与主视觉对齐」的推导值，
   源头都是这一个数，写死在这里就等于版式基准没有单一真相。
   fallback 的理由见上面 body 那条。 */
.container { max-width: var(--measure, 760px); margin: 0 auto; padding: 0 24px; }

/* 注：body 上的 overflow-x: hidden（第 32 行）是最后一道网，别指望它裁干净 ——
   它挡得住滚动条，挡不住布局视口被撑宽（被撑宽之后，position: fixed 的底部胶囊
   会按更宽的盒子居中）。装饰光斑那种横向溢出，在各页自己的块上就地裁掉
   （见 index.html 里 .hero 的 overflow-x: clip）。 */

/* ---------- 纸感颗粒 + 环境光晕 ---------- */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background-image: var(--grain);
  opacity: 0.045;
  mix-blend-mode: multiply;
}
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(46% 34% at 82% 4%, var(--glow-a), transparent 72%),
    radial-gradient(40% 32% at 8% 96%, var(--glow-b), transparent 74%);
}

/* ---------- 主题（全部亮色，不碰暗色） ----------
   换主题 = 换 <html data-theme>，只改变量，不动结构。
   三套主题的变量值都在 tokens.css 里 —— 那里是全站唯一配色源。 */
html[data-theme="sea"] body::after { display: none; }

/* ---------- 纸色流转（始终亮色，不碰暗色） ---------- */
html.season-spring body { background: #EFF3E7; }
html.season-summer body { background: #F3F4EA; }
html.season-autumn body { background: #F1EFE3; }
html.season-winter body { background: #EFF2F3; }
html.season-spring[data-theme="sea"] body,
html.season-summer[data-theme="sea"] body,
html.season-autumn[data-theme="sea"] body,
html.season-winter[data-theme="sea"] body { background: #F1F5F9; }
/* 青竹保留纸的呼吸感：季节只挪动很小的冷暖，不换底色 */
html.season-spring[data-theme="bamboo"] body { background: #EFF1E4; }
html.season-summer[data-theme="bamboo"] body { background: #F1F2E4; }
html.season-autumn[data-theme="bamboo"] body { background: #F1EFE1; }
html.season-winter[data-theme="bamboo"] body { background: #EFF1F0; }

/* 四季只换光晕的色相，亮度都压得很低 */
html.season-spring { --glow-a: rgba(138, 178, 122, 0.13); --glow-b: rgba(224, 160, 59, 0.12); }
html.season-summer { --glow-a: rgba(240, 186, 74, 0.15);  --glow-b: rgba(146, 186, 160, 0.12); }
html.season-autumn { --glow-a: rgba(224, 160, 59, 0.15); --glow-b: rgba(201, 139, 127, 0.14); }
html.season-winter { --glow-a: rgba(150, 178, 196, 0.14); --glow-b: rgba(224, 160, 59, 0.1); }
html[data-theme="bamboo"].season-spring { --glow-a: rgba(127, 169, 140, 0.18); --glow-b: rgba(201, 162, 39, 0.1); }
html[data-theme="bamboo"].season-summer { --glow-a: rgba(201, 162, 39, 0.14); --glow-b: rgba(127, 169, 140, 0.16); }
html[data-theme="bamboo"].season-autumn { --glow-a: rgba(201, 162, 39, 0.15); --glow-b: rgba(150, 120, 60, 0.1); }
html[data-theme="bamboo"].season-winter { --glow-a: rgba(120, 150, 165, 0.14); --glow-b: rgba(127, 169, 140, 0.11); }
html[data-theme="sea"].season-spring { --glow-a: rgba(43, 92, 138, 0.13); --glow-b: rgba(120, 170, 150, 0.09); }
html[data-theme="sea"].season-summer { --glow-a: rgba(90, 150, 190, 0.14); --glow-b: rgba(224, 138, 60, 0.08); }
html[data-theme="sea"].season-autumn { --glow-a: rgba(224, 138, 60, 0.1); --glow-b: rgba(43, 92, 138, 0.12); }
html[data-theme="sea"].season-winter { --glow-a: rgba(150, 178, 196, 0.16); --glow-b: rgba(43, 92, 138, 0.11); }

/* ---------- 深夜转暖（batch 9） ----------
   ⚠️ 不是暗色模式，也不做成可切换的主题 —— 就是「深夜 23:00–05:00 时
   纸色往琥珀方向推一点」。深夜读书需要更暖更松弛的底色，所以是「更暖」，
   不是「更暗」；纸永远是亮的。

   ⚠️ 特异性：季节底色用的是 html.season-* body（0,1,2）。光写 html.night body
   同样是（0,1,2），**平级** —— 谁写在后面谁赢，而季节规则写在前面，
   于是 night 会被季节盖掉，压根不生效（这个坑踩过一次）。
   所以必须写成 html.night.season-* body（0,2,2）才稳赢。
   海盐蓝走冷白路线，深夜不转暖（那会跑偏成另一种颜色）。 */
html.night.season-spring body,
html.night.season-summer body,
html.night.season-autumn body,
html.night.season-winter body { background: #F6EBD6; }
html.night[data-theme="bamboo"] body { background: #EFEBDC; }
html.night[data-theme="sea"] body { background: #F1F5F9; }
/* 光晕跟着暖一档，不改主色 */
html.night { --glow-a: rgba(224, 160, 59, 0.16); --glow-b: rgba(201, 139, 127, 0.13); }
html.night[data-theme="bamboo"] { --glow-a: rgba(201, 162, 39, 0.15); --glow-b: rgba(127, 169, 140, 0.12); }

/* ---------- 滚动进度条 ---------- */
.scroll-progress {
  position: fixed; top: 2px; left: 0; height: 3px; width: 100%;
  transform-origin: 0 50%; transform: scaleX(0);
  background: var(--grad); z-index: 60;
  border-radius: 0 3px 3px 0;
  /* 光晕往条下方倒。原来是 top:0 配 0 0 12px，有一半落在 y:0 之上被视口裁掉，
     在屏幕顶边留下一道硬亮线 —— 每个页面、每次滚动都在。 */
  box-shadow: 0 4px 10px -4px color-mix(in srgb, var(--honey) 55%, transparent);
}

/* ---------- 飞行胶囊顶栏（PC） ---------- */
.site-header {
  position: fixed; top: 16px; left: 50%;
  transform: translateX(-50%);
  width: auto; max-width: calc(100% - 32px);
  padding: 8px 8px 8px 14px;
  background: var(--glass-strong);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: 0 16px 40px -18px color-mix(in srgb, var(--ink) 55%, transparent);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  z-index: 40;
  transition: padding var(--dur-4) var(--ease), box-shadow var(--dur-4) var(--ease);
}
.site-header.is-scrolled { padding: 6px 8px 6px 12px; box-shadow: 0 20px 48px -20px color-mix(in srgb, var(--ink) 60%, transparent); }
.site-header .container {
  width: auto; max-width: none; margin: 0; padding: 0;
  display: flex; align-items: center; gap: 14px;
}

.brand {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--serif); font-weight: 700; font-size: 1.05rem;
  color: var(--ink); text-decoration: none; letter-spacing: 0.04em;
  margin-right: 2px;
  /* 导航加了动态项之后会变宽；没有 nowrap 时站名会被挤成竖着的一列 */
  white-space: nowrap; flex-shrink: 0;
}
.brand-mark {
  width: 30px; height: 30px; display: grid; place-items: center;
  border-radius: 50%; background: var(--grad); color: var(--on-grad);
  box-shadow: 0 4px 14px -4px color-mix(in srgb, var(--honey) 70%, transparent);
}
.brand-mark svg { width: 16px; height: 16px; }
/* 后台换了自定义 LOGO：品牌位由 site.js 塞进一张 <img> 并打上 .has-logo，
   底下的渐变底和光晕就让开，免得在图片边缘透出一圈色。 */
.brand-mark.has-logo { background: none; box-shadow: none; }
.brand-mark img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }

.nav { display: flex; align-items: center; gap: 2px; font-size: var(--nav-fs); white-space: nowrap; }
/* 导航字号的单一来源：.nav 和它的兄弟 .nav-dice 都读这一个值，
   ≤640px 那条只改变量、不改两处规则（同 --mnav-pad 的理由：手抄必漂）。 */
:root { --nav-fs: 0.95rem; }
.nav a {
  padding: 7px 14px; border-radius: 999px;
  color: var(--ink-soft); text-decoration: none;
  white-space: nowrap; flex-shrink: 0;
  transition: color var(--dur-3), background var(--dur-3);
}
.nav a:hover { background: var(--honey-tint); color: var(--plum); }
/* active 的渐变底由 .nav-ind 那块独立的高亮块来画（site.js 的 syncNavIndicator
   定位它，切换时是滑过去而不是瞬移）。所以这里只留文字色，
   别再给 a 自己上 background —— 否则会把下面那块盖住，滑动效果就看不见了。 */
.nav a.is-active { color: var(--on-grad); }

/* ---------- 顶栏里的「翻一篇」（batch 27） ----------
   用户要求把随机入口放到顶栏。它由服务端出页时注入（server.js 的
   randomEntryHtml），形状是「和 .nav a 同款的胶囊」——所以尺寸、圆角、
   hover 底色全部照 .nav a 那一套走，不另起一套视觉，也不写死任何宽高。

   ⚠️ 它必须是 .nav 的**兄弟**而不是子元素：site.js 的 renderNav() 会对
      .nav 和 .mnav-strip 各做一次 textContent='' 再按后台配置重建，
      塞进那两处的东西会被整条抹掉（表现为「刷新后偶尔少一颗骰子」）。
   ⚠️ [hidden] 那条是必需的：下面把 .nav-dice 设成 inline-flex，
      display 会盖掉 UA 对 hidden 的收起规则（同型坑见 .hero-sign / .otd）。
   ⚠️ 移动端那颗在 .mobile-nav 里、和 .mnav-strip 平级，尺寸另给一条
      （底栏的命中盒要求是 44px，顶栏的胶囊项不需要）。 */
.nav-dice {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink-soft); text-decoration: none;
  white-space: nowrap; flex: none;
  transition: color var(--dur-3), background var(--dur-3), border-color var(--dur-3);
}
.nav-dice svg { width: 15px; height: 15px; flex: none; stroke: var(--plum); fill: none; stroke-width: 1.6; }
.nav-dice svg .dot { fill: var(--plum); stroke: none; }
.nav-dice:hover { background: var(--honey-tint); border-color: var(--honey); color: var(--plum); }
.nav-dice[hidden] { display: none; }
/* 字号走 --nav-fs 这个单一来源（见上面 .nav）：它是 .nav 的兄弟，
   不继承 .nav 的字号，而 ≤640px 那条要把导航整体收紧 ——
   两边各写一个数就会漂，窄窗里出现「这颗骰子比别的导航项大一圈」。 */
.nav-dice { font-size: var(--nav-fs); }

/* ---------- 底部胶囊导航（触屏） ---------- */
/* 避让几何单一来源：以前 .mobile-nav 的 bottom、body 的 padding-bottom、
   .back-top 的 bottom 在三处各写了一遍 84px，改了 item 尺寸却忘了改避让，
   就会「抬高按钮却新造遮挡」。这里把两个量收成变量，全站只认变量：
     --mnav-gap：胶囊到视口底的下沉量（视觉悬浮 16px + 刘海安全区）
     --mnav-pad：正文与 back-top 的避让 = 胶囊下缘 + 胶囊高度 + 呼吸余量
   改 .mnav-item 尺寸时，只动 --mnav-pad 一个数，别在两处手抄。 */
:root {
  --mnav-gap: calc(16px + env(safe-area-inset-bottom));
  /* 胶囊高度 = item(44~52) + 上下 padding(5+5) + 上下边框(1+1) ≈ 58；
     16 悬浮 + 58 胶囊 + 14 呼吸 = 88。item 涨到 52px 时胶囊≈62，避让仍够。 */
  --mnav-pad: calc(88px + env(safe-area-inset-bottom));
}
.mobile-nav {
  display: none;
  position: fixed;
  left: 0; right: 0; margin: 0 auto;
  bottom: var(--mnav-gap);
  width: fit-content; max-width: calc(100% - 32px);
  z-index: 55;
  padding: 5px 9px;
  border-radius: 999px;
  border: 1px solid var(--line);
  box-shadow: 0 14px 34px -14px color-mix(in srgb, var(--ink) 50%, transparent);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  background: var(--glass);
}
.mobile-nav .mnav-strip {
  display: flex; gap: 6px;
  /* safe center：装得下才居中；装不下就从左排。普通的 center 会把左边几个
     顶到滚动区外头，手指划不回来。导航项现在由后台自由增删，这里必须能横滑。 */
  justify-content: safe center;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none;
}
.mobile-nav .mnav-strip::-webkit-scrollbar { display: none; }
/* 动态导航拉回来之前先藏住，避免「先四条后六条」当着用户的面跳一下。
   site.js 的 400ms 定时器兜底，拉不到也会露出来。
   ⚠️ 那条兄弟选择器不能省：.nav-dice 是 .nav 的**兄弟**（必须如此，
      放进 .nav 会被 renderNav() 清空重建抹掉），所以它不在上面那条规则的
      覆盖范围内。实测后果（.deploy/verify/b27/dice-watch.js）：顶栏胶囊
      在导航落地时从 426px 长到 626px，骰子被往右推 100px（x 744→806→844），
      而位移分数只统计**可见**元素 —— 藏着的 .nav 不记账，露着的骰子全额记账，
      于是「翻一篇搬进顶栏」这一改自己造出一笔 CLS（844×390 档 15/25 样本
      含骰子位移，占该视口总位移分数 85%）。跟着一起藏就对了：
      它和导航是同一批会重排的东西，没有理由单独露着。 */
.nav[data-nav-pending], .mnav-strip[data-nav-pending] { visibility: hidden; }
.nav[data-nav-pending] ~ .nav-dice, .mnav-strip[data-nav-pending] ~ .nav-dice { visibility: hidden; }
/* 只显示图标：文字标签由 font-size:0 隐去 */
.mnav-item {
  /* flex: none —— 条子被 max-width 卡住时要横滑，不许把按钮挤扁 */
  flex: none;
  display: flex; align-items: center; justify-content: center; gap: 0;
  /* 触屏最小命中盒 44px（Apple HIG）。用 clamp 而不是写死：窄到 360 也不小于 44，
     宽屏顶到 52 后由 .mnav-strip 的 max-width + 横滑兜底，不会撑破视口。
     别写成 44px 定值 —— 那样在 320px 这种极窄屏仍是 44，但这里 clamp 下限守住就好。 */
  width: clamp(44px, 12vw, 52px); height: clamp(44px, 12vw, 52px); padding: 0; min-width: 0; color: var(--muted);
  text-decoration: none; font-size: 0; border-radius: 999px;
  white-space: nowrap;
  transition: color var(--dur-3), background var(--dur-3);
}
.mnav-item svg { width: 20px; height: 20px; }
.mnav-item.is-active { background: var(--grad); color: var(--on-grad); }

/* ---------- 返回顶部 ---------- */
.back-top {
  position: fixed; right: 20px;
  bottom: calc(24px + env(safe-area-inset-bottom)); z-index: 58;
  width: 46px; height: 46px; border-radius: 50%; border: none;
  background: var(--grad); color: var(--on-grad); cursor: pointer;
  display: grid; place-items: center;
  box-shadow: 0 12px 26px -10px color-mix(in srgb, var(--honey) 70%, transparent);
  opacity: 0; transform: translateY(16px) scale(0.8); pointer-events: none;
  transition: opacity var(--dur-4), transform var(--dur-4) var(--ease);
}
.back-top.show { opacity: 1; transform: none; pointer-events: auto; }
.back-top:active { transform: scale(0.9); }
.back-top svg { width: 20px; height: 20px; }

/* ---------- 滚动渐显 ---------- */
/* batch 28（P0）：下面两条保持原样，隐藏态照旧由观察器加 .is-in 解除。
   只补一条「脚本不可用时永远可见」的反向兜底（就在下面）。
   为什么原样不动：这两条的同特异度顺序关系是全站依赖的 ——
   `.reveal.is-in`(0,2,0) 靠后写赢过 `.reveal`(0,1,0)；首页那条 20px 位移
   也是有意的同特异度页面级覆盖（见 index.html）。一改成 html.js 前缀
   就变成 (0,2,1)，反而压过 .is-in —— 那会让**开着脚本**的人永久看不见正文，
   比原来的缺陷更严重（本仓的坑记录里同类「判据/样式反向压过」已踩过多次）。
   所以方向反过来：默认态不动，只在 js 缺席时整体还原。 */
.reveal { opacity: 0; transform: translateY(18px); transition: opacity var(--dur-6) var(--ease), transform var(--dur-6) var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }
/* 禁用脚本的访客（无 JS 浏览器、只抓 HTML 的阅读器、被 CSP 挡掉的场景）
   以前看到的是**整页空白**：a11y 巡检用 CDP 关掉脚本执行实测，首页 / 关于 /
   归档各 4 个 .reveal 的 computed opacity 全为 0，而文本本身都在
   （mainTextLen 284 / 531 / 63）—— 有字却看不见，属于 P0。
   特异度 (0,2,1) 高于上面两条与任何页面级 .reveal 覆盖，且它是「js 缺席」
   时唯一生效的一条：那种情况下没人会加 .is-in，所以不会和渐显终态打架。
   依赖的是各页头部同步打上的 html.js 标记（links/photos/post 三页早就是
   这套套路，本轮补上 index/about/archive）。 */
html:not(.js) .reveal { opacity: 1; transform: none; }

/* ---------- 动效收编 + 微交互（batch 9） ----------
   以前这个文件里一个 @keyframes 都没有：动画全是各页 <style> 里的孤岛
   （draw/rise/caretBlink 在 index，steam 在 post），共享层只提供 transition。
   结果是每加一次交互都要在 8 个 HTML 里各写一遍，而且时长各写各的。

   现在动画收在这一层。⚠️ 页面里原来的 @keyframes 定义**故意留着不删** ——
   同名 keyframes 后定义的那份生效，页面覆盖能力不变，不会有回归风险。

   全部动效都尊重 prefers-reduced-motion：下面那个媒体查询里
   animation/transition 被整体掐掉，并把这些元素坐实到终态。 */

/* 收编：页面里同名的四个 */
@keyframes draw { to { stroke-dashoffset: 0; } }
@keyframes rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes caretBlink { 50% { opacity: 0; } }
@keyframes steam { 0% { transform: translateY(0) scaleX(1); opacity: 0; } 35% { opacity: 0.8; } 100% { transform: translateY(-14px) scaleX(1.3) rotate(3deg); opacity: 0; } }

/* 按下时轻微回弹。给的是 .97 而不是压扁，因为「按下去」的物理直觉是
   陷进去一点点，很快弹回来，不是被按扁。transform-origin 留在默认中心。 */
@keyframes press { 50% { transform: scale(0.97); } }

/* 输入框聚焦：描边从中心洇开，像墨滴进纸里。spread 由 0 长到 --ring 那一档，
   比单纯换 border-color 有「被点亮」的层次。 */
@keyframes ink { from { box-shadow: 0 0 0 0 transparent; } to { box-shadow: var(--ring); } }

/* 导航滑动高亮块：跟着 current 项走。定位用 JS 写 left/width（见 site.js 的
   syncNavIndicator），这里只负责过渡。left/width 都参与过渡，所以从一个项
   跳到另一个项时是「滑过去」而不是瞬移。
   z-index:0 是关键 —— 它是 appendChild 进去的（排在所有 a 后面），
   不压一层的话会盖在文字上；a 全部 position:static 时 z-index 不生效，
   所以给 a 补 position:relative;z-index:1 把文字抬到高亮块之上。 */
.nav { position: relative; }
.nav a { position: relative; z-index: 1; }
.nav-ind {
  position: absolute; top: 50%; left: 0; height: 100%; z-index: 0;
  border-radius: 999px; background: var(--grad);
  transform: translateY(-50%);
  opacity: 0; pointer-events: none;
  transition: transform var(--dur-5) var(--ease-out),
              width var(--dur-5) var(--ease-out),
              opacity var(--dur-3) var(--ease);
}
.nav-ind.on { opacity: 1; }

/* 按钮按下：只给真正可点的元素，纯文字链接按下不该缩。
   这些类名都是实际存在的（btn-home 在 404、tea-btn 在文章页喝茶组件、
   .btn 是评论表单的提交键、.lb-close 由 site.js 的 initLightbox 建）。 */
.mini-btn:active, .load-more:active, .tea-btn:active,
.btn:active, .btn-home:active, .lb-close:active,
.admin .btn:active, .admin button:active {
  animation: press var(--dur-1) var(--ease);
}

/* 输入框聚焦墨水洇开。只在键盘导航（focus-visible）时出现 ——
   鼠标点进去就发一圈光晕太吵了。
   选择器对应实际存在的表单：文章页评论表单、首页搜索、后台编辑器与设置。 */
.comment-form input:focus-visible,
.comment-form textarea:focus-visible,
#search-input:focus-visible,
.md-area textarea:focus-visible,
#f_title:focus-visible,
.admin input:focus-visible,
.admin textarea:focus-visible {
  animation: ink var(--dur-3) var(--ease-out);
}

/* 灯箱：display:none/grid 之间补一段淡入 + 轻微放大，
   原来是「啪」一下出现的，加了之后像照片浮起来。
   灯箱的图是 JS 建的一个裸 <img>，没有类名（见 site.js 的 initLightbox），
   所以只能靠 .lightbox > img 选它。transform 写在终态上，开场才有动画。 */
.lightbox > img { transform: scale(0.965); opacity: 0; transition: transform var(--dur-5) var(--ease-out), opacity var(--dur-5) var(--ease); }
.lightbox.show > img { transform: none; opacity: 1; }

/* 打印隐藏清单里要加上 .nav-ind —— 它是个纯装饰的渐变块，
   打印时必须跟着导航一起消失。 */

/* ---------- 页脚 ---------- */
.site-footer {
  border-top: 1px solid var(--line);
  padding: 36px 0;
  text-align: center;
  color: var(--muted);
  font-size: 0.88rem;
}
  /* ♡ 是页脚里一个会被人认真读到的符号，不是色块。--rose 是"花色"档，
     实测压在纸上只有 2.43 / 2.28 / 2.42，三套全废；换成可读版 --rose-ink。 */
  .site-footer .heart { color: var(--rose-ink); }
.site-footer a { color: var(--muted); }

/* ---------- 通用小胶囊 ---------- */
.chip {
  font-size: 0.8rem; padding: 3px 14px; border-radius: 999px;
  background: var(--t1-bg); color: var(--t1-fg); font-weight: 500;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--card) 35%, transparent);
}
.chip.c2 { background: var(--t2-bg); color: var(--t2-fg); }
.chip.c3 { background: var(--t3-bg); color: var(--t3-fg); }
.chip.c4 { background: var(--t4-bg); color: var(--t4-fg); }
.chip.c5 { background: var(--t5-bg); color: var(--t5-fg); }
.chip.c6 { background: var(--t6-bg); color: var(--t6-fg); }

.tag-pill {
  display: inline-flex; align-items: center;
  padding: 3px 12px; border-radius: 999px;
  background: var(--honey-tint); color: var(--plum);
  font-size: 0.8rem; font-weight: 500; text-decoration: none;
  transition: background var(--dur-3) var(--ease), color var(--dur-3) var(--ease), transform var(--dur-3) var(--ease);
}
.tag-pill:hover { background: var(--grad); color: var(--on-grad); transform: translateY(-1px); }

/* ---------- 卡片：悬停时浮起 + 渐变描边 ---------- */
.lift {
  position: relative;
  transition: transform var(--dur-4) var(--ease), box-shadow var(--dur-4) var(--ease);
}
.lift::after {
  content: "";
  /* inset:0（不是 -1px）：让渐变环坐在边框内侧。配 .lift:hover 把自己的
     border-color 让成 transparent，就不会和底下那条灰边叠成两条线 ——
     这样 .heatmap-section（本来就没有 border）不用改也能表现一致。 */
  position: absolute; inset: 0;
  border-radius: inherit; padding: 1px;
  background: var(--grad);
  -webkit-mask: linear-gradient(var(--mask-solid) 0 0) content-box, linear-gradient(var(--mask-solid) 0 0);
  /* 只有 -webkit- 那一行的话，Firefox 没有 mask-image，悬停时会糊一整块渐变
     上去而不是描个环。标准属性补齐（写在 mask-composite 之前，别被简写重置掉）。 */
  mask: linear-gradient(var(--mask-solid) 0 0) content-box, linear-gradient(var(--mask-solid) 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-4) var(--ease);
}
.lift:hover { transform: translateY(-3px); border-color: transparent; box-shadow: 0 18px 36px -22px color-mix(in srgb, var(--ink) 50%, transparent); }
.lift:hover::after, .lift:focus-within::after { opacity: 0.55; }

/* ---------- 小圆角按钮 ---------- */
.mini-btn {
  font-family: inherit; font-size: 0.82rem; cursor: pointer;
  color: var(--ink-soft); background: var(--card);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 6px 14px; white-space: nowrap;
  transition: color var(--dur-2), border-color var(--dur-2), background var(--dur-2);
}
.mini-btn:hover { color: var(--plum); border-color: var(--honey); background: var(--honey-tint); }
.mini-btn[hidden] { display: none; }

/* 元素级 [hidden] 兜底。各页里那些 `.tag-cloud[hidden] { display: none }` 这类规则
   说明作者样式表里的 display 能盖过浏览器默认的 [hidden] —— 而 site.js 现在靠
   hidden 属性收起「后台里被清空」的绑定位（自我介绍、没填的邮箱链接……），
   所以得有一条 !important 的规则保证它真的藏得住，不留空段落和点不动的链接。 */
[hidden] { display: none !important; }

/* ---------- 键盘按键（/ 聚焦搜索这类提示） ---------- */
kbd {
  font-family: var(--sans); font-size: 0.72rem; line-height: 1;
  padding: 3px 6px; border-radius: 6px;
  background: var(--card); border: 1px solid var(--line);
  box-shadow: 0 1.5px 0 var(--line);
  color: var(--ink-soft);
}

/* ---------- 轻提示 ---------- */
.toast {
  position: fixed; left: 50%;
  bottom: calc(28px + env(safe-area-inset-bottom));
  transform: translate(-50%, 14px);
  z-index: 90; max-width: min(84vw, 380px);
  padding: 9px 18px; border-radius: 999px;
  background: var(--ink); color: var(--paper);
  font-size: 0.86rem; line-height: 1.5; text-align: center;
  box-shadow: 0 16px 34px -14px color-mix(in srgb, var(--ink) 60%, transparent);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-3), transform var(--dur-4) var(--ease);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@media (hover: none) { .toast { bottom: calc(96px + env(safe-area-inset-bottom)); } }

/* ---------- 图片灯箱 ---------- */
.lightbox {
  position: fixed; inset: 0; z-index: 95;
  display: none; place-items: center;
  padding: 24px;
  background: var(--scrim);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  cursor: zoom-out;
}
.lightbox.show { display: grid; }
.lightbox img {
  max-width: 100%; max-height: 86vh;
  border-radius: 12px;
  box-shadow: 0 30px 60px -20px var(--scrim);
  cursor: default;
}
.lightbox .lb-cap {
  position: absolute; left: 0; right: 0; bottom: 16px;
  margin: 0; padding: 0 20px; text-align: center;
  color: var(--paper); font-size: 0.82rem; opacity: 0.8;
}
.lightbox .lb-close {
  position: absolute; top: 14px; right: 16px;
  width: 38px; height: 38px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--card) 35%, transparent);
  background: color-mix(in srgb, var(--card) 12%, transparent); color: var(--paper);
  font-size: 1rem; line-height: 1; cursor: pointer;
}
.lightbox .lb-close:hover { background: color-mix(in srgb, var(--card) 24%, transparent); }

/* ---------- 正文排版（文章 / 自定义页共用） ----------
   从 post.html 提到这里：自定义页要一模一样的排版，抄一份就会和文章页慢慢分叉。
   post.html 的 .post-content 现在只是 .prose 的别名（见该文件顶部）。 */
/* ---------- 正文 ---------- */
.prose {
  font-size: var(--fs-body);
  color: var(--ink-soft);
}

.prose p { margin: 0 0 18px; }

/* ---------- 正文首字下沉（batch 16）----------
   只下沉**第一段**，而且是 JS 认定「值得下沉」的那一段（见 site.js 的 markLede）。

   为什么字号用 2lh 而不是 em：lh 是「行高」单位，跟着实际行高走；
   em 是手工换算的定值，而行高在本站**本来就不止一个值**：
     宽屏 .post-content = 1.02rem / 1.85 → 行高 30.192px
     窄屏 ≤720px 内联改成 1rem / 1.9 → 行高 30.4px
     打印灰阶板 = 11.5pt / 1.7
   实测一个 em 值走三档会偏：3.7em 在窄屏算出 59.2px，而两行是 60.8px，
   差 1.6px —— 下沉字比两行矮一截，第二行文字会往大字里挤进去。
   2lh 在同样两处分别是 60.78 / 60.34px，与两行只差 0.02–0.04px
   （Chrome 对 lh 的舍入，肉眼不可见，但别写成「精确等于」）。
   ⚠️ 3.7em 不是「没用的兜底」：lh 单位不算老，所以它留在前面当保底，
   @supports 里才升级到 2lh。顺序不能反，反了不支持 lh 的浏览器连 3.7em 一起丢。

   为什么 float 而不是 initial-letter：initial-letter 在本机 Chrome 根本没实现
   （实测首字仍是 16×23，写 3 和什么都不写一模一样，initialLetter 也不在
   CSSStyleDeclaration.prototype 上），要等。
   ⚠️ 尤其别想用 @supports (initial-letter: 3) 做渐进增强：它在本机返回 **true**，
   也就是「假报支持然后什么都不发生」—— 这条特性不能靠特性检测兜，只能等实现。

   为什么这里要 display:flow-root：下沉字是浮动的，段落文字不足两行时
   浮动盒会**溢到下一段**去占位（实测无 flow-root 的短段落盒高只有 25.6px，
   而浮动字实际高 71px）。flow-root 让段落自己包住浮动，实测盒高 25.6 -> 43.6。
   加在 .lede 上而不是全站 .prose p：只有带下沉字的段落才有浮动要包。
   实测 flow-root 不影响段间距和段高（有/无都是 gap 18px、单行段高 30.2px），
   所以它只为「万一有浮动」的段落兜底，不会改变正常段落。

   ⚠️ 颜色取 --cat-fg（分类的墨色）而不是 --cat-bg（底色）：
   第 14 轮量过「技术」类 --cat-bg = #F0E7CE 与纸底亮度差只有 9，画上去等于没画；
   --cat-fg 差 134。分类拿不到时退到 --ink。打印时 site.css 的灰阶板把 --cat-fg
   强制成 #000，所以下沉字在纸上是黑的 —— 不用为打印另写规则。 */
.prose p.lede {
  display: flow-root;
}
.prose p.lede::first-letter {
  float: left;
  font-family: var(--serif);
  font-weight: 700;
  font-size: 3.7em;
  line-height: 1;
  margin: 0 0.1em 0 0;
  color: var(--cat-fg, var(--ink));
}
@supports (font-size: 1lh) {
  .prose p.lede::first-letter { font-size: 2lh; }
}

.prose h1, .prose h2, .prose h3 {
  font-family: var(--serif); font-weight: 700; color: var(--ink);
  line-height: 1.4; margin: 34px 0 12px;
  position: relative;
  scroll-margin-top: 90px;
}
/* 字号阶梯（batch 9）。原来 1.5 / 1.32 / 1.16 三级每级只大 0.1rem 左右，
   几乎等距 —— 正文里连着几个小标题扫过去分不出主次。现在按 --fs-* 拉开，
   h1 明显是「一节开始」，h3 是「小节里再分一层」。 */
.prose h1 { font-size: var(--fs-h1); }
.prose h2 { font-size: var(--fs-h2); }
.prose h3 { font-size: var(--fs-h3); }
.prose h1:first-child, .prose h2:first-child, .prose h3:first-child { margin-top: 0; }
/* 标题前的菱形小装饰 */
.prose h2::before, .prose h3::before {
  content: "";
  display: inline-block; vertical-align: middle;
  width: 8px; height: 8px; border-radius: 3px;
  background: var(--grad);
  margin-right: 10px; transform: rotate(45deg);
}

.prose ul, .prose ol { margin: 0 0 18px; padding-left: 26px; }
.prose li { margin-bottom: 6px; }
.prose li:last-child { margin-bottom: 0; }

.prose blockquote {
  margin: 0 0 18px; padding: 12px 18px;
  border-left: 3px solid transparent;
  border-image: linear-gradient(180deg, var(--honey), var(--rose)) 1;
  background: var(--card);
  border-radius: 0 12px 12px 0;
  color: var(--muted);
}
.prose blockquote p { margin: 0; }

.prose code {
  font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
  font-size: 0.88em;
  background: var(--honey-tint);
  color: var(--plum);
  padding: 2px 6px; border-radius: 6px;
}
.prose pre {
  background: var(--card); border: 1px solid var(--line);
  border-radius: 12px; padding: 16px 18px; margin: 0 0 18px;
  overflow-x: auto; line-height: 1.6;
  box-shadow: 0 10px 26px -16px color-mix(in srgb, var(--ink) 40%, transparent);
}
.prose pre code { background: none; padding: 0; color: var(--ink-soft); }
/* 横向滚动条长在 .prose pre 上（overflow-x:auto 是它），原来写成 pre code 上，
   那两条规则从来没生效过。 */
.prose pre::-webkit-scrollbar { height: 6px; }
.prose pre::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }

/* ---------- 代码块一键复制（batch 31）----------
   按钮**不在 pre 里面**：.prose pre 是 overflow-x:auto，按钮若做它的子元素，
   代码横向滚动时按钮会跟着滚走（滚到右边就找不到复制键了），
   而且它会被算进滚动宽度，凭空多出一截横向溢出。
   所以由 site.js 把 pre 包进 .code-box，按钮贴在壳上、永远不动。

   ⚠️ 为什么还要加高 padding-top，而不是直接压在代码第一行上：
      按钮位置固定、代码可以横向滚动，两者一旦同处一条水平带，
      滚到中段时按钮就会盖住某一行里的字符 —— 那正是本仓骂过的
      "fixed/absolute 元素压住可交互或可读内容"。
      把第一条水平带整个让给按钮（padding-top 36px > 按钮占到的 30px），
      无论怎么滚，代码永远在按钮下方。 */
.code-box { position: relative; }
.code-box > pre { padding-top: 36px; }
.code-copy {
  position: absolute; top: 6px; right: 8px; z-index: 1;
  font-family: var(--sans); font-size: 0.74rem; line-height: 1;
  color: var(--plum); background: var(--honey-tint);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 7px 11px; cursor: pointer;
  transition: color var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease),
              background var(--dur-2) var(--ease);
}
.code-copy:hover { border-color: var(--honey); color: var(--ink); }
/* 命中盒扩到 44：按钮本身只有 30 上下高，但"点它把代码带走"正是手机上
   最需要成的那一下。用一个没有背景的 ::before 撑开热区 —— 视觉上一点不变,
   但拇指按得到。绝对定位居中，不依赖父级是 flex。 */
.code-copy::before {
  content: ''; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 100%; min-width: 44px; height: 100%; min-height: 44px;
}
.code-copy.is-done { border-color: var(--ok); }
.code-copy:focus-visible { outline: 2px solid var(--plum); outline-offset: 2px; }
/* 关掉功能时（后台 code_copy_show=0）DOM 里根本没有这颗按钮，
   所以这里不需要任何"隐藏"规则 —— 见 site.js addCodeCopy 的第 3 条设计决定。 */

.prose hr { border: none; border-top: 1.5px dashed var(--line); margin: 30px 0; }
/* 正文链接：悬停时蜂蜜色下划线从左铺开 */
.prose a {
  color: var(--plum);
  background-image: linear-gradient(var(--honey), var(--honey));
  background-repeat: no-repeat; background-position: left 92%;
  background-size: 0% 2px; text-decoration: none;
  transition: background-size var(--dur-4) var(--ease), color var(--dur-4);
}
.prose a:hover { background-size: 100% 2px; color: var(--plum); }

/* 正文标题的锚点：悬停才出现，点一下复制直达链接。
   原来只写在 post.html 的 .post-content 作用域里，自定义页（page.html）同样调
   enhanceArticle 却拿不到样式，每个小标题后面就挂着一个裸的 # —— 挪到 .prose 上，
   前台所有正文（文章 + 自定义页）共用一份。 */
.prose .h-anchor {
  font-family: var(--sans); font-size: 0.66em; font-weight: 400;
  /* 小标题后面那个 # 锚点：--honey 在暖纸是浅黄，压在卡片底只有 1.96:1，
     它虽然只在 hover 时出现，但出现的目的就是"让人看见并点它"。
     ⚠️ opacity: 0 只是把它藏起来，一旦显示出来这行字就是要读的，
        所以判据不能因为它默认不可见就放过（探针上一版正是这么假绿的）。 */
  margin-left: 8px; color: var(--honey-ink);
  text-decoration: none; opacity: 0;
  transition: opacity var(--dur-2), transform var(--dur-2) var(--ease);
  display: inline-block;
  /* 为下面 @media (hover:none) 的 ::before 命中区做定位参照。
     position:relative 不改变布局（它本来就是 inline-block）。 */
  position: relative;
  /* 隐形就不许吃点击。原来 opacity:0 而 pointer-events 仍是 auto，实测
     （1280px 与 390px 两档，elementFromPoint 打在锚点中心）命中的就是这个
     看不见的 A.h-anchor —— 它挂在标题尾部，8×19 大小，读者不知道它在那儿，
     点到就弹一句「直达链接已复制」。
     试过 visibility:hidden（连过渡一起干掉，出现时会硬跳），也试过 JS 按
     hover 能力摘除节点（锚点是 site.js 动态生成的，要多一处状态同步）；
     pointer-events 这一条既不改变布局也不打断淡入，是最小的修法。
     注意 pointer-events:none 不影响键盘 Tab 聚焦，所以下面 :focus-visible
     仍然能把可点状态打开，键盘用户不受损。 */
  pointer-events: none;
}
.prose h2:hover .h-anchor,
.prose h3:hover .h-anchor,
.prose .h-anchor:focus-visible { opacity: 0.85; pointer-events: auto; }
.prose .h-anchor:hover { opacity: 1; transform: translateY(-1px); }

/* 触屏没有 hover，上面那套「悬停才出现」在手机上等于永远不出现。
   以前它的表现是「看不见但能误点」；现在既然不吃点击了，就得给一条真出路：
   hover 能力为零时直接把锚点常驻显示（半透明，不抢正文），并把命中区用
   伪元素外扩到约 28×39 —— 8×19 那个尺寸在手指下基本点不准，而 padding
   会把标题行撑宽，所以用不占布局的 ::before 覆盖层。 */
@media (hover: none) {
  .prose .h-anchor { opacity: 0.45; pointer-events: auto; }
  /* 命中区外扩：纵向各 10px（标题行高 1.9，上下是空的，不会碰到别的可点物），
     横向各 8px —— 左边那 8px 正好是 margin-left 留出的间隙，不压住标题文字，
     右边那 8px 是行尾的空白。于是 8×19 变成 24×39，够手指点（WCAG 2.5.8
     最低要求 24×24），又不会把 ::before 顶到容器外缘去撑出横向滚动条。 */
  .prose .h-anchor::before {
    content: ""; position: absolute; inset: -10px -8px;
  }
}

/* 表格 */
.prose img {
  max-width: 100%; height: auto; border-radius: 10px; margin: 8px 0;
  cursor: zoom-in; transition: box-shadow var(--dur-3) var(--ease), transform var(--dur-3) var(--ease);
}
.prose img:hover { box-shadow: 0 12px 26px -14px color-mix(in srgb, var(--ink) 50%, transparent); transform: scale(1.005); }
.table-wrap { overflow-x: auto; margin: 0 0 18px; }
.prose table { border-collapse: collapse; width: 100%; font-size: 0.92rem; }
.prose th, .prose td { border: 1px solid var(--line); padding: 8px 12px; text-align: left; }
.prose th { background: var(--honey-tint); color: var(--ink); font-weight: 600; }
.prose tr:nth-child(even) td { background: color-mix(in srgb, var(--honey-tint) 35%, transparent); }

/* 任务列表 */
.prose li.task { list-style: none; margin-left: -18px; }
.prose li.task label { display: flex; align-items: center; gap: 8px; }
.prose li.task input[type="checkbox"] { accent-color: var(--honey); }

/* 目录 */
.toc {
  background: var(--card);
  background-image: linear-gradient(var(--card), var(--card));
  border: 1px solid var(--line); border-radius: 12px;
  padding: 16px 20px; margin: 0 0 26px; font-size: 0.9rem;
  box-shadow: 0 10px 26px -16px color-mix(in srgb, var(--ink) 35%, transparent);
}
.toc-title { font-family: var(--serif); font-weight: 600; color: var(--ink); margin: 0 0 8px; font-size: 0.98rem; }
.toc ol { margin: 0; padding-left: 22px; }
.toc li { margin: 4px 0; }
.toc li a { color: var(--ink-soft); text-decoration: none; }
.toc li a:hover { color: var(--plum); text-decoration: underline; }
.toc .toc-l2 { margin-left: 14px; }
.toc .toc-l3 { margin-left: 28px; }

/* ---------- 焦点可见性 ---------- */
a:focus-visible, button:focus-visible, input:focus-visible,
textarea:focus-visible, select:focus-visible {
  outline: 3px solid var(--honey); outline-offset: 3px; border-radius: 6px;
}

/* ---------- 响应式：容器留白 ---------- */
/* 导航项是 flex-shrink:0 + nowrap，胶囊 max-width 到顶就溢出、不换行。
   链接从 4 条加到 6 条后窄桌面窗容易撑破，所以这里把间距收紧而不是放开
   （原来 14px 的 gap 在最需要收紧的地方反而把胶囊撑得更宽）。 */
@media (max-width: 640px) {
  /* 导航字号收一档改在变量上，.nav 和 .nav-dice 一起跟着走 */
  :root { --nav-fs: 0.86rem; }
  .container { padding-left: var(--comfort); padding-right: var(--comfort); }
  .nav { gap: 4px; }
  .nav a { padding: 7px 9px; }
  .nav-dice { padding: 7px 9px; }
  .site-header { padding: 7px 6px 7px 11px; }
  .site-header .container { gap: 8px; }
}
/* 这里原来有一条 .nav a[data-nav="contact"] { display: none }（≤480px 让位给页面链接）。
   顶部胶囊在 ≤520 就已经整体让位给底部胶囊了，这条永远匹配不到，已删除。
   「联络」在页脚和关于页都有，底部胶囊装不下时可横滑（见上面的 .mnav-strip）。 */

/* ---------- backdrop-filter 兜底：不支持时用不透明底色，别让内容糊在背景上 ---------- */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header { background: var(--card); }
  .mobile-nav { background: var(--card); }
}

/* 顶部胶囊固定定位后脱离文档流，桌面端把首屏内容下移避开。
   ⚠️ 这一条必须写在下面「触屏 / 窄窗」那一段**之前**：两处都在给 body 写
   padding-top，同特异性下后写的赢。batch 25 把三个媒体块合并成共用块时，
   顺手把窄窗那条挪到了这里前面，于是窄桌面窗（hover:hover 且宽度<=520）里
   .site-header 已经 display:none，正文却仍被这条盖回 96px —— 首屏顶上白多出
   一条 96px 空带。浏览器实测（320/360/390/414/460/520 六档全中）与静态判据
   .deploy/check-cascade.js 都抓到了它。改这段前先想清楚谁写在后面。 */
@media (hover: hover) {
  body { padding-top: 96px; }
}

/* ---------- 触屏 / 窄窗：隐藏顶部胶囊，露出底部胶囊 ----------
   原来 (hover:none) 与 (hover:hover) and (max-width:520px) 两块各手抄了一遍
   padding-bottom:84px 和 .back-top 的 bottom —— 这正是「改了 item 尺寸却忘了改
   避让」会再次腐坏的坑（本项目踩过两次）。现在合并成一份共用规则，数值全部来自
   :root 的 --mnav-gap / --mnav-pad 单一来源，两处媒体查询只负责「什么时候露出底胶囊」。
   窄桌面窗（hover:hover 但宽度放不下）：品牌名+5~6 条链接在 375~460 必然撑破顶部胶囊，
   直接借用 fit-content 居中的底部胶囊（6 项在 375 也放得下），比继续压字号更稳。 */
@media (hover: none) {
  .site-header { display: none; }
  .mobile-nav { display: block; }
}
@media (hover: hover) and (max-width: 520px) {
  .site-header { display: none; }
  .mobile-nav { display: block; }
  /* 覆盖上面桌面块的 padding-top:96px —— 窄窗顶栏已隐藏，留 96px 会变成顶部空白 */
  body { padding-top: 0; }
}
/* 共用避让：只要底胶囊会显示（触屏 或 窄窗），正文就留出 --mnav-pad，back-top 落到胶囊之上 */
@media (hover: none), (hover: hover) and (max-width: 520px) {
  body { padding-bottom: var(--mnav-pad); }
  .back-top { bottom: var(--mnav-pad); }
}

/* ---------- 底栏里的「翻一篇」（batch 27） ----------
   顶栏在触屏/窄窗整条隐藏，所以移动端要有入口只能落在底栏。
   ⚠️ 这里必须把 .mobile-nav 从 block 换成 flex 行：那颗骰子是 .mnav-strip 的
      兄弟节点（strip 会被 renderNav() 清空重建，不能往里塞），
      而 block 下兄弟节点会另起一行 —— 底栏就会变高，
      --mnav-pad 那套「胶囊下缘 + 胶囊高度 + 呼吸余量 = 88px」的避让几何当场作废，
      正文和 back-top 都会被压住一截（batch 25 刚治理完的那类遮挡）。
      flex + align-items:center 让它和导航项并排，高度由最高的 .mnav-item
      （clamp 44~52）决定，骰子本身约 39px，不会把条子顶高。
   ⚠️ strip 要 flex:1 + min-width:0：否则它按内容撑开，装不下时不横滑而是把骰子挤出视口。 */
@media (hover: none), (hover: hover) and (max-width: 520px) {
  .mobile-nav { display: flex; align-items: center; gap: 6px; }
  .mobile-nav .mnav-strip { flex: 1; min-width: 0; }
  .mobile-nav .nav-dice { flex: none; }
}

/* ═══════════════════════════════════════════════════════════════
   工艺资产（batch 10-1）：朱砂印 / 毛边纸 / 宣纸纤维+帘纹
   ───────────────────────────────────────────────────────────────
   三组都是**纯装饰**，所以有一条贯穿全组的规矩：

   1. 全都不自动上页面。想用哪个元素，加哪个类；不加就一点痕迹都没有。
      唯独 body::after 那层已有 grain 是全站常驻的，本轮**故意不动它** ——
      改一个所有页面共用的固定层，代价和风险都远大于收益，另开一轮再说。
   2. 全部不写死色值，一律走 tokens.css。
   3. 全部进 @media print 的隐藏清单（见文件末尾）—— 这是本项目
      踩过两次的坑，所以是「写的时候就登记」，不是做完补。
   ═══════════════════════════════════════════════════════════════ */

/* ---------- 朱砂印 ----------
   做法拆成三件，互相独立、各自可关：
     崩边  → mask-image: var(--seal-mask)（SVG 内跑完 turbulence +
             displacement，输出位图；不用 `filter:`）
     印泥不匀 → 两层 radial-gradient 压在实色底上
     印面 → 就是元素本身，字由调用方给

   ⚠️ 为什么不用 `filter: url(#…)`：CSS 的 filter 属性引用 SVG filter
   在 Firefox 上长期不稳，失败是**静默**的 —— 不生效时就是一个方方正正
   的红块，比不做还难看。走 mask 是普通图片解码路径，全平台一致。

   ⚠️ 性能红线：mask 只作用在这一枚印上。真正的开销不是 mask 本身
   （那张 SVG 渲染一次就被缓存成位图），而是如果有人拿 .seal 的
   思路去 filter 整页 —— feDisplacementMap 那套逐像素重采样每帧都重跑。
   尺寸钉在 48/56 两档；要更大请另做，不要改这两档。 */
.seal {
  --seal-size: 48px;
  position: relative;
  display: inline-grid;
  place-items: center;
  inline-size: var(--seal-size);
  block-size: var(--seal-size);
  /* 印框是方的，圆角只给到 2px —— 手刻的印，边是崩的但**角还是方的**，
     给圆角会立刻变成一枚 app 图标。 */
  border-radius: 2px;
  flex: none;
  user-select: none;

  /* 印泥不匀：底下是实色朱砂，上面压两团浓淡不一的 radial-gradient。
     真实的印泥是「按下去的地方重、没吃足的地方浅」，所以两团都从
     偏心位置起（22% 18% 和 82% 88%），不是同心圆 —— 同心圆会像
     打了一层高光，而不是蘸墨不匀。 */
  background:
    radial-gradient(78% 74% at 22% 18%,
      color-mix(in srgb, var(--paper) 22%, transparent) 0%, transparent 62%),
    radial-gradient(86% 82% at 82% 88%,
      color-mix(in srgb, var(--ink) 16%, transparent) 0%, transparent 66%),
    var(--seal);
  color: var(--seal-fg);

  /* 崩边。132% 对应 tokens.css 里矩形内缩 24/200=12%（两边都要配套改）。 */
  -webkit-mask: var(--seal-mask) center / 132% 132% no-repeat;
  mask: var(--seal-mask) center / 132% 132% no-repeat;

  /* 印面放字：字跟着 mask 一起被崩掉一点点边，才是「盖上去」的。
     font-size 给到 0.62em 是让 48px 的印里单字不至于顶满（要留天头地脚）。 */
  font-family: var(--serif);
  font-size: 0.62em;
  line-height: 1;
  letter-spacing: 0.02em;
}
/* 印框外圈的一线朱砂，像刻痕的边。单独一个元素/类才画，
   直接给 .seal 加 border 的话，mask 会把 border 一起啃掉。 */
.seal-lg { --seal-size: 56px; }
/* 阳文（红字留白）：字和底对调。想要这版就把 class 加上。 */
.seal-yang { background: var(--seal); color: var(--seal-fg); }
.seal-yang .seal-ink,
.seal-yang > * { color: var(--paper); }

/* ---------- 毛边纸：本轮砍掉，别再试第三条路 ----------
   试过两版都不成立，结论记下来省得下人重蹈：
   1. 「整张撕口 SVG mask 整个元素」—— 300x300 的图在扁元素上被
      preserveAspectRatio 等比 letterbox，只有中间一块可见，两边内容裁没。
      改 preserveAspectRatio='none' 后 Chrome 的 mask 光栅化仍不按预期拉伸；
      删掉 width/height 让 SVG 无固有尺寸，行为更不可测。
   2. 「只撕外沿一圈」（mask-composite: exclude 挖环）—— 环本身是直的，
      撕口图只覆盖 12px 宽的环，视觉上等于加了个歪边框，不如不做。
   根因：CSS mask 对 data-URI SVG 的缩放策略跨浏览器不一致，
   「一张固定图案盖满任意形状」这件事在这个平台上做不干净。
   朱砂印（正方形元素）和宣纸（平铺 background）没这个问题，都留下了。
   真要毛边，得走「四边各一条 repeat-x/repeat-y 的窄条 mask」，
   接缝处理另开一轮。 */

/* ---------- 宣纸纤维 + 帘纹 ----------
   两层分开、都能单独关，因为「有纤维没帘纹」和「只有帘纹」都成立：
     .xuan-fiber  纤维（长条噪点）
     .xuan-laid   帘纹（竹帘横线）
     .xuan        两个都要（宣纸 = 纤维 + 帘纹）

   ⚠️ 极淡是硬要求。已有的 body::after grain 是 opacity .045，
   这里纤维两层合计 .048、帘纹 .5 × 4% ≈ .02 —— 三层都在同一个
   「几乎看不见但拿掉就发空」的量级。再往上叠就是脏纸。

   ⚠️ mix-blend-mode: multiply 跟 body::after 是同一套算术（暗处更暗），
   所以两层叠加不会互相「提亮」，不会叠出灰蒙蒙的脏块。
   海盐蓝主题把 --fiber / --laid-line 关成了 none，这两组类在那边
   自然退化成无效果 —— 主题声明过不要纸纹，不走后门。

   ⚠️ 纤维用两个伪元素（::before / ::after），因为一层 background-image
   里的多张图只能共用一个 opacity，没法做出「一浓一淡」；
   帘纹直接画在元素自己的 background 上，不占伪元素。
   层序天然是「帘纹在下、纤维在上」—— 正是宣纸该有的关系。
   伪元素是绝对定位 + inset:0，不参与布局；pointer-events: none 且只画
   噪点，不会挡住文字，也不影响命中测试。

   ⚠️ 唯一用法约束：给 .xuan* 的祖先或自身加 transform / filter / opacity
   会创建层叠上下文、把绝对定位的伪元素关进同一个合成层，
   噪点会和背景错位。要做动效请在**外层**套一个容器再动。 */
.xuan-fiber, .xuan-laid, .xuan { position: relative; }

/* 帘纹：铺在元素自身的 background（最底层）。深浅不用 opacity 控制 ——
   opacity 会把**元素上的文字也一起压淡**；帘纹的浓度由 tokens.css 里
   --laid-line 自带的 color-mix 百分比承担（4%），粒度更细。
   不想要帘纹就把类换成 .xuan-fiber，别指望再调一个总闸。 */
.xuan-laid, .xuan {
  background-image: var(--laid-line);
  background-repeat: repeat;
}

/* 纤维层 A（浓）。位置错开是刻意的：另一层只铺在偏移 74/118 处，
   于是噪点有的地方重叠（深）、有的地方只有一层（浅）、有的地方
   什么都没有（露纸）—— 这就是纸浆的「不匀」。 */
.xuan-fiber::before, .xuan::before {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background-image: var(--fiber);
  background-size: 200px 200px;
  background-repeat: repeat;
  opacity: var(--fiber-a);
  mix-blend-mode: multiply;
}
/* 纤维层 B（淡）：位置再错开一点，避免和 A 层的噪声有可见的对应关系。 */
.xuan-fiber::after, .xuan::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background-image: var(--fiber);
  background-size: 200px 200px;
  background-position: 74px 118px;
  background-repeat: repeat;
  opacity: var(--fiber-b);
  mix-blend-mode: multiply;
}

/* ---------- 减少动效 ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation: none !important; transition: none !important; }
  /* 特异度要和上面那条隐藏态齐平（(0,1,1)）才能靠文档顺序赢回来：
     上一轮 a11y 巡检实测 reduced-motion 生效时首页仍有一个
     article.article.reveal 停在 opacity:0，根因就是这里写的是 (0,1,0)、
     压不过隐藏态。过渡被上面那条全掐了，观察器来不及补的就会永久隐形。 */
  /* 根因更正（本轮实测）：不是压不过本文件的隐藏态，而是**各页 <style> 里的
     同名孤岛**比它晚 —— index.html 有一条同特异度 (0,1,0) 的
     `.reveal { opacity: 0; ... }`，文档顺序在 site.css 之后，于是把这条还原
     顶掉了。用 !important 才压得住页面级孤岛，和本块其余几条的写法一致
     （它们也是因为「过渡被全掐后不会自己走到终态」而用了 !important）。 */
  .reveal { opacity: 1 !important; transform: none !important; }
  /* 上面那条把动画全掐了 —— 于是「靠动画才出现」的元素就永远停在初始态：
     首页首屏（.hero > * 是 opacity:0 + rise 动画）整块看不见，
     下划线（.swoosh path 的 dashoffset 由 draw 动画归零）也画不出来。
     这里把它们的终态直接坐实，不靠动画。 */
  .hero > * { opacity: 1 !important; }
  .swoosh path { stroke-dashoffset: 0 !important; }
  /* batch 9 新加的：灯箱的图是 transform/opacity 起始态 + 过渡到终态，
     过渡被掐掉之后不会自己走到终态，会永远停在半透明的小图上。坐实它。 */
  .lightbox > img { transform: none !important; opacity: 1 !important; }
  /* 导航滑动高亮块同理 —— 宽度/位移全靠过渡，掐掉就停在原地的透明块。 */
  .nav-ind { transition: none !important; }
}

/* ---------- 打印：只留能读的东西 ----------
   注意特异性：季节底色是 html.season-* body（0,1,2），光写 html, body（0,0,1）
   压不住它，打印时会整页铺一层暖色。这里用同样的 html.season-* 选择器盖回去。 */
@media print {
  /* .bookmark（文章页右侧那根随进度变长的丝带）已从 post.html 整条删除，
     所以这里不再点名它 —— 留着就是一条永远匹配不到的死规则。 */
  .site-header, .mobile-nav, .scroll-progress, .back-top,
  .comment-form, .tea-block, .post-nav, .back, .toast, .lightbox,
  .nav-ind,
  .hero-links, .filters, .search-row, .search-info, .tag-cloud,
  .load-more-wrap, .h-anchor, .post-tools, .post-views, .cf-count,
  /* 代码块复制按钮（batch 31）和 .h-anchor 是同一类东西：屏幕上才成立的交互把手，
     印到纸上就是代码块角上一个按不动的方框。
     ⚠️ 光隐藏按钮还不够 —— .code-box > pre 那条 padding-top:36px 是**为了让出按钮那
        一条水平带**才加的，按钮藏了它若不跟着还原，每张纸顶端会凭空多一条空白。
        还原写在下面单独一条：它的选择器比 .prose pre 和 .code-box > pre 都特异，
        所以是直接压过那两条，不需要靠文档顺序。 */
  .code-copy,
  .view-count, .read-time, .comment-count, .archive-item .c,
  .mini-btn, .heatmap-section, .heatmap-tooltip,
  .hero::before, .hero::after, .note::before, .swoosh, .stamp::after,
  .prose h2::before, .prose h3::before,
  .links-rule, .pw-rule,
  body::before, body::after {
    display: none !important;
  }
  .prose .code-box > pre { padding-top: 16px; }

  /* batch 10 预登记：这一轮要新增的装饰件**先写进清单**，不是做完再补。
     之前两次是做完才发现忘了加，印出来一堆灰影。这里提前占好位置：
       .seal          文末朱印（batch 10-3）
       .seal-strip    归档页「写下的日子」印章排（batch 10-4）
       .visits-section 到访热力图区块（batch 10-4）
       .rule-frame    天头地脚 / 界栏的装饰线（batch 10-4）
     这些类名建起来之前，下面几条不会命中任何东西 ——
     提前登记比事后补安全，因为「忘记加」这个坑只发生在事后。 */
  .seal, .seal-strip, .visits-section, .rule-frame {
    display: none !important;
  }

  /* batch 10-1 的工艺件，同样是「写的时候就登记」。
     .seal / .seal-strip 已在上面那条隐藏清单里（batch 10 预登记）。
     ⚠️ .xuan* 这里**不能**整块 display:none —— 它是加在正文卡片、
     引言块这类**有内容的容器**上的类，藏掉元素等于把内容一起不打印。
     正确做法是只剥掉装饰层：纤维是 ::before/::after 伪元素（藏它们），
     帘纹是 background-image（清掉）。元素本体和文字照常打印。
     （mix-blend-mode: multiply 在打印分色下会印成实心灰块，所以必须剥。） */
  .xuan-fiber::before, .xuan::before,
  .xuan-fiber::after, .xuan::after {
    display: none !important;
  }
  .xuan, .xuan-fiber, .xuan-laid {
    background-image: none !important;
    mix-blend-mode: normal !important;
  }

  /* 底色与纸感：季节类、主题类、body 都要点名，否则铺满整页的颜色会跟着打印
     顺带把两档灰字压深：#8B7359 印在白纸上偏淡，纸面上要能看清
     batch 9：深夜转暖的 .night 也必须一起点名，否则夜里打出来的纸是暖黄的。 */
  :root, html, body, html.season-spring, html.season-summer,
  html.season-autumn, html.season-winter, html.night,
  html.season-spring body, html.season-summer body,
  html.season-autumn body, html.season-winter body, html.night body,
  html[data-theme], html[data-theme] body,
  html.season-spring[data-theme] body, html.season-summer[data-theme] body,
  html.season-autumn[data-theme] body, html.season-winter[data-theme] body,
  html.night[data-theme] body {
    background: #fff !important;
    color: #000; padding: 0;
    /* 主题色板整体换成灰阶：!important 保证压过 html[data-theme=*] 的定义 */
    /* 第 7 轮：这里的 --paper-2 / --rose-tint 已随之删除 —— 变量本体在
       tokens.css 里没有了，打印时再覆盖一次就是个空操作，
       留着只会让人以为「还有这两个色可覆盖」。 */
    --paper: #fff !important; --card: #fff !important;
    --ink: #1a1a1a !important; --ink-soft: #333 !important;
    --muted: #4a4a4a !important; --line: #bbb !important;
    --honey: #000 !important; --honey-tint: #fff !important; --honey-ink: #000 !important;
    --rose: #000 !important;
    --plum: #000 !important; --glass: #fff !important; --glass-strong: #fff !important;
    --honey-deep: #000 !important; --ring: none !important;
    /* batch 30 新增的三个语义 token 必须一起点名。
       --on-honey 在青竹/海盐蓝是 #FFFFFF —— 打印时白字落在白纸上等于字消失，
       所以这里强制塌成黑，而不是让它跟着主题走。 */
    --rose-ink: #000 !important; --on-honey: #000 !important; --honey-hover: #000 !important;
    --glow-a: transparent !important; --glow-b: transparent !important;
    --grain: none !important;
    --t1-bg: #fff !important; --t1-fg: #000 !important;
    --t2-bg: #fff !important; --t2-fg: #000 !important;
    --t3-bg: #fff !important; --t3-fg: #000 !important;
    --t4-bg: #fff !important; --t4-fg: #000 !important;
    --t5-bg: #fff !important; --t5-fg: #000 !important;
    --t6-bg: #fff !important; --t6-fg: #000 !important;
    /* batch 10-1：朱砂也进灰阶板。印面本身已经被上面的隐藏清单摘掉了，
       但 --seal 可能被别处当普通强调色引用（比如某个 .seal-tiny），
       不点名的话会有一块朱红跟着印到纸上 —— 打印就没有颜色可讲，
       全都应该塌成黑白。--seal-fg 一起点名（它是印面上的字）。 */
    --seal: #000 !important; --seal-fg: #fff !important;
    /* 纸纹类同理：分层纸张在分色下会印成可见的横线/噪点，
       和 --grain 一起关掉。 */
    --fiber: none !important; --laid-line: none !important;
    --seal-mask: none !important;
    /* batch 10：分类色可能以 --cat-bg/--cat-fg 的形式挂在 <html> 上
       （文章页 loadPost 会写），所以灰阶板也得点名它，
       否则文章页打印会带一块彩色底。 */
    --cat-bg: #fff !important; --cat-fg: #000 !important;
    font-size: 11.5pt; line-height: 1.7;
  }
  .container { max-width: none; padding: 0; }
  .reveal { opacity: 1 !important; transform: none !important; }

  /* 渐变文字在打印时多半不上墨，统一换回纯黑 */
  .post-title, .name-text, .sec-title .latin, .year-head .yr,
  .page-title .en, .hero .tagline, .comment-name {
    background: none !important; color: #000 !important;
    -webkit-text-fill-color: #000 !important;
    -webkit-background-clip: border-box !important;
    background-clip: border-box !important;
    filter: none !important;
  }

  /* 胶囊、色块、阴影都吃墨，一律压成白底细边 */
  .chip, .tag, .tag-pill, .filter, .eyebrow, .note, .pn-label,
  .prose pre, .prose code, .prose th, .comment .cf-reply {
    background: #fff !important; color: #000 !important;
    box-shadow: none !important;
    border: 1px solid #ccc !important;
  }
  .prose blockquote {
    background: #fff !important; color: #333 !important;
    border-left: 2px solid #999 !important; border-image: none !important;
    box-shadow: none !important;
  }
  /* 日期章保留（它带日期），但去掉糖纸感和发光 */
  .stamp { background: #fff !important; box-shadow: none !important; border: 1px solid #bbb; }
  .toc, .about-card, .heatmap-section, .archive-item:hover {
    box-shadow: none !important; background: #fff !important; border: 1px solid #ddd;
  }
  /* 年份头是个 <button>，去掉按钮底色和边框，只留一条下划线 */
  .year-head {
    background: none !important; box-shadow: none !important;
    border: 0; border-bottom: 2px solid #000 !important;
    padding: 0 0 6px; color: #000; cursor: default;
  }
  .year-caret { display: none !important; }
  .year-head .cnt { color: #444; }
  /* 月份标签是玫瑰色，印在白纸上偏淡，压深一档 */
  .month-label { color: #7a4f45 !important; }
  /* 关于页的小勾和底部链接按钮 */
  .tick { display: none !important; }
  .about-links a, .btn-home {
    background: #fff !important; color: #000 !important;
    box-shadow: none !important; border: 1px solid #999 !important;
  }
  /* 404 页那个大数字。batch 9 起它已经不是渐变挖空了（改成 --ink 实色 +
     一圈 drop-shadow 光晕），但光晕同样不上墨 —— filter 也要一起掐掉，
     否则纸面上会印一团灰影。 */
  .hero-code, .hero-code::after {
    background: none !important; color: #000 !important;
    -webkit-text-fill-color: #000 !important;
    -webkit-background-clip: border-box !important;
    background-clip: border-box !important;
    filter: none !important;
  }
  .prose tr:nth-child(even) td { background: #f7f7f7 !important; }
  .prose a {
    background-image: none !important; color: #000 !important; text-decoration: underline;
  }
  /* 文章列表标题的 hover 下划线是 background-image 画的，打印时直接去掉 */
  .article h3 a, .article h2 a {
    background-image: none !important; color: #000 !important; text-decoration: underline;
  }
  .eyebrow .dot { display: none !important; }

  /* 分页控制：标题不落单，代码/引用/表格不劈开 */
  .prose h1, .prose h2, .prose h3,
  .sec-head, .year-head, .comments-title { page-break-after: avoid; break-after: avoid; }
  .prose pre, .prose blockquote, .prose table,
  .prose img, .about-card, .article, .comment { page-break-inside: avoid; break-inside: avoid; }
  .prose img { max-width: 100% !important; }

  a { color: #000; text-decoration: underline; }
  .site-footer { border-top: 1px solid #ddd; padding-top: 12px; color: #555; }
  .year-group.is-collapsed .month-block,
  .year-group.is-collapsed .month-label { display: block !important; }
}
