/* ===========================================================================
   代码川 · 前台样式

   版式对齐参考站：
     - 顶部一条通栏导航（60px），站名在左、菜单在右；内容宽度与导航内沿对齐。
     - 正文与右侧卡片栏同处一个居中容器（1200px）。没有常驻的左侧栏——
       头像与菜单收在汉堡抽屉里，任何屏宽都能叫出来。
     - 全站 HTML 页面铺同一张实拍背景图（后台配置的图片，每次访问随机一张），
       取不到图才退回纯色；正文始终在白卡上，照片只透在卡外那一圈。
     - 卡片扁平、圆角小、阴影浅；层次靠留白和发丝线，不靠渐变与光斑。

   性能：不引外部字体与图标字体，图标全是内联 SVG；背景图固定定位不重排；
   动效只用 transform/opacity，低端机与 prefers-reduced-motion 下自动落零。
   =========================================================================== */

/* ---------- 1. 设计令牌 ---------- */
:root {
  /* 唯一的强调色。低饱和钢蓝：够点得出来，又不像营销页那样吵 */
  --accent: #3d6b96;
  --accent-hover: #2f5578;
  --accent-soft: rgba(61, 107, 150, .10);

  --notice: #c8503f;   /* 公告等少量提示位用的红 */

  --bg: #eef1f6;
  --card: #ffffff;
  --card-2: #f6f8fb;
  --text: #4c4948;      /* 正文取参考站的暖调近黑，而不是冷调深蓝灰 */
  --text-2: #6b6b6b;    /* 次级正文，落在正文与元信息之间 */
  /* 元信息/弱化文字（日期、阅读数、分类名、归档计数……）。
     这些字号都在 11-13px，属于 WCAG 里的「普通文本」，门槛是 4.5:1，不是大字那档 3:1。
     原先的 #858585 在白卡上只有 3.69:1、在 --card-2 上更降到 3.47 —— 全站几十处
     小字都在这条线以下（版权块、站点信息、标签、目录三级项…）。
     现在取 #6e6b68：白卡 5.30:1、--card-2 4.98:1，最差背景也过 AA，
     且保留暖调（纯灰 #6e6e6e 会让元信息偏冷，与正文不搭）。
     **改动这一条前先算对比度**，别为了「更淡更透气」把它调回 4.5 以下。 */
  --text-3: #6e6b68;
  --border: rgba(27, 32, 43, .10);
  --border-2: rgba(27, 32, 43, .06);
  --shadow-sm: 0 1px 2px rgba(27, 32, 43, .04), 0 2px 10px rgba(27, 32, 43, .05);
  --shadow-md: 0 4px 14px rgba(27, 32, 43, .08), 0 12px 28px rgba(27, 32, 43, .07);

  /* 页脚压在照片上，文字偏浅，另给一套前景色 */
  --foot-text: #b9bfca;

  --wrap: 1200px;      /* 与参考站一致的内容宽度 */
  --nav-h: 60px;
  --aside-w: 304px;
  --cover-ph: #dfe4ec; /* 封面占位色：图没传到图床时露出来的底 */

  /* 文章卡片的三个值对齐参考站（Butterfly 的 --card-* 系列）。
     阴影比 --shadow-sm 更宽更柔，是这套列表「卡片浮在纸上」观感的来源。 */
  --card-shadow: 0 3px 8px 6px rgba(7, 17, 27, .05);
  --card-shadow-hover: 0 6px 18px 10px rgba(7, 17, 27, .08);
  --card-title: #1f2d3d;   /* 标题比正文更深更冷，视觉重心落在标题上 */
  /* 卡片里的元信息（日期 / 阅读数）。原先写死 #858585，与 --text-3 重复了两个值，
     调暗其中一个就漏掉另一个 —— 直接引用同一个令牌，对比度约束见 --text-3 那段。 */
  --card-meta: var(--text-3);

  /* 缩略图悬停放大。文章卡片、侧栏最新、归档时间线三处用的是同一组数值，
     各自写死一遍时，调手感要改三处，漏一处就出现「首页放大、归档不动」。 */
  --thumb-zoom: 1.1;
  --thumb-transition: filter 375ms ease-in .2s, transform .6s, opacity var(--t);

  --radius: 10px;
  --radius-sm: 8px;

  --t-fast: .18s;
  --t: .3s;

  color-scheme: light;
}

/* ---------- 2. 基础重置 ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* 锚点跳转的全局让位：顶栏是 sticky 的 60px，不留这一条，
     点目录 / 侧栏归档锚点时标题会正好停在顶栏底下（前半截被盖住）。
     ⚠️ scroll-padding 和元素自己的 scroll-margin **是相加、不是覆盖**。
     所以正文标题**不能**再单独写 scroll-margin-top：76 + 80 会把标题顶到 156px，
     比右侧吸顶目录（74px）低 80 多像素，点目录时看着就是「没对上」。 */
  scroll-padding-top: calc(var(--nav-h) + 16px);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
               "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 15px;
  line-height: 1.75;
  transition: background-color var(--t), color var(--t);
  overflow-x: hidden;
}

a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }
button { font: inherit; color: inherit; }

/* 选中文本：默认的蓝黄撞色在浅灰画布上很跳，压成主色的极浅一档。
   同时保留 outline 型的焦点圈不受影响。 */
::selection { background: var(--accent-soft); color: var(--text); }
/* 移动端点按：默认的高亮灰块压在卡片上像一块补丁。
   关掉它，但保留 :focus-visible 的轮廓 —— 键盘用户不受影响。 */
a, button, [role="button"] { -webkit-tap-highlight-color: transparent; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
:focus:not(:focus-visible) { outline: none; }

.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;
}
.sr-only--focusable:focus {
  position: fixed; top: 12px; left: 12px; z-index: 999;
  width: auto; height: auto; margin: 0; padding: 10px 16px;
  clip: auto; background: var(--accent); color: #fff; border-radius: var(--radius-sm);
  font-size: 13px; box-shadow: var(--shadow-md);
}

/* ---------- 3. 背景层 ---------- */
/* 纯色是兜底状态；每个 HTML 页面都会多插一张 .bg-photo（见 layout-head.ejs）。
   固定定位 + object-fit:cover，滚动时背景不动，也不会引起任何重排。 */
.bg-layer {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  background: var(--bg);
}
.bg-photo {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center;
  /* 背景图来自图床，明暗差别极大：亮图直接铺上去，顶栏菜单和面包屑的文字会糊掉。
     轻微虚化把照片降格成一层底纹，不管哪张图都能压住文字。 */
  filter: blur(2px);
  transform: scale(1.02);   /* 虚化会往内缩边，放大一点点盖住模糊出来的边 */
}
/* 再压一层薄纱。压完照片还认得出来，但绝不跟正文抢注意力。 */
.bg-scrim { position: absolute; inset: 0; background: rgba(255, 255, 255, .42); }

/* 面包屑原先是文章页唯一裸露在照片底纹上的一行正文色文字，所以这里曾给它单开过
   两条提亮规则。它已经收进白卡（见第 12 节），回到正常配色，那两条随之撤掉 ——
   留着不是「以防万一」，而是会实打实盖掉卡内 .crumb-now 的颜色。 */

/* 顶部滚动进度条 */
.scroll-progress { position: fixed; top: 0; left: 0; z-index: 60; width: 100%; height: 3px; pointer-events: none; }
.scroll-progress__bar { width: 0%; height: 100%; background: var(--accent); transition: width .08s linear; }

/* ---------- 4. 顶栏 ---------- */
/* 半透明白：底下是背景照片，透一点出来才不像是「贴了根白条」。
   宽屏在特效层里再叠一层 backdrop-filter（见 16.4），窄屏不叠 ——
   那一档的顶栏跟下拉菜单连在一起，多一层滤镜会让菜单跟着重算背景。
   无条件保持一层不透明底兜底：滤镜不支持的浏览器上文字照样压得住照片。 */
.topbar {
  position: sticky; top: 0; z-index: 40;
  height: var(--nav-h);
  background: rgba(255, 255, 255, .8);
  box-shadow: rgba(133, 133, 133, .6) 0 5px 6px -5px;
  transition: background-color var(--t);
}
/* 通栏铺满，不套 1200px 居中容器——对齐参考站。
   若套居中容器（max-width + margin:auto），站名会贴着正文左边、菜单贴着右栏右边，
   顶栏比正文窄一截，看着像嵌在页面里而不是浮在上面。 */
.topbar-inner {
  width: 100%;
  padding: 0 36px;
  height: var(--nav-h);
  display: flex; align-items: center; gap: 16px;
}
.topbar.is-solid { box-shadow: 0 1px 0 var(--border-2), rgba(133, 133, 133, .5) 0 5px 8px -6px; }

.site-name {
  font-size: 18px; font-weight: 700; letter-spacing: .04em;
  white-space: nowrap;
  transition: color var(--t-fast);
}
.site-name:hover { color: var(--accent); }

/* 宽屏菜单就在头部里排开，不需要汉堡。
   汉堡按钮在 .tool-btn 的通用样式之后被单独藏掉，窄屏断点里再放回来（第 15 节），
   全文件只此一条隐藏规则。 */
#burger { display: none; }

/* ---------- 5. 顶栏下拉菜单（窄屏用） ----------
   窄屏菜单就地展开在头部下方，不弹侧边抽屉：页面不横移、不遮内容，
   也不用额外维护一个侧栏。 */
.menus { display: flex; align-items: center; gap: 2px; margin-left: auto; }
.menus a {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 6px 11px; border-radius: 7px;
  font-size: 14px; color: var(--text-2);
  transition: background var(--t-fast), color var(--t-fast);
}
.menus a svg { width: 15px; height: 15px; fill: currentColor; opacity: .8; }
.menus a:hover { background: var(--card-2); color: var(--text); }
.menus a.is-active { color: var(--accent); font-weight: 600; }
.menus a.is-active svg { opacity: 1; }

/* 「好站集」入口：顶栏里唯一一颗实心胶囊，和三个文字菜单拉开层次。
   下划线动效在这里换成一道流光扫过 —— 胶囊底色上再压一条线会显脏。 */
.menus a.menu-hub {
  position: relative;
  margin-left: 8px;
  padding: 6px 11px 6px 10px;
  border-radius: 999px;
  color: #fff;
  font-weight: 600;
  background-image: linear-gradient(135deg, var(--accent) 0%, #6f9fbf 100%);
  box-shadow: 0 4px 12px rgba(61, 107, 150, .30), inset 0 1px 0 rgba(255, 255, 255, .28);
  overflow: hidden;
  transition: transform var(--t-fast), box-shadow var(--t-fast), filter var(--t-fast);
}
.menus a.menu-hub::after { display: none; }
.menus a.menu-hub::before {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 24%, rgba(255, 255, 255, .5) 50%, transparent 76%);
  transform: translateX(-130%);
  transition: transform .7s var(--fx-ease);
  pointer-events: none;
}
.menus a.menu-hub:hover::before { transform: translateX(130%); }
.menus a.menu-hub:hover {
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 7px 18px rgba(61, 107, 150, .42), inset 0 1px 0 rgba(255, 255, 255, .34);
  filter: brightness(1.06);
}
.menus a.menu-hub:active { transform: translateY(0); }
.menus a.menu-hub svg { opacity: 1; }
.menus a.menu-hub .menu-hub-star { width: 13px; height: 13px; }
.menus a.menu-hub .menu-hub-arr {
  width: 12px; height: 12px;
  fill: none; stroke: currentColor; stroke-width: 2.2;
  stroke-linecap: round; stroke-linejoin: round;
  opacity: .85;
  transition: transform .3s var(--fx-ease), opacity .3s;
}
.menus a.menu-hub:hover .menu-hub-arr { transform: translate(2px, -2px); opacity: 1; }

/* 桌面端汉堡不显示，但这一栏常驻：语言切换按钮住在这里，宽屏窄屏都要能
   一眼看到「换个语言」的入口。汉堡用 #burger 单独藏，而不是藏整栏——
   藏整栏会留下 29px 幽灵空隙 + 一道竖线，而栏里还有别的按钮。 */
.nav-tools { display: flex; align-items: center; gap: 4px; padding-left: 8px; margin-left: 4px; border-left: 1px solid var(--border-2); }
.tool-btn {
  position: relative;
  width: 32px; height: 32px;
  display: grid; place-items: center;
  border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--text-2);
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}
.tool-btn:hover { background: var(--card-2); color: var(--accent); }
.tool-btn svg { width: 17px; height: 17px; fill: currentColor; }

/* 语言切换：一颗胶囊里并排放两种语言，当前那段填白高亮，另一段才是链接。
   不做成「切到 X」的单按钮 —— 那种写法必须用另一种语言写字（中文页上顶着个
   English），访客读成「这页是英文的」，两边都觉得提示语说反了。
   两段都摆出来、亮着所在的那一段，就没有可反的余地。
   顺序固定（中文在左、English 在右），换语言时控件不跳位；
   32px 高与汉堡按钮同档，压在顶栏里不抢视线。 */
.lang-switch {
  display: inline-flex; align-items: center; gap: 2px;
  height: 32px; padding: 3px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card-2);
}
.lang-seg {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 48px; height: 100%; padding: 0 8px;
  border-radius: 999px;
  font-size: 12.5px; line-height: 1;
  color: var(--text-2); text-decoration: none; white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast), box-shadow var(--t-fast);
}
/* 当前语言：白底 + 主色 + 一层极浅投影，像被按起来的那一格 */
.lang-seg.is-current {
  background: var(--card); color: var(--accent); font-weight: 600;
  box-shadow: 0 1px 2px rgba(27, 32, 43, .07);
}
a.lang-seg:hover { color: var(--accent); background: var(--accent-soft); }


/* ---------- 5b. 作者信息（右侧卡片栏） ---------- */
/* 头像：圆形边框 + logo 在圆内「光学」居中。
   —— 为什么不是几何居中
   实测 logo.png 的墨迹质心比画布几何中心偏右 3.31%、偏下 9.40%：
   字形右侧是一根贯通全高的竖笔、左侧只是一段细弧，底部还有一段下沉的笔画。
   所以几何上摆正了，人眼看到的仍是「往右下坠着」。这里按质心反向补偿，
   补偿量 = 偏移比例 × 实际显示尺寸，换尺寸时必须同步换算。
    —— logo 定 82px 的依据
    尺寸按逐像素统计的真实墨迹计算：包围盒 ≠ 墨迹（字形四角本来就是空的），
    按「方框四角必须在圆内」估算会严重低估可用范围。116px 的圆
    （内半径 57）的可用范围如下：
     88px → 墨迹最远 54.4px，空隙仅 2.6px（贴边，看着顶到圈上）
     84px → 52.0px，空隙 5.0px
     82px → 50.8px，空隙 6.2px  ← 采用
   留 6px 左右，视觉上既有呼吸感又不至于把 logo 缩得太小。 */
.avatar-ring {
  width: 116px; height: 116px;
  margin: 0 auto 14px;
  border-radius: 50%;
  border: 1px solid var(--border-2);
  background: var(--card-2);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  transition: background var(--t);
}
.avatar-ring:hover { background: var(--accent-soft); }
.avatar {
  width: 82px; height: 82px;
  object-fit: contain;
  display: block;
  transition: transform var(--t);
}
/* 光学补偿只挂在圆环里的头像上：那 -2.7px/-7.7px 是照 116px 圆环的墨迹质心算的，
   脱离圆环就是无端偏移。文章页作者卡曾经用裸 <img class="avatar">，
   照样吃到这段补偿，在居中卡片里偏出 8px。挂到子选择器上，
   裸 .avatar 默认就是正中的，将来再写错也不会歪。 */
.avatar-ring .avatar { transform: translate(-2.7px, -7.7px); }
.avatar-ring:hover .avatar { transform: translate(-2.7px, -7.7px) scale(1.06); }

/* 社交图标：参考站是裸图标，不套方块底。给它加底反而把注意力从头像上拽走。
   图标尺寸用 em 跟随 font-size —— 参考站那是 FontAwesome 字形，靠 em 定尺；
   这里换成内联 SVG 后若写 width:100%，容器宽度反过来又由 SVG 撑开，
   两边互相依赖会解成一个很大的值（图标直接撑爆整张卡）。 */
/* 社交图标：与统计同属「出口」那一层，排在统计下方一小段距离。
   用正的 margin-top 拉开即可，负下边距会把它顶到卡片边缘、像要掉出去。 */
.social-icons { display: flex; justify-content: center; align-items: center; margin-top: 13px; }
.social-icons a, .social-icon {
  position: relative;
  display: grid; place-items: center;
  margin: 0 5px;
  color: var(--text-3);
  font-size: 1.3em;
  cursor: pointer;
  transition: color var(--t-fast), transform var(--t-fast);
}
.social-icons a:hover, .social-icon:hover { color: var(--accent); transform: translateY(-2px); }
.social-icons svg { width: 1em; height: 1em; fill: currentColor; }
/* QQ 企鹅单独缩一档：官方 path 几乎铺满 448×512 的视口，
   而旁边几个图标在 24×24 里只占七成——同样 1em 的盒子，视觉重量自然差一截。 */
.social-icons svg.icon-qq { width: .82em; height: .82em; }

/* 微信：悬停（或键盘聚焦）弹出二维码，纯 CSS，不引 JS */
.social-icon--wx { outline: none; }
.wx-qr {
  position: absolute; bottom: calc(100% + 12px); left: 50%;
  z-index: 20;
  width: 132px; height: 132px;
  /* 必须显式关掉全局的 img{max-width:100%}：
     包含块是那颗只有 1em 宽的图标元素，不覆盖的话二维码会被压成一条竖缝。 */
  max-width: none;
  transform: translateX(-50%) translateY(6px);
  border-radius: var(--radius-sm);
  background: #fff;
  padding: 6px;
  box-shadow: 0 6px 24px rgba(27, 32, 43, .16);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .2s ease, transform .2s ease, visibility .2s;
}
/* 小尖角，指回微信图标 */
.wx-qr::after {
  content: ''; position: absolute; left: 50%; bottom: -5px;
  width: 10px; height: 10px; margin-left: -5px;
  background: #fff;
  transform: rotate(45deg);
  box-shadow: 3px 3px 8px rgba(27, 32, 43, .08);
}
.social-icon--wx:hover .wx-qr,
.social-icon--wx:focus .wx-qr {
  opacity: 1; visibility: visible; pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}
/* 触屏没有 hover：二维码靠 :hover / :focus 才现身，触屏上就成了一张永远点不开的图标
   （iOS 会在点击后短暂模拟 :hover，一移开就没了，读者根本来不及扫码）。
   与 .head-anchor / .code-copy 同一套处理：这类「悬停才出现」的控件在触屏上
   默认可见（弹出方向仍在上方），并带一个常态的浅投影，
   免得看起来像浮在半空的脏东西。 */
@media (hover: none) {
  .wx-qr {
    opacity: 1; visibility: visible; pointer-events: auto;
    transform: translateX(-50%) translateY(0);
    box-shadow: 0 6px 24px rgba(27, 32, 43, .10);
  }
}

/* 三格统计：只用发丝线与上方分隔，不加灰底方块。
   底色块比数字本身更重，一眼看过去先看见的是盒子而不是数字。 */
.site-data {
  display: grid; grid-template-columns: repeat(3, 1fr);
  margin-top: 15px; padding: 13px 0 0;
  border-top: 1px solid var(--border-2);
}
.site-data a {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  border-radius: 6px; padding: 5px 2px;
  transition: background var(--t-fast);
}
.site-data a:hover { background: var(--accent-soft); }
.site-data span { font-size: 12px; color: var(--text-3); }
/* 数字与标签的行距交给 gap，不用负 margin 收。
   等宽数字形是为了 main.js 那个「从 0 数到目标值」动画：位宽固定，跑起来不左右跳。 */
.site-data b { font-size: 17px; font-weight: 600; color: var(--card-title); font-variant-numeric: tabular-nums; line-height: 1.25; }
/* ---------- 6. 回到顶部 ---------- */
/* 常驻圆钮 + 外圈阅读进度环。
   常驻右下角而不是「藏到屏幕外、鼠标移到右缘才滑出」：只剩一颗按钮时，
   藏法等于把唯一的入口藏没了——读者不会无缘无故把鼠标往屏幕边缘蹭。
   环随阅读进度生长，既是按钮也是进度指示。 */
.right-rail {
  position: fixed; right: 22px; bottom: 34px; z-index: 55;
  display: flex; flex-direction: column; align-items: flex-end;
}

.rail-top {
  position: relative;
  width: 48px; height: 48px;
  display: grid; place-items: center;
  border: none; border-radius: 50%;
  background: var(--card);
  color: var(--text-2);
  box-shadow: 0 2px 10px rgba(27, 32, 43, .10), 0 6px 20px rgba(27, 32, 43, .08);
  cursor: pointer;
  transition: transform .3s cubic-bezier(.4, 0, .2, 1), box-shadow .3s, color var(--t-fast);
}
.rail-top:hover {
  color: var(--accent);
  box-shadow: 0 4px 14px rgba(27, 32, 43, .14), 0 10px 28px rgba(27, 32, 43, .10);
  transform: translateY(-2px);
}
.rail-top:active { transform: translateY(0) scale(.95); transition-duration: .1s; }

.rail-arrow {
  position: relative; z-index: 1;
  width: 19px; height: 19px; fill: currentColor;
  transition: transform .3s cubic-bezier(.4, 0, .2, 1);
}
.rail-top:hover .rail-arrow { transform: translateY(-2px); }

/* 进度环：底圈几乎透明，只留一道浅轨；前景弧随阅读进度增长 */
.rail-ring {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  transform: rotate(-90deg);   /* 从 12 点方向起画 */
  pointer-events: none;
}
.rail-ring circle { fill: none; stroke-width: 2.5; }
.rail-ring-track { stroke: var(--border); }
.rail-ring-arc {
  stroke: var(--accent);
  stroke-linecap: round;
  transition: stroke-dashoffset .12s linear;
}

/* ---------- 7. 页面骨架 ---------- */
.content {
  max-width: var(--wrap); margin: 0 auto;
  /* 顶部留白：顶栏是 60px 通栏，紧贴着顶栏排会让正文显得被压住。
     参考站内容与顶栏之间约有 40px 呼吸，这里取 30px 略收一档。 */
  padding: 30px 15px 40px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--aside-w);
  gap: 20px;
  align-items: start;
  min-height: calc(100vh - var(--nav-h));
}
.col-main { min-width: 0; }

/* ---------- 8. 卡片与通用块 ---------- */
.card { background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow-sm); }

.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 14px;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--card); color: var(--text-2);
  font-size: 13.5px; cursor: pointer;
  /* 显式列属性：按钮悬停时真正会变的是这三样，all 会把 box-shadow / transform
     也拉进比对，属于白付的重绘成本。 */
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.btn:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.btn--primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; }
.empty { padding: 52px 24px; text-align: center; color: var(--text-3); font-size: 14px; }
/* 空态里的「关于本站」是这一屏唯一的出口，给一点下划线让它看得出可点 */
.empty a { color: var(--accent); text-decoration: underline; text-decoration-color: var(--accent-soft); text-underline-offset: 3px; }
.empty a:hover { text-decoration-color: var(--accent); }

/* ---------- 9. 文章列表（封面在左/右交替） ---------- */
.card-list { display: flex; flex-direction: column; gap: 20px; }

/* 横向卡片 + 文字侧垂直居中，对齐参考站。
   文字侧 align-items:center：顶对齐 + 固定最小高度时，摘要一短
   卡片下半部就空出一大块，整列表看着像没排完。
   封面贴着卡片外沿（卡片无 padding，内边距只加在文字那一侧），
   左右交替时图片能顶到边；封面宽度用百分比而非固定像素，
   容器随视口变窄时图文比例不会走形。 */
.card-item {
  position: relative; overflow: hidden;
  display: flex; flex-direction: row; align-items: center;
  /* 高度必须写死，不能用 min-height。
     封面 img 带了 width/height 属性（4:3），在高度不确定时浏览器会按固有比例
     撑出 368×276，把卡片顶到比设定值更高——min-height 拦不住，实测仍高 265px。
     固定 height 才是参考站的做法：图片交给 object-fit:cover 裁切，卡片高度恒定。
     文字侧有两行截断（标题/摘要各 2 行），最长约 157px，不会溢出 235px。 */
  height: 235px;
  background: var(--card);
  border-radius: var(--radius-sm);
  box-shadow: var(--card-shadow);
  transition: box-shadow var(--t);
}
/* 偶数条把封面甩到右边：只翻 order，列宽保持不变 */
.card-item:nth-child(even) .card-main { order: 1; }
.card-item:nth-child(even) .card-cover { order: 2; }
.card-item:hover { box-shadow: var(--card-shadow-hover); }

.card-main { flex: 1 1 auto; min-width: 0; padding: 0 40px; }
.card-title {
  margin: 0 0 6px; font-size: 1.55em; font-weight: 600; line-height: 1.4;
  color: var(--card-title);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.card-title a { transition: color .2s ease-in-out; }
.card-title a:hover { color: var(--accent); }

.card-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin: 6px 0; font-size: .9em; color: var(--card-meta); }
.card-meta time { font-variant-numeric: tabular-nums; }
.meta-icon { width: 13px; height: 13px; fill: currentColor; opacity: .8; }
.meta-dot { opacity: .5; }

.card-excerpt {
  margin: 0;
  font-size: 13.5px; line-height: 1.85; color: var(--text-2);
  word-break: break-word;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-cover { position: relative; flex: 0 0 42%; height: 100%; display: block; background: var(--cover-ph); overflow: hidden; }
.card-cover img {
  width: 100%; height: 100%; object-fit: cover;
  transition: var(--thumb-transition);
}
/* 悬停放大的是图片本身，不是整张卡：长列表里抬整张卡太晃 */
.card-item:hover .card-cover img { transform: scale(var(--thumb-zoom)); }
.card-cover img.is-broken { opacity: 0; }

/* ---------- 10. 分页（编号 + 前后翻页） ---------- */
.pager { display: flex; align-items: center; justify-content: center; gap: 6px; margin: 22px 0 6px; }
.pager a, .pager span {
  min-width: 2.5em; height: 2.5em; padding: 0 10px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  border: none;
  background: var(--card);
  box-shadow: var(--card-shadow);
  font-size: 13px; color: var(--text-2);
  font-variant-numeric: tabular-nums;
  /* 显式列属性而不是 all：卡片有 box-shadow，all 会让阴影也跟着做一次
     无意义的重绘，长列表滚动时白白多占一帧合成预算。 */
  transition: color var(--t), box-shadow var(--t);
}
.pager a:hover { color: var(--accent); box-shadow: var(--card-shadow-hover); }
.pager .is-current { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; box-shadow: var(--card-shadow); }
.pager .is-disabled { opacity: .4; pointer-events: none; }
.pager .pager-gap { border: 0; background: transparent; color: var(--text-3); box-shadow: none; }
.pager .pager-step { padding: 0 12px; }

/* ---------- 11. 右侧卡片栏 ---------- */
.aside { display: flex; flex-direction: column; gap: 16px; position: sticky; top: calc(var(--nav-h) + 14px); }

/* 侧栏：**不自己滚** —— 一条独立滚动条挤在 304px 的栏里既难看，又会把滚轮吃掉
   （鼠标停在侧栏上滚的是侧栏，整页看着像卡住）。所以装得下视口时整栏吸附，
   装不下时由 main.js 加 .is-static 取消吸附、整栏跟着页面滚；
   代价是底部卡片会被切在视口外，于是给目录卡单独开一条 sticky：
     ① 装得下：整栏吸附，停在顶栏下方；
     ② 装不下（.is-static）：整栏跟页面走，但目录钉住 —— 长文最需要它。
        必须 align-self: stretch 撑满整行：sticky 的活动范围是包含块，侧栏若只有
        内容那么高（约 1000px），目录滚过这一段就跟着走了，钉不住整篇文章。
        也只在 .is-static 时生效：装得下时整栏本来就吸附，再套一层会和父级一起
        移动，反而算错位置。
   窄屏（≤1000px）本来就是静态排列。 */
@media (min-width: 1001px) {
  .aside { align-self: start; }
  .aside.is-static { position: static; align-self: stretch; }
  .aside.is-static .widget-toc {
    position: sticky;
    top: calc(var(--nav-h) + 14px);
    z-index: 2;
  }
}

/* 卡片基座与文章列表同源：同一套圆角与阴影，侧栏和主列表才像一套东西。
   transition 只列实际会变的属性（阴影），不用 all —— 侧栏卡片多，all 会让
   每张卡在滚动合成时都去比对一遍不相关属性。 */
.widget { position: relative; background: var(--card); border-radius: var(--radius-sm); box-shadow: var(--card-shadow); padding: 16px; transition: box-shadow var(--t), transform .35s var(--fx-ease); }
/* 标题栏：不画分隔线。参考站就是靠「标题 + 留白」分组，
   加一条 border-bottom 会把每张卡切成上下两半，反而碎。 */
.widget-head { position: relative; display: flex; align-items: center; gap: 6px; margin: 0 0 10px; padding-bottom: 6px; }
.widget-head svg { width: 15px; height: 15px; fill: var(--text-3); flex: none; }
.widget-head h2 { margin: 0; font-size: 1.2em; font-weight: 600; }
.widget-head .widget-more { margin-left: auto; font-size: 12px; color: var(--text-3); }
.widget-head .widget-more:hover { color: var(--accent); }

/* 作者卡分两层：上层「这是谁」（头像 / 姓名 / 副标题），下层「出口」
   （三格统计 + 社交图标），两层之间用一道发丝线分开。
   统计块不加灰底方块：它的视觉重量会仅次于头像，装的却是最少要的三项数据
   （文章数、月份数、运行天数），把主角压了下去。站内其余卡片一律
   「层次靠留白和发丝线，不靠渐变与光斑」，这里保持同一套语言。 */
.author-card { text-align: center; }
.author-name { margin: 0 0 5px; font-size: 1.57em; font-weight: 500; color: var(--card-title); }
/* 只给正的 margin，不用负上边距把副标题往姓名上拽：
   负边距会让头像 / 姓名 / 副标题 / 统计四段间距失去节奏，改一处字号就散架。
   行距交给 line-height 管。 */
.author-desc { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--text-3); }

.announce { display: flex; gap: 8px; align-items: flex-start; font-size: 13px; color: var(--text-2); line-height: 1.7; }
.announce svg { width: 15px; height: 15px; fill: var(--notice); flex: none; margin-top: 4px; }

.mini-list { display: flex; flex-direction: column; }
.mini-item { display: flex; gap: 10px; align-items: center; padding: 6px 0; }
.mini-thumb { flex: none; width: 52px; height: 40px; border-radius: 6px; overflow: hidden; background: var(--cover-ph); }
.mini-thumb img { width: 100%; height: 100%; object-fit: cover; transition: var(--thumb-transition); }
.mini-item:hover .mini-thumb img { transform: scale(var(--thumb-zoom)); }
.mini-thumb img.is-broken { opacity: 0; }
.mini-body { min-width: 0; }
.mini-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  font-size: 13px; line-height: 1.5; color: var(--text);
  transition: color var(--t-fast);
}
.mini-item:hover .mini-title { color: var(--accent); }
.mini-date { display: block; font-size: .85em; color: var(--card-meta); font-variant-numeric: tabular-nums; }

/* 归档速览：参考站的行内边距只有 2px，一屏能塞下更多月份 */
.archive-list { display: flex; flex-direction: column; }
.archive-list a {
  display: flex; align-items: center; justify-content: space-between;
  margin: 2px 0; padding: 2px 8px; border-radius: 6px;
  font-size: 13px; color: var(--text-2);
  transition: background var(--t), color var(--t);
}
.archive-list a:hover { background: var(--accent-soft); color: var(--accent); }
.archive-list .count { min-width: 22px; text-align: center; padding: 1px 7px; border-radius: 20px; background: var(--card-2); font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }

.webinfo { display: flex; flex-direction: column; font-size: 12.5px; }
/* 键值两栏：左侧标签占满剩余宽度并留 20px 呼吸位，右侧数值靠右 */
.webinfo-row { display: flex; align-items: center; padding: 2px 10px 0; }
.webinfo-row .k { flex: 1; padding-right: 20px; color: var(--text-3); }
.webinfo-row .v { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---------- 12. 文章详情页 ---------- */
.reading-bar { position: fixed; top: 0; left: 0; z-index: 58; width: 100%; height: 3px; }
.reading-bar__fill { width: 0%; height: 100%; background: var(--accent); transition: width .1s linear; }

/* 面包屑：白卡内、标题上方的一道眉标。
   它原先浮在卡片外面的照片底纹上，为了让最浅的一档文字还能读，得单开两条
   提亮规则 —— 那是为背景让路的妥协。收进卡里之后回到正常配色，
   那两条规则连同 body 上的 has-bg 根类一起撤掉了。

   深浅是刻意反过来的：**能点的（首页/归档）深，当前页浅**。
   常规面包屑都把当前页加重，可这里当前页的同一句话紧跟着就是下方的大标题，
   再加重一遍等于把同一行字强调两次 —— 现在它退成注脚，可点的两段才是主角。 */
.crumb {
  display: flex; align-items: center; gap: 7px;
  margin: 0 0 16px; min-width: 0;
  font-size: 12.5px;
}
.crumb a {
  flex: none;
  color: var(--text-2); text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.crumb a:hover { color: var(--accent); border-bottom-color: var(--accent-soft); }
/* 分隔箭头用 --text-3 上色：--border-2 是 6% 的黑，打印出来基本是个看不见的小点。
   --text-3 才读得出「这里有个层级」。内联 SVG 与全站图标同一套画法。 */
.crumb .crumb-sep {
  flex: none; width: 12px; height: 12px;
  fill: var(--text-3);
  transition: fill var(--t-fast);
}
.crumb a:hover + .crumb-sep { fill: var(--accent); }
.crumb-now {
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text-3);
}

/* 英文地址但该文章还没翻译：整页回落中文，正文上方先说清楚。
   做成一条压在标题上的浅色说明条，不做成弹窗或彩色告警 —— 它不是错误，
   读者要的是「我知道我在读原文」，而不是被拦下来做一次决定。 */
.notice-no-en {
  margin: 0 0 16px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--card-2);
  color: var(--text-2);
  font-size: 13px;
  line-height: 1.7;
}
.notice-no-en-title { display: block; color: var(--text); font-weight: 600; }
.notice-no-en-body { margin: 2px 0 0; }
.notice-no-en-link { color: var(--accent); border-bottom: 1px solid var(--accent-soft); }
.notice-no-en-link:hover { color: var(--accent-hover); border-bottom-color: var(--accent-hover); }

.article-card { padding: 26px 30px; }
/* 标题区、正文、底部信息块共用同一条内容列（见上面 .page-article 那三条）：
   满宽标题配窄正文，卡片右侧会空出一道对不齐的白档；三条一起收窄，
   卡片读起来就是「一条内容列 + 右侧留白」，留白才是设计而不是失误。 */
.article-head { margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--border-2); }
/* 标题：30px / 600 / 行高 1.35。
   不照搬参考站的 2.5em（40px）+ 不加粗 + 行高 1.5：那套在 14px 基准下成立，
   换到本站 16px 就是 40px，两行长标题把首屏吃掉大半，字重 400 在这个尺寸又偏单薄。
   收小一档并把字重提到 600：中文正文 15px 时，标题约两倍字号，够撑起层级又
   不抢戏；行高从 1.5 收到 1.35，是因为大字号下行距要跟着收，1.5 会让两行之间
   出现一段空洞。 */
.article-title {
  margin: 0 0 10px;
  font-size: 30px; font-weight: 600; line-height: 1.35;
  letter-spacing: -.005em;
  color: var(--card-title);
  /* 两行以上的长标题逐行摊匀，避免第一行塞满、第二行只剩两三个字。
     不支持 text-wrap: balance 的浏览器忽略这一行，标题照常折行。 */
  text-wrap: balance;
}
.article-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 9px; margin: 0; font-size: 13px; color: var(--text-3); }
/* 分隔竖线要贴着两侧文字（参考站那一档只有 5px 边距），
   直接吃 flex 的 gap 会离得太远，中间浮着一个孤零零的「|」。 */
.article-meta .meta-dot { margin: 0 -3px; }

/* 正文按卡片的可用宽度铺满。这里曾经限到 40em（约 40 个汉字一行）以求好读，
   但卡片本身只有 ~775px，限完右侧空出一大片白 —— 权衡下来，那道白档比行长更扎眼。
   字号 15px 下 715px 约合 47 个汉字，尚在中文正文的可读上限内。 */
.prose { font-size: 15px; line-height: 1.9; color: var(--text); word-wrap: break-word; }
.prose > *:first-child { margin-top: 0; }
.prose h2, .prose h3, .prose h4 { margin: 2em 0 .8em; font-weight: 600; line-height: 1.45; }
.prose h2 { font-size: 20px; padding-left: 11px; border-left: 3px solid var(--accent); }
.prose h3 { font-size: 17px; }
/* 目录跳转的落点提示：一层很淡的主色底铺开后淡掉。
   只动 background-color —— 改 padding / 字号都会让落点多跳一下。 */
.prose h2.is-jump-target,
.prose h3.is-jump-target { animation: tocLand 1.05s ease-out; border-radius: 4px; }
@keyframes tocLand {
  0%   { background-color: rgba(61, 107, 150, 0); }
  18%  { background-color: rgba(61, 107, 150, .10); }
  100% { background-color: rgba(61, 107, 150, 0); }
}
/* 标题锚点：这个 # 是「复制本节链接」的入口。
   小一号的浅灰、默认不占视觉，悬停标题或键盘聚焦时才显形——
   若按标题自身的字号字重渲染，会像标题里混进了一个乱码符号。
   touch 设备没有 hover，给一个常驻的浅色 —— 与 .code-copy 同一套处理。 */
.head-anchor {
  margin-left: 6px;
  font-size: .72em; font-weight: 400;
  color: var(--text-3);
  text-decoration: none;
  opacity: 0;
  transition: opacity var(--t-fast), color var(--t-fast);
}
.prose h2:hover .head-anchor, .prose h3:hover .head-anchor,
.prose h4:hover .head-anchor, .head-anchor:focus-visible { opacity: 1; }
.head-anchor:hover { color: var(--accent); }
@media (hover: none) { .head-anchor { opacity: .5; } }
.prose p { margin: 1.1em 0; }
.prose a { color: var(--accent); border-bottom: 1px solid var(--accent-soft); }
.prose a:hover { border-bottom-color: var(--accent); }
.prose ul, .prose ol { padding-left: 1.4em; margin: 1.1em 0; }
.prose li { margin: .45em 0; }
/* 已知限制（CLS）：Markdown 正文里的配图由 api 的 vendor/markdown.js 输出，
   只有 src/alt/loading，没有 width/height（文章编辑时上传服务没存图片尺寸）。
   图片加载前元素高度为 0，到位时把下方内容整体推下去。
   这里**不用 min-height / aspect-ratio 去猜一个高度** —— 猜错就是「图片小了一大块空白」
   比位移更难看。真正的解法是上传时记下宽高、由渲染层写进 <img>，
   属于数据管线改动（上传 → 存储 → 渲染），不是样式层能补的。
   首页/侧栏的封面图不受影响：那些模板里手写了 width/height。 */
.prose img { border-radius: var(--radius-sm); margin: 1.2em 0; }
.prose blockquote {
  margin: 1.3em 0; padding: 12px 16px;
  border-left: 3px solid var(--accent);
  background: var(--card-2);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--text-2);
}
.prose blockquote p { margin: .4em 0; }
.prose code {
  padding: 2px 5px; border-radius: 4px;
  background: var(--card-2); border: 1px solid var(--border-2);
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
  font-size: .88em; color: var(--accent);
}
.prose pre {
  position: relative; margin: 1.3em 0; padding: 15px 16px;
  background: var(--card-2); border: 1px solid var(--border-2);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.prose pre code { padding: 0; background: none; border: 0; color: var(--text); font-size: 13px; line-height: 1.7; }
/* 宽表在 360px 视口里会撑破正文列。
   滚动容器是服务端加的（见 apps/www/post_enricher.py 的 _apply_table_scroll：
   <table> 外包一层 .table-scroll），不是靠 CSS 把 table 转成 display:block 硬凑的 —— 那种写法会让
   <table> 脱离表格布局，border-collapse 与列宽计算一起失效，单元格边框开始重叠。
   这里只给那层 div 装滚动，表格本身仍是 table。 */
.table-scroll {
  margin: 1.3em 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  /* 窄屏拖动表格时给个边界提示：横向还能拖 */
  scrollbar-width: thin;
}
.table-scroll::-webkit-scrollbar { height: 6px; }
.table-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.prose table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.prose th, .prose td { padding: 8px 12px; border: 1px solid var(--border); text-align: left; }
.prose th { background: var(--card-2); font-weight: 600; }
.prose hr { border: 0; border-top: 1px solid var(--border-2); margin: 2em 0; }

.code-copy {
  position: absolute; top: 8px; right: 8px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--card); color: var(--text-3);
  font-size: 11.5px; line-height: 1.5;
  cursor: pointer; opacity: 0;
  transition: opacity var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.prose pre:hover .code-copy, .code-copy:focus-visible { opacity: 1; }
.code-copy:hover { color: var(--accent); border-color: var(--accent); }
.code-copy.is-done { color: var(--accent); border-color: var(--accent); }
@media (hover: none) { .code-copy { opacity: 1; } }

.copyright {
  margin-top: 26px; padding: 14px 16px;
  background: var(--card-2);
  border-radius: var(--radius-sm);
  font-size: 13px; color: var(--text-2);
}
.copyright .row { display: flex; gap: 10px; margin: 5px 0; }
.copyright .label { flex: none; color: var(--text-3); }
.copyright a { color: var(--accent); word-break: break-all; }
.copyright .note { margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--border-2); font-size: 12.5px; color: var(--text-3); }

.articlenav { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 16px; }
.articlenav-item {
  display: flex; flex-direction: column; gap: 5px;
  padding: 14px 16px;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: transform var(--t), box-shadow var(--t);
}
.articlenav-item:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.articlenav-item--next { text-align: right; }
.articlenav-label { font-size: 12px; color: var(--text-3); }
.articlenav-title { font-size: 14px; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-wrap: balance; }
.articlenav-item:hover .articlenav-title { color: var(--accent); }

/* 目录：长文会给出几十条，卡片本身限高并自己滚。
   顶部留 4px 是给 :hover 的浅色底 —— 它比文字高出一点点，
   贴着上边缘时会被切掉半条，看着像渲染缺了一块。 */
.toc { display: flex; flex-direction: column; gap: 1px; max-height: 60vh; overflow-y: auto; overscroll-behavior: contain; padding-top: 2px; scrollbar-width: thin; }
.toc a {
  padding: 6px 9px; border-radius: 6px;
  font-size: 13px; color: var(--text-2); line-height: 1.6;
  border-left: 2px solid transparent;
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
.toc a:hover { color: var(--accent); background: var(--accent-soft); }
.toc a.lv-3 { padding-left: 20px; font-size: 12.5px; color: var(--text-3); }
.toc a.is-current { color: var(--accent); border-left-color: var(--accent); background: var(--accent-soft); font-weight: 600; }
/* 目录卡本身在 toc 为空时整块不输出（见 sidebar.ejs），没有空态样式要配 */

/* ---------- 13. 归档页 / 关于页 / 404 ---------- */
/* 归档页（/archive.html，页面上叫「全部文章」）：一张白卡，自上而下四层 ——

     页头（archive-hero）   大标题 + 一句说明 + 三格概览，格与格之间是竖发丝线
     年份条（archive-years）跨年份跳转，只有一个年份时不输出
     年份（archive-year）   大字号数字 + 一条延伸到右缘的细线，像书的章节页眉
     月份（archive-month） 左栏月份标题 + 右栏条目，整块由发丝线分隔

   为什么不用「竖线 + 空心圆点」的轨道：轨道要先包住整页，年份既要当大标题撑版式、
   又要压在竖线上，圆点总差半格；竖线还会和条目自己的缩略图、发丝线抢同一层线索，
   一页里三条竖线并排，眼睛不知道该跟哪条。
   两栏布局（左月份、右条目）让层级只剩「年 → 月 → 条」三层，
   与目录的读法一致；竖向的分隔从「一根线」降级成「一条发丝线」，不再抢戏。

   整段规则挂在 body.page-archive 下（archive.ejs 传 bodyClass），改归档不会外溢。 */
.page-archive .archive-page {
  background: var(--card);
  border-radius: var(--radius-sm);
  box-shadow: var(--card-shadow);
  /* 这里不能加 overflow: hidden —— 它会让 .archive-page 变成一个（虽然不可滚的）
     滚动容器，月份标题的 position: sticky 就改为相对它定位，而它永远不滚，
     吸附等于作废。页头那层渐变因此自己按圆角裁切（背景默认按 border-box 裁），
     不依赖父级裁切。 */
}

/* ---------- 页头 ---------- */
.page-archive .archive-hero {
  display: flex; align-items: flex-end; justify-content: space-between;
  flex-wrap: wrap; gap: 18px 30px;
  padding: 34px 32px 26px;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  border-bottom: 1px solid var(--border-2);
  /* 极浅的自上而下渐变：让头部从正文区里「浮」出来一点。
     刻意只用 card-2 → 白 两站，再深就成了渐变装饰，与站内
     「层次靠留白和发丝线」的基调打架。 */
  background: linear-gradient(180deg, var(--card-2), #fff);
}
.page-archive .archive-hero-main { min-width: 0; }
.page-archive .archive-hero-title {
  margin: 0 0 7px;
  font-size: 32px; font-weight: 700; line-height: 1.2;
  letter-spacing: .01em;
  color: var(--card-title);
  text-wrap: balance;
}
.page-archive .archive-hero-sub { margin: 0; font-size: 13px; color: var(--text-3); }

/* 三格概览：竖发丝线分隔而不是 26px 空隙 —— 有分隔线时三格读作一组量出来的数，
   纯空隙时它们是三个各自漂浮的孤岛 */
.page-archive .archive-hero-stats { display: flex; margin: 0; }
.page-archive .archive-stat {
  display: flex; flex-direction: column; gap: 3px;
  padding: 0 20px;
  border-left: 1px solid var(--border-2);
}
.page-archive .archive-stat:first-child { padding-left: 0; border-left: 0; }
.page-archive .archive-stat dt { font-size: 11.5px; color: var(--text-3); }
/* 等宽数字：概览与下面的月份篇数共用一套数字，位宽必须一致 */
.page-archive .archive-stat dd {
  margin: 0;
  font-size: 26px; font-weight: 700; line-height: 1.05;
  color: var(--card-title);
  font-variant-numeric: tabular-nums;
}

/* ---------- 年份跳转条 ---------- */
.page-archive .archive-years {
  display: flex; gap: 8px; flex-wrap: wrap;
  padding: 13px 32px;
  border-bottom: 1px solid var(--border-2);
}
.page-archive .archive-year-chip {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 4px 13px;
  border: 1px solid var(--border);
  border-radius: 20px;
  font-size: 12.5px; line-height: 1.6;
  color: var(--text-2);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.page-archive .archive-year-chip-num { font-weight: 600; color: var(--card-title); }
.page-archive .archive-year-chip-count { font-size: 11px; color: var(--text-3); }
.page-archive .archive-year-chip:hover,
.page-archive .archive-year-chip:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* ---------- 年份章节 ---------- */
.page-archive .archive-body { padding: 0 32px 34px; }
/* 年份锚点（#year-2026）的落点由 html 的 scroll-padding-top 决定。
   这里不要再写 scroll-margin-top：两者是相加的，76 + 84 会让年份标题
   落在 160px 处，点年份胶囊跳过去会看见上一年的尾巴。 */
.page-archive .archive-year { padding-top: 30px; }
.page-archive .archive-year:first-child { padding-top: 20px; }

.page-archive .archive-year-title {
  display: flex; align-items: center; gap: 14px;
  margin: 0 0 14px;
}
/* 年份比页面大标题小一档：它是章节号，不是标题。
   同为 28-30px/700 时，「2026」和「全部文章」几乎一样重，
   一眼看过去分不出谁是页头谁是章节。 */
.page-archive .archive-year-num {
  font-size: 24px; font-weight: 600; line-height: 1.1;
  letter-spacing: 0;
  color: var(--card-title);
  font-variant-numeric: tabular-nums;
}
/* 中间那条延伸到右缘的细线：把年份标题和右侧篇数在视觉上连成一条，
   不用再加一个色块 —— 章节感靠留白与这条线就够了 */
.page-archive .archive-year-rule {
  flex: 1; height: 1px; background: var(--border-2);
}
.page-archive .archive-year-count {
  font-size: 12px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* ---------- 月份：左栏标题 + 右栏条目 ---------- */
.page-archive .archive-months { border-top: 1px solid var(--border-2); }
.page-archive .archive-month + .archive-month { border-top: 1px solid var(--border-2); }

.page-archive .archive-month {
  position: relative;
  display: grid;
  grid-template-columns: 108px minmax(0, 1fr);
  column-gap: 20px;
  padding: 16px 0 16px 16px;   /* 左内边距让出竖向强调条的位置 */
}
.page-archive .archive-month-title {
  /* align-self: start + sticky：格子被压回内容高度，但仍能在整行高度里吸附，
     一个月翻不完时左栏的月份不会跟着滚走。
     top 与 html 的 scroll-padding-top 取同一个值（nav + 16），
     侧栏速览按 #2026-10 跳进来时标题正好停在这条线上，不需要再叠 scroll-margin。 */
  position: sticky; top: calc(var(--nav-h) + 16px); align-self: start;
  display: flex; flex-direction: column; gap: 4px;
  margin: 0;
}
/* 当前月份的高亮：标题左侧一小段强调条 + 文字变主题色。
   强调条画在左栏自己的位置里，无需偏移量对齐任何基准（时间轴无竖线） */
.page-archive .archive-month-title::before {
  content: '';
  position: absolute;
  left: -16px; top: 2px;
  width: 2px; height: 19px;
  border-radius: 2px;
  background: transparent;
  transition: background var(--t-fast);
}
.page-archive .archive-month-label {
  font-size: 13px; font-weight: 600; line-height: 1.35;
  color: var(--text-2);
  transition: color var(--t-fast);
}
.page-archive .archive-month-count {
  font-size: 11.5px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
/* 滚动到当前月份时由 main.js 打标（见 data-arch-month） */
.page-archive .archive-month.is-current .archive-month-label { color: var(--accent); }
.page-archive .archive-month.is-current .archive-month-title::before { background: var(--accent); }

/* ---------- 条目 ---------- */
.page-archive .archive-items { list-style: none; margin: 0; padding: 0; }
.page-archive .archive-item + .archive-item { margin-top: 4px; }

.page-archive .archive-item-link {
  position: relative;
  display: flex; align-items: flex-start; gap: 14px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: background var(--t-fast), transform var(--t-fast);
}
/* 悬停：整行底色 + 标题变主题色 + 右端箭头滑入。
   位移只给 3px：归档页一屏十几行，位移一大整列都在动，读起来反而累。
   不加阴影 —— 卡片本身已经浮在纸上，再叠一层就变成两份浮起来了。 */
.page-archive .archive-item-link:hover,
.page-archive .archive-item-link:focus-visible {
  background: var(--card-2);
  transform: translateX(3px);
}

.page-archive .archive-item-thumb {
  flex: none;
  overflow: hidden;
  width: 100px; height: 68px;
  border-radius: 6px;
  background: var(--cover-ph);
}
.page-archive .archive-item-thumb img {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: var(--thumb-transition);
}
.page-archive .archive-item-link:hover .archive-item-thumb img { transform: scale(var(--thumb-zoom)); }
/* 封面没传到图床时（main.js 给 img 打的 is-broken）：只留下占位底色。
   不加这条，浏览器会在灰底上再画一个破图图标，看着像加载失败而不是「本来就没图」。 */
.page-archive .archive-item-thumb img.is-broken { opacity: 0; }

/* 标题在前、元信息在后：先说「是什么」，再说「什么时候、多少人看」。
   日期放标题上方时，扫一眼列表先撞到的是一列重复的年月日。 */
.page-archive .archive-item-body { flex: 1; min-width: 0; padding-top: 1px; }
.page-archive .archive-item-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  font-size: 15px; font-weight: 600; line-height: 1.5;
  color: var(--text);
  transition: color var(--t-fast);
}
.page-archive .archive-item-link:hover .archive-item-title { color: var(--accent); }
.page-archive .archive-item-meta {
  display: flex; align-items: center; gap: 6px;
  margin-top: 5px;
  font-size: 11.5px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.page-archive .archive-item-dot { font-style: normal; opacity: .5; }
/* 摘要：横向有富余时多给一句，窄屏由 720px 断点关掉（见第 12 节末尾） */
.page-archive .archive-item-sum {
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
  margin-top: 4px;
  font-size: 12.5px; line-height: 1.5;
  color: var(--text-3);
}

/* 右端箭头：默认藏在行外，悬停时滑进来 —— 与全站其他卡片的箭头读法一致 */
.page-archive .archive-item-arrow {
  flex: none;
  width: 15px; height: 15px;
  margin-top: 3px;
  fill: var(--accent);
  opacity: 0;
  transform: translateX(-6px);
  transition: opacity var(--t-fast), transform var(--t-fast);
}
.page-archive .archive-item-link:hover .archive-item-arrow,
.page-archive .archive-item-link:focus-visible .archive-item-arrow { opacity: 1; transform: none; }

/* ---------- 空态 ---------- */
.page-archive .archive-empty { padding: 46px 32px 50px; text-align: center; font-size: 14px; color: var(--text-3); }
.page-archive .archive-empty a { margin-left: 6px; color: var(--accent); }


.about-card { padding: 26px 30px; }
.sec-title {
  display: flex; align-items: center; gap: 8px;
  margin: 28px 0 12px; padding-top: 18px;
  border-top: 1px solid var(--border-2);
  font-size: 16px; font-weight: 600;
}
.sec-title::before { content: ""; width: 3px; height: 14px; border-radius: 2px; background: var(--accent); }

.def-list { margin: 0; display: flex; flex-direction: column; gap: 1px; }
.def-row { display: grid; grid-template-columns: 124px 1fr; gap: 14px; padding: 9px 10px; border-radius: 7px; transition: background var(--t-fast); }
.def-row:hover { background: var(--card-2); }
.def-row dt { font-size: 13.5px; font-weight: 600; color: var(--text-2); margin: 0; }
.def-row dd { margin: 0; font-size: 13.5px; color: var(--text-2); }
.def-row a { color: var(--accent); }

.error-page { padding: 64px 24px; text-align: center; }
.error-code { margin: 0 0 6px; font-size: 88px; font-weight: 700; line-height: 1; color: var(--accent); opacity: .28; }
.error-title { margin: 0 0 10px; font-size: 19px; font-weight: 600; }
.error-desc { margin: 0 auto 22px; max-width: 44ch; font-size: 14px; color: var(--text-3); }
.error-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }

/* ---------- 14. 页脚 ---------- */
/* 页脚不铺自己的底色，只压一层半透明黑：背景照片从底下透上来，
   文字是浅色。参考站就是这么处理的——footer 只盖一层色，照片照旧在后面。 */
.footer { margin-top: 10px; background: rgba(0, 0, 0, .32); }
/* 内边距对齐参考站的 40px 20px；分隔符收到 4px，参考站那样点断句更利落 */
.footer-inner { max-width: var(--wrap); margin: 0 auto; padding: 40px 20px; text-align: center; }
.footer-nav { display: flex; justify-content: center; gap: 18px; margin-bottom: 10px; flex-wrap: wrap; }
.footer-nav a { font-size: 13.5px; color: var(--foot-text); transition: color var(--t-fast); }
.footer-nav a:hover { color: #fff; }
.footer-meta, .footer-copy, .footer-beian { margin: 0 0 4px; font-size: 12.5px; color: var(--foot-text); line-height: 1.9; }
.footer-meta b, .footer-copy b { color: #fff; font-weight: 600; font-variant-numeric: tabular-nums; }
.footer-copy a, .footer-beian a { color: var(--foot-text); }
.footer-copy a:hover, .footer-beian a:hover { color: #fff; }
.footer-meta .sep, .footer-copy .sep, .footer-beian .sep { opacity: .4; margin: 0 4px; }
.fs-run b { margin: 0 1px; }
/* 公安备案图标：20px 见方，基线对齐到文字下方，和参考站一致。
   必须显式 inline-block——全局 img 是 display:block，
   不改的话图标会自己占一行，链接文字被甩到下一行去。 */
.beian-icon {
  display: inline-block;
  width: 20px; height: 20px; margin-right: 4px;
  vertical-align: -4px;
  border-radius: 2px;
}

/* ---------- 15. 响应式 ---------- */
/* 文章卡片的封面宽度是百分比，窄屏不需要单独覆盖：
   容器变窄时图文比例自动跟着走，封面不会挤掉文字。 */

/* 卡片栏放不下宽度了：掉到正文下方 */
@media (max-width: 1000px) {
  .content { grid-template-columns: minmax(0, 1fr); }
  .aside { position: static; }
}

/* 窄屏：菜单从头部底下就地展开，不往左弹抽屉 */
@media (max-width: 860px) {
  .topbar-inner { position: relative; }
  #burger { display: grid; }
  /* .nav-tools 的 display 本来就是 flex（宽屏常驻语言切换），这里只需把汉堡放回来，
     不必再写一遍 display:flex —— 那条规则从「藏整栏」时代留到现在，一直是空转。 */
  .menus {
    position: absolute;
    top: var(--nav-h); left: 0; right: 0;
    margin-left: 0; padding: 6px 8px;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--card);
    border-bottom: 1px solid var(--border-2);
    box-shadow: var(--shadow-md);
    display: none;
    /* 展开时给一小段下坠：顶栏本体是半透明白，菜单是实白，
       两者交界处有一道「纸叠纸」的硬边，下坠 6px 让它读作「从顶栏里放下来」。 */
    animation: menu-drop var(--t-fast) ease-out;
  }
  .menus.is-open { display: flex; }
  .menus a { padding: 11px 12px; border-radius: var(--radius-sm); }
  /* 胶囊在竖向面板里也跟着拉满，居中即可，别再顶着左边 */
  .menus a.menu-hub { margin: 6px 0 0; justify-content: center; }
}

/* 下拉展开：只动 transform/opacity，不碰 height —— 动 height 会让整个
   顶栏下面的内容每帧重排，窄屏上正是最该守住帧率的地方。 */
@keyframes menu-drop {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .menus { animation: none !important; }
}

/* 手机：封面与文字上下堆叠，取消左右交替 */
@media (max-width: 720px) {
  body { font-size: 14.5px; }
  :root { --nav-h: 54px; }

  /* 标题跟着降一档：窄屏正文也降到 14.5px，30px 的标题在手机上两行就顶满半屏 */
  .article-title { font-size: 25px; }

  .topbar-inner { padding: 0 12px; gap: 10px; }
  .site-name { font-size: 17px; }
  .nav-tools { display: flex; margin-left: auto; padding-left: 0; border-left: 0; }

  .content { padding: 8px 12px 30px; }

  /* 手机端上下堆叠：高度放开，封面按 16:9 自己走。
     .card-main 的 order 基线已经是 1（偶数条），这里只需把封面设成 0，
     排在文字之前 —— 主列的 order 不必重写。 */
  .card-item { flex-direction: column; align-items: stretch; height: auto; }
  .card-item:nth-child(even) .card-cover { order: 0; }
  .card-cover { flex: none; width: 100%; height: auto; aspect-ratio: 16 / 9; }
  .card-main { padding: 20px 20px 30px; }
  .card-title { font-size: 1.3em; }

  .article-card, .about-card { padding: 20px 17px; }
  .article-title { font-size: 21px; }   /* ≤680px 档：一行能读完，不折行 */
  .prose { font-size: 14.5px; line-height: 1.85; }
  .prose h2 { font-size: 18.5px; }
  .prose h3 { font-size: 16.5px; }

  .articlenav { grid-template-columns: minmax(0, 1fr); }
  .articlenav-item--next { text-align: left; }

  .def-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  /* 归档页在窄屏上收窄：页头三格概览的竖线间距收一档、年份与月份降一档、
     两栏（月份标题 / 条目）退回单列、缩略图转小方图、条目行取消横向位移。
     摘要也在这一档关掉 —— 横向本来就不够，再塞一行只会把目录撑成卡片流。
     页面根类名跟着一起带上，覆盖规则不会漏到别的页面。 */
  .page-archive .archive-hero { padding: 24px 20px 20px; }
  .page-archive .archive-hero-title { font-size: 24px; }
  .page-archive .archive-stat { padding: 0 14px; }
  .page-archive .archive-stat dd { font-size: 21px; }
  .page-archive .archive-years { padding: 11px 20px; }
  .page-archive .archive-body { padding: 0 20px 26px; }
  .page-archive .archive-year { padding-top: 24px; }
  .page-archive .archive-year-num { font-size: 21px; }
  /* 两栏退回单列：月份标题变回一行小标题，贴顶吸附也随之取消
     （单列下它与条目同处一个流，吸附只会挡住自己的内容）。
     这里必须是 position: relative 而不是 static —— 标题上的 ::before 强调条是
     绝对定位的，参照的是标题自己；一旦退回 static，它的定位基准就掉到
     .archive-month 上，条会跑到块角，与月份文字完全错位。
     top 必须同时归零：基础规则的 top（nav + 16，给 sticky 用）留在 relative
     上会变成真实位移，把标题整体压到条目上。 */
  .page-archive .archive-month {
    grid-template-columns: minmax(0, 1fr); gap: 10px;
    padding: 14px 0 14px 12px;
  }
  .page-archive .archive-month-title { position: relative; top: auto; flex-direction: row; align-items: baseline; gap: 8px; }
  .page-archive .archive-month-title::before { left: -12px; top: 3px; height: 15px; }
  .page-archive .archive-item-link { gap: 11px; padding: 9px 8px; transform: none; }
  .page-archive .archive-item-thumb { width: 72px; height: 72px; }
  .page-archive .archive-item-title { font-size: 14px; }
  .page-archive .archive-item-sum { display: none; }

  .error-code { font-size: 66px; }

  /* 手机上按钮已经常驻，无需再按悬停处理；只把它挪开一点免得压住页脚内容 */
  .right-rail { right: 14px; bottom: 18px; }
  .rail-top { width: 42px; height: 42px; }
  .rail-arrow { width: 17px; height: 17px; }
  /* 页脚多了公安备案这一行，底部要让出按钮的高度，否则窄屏会被圆钮压住半截 */
  .footer-inner { padding-bottom: 76px; }
}

@media (max-width: 380px) {
  .content { padding: 8px 10px 26px; }
  /* .site-data 的 padding-top 撑的是发丝线上方那段留白，窄屏收一档即可。
     不要写成 8px 2px 那种带横向内边距的值 —— 那会横向多撑出一截空白。 */
  .site-data { padding-top: 10px; }
  .site-data b { font-size: 16px; }
}

/* 打印：去掉所有装饰，只留正文。
   背景图与卡片阴影在纸上一律不出现 —— 浏览器默认不打印背景，
   强制 -webkit-print-color-adjust 会把整张背景图印满每一页。 */
@media print {
  .topbar, .right-rail, .bg-layer, .aside, .scroll-progress, .reading-bar, .footer { display: none !important; }
  .article-card, .about-card { box-shadow: none; padding: 0; }
  .content { display: block; max-width: none; padding: 0; }
  .prose { font-size: 12pt; line-height: 1.7; }
  /* 代码块与表格在纸上是横向滚不动的，直接截断比印出空白更诚实，
     同时保留边框让读者知道这里原本有一块内容 */
  .prose pre, .table-scroll { overflow: visible; }
  .prose pre, .prose table { break-inside: avoid; }
  .prose h2, .prose h3, .prose h4 { break-after: avoid; }
  .code-copy, .head-anchor { display: none !important; }
  /* 版权块与相邻文章是纸面上最有用的两块，别被分页切散 */
  .copyright, .articlenav { break-inside: avoid; }
  body { background: #fff; color: #000; }
}

/* ===========================================================================
   16. 特效层

   全部挂在既有类名上，不新增任何模板节点。分三类：
     · 氛围：极光光斑、细网格、渐变发丝线（常驻，不依赖交互）
     · 交互：流光边、光晕跟随、封面扫光、封面倾斜、水波纹（hover / click）
     · 反馈：进度条光点、目录当前项、复制成功脉冲（滚动 / 状态）

   三条硬约束，改这一层前先读：
     1. 只动 transform / opacity / filter。会触发重排或重绘大面积的属性
        （width、left、background-position、box-shadow 的持续动画）一律不进这一层。
     2. 不承担「内容可见性」。所有效果只做位移与描边，起手帧绝不写 opacity: 0 ——
        动画没跑完、被中断、截图冻结在首帧时，正文必须仍然完整可见。
     3. prefers-reduced-motion 下整层落零，印刷样式里整层隐藏（两条都在本节末尾）。

   配色沿用站点唯一的钢蓝强调色，只补一档更浅的雾蓝与一档暖砂做冷暖对位，
   不引入第二套高饱和色 —— 正文是主角，特效只是纸面下的光。
   =========================================================================== */

/* ---------- 16.1 特效层的补充令牌 ---------- */
/* 三档光斑强度。背景照片本身明暗差很大（每张图都不一样），
   这几个值在浅色图上刚好可见、在深色图上不会糊成一片；
   再往上调就会盖过正文，往下调在这一屏上等于不存在。
   通用令牌仍集中在第 1 节，这里只放特效层独有的几项。 */
:root {
  --fx-blue: rgba(61, 107, 150, .42);
  --fx-mist: rgba(86, 140, 172, .32);
  --fx-sand: rgba(196, 145, 88, .26);
  --fx-line: linear-gradient(90deg, transparent, var(--accent) 45%, rgba(111, 159, 191, .85) 72%, transparent);
  --fx-ease: cubic-bezier(.22, 1, .36, 1);
}

/* 旋转角：conic 流光边与头像光环共用一个自定义属性 */
@property --fx-ang {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@keyframes fx-spin { to { --fx-ang: 360deg; } }

/* ---------- 16.2 背景氛围 ---------- */
/* 极光：四团极软的光斑落在照片之上、内容之下。
   用 radial-gradient 的软停靠代替 filter: blur() —— 满屏元素套模糊滤镜
   在集显上是实打实的一笔开销，而渐变本身就够软。
   整层只做 transform 漂移，不碰 opacity，避免和照片合成时反复建层。 */
.bg-layer::before {
  content: "";
  position: absolute;
  inset: -14%;
  z-index: 2;
  background:
    radial-gradient(34% 32% at 14% 16%, var(--fx-blue), transparent 56%),
    radial-gradient(28% 26% at 86% 10%, var(--fx-mist), transparent 54%),
    radial-gradient(40% 34% at 74% 88%, var(--fx-sand), transparent 58%),
    radial-gradient(26% 24% at 26% 92%, var(--fx-mist), transparent 56%),
    radial-gradient(22% 20% at 50% 42%, rgba(255, 255, 255, .30), transparent 60%);
  animation: fx-aurora 36s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes fx-aurora {
  from { transform: translate3d(-2%, -1%, 0) scale(1); }
  50%  { transform: translate3d(2.5%, 2%, 0) scale(1.07); }
  to   { transform: translate3d(-1%, 3%, 0) scale(1.03); }
}

/* 细网格 + 颗粒：给照片一层「纸」的质感。
   两者都是静态贴图，不参与动画 —— 满屏重绘的代价远大于这点装饰收益。
   网格用径向遮罩向下淡出，只在页头附近成形，不抢正文。 */
.bg-layer::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  background-image:
    linear-gradient(rgba(27, 32, 43, .12) 1px, transparent 1px),
    linear-gradient(90deg, rgba(27, 32, 43, .12) 1px, transparent 1px),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E");
  background-size: 68px 68px, 68px 68px, 140px 140px;
  opacity: .72;
  -webkit-mask-image: radial-gradient(150% 96% at 50% -16%, #000 0%, rgba(0, 0, 0, .45) 46%, transparent 78%);
  mask-image: radial-gradient(150% 96% at 50% -16%, #000 0%, rgba(0, 0, 0, .45) 46%, transparent 78%);
}
/* 照片调色：中心留白、四角压暗。白纱不能压太厚 ——
   压到七成以上照片就没了，极光也一起被冲淡成一片灰白。 */
.bg-scrim {
  background:
    radial-gradient(120% 92% at 50% -6%, rgba(255, 255, 255, .30), rgba(255, 255, 255, .50) 58%, rgba(238, 242, 248, .60) 100%),
    radial-gradient(136% 106% at 50% 54%, transparent 46%, rgba(20, 30, 46, .26) 100%);
}

/* ---------- 16.3 顶部滚动进度条：流光 + 末端光点 ---------- */
.scroll-progress__bar {
  background: linear-gradient(90deg, var(--accent-hover), var(--accent) 55%, #78a8c6);
  box-shadow: 0 0 10px rgba(61, 107, 150, .5);
  position: relative;
}
/* 光点由 JS 在进度 > 0 时挂 .is-live 才出现：
   宽度为 0 时它会在左上角留下一个孤立的白点，看着像渲染错误。 */
.scroll-progress__bar::after {
  content: "";
  position: absolute;
  right: -1px; top: 50%;
  width: 9px; height: 9px; margin-top: -4.5px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 12px 2px rgba(61, 107, 150, .75);
  opacity: 0;
  transition: opacity .25s;
  pointer-events: none;
}
.scroll-progress__bar.is-live::after { opacity: .95; }

/* ---------- 16.4 顶栏 ---------- */
/* 毛玻璃只在宽屏成立：窄屏顶栏与下拉菜单叠在一起，
   多一层 backdrop-filter 会让弹出的菜单跟着重算背景。 */
@media (min-width: 861px) {
  .topbar {
    background: rgba(255, 255, 255, .72);
    -webkit-backdrop-filter: blur(14px) saturate(1.35);
    backdrop-filter: blur(14px) saturate(1.35);
  }
  .topbar.is-solid { background: rgba(255, 255, 255, .88); }
}

/* 站名：悬停时底色从左侧扫过。用伪元素铺底而不是给文字上渐变 ——
   渐变文字一旦 background-clip 不被支持，字就整块消失。 */
.site-name { position: relative; isolation: isolate; }
.site-name::after {
  content: "";
  position: absolute;
  inset: -3px -8px;
  z-index: -1;
  border-radius: 7px;
  background: linear-gradient(90deg, var(--accent-soft), rgba(111, 159, 191, .20) 60%, transparent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .45s var(--fx-ease);
}
.site-name:hover::after { transform: scaleX(1); }

/* 顶栏下沿的渐变发丝线：只在顶栏压上底色（滚过 10px）后浮现，
   把导航和下面的照片分开，又不至于一进页面就横一道线。 */
.topbar::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(61, 107, 150, .45) 28%, rgba(196, 150, 96, .35) 64%, transparent);
  opacity: 0;
  transition: opacity var(--t);
  pointer-events: none;
}
.topbar.is-solid::after { opacity: 1; }

/* 导航项：底部一道渐变下划线，从左展开 */
.menus a { position: relative; }
.menus a::after {
  content: "";
  position: absolute;
  left: 11px; right: 11px; bottom: 3px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent), rgba(111, 159, 191, .9));
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .38s var(--fx-ease);
  pointer-events: none;
}
.menus a:hover::after,
.menus a.is-active::after { transform: scaleX(1); }

/* ---------- 16.5 文章卡片 ---------- */
/* 卡片本身加两层：外圈流光描边（::before）+ 跟随指针的光晕（::after）。
   两者都 inset 在卡片内部，卡片高度写死 235px，加上去不改变任何尺寸。 */
.card-item { --fx-mx: 62%; --fx-my: 34%; }
/* 悬停时整张卡片抬起 4px 并带一层主色外发光。
   位移只作用在卡片自己身上、幅度小，长列表里一眼扫过去是「被光标挑起」的那一张。 */
@media (hover: hover) and (pointer: fine) {
  .card-item { transition: box-shadow var(--t), transform .38s var(--fx-ease); }
  .card-item:hover {
    transform: translateY(-4px);
    box-shadow: var(--card-shadow-hover), 0 18px 42px -24px rgba(61, 107, 150, .55);
  }
}
.card-item::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(from var(--fx-ang),
    rgba(61, 107, 150, .9), rgba(111, 159, 191, .12) 32%,
    rgba(196, 150, 96, .75) 58%, rgba(61, 107, 150, .9));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: 0;
  transition: opacity .45s var(--fx-ease);
  pointer-events: none;
}
/* 旋转只在悬停时启动。常驻动画在 opacity:0 下依然会让浏览器每帧重算一次
   conic 渐变，一屏十张卡就是十条空转的动画时间线 —— 看不见，但一直在算。 */
.card-item:hover::before {
  opacity: 1;
  animation: fx-spin 6.5s linear infinite;
}

/* 光晕压在正文下面：z-index 高于卡片自身的白底、低于两侧内容。
   .card-cover / .card-main 提到 1 层，光晕才不会在文字上盖一层蓝纱。 */
.card-item::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  background: radial-gradient(340px circle at var(--fx-mx) var(--fx-my), rgba(61, 107, 150, .11), transparent 62%);
  opacity: 0;
  transition: opacity .4s;
  pointer-events: none;
}
.card-item:hover::after { opacity: 1; }
.card-cover, .card-main { z-index: 1; }

/* 文字侧左缘的强调条：奇数条落在封面右沿，偶数条落在卡片左沿，两种都成立。
   用 scaleY 而不是 height —— 高度是布局属性，每帧都要重排一次。 */
.card-main { position: relative; }
.card-main::before {
  content: "";
  position: absolute;
  left: 0; top: 50%;
  width: 3px; height: 58px;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--accent), rgba(111, 159, 191, .9));
  transform: translateY(-50%) scaleY(0);
  transition: transform .45s var(--fx-ease);
  pointer-events: none;
}
.card-item:hover .card-main::before { transform: translateY(-50%) scaleY(1); }

/* 标题下划线：悬停时从左铺到右。铺在最后一行底部，不占行高。 */
.card-title a {
  background-image: linear-gradient(90deg, var(--accent), rgba(111, 159, 191, .85));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 2px;
  transition: color .2s ease-in-out, background-size .45s var(--fx-ease);
}
.card-title a:hover { background-size: 100% 2px; }

/* 封面扫光：一道斜向高光从左侧扫出画面 */
.card-cover::after {
  content: "";
  position: absolute;
  top: -60%; left: -90%;
  width: 55%; height: 220%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .58), transparent);
  transform: skewX(-20deg);
  transition: left .8s var(--fx-ease);
  pointer-events: none;
  z-index: 2;
}
.card-item:hover .card-cover::after { left: 138%; }

/* 首屏第一张封面：进页面后自动扫一次光。
   单独占 ::before（hover 那道在 ::after 上），两者互不干扰；
   动画结束停在画面外，之后照常由 hover 接管。 */
.card-item:first-child .card-cover::before {
  content: "";
  position: absolute;
  top: -60%; left: -90%;
  width: 55%; height: 220%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .62), transparent);
  transform: skewX(-20deg);
  pointer-events: none;
  z-index: 2;
  animation: fx-sheen-once 1.5s var(--fx-ease) .55s 1 both;
}
@keyframes fx-sheen-once {
  from { left: -90%; }
  to   { left: 138%; }
}

/* 封面倾斜：位移量由 JS 按指针位置写进 --fx-tx / --fx-ty。
   只在真有指针的设备上开 —— 触屏没有 hover，倾斜只会变成一次突兀的位移。 */
@media (hover: hover) and (pointer: fine) {
  .card-cover img {
    transition: filter 375ms ease-in .2s, transform .22s ease-out, opacity var(--t);
    will-change: transform;
  }
  .card-item:hover .card-cover img {
    transform: scale(var(--thumb-zoom)) translate3d(var(--fx-tx, 0px), var(--fx-ty, 0px), 0);
  }
}

/* ---------- 16.6 按钮与分页：水波纹 ---------- */
.btn, .pager a, .pager span.is-current { position: relative; overflow: hidden; }
/* 波纹节点由 JS 生成后自行移除；这里只描述它的形态与动画 */
.fx-ripple {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(61, 107, 150, .28), transparent 62%);
  transform: scale(0);
  animation: fx-ripple .65s ease-out forwards;
  pointer-events: none;
  z-index: 2;
}
@keyframes fx-ripple {
  to { transform: scale(3.4); opacity: 0; }
}
/* 当前页那颗是实心蓝底，白波纹才看得见 */
.pager .is-current .fx-ripple { background: radial-gradient(circle, rgba(255, 255, 255, .7), transparent 62%); }

/* 分页按钮悬停时抬起 1px，与卡片的浮起语言一致 */
.pager a { transition: color var(--t-fast), box-shadow var(--t), transform .3s var(--fx-ease); }
.pager a:hover { transform: translateY(-1px); }

/* ---------- 16.7 右侧卡片栏 ---------- */
/* 基座（背景 / 圆角 / 阴影 / transition）与标题栏的定位都写在 11 节，
   这里只补悬停与装饰，别再重复声明一遍。 */
.widget:hover { box-shadow: var(--card-shadow-hover); transform: translateY(-2px); }
/* 标题下的渐变发丝线：静态，不做动画 */
.widget-head::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--fx-line);
  opacity: .75;
}

/* 头像外圈：一圈缓慢自转的流光，裁在圆内（圆环有 overflow: hidden） */
.avatar-ring { position: relative; }
.avatar-ring::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  padding: 2px;
  background: conic-gradient(from var(--fx-ang),
    rgba(61, 107, 150, .85), rgba(111, 159, 191, .08) 34%,
    rgba(196, 150, 96, .7) 62%, rgba(61, 107, 150, .85));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: fx-spin 9s linear infinite;
  pointer-events: none;
}

/* 最新文章：悬停时左缘浮起一道细条（scaleY，不动 height） */
.mini-item { position: relative; }
.mini-item::before {
  content: "";
  position: absolute;
  left: -16px; top: 50%;
  width: 2px; height: 26px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--accent), rgba(111, 159, 191, .9));
  transform: translateY(-50%) scaleY(0);
  transition: transform .3s var(--fx-ease);
  pointer-events: none;
}
.mini-item:hover::before { transform: translateY(-50%) scaleY(1); }

/* 归档计数条：悬停时底色自左铺满（scaleX，不动 width） */
.archive-list a { position: relative; overflow: hidden; }
.archive-list a::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 100%;
  background: linear-gradient(90deg, var(--accent-soft), transparent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .35s var(--fx-ease);
  pointer-events: none;
}
.archive-list a:hover::before { transform: scaleX(1); }

/* 统计数字：渐变文字。包在 @supports 里 ——
   不支持 background-clip: text 的浏览器会把 color: transparent 变成「数字消失」，
   那比没有渐变糟糕得多，所以只在确认支持时才上色。
   渐变两端都取深色：这些数字只有 17px 粗体，属于 WCAG 的「普通文本」，
   门槛 4.5:1，浅蓝一端压不住。 */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .site-data b {
    background-image: linear-gradient(120deg, var(--accent) 0%, #33587a 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

/* ---------- 16.8 文章详情页 ---------- */
/* 卡片顶沿一道渐变线：与侧栏卡片的发丝线同族，把阅读区从背景里「托」起来 */
.article-card { position: relative; overflow: hidden; }
.article-card::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: var(--fx-line);
  opacity: .85;
  pointer-events: none;
}

/* 主标题：入场时一道高光自左向右掠过（只动背景位置，不改透明度）。
   同样包在 @supports 里：渐变裁切失败时标题必须还是原色可读的。 */
.article-title { position: relative; }
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .article-title {
    /* 渐变只从近黑走到深钢蓝，再回近黑：标题不是链接，
       不能整段染成主色 —— 那会让人以为可以点。 */
    background-image: linear-gradient(100deg, var(--card-title) 0%, #2f5578 46%, #3d6b96 56%, var(--card-title) 78%);
    background-size: 260% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    animation: fx-title-sheen 1.5s var(--fx-ease) .1s both;
  }
}
@keyframes fx-title-sheen {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}

/* 小节标题左侧竖条：在原生的 3px 实条上叠一层呼吸光。
   只让伪元素的 opacity 起伏 —— box-shadow 本身不参与动画，
   否则长文里几十个标题会各自每帧重绘一遍阴影，滚动时白掉帧。
   实条始终在，呼吸的间隙里标题也不会变成没有标记的裸标题。 */
.prose h2 { position: relative; }
.prose h2::before {
  content: "";
  position: absolute;
  left: -3px; top: .12em; bottom: .12em;
  width: 3px;
  border-radius: 3px;
  background: var(--accent);
  box-shadow: 0 0 10px 2px rgba(61, 107, 150, .55);
  opacity: 0;
  animation: fx-bar-glow 4.2s ease-in-out infinite;
  pointer-events: none;
}
@keyframes fx-bar-glow {
  0%, 100% { opacity: .12; }
  50%      { opacity: .95; }
}

/* 正文链接：悬停时下划线由浅转实并向右延伸 */
.prose a {
  background-image: linear-gradient(90deg, var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 1px;
  transition: background-size .35s var(--fx-ease), color var(--t-fast);
}
.prose a:hover { background-size: 100% 1px; border-bottom-color: transparent; }

/* 代码块：悬停时浮起一层极淡的主色底 */
.prose pre { transition: box-shadow .35s; }
.prose pre:hover { box-shadow: 0 0 0 1px var(--accent-soft), 0 8px 24px rgba(27, 32, 43, .07); }

/* 复制成功：按钮向外扩一圈光环再收掉 */
.code-copy.is-done { animation: fx-copy-ok .6s var(--fx-ease); }
@keyframes fx-copy-ok {
  0% { box-shadow: 0 0 0 0 rgba(61, 107, 150, .45); }
  100% { box-shadow: 0 0 0 10px rgba(61, 107, 150, 0); }
}

/* 目录当前项：只保留 `.toc a.is-current` 的那道 2px 左边框。
   这里曾经再加过一条 `::before` 渐变光条（left:0 + 20% 内缩 + 光晕），
   它落在透明左边框的内侧，和边框并排成两道竖线，看着就是「左边样式重复」。
   位置指示只留一道。 */

/* 相邻文章：悬停时一道短线从左滑出。
   固定 16px 宽再 scaleX —— 不占位的话悬停瞬间会推一下旁边的文字。 */
.articlenav-item .articlenav-label::after {
  content: "";
  display: inline-block;
  width: 16px; height: 1px;
  margin-left: 6px;
  vertical-align: middle;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .3s var(--fx-ease);
}
.articlenav-item:hover .articlenav-label::after { transform: scaleX(1); }

/* ---------- 16.9 归档页 / 关于页 ---------- */
/* 年份切换条：悬停时底色自左填满，字色反白 */
.archive-year-chip { position: relative; overflow: hidden; isolation: isolate; transition: color .3s; }
.archive-year-chip::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(120deg, var(--accent), #5f8fae);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .35s var(--fx-ease);
}
.archive-year-chip:hover { color: #fff; }
.archive-year-chip:hover::before { transform: scaleX(1); }
/* 计数在反白底上要跟着换色，否则深灰压深蓝等于看不清 */
.archive-year-chip:hover .archive-year-chip-count { color: rgba(255, 255, 255, .85); }

/* 年份章节标题：左侧数字带一层背光。
   isolation 不能省：伪元素是 z-index:-1，没有独立层叠上下文时它会穿到
   最近一层（这里是 .card 的白底）之下，最后什么也看不见。 */
.archive-year-num { position: relative; isolation: isolate; }
.archive-year-num::after {
  content: "";
  position: absolute;
  inset: -30% -40%;
  z-index: -1;
  background: radial-gradient(closest-side, rgba(61, 107, 150, .18), transparent 70%);
  pointer-events: none;
}

/* 归档条目：悬停时左侧竖起一道光条，与首页卡片同一套语言（scaleY） */
.archive-item-link { position: relative; }
.archive-item-link::before {
  content: "";
  position: absolute;
  left: 0; top: 19%;
  width: 2px; height: 62%;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--accent), rgba(111, 159, 191, .9));
  transform: scaleY(0);
  transform-origin: center;
  transition: transform .3s var(--fx-ease);
  pointer-events: none;
}
.archive-item-link:hover::before { transform: scaleY(1); }

/* 概览数字：与侧栏统计同一套渐变文字（两端同取深色，理由见那一处） */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .archive-stat dd {
    background-image: linear-gradient(120deg, var(--accent) 0%, #33587a 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

/* 关于页信息行：左侧标签变主色，右侧值加一条发丝下划线 */
.def-row dt { transition: color .3s; }
.def-row:hover dt { color: var(--accent); }

/* ---------- 16.10 回到顶部 ---------- */
/* 进度环发光；按钮悬停时外扩一圈光环 */
.rail-ring-arc { filter: drop-shadow(0 0 4px rgba(61, 107, 150, .45)); }
.rail-top::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: 0 0 0 0 rgba(61, 107, 150, .35);
  transition: box-shadow .45s var(--fx-ease);
  pointer-events: none;
}
.rail-top:hover::after { box-shadow: 0 0 0 8px rgba(61, 107, 150, .09); }

/* ---------- 16.11 页脚 ---------- */
/* 页脚顶沿的流光：整块 200% 宽、图案按视口宽重复，靠 translateX(-50%)
   走完正好一个周期 —— 用 transform 而不是 background-position，
   后者按百分比换算位移量，走满 -220% 时元素会整块移出、露出一段空白。 */
.footer { position: relative; overflow: hidden; }
.footer::before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 200%; height: 1.5px;
  background: repeating-linear-gradient(90deg,
    transparent 0 30%,
    rgba(61, 107, 150, .55) 36%,
    rgba(196, 150, 96, .45) 44%,
    transparent 50%);
  background-size: 50% 100%;
  animation: fx-footer-flow 11s linear infinite;
  pointer-events: none;
}
@keyframes fx-footer-flow { to { transform: translateX(-50%); } }

/* 页脚导航：悬停时下划线展开 */
.footer-nav a { position: relative; }
.footer-nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -3px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .3s var(--fx-ease);
}
.footer-nav a:hover::after { transform: scaleX(1); }

/* ---------- 16.12 特效层降级 ---------- */
/* 减弱动效：氛围层留着（静态好看），一切动画与指针特效关掉。
   全局那条 animation-duration: .01ms 只把时长压到 0，伪元素仍会停在末帧；
   这里显式把纯装饰层摘掉，避免留一块静止的光斑在半空。 */
@media (prefers-reduced-motion: reduce) {
  .bg-layer::before { animation: none !important; }
  .scroll-progress__bar::after { display: none !important; }
  .card-item::before,
  .card-item::after,
  .card-cover::after,
  .card-item:first-child .card-cover::before,
  .avatar-ring::before { display: none !important; }
  .article-title { animation: none !important; color: var(--card-title); }
  .prose h2::before { display: none !important; }
  .menus a::after { display: none !important; }
  .menus a.menu-hub::before { display: none !important; }
  .widget:hover,
  .pager a:hover { transform: none !important; }
}

/* 印刷：特效层整层撤掉（背景、光斑、描边、扫光在纸上都是墨点） */
@media print {
  .bg-layer::before,
  .bg-layer::after,
  .card-item::before,
  .card-item::after,
  .card-main::before,
  .card-cover::after,
  .card-item:first-child .card-cover::before,
  .article-card::before,
  .avatar-ring::before,
  .footer::before,
  .rail-top::after,
  .widget-head::after,
  .archive-year-chip::before,
  .archive-year-num::after,
  .scroll-progress__bar::after { display: none !important; }
  .article-title {
    background: none !important;
    -webkit-background-clip: border-box !important;
    background-clip: border-box !important;
    color: #000 !important;
  }
  .prose h2 { border-left: 3px solid #999 !important; }
  .prose h2::before { display: none !important; }
  .site-data b,
  .archive-stat dd {
    background: none !important;
    -webkit-background-clip: border-box !important;
    background-clip: border-box !important;
    color: #000 !important;
  }
}
