@font-face{font-family:"Noto Sans SC";font-style:normal;font-weight:400;font-display:swap;src:url("/assets/fonts/noto-sans-sc-400.9ea0bff0.woff2") format("woff2");unicode-range:U+2E80-2EFF,U+3000-303F,U+3400-4DBF,U+4E00-9FFF,U+F900-FAFF,U+FE30-FE4F,U+FF00-FFEF}
@font-face{font-family:"Noto Sans SC";font-style:normal;font-weight:700;font-display:swap;src:url("/assets/fonts/noto-sans-sc-700.e50551e9.woff2") format("woff2");unicode-range:U+2E80-2EFF,U+3000-303F,U+3400-4DBF,U+4E00-9FFF,U+F900-FAFF,U+FE30-FE4F,U+FF00-FFEF}
/* ============================================================
   Feeeiyuuu — humanist editorial design system.

   Swiss skeleton (flush left, meta-rail grid, functional
   uppercase labels) carried by publication typography: serif
   reading text, grotesque sans for wayfinding, generous
   MUJI-style whitespace. International-style palette — white
   ground, ink black, zero-chroma greys, and one tech-blue accent
   that only ever draws rules and fills — never type; no
   illustration, no decoration, no dark mode. Motion serves
   navigation and arrival: the slow-load wave, the caret on the
   home status line, the page transition, and the rise — content
   fading up 8px into place, on load and on scroll (see 「浮现」).
   All of it runs off the --rise-* tokens, so the whole site keeps
   one tempo. The one exception is the Telegram and Email marks
   moving under the cursor — decoration, kept deliberately, and
   gated on prefers-reduced-motion, as the rise is too.

   Every size in this file comes from the token scales below —
   don't introduce ad-hoc values, extend the scale instead.
   ============================================================ */

:root {
  color-scheme: light;

  /* ── Color tokens ──────────────────────────────────────────
     2026-08-08：整套换成 gregorygundersen.com/css/blog.css 的取色，用户要求
     1:1 复刻。那套配色只有一个构造式——**黑色加不透明度**，纸是纯白，其余
     全是 rgba(0,0,0,α)：正文 0.8、次级 0.7、弱化 0.5、规线 0.2 / 0.1。
     没有第二个色相，所以此前那条「蓝只做线和面」的用色规则连同 IBM Blue
     一起退场了（--accent 现在就是正文墨色，留着只为不改二十多处引用）。

     用 α 而不是等效的灰值是刻意的：叠在纯白上算出来是一样的，但叠在
     --panel / --code-bg 那种浅底上会自然再深一档，省掉一套「压在面色上要
     重算」的账——原来那份对比度台账因此整段删掉了。文字色最浅的一档
     --muted 对白纸是 10.4:1，远在 AA 之上，参考站也是这么用的。 */
  --bg: #ffffff;
  --text: rgba(0, 0, 0, 0.8);
  --soft: rgba(0, 0, 0, 0.7);
  --muted: rgba(0, 0, 0, 0.5);
  --line: rgba(0, 0, 0, 0.1);
  --line-strong: rgba(0, 0, 0, 0.2);
  /* 科技蓝，取 IBM Blue 60。参考站是纯黑白的，这两档是**刻意留下的偏离**
     ——1:1 复刻那一版把它们改成了墨色别名，用户要回来了。
     用色规则没变，仍然是**蓝只做线和面，不做字**：现在吃到蓝的只有链接下划线
     （--link-line）、focus ring 和首页状态行的光标（--accent），链接本身仍是
     墨色。--link-line 比 --accent 浅一档，对白纸 3.01:1——它是线不是字，验收
     线按非文本的 3:1 算，要换深浅先回来核这一条。 */
  --accent: #0f62fe;
  --link-line: #6b93ea;
  /* 唯二的面色。参考站几乎不用底色（代码块只有上下两条线），所以这两档都
     压到极浅，只够把 tag 和行内 code 从正文里托一下。 */
  --panel: rgba(0, 0, 0, 0.06);
  --code-bg: rgba(0, 0, 0, 0.05);
  --selection: rgba(0, 0, 0, 0.12);

  /* ── Spacing scale (8pt grid) ────────────────────────────── */
  --space-1: 0.25rem; /*   4px */
  --space-2: 0.5rem; /*   8px */
  --space-3: 0.75rem; /*  12px */
  --space-4: 1rem; /*  16px */
  --space-5: 1.5rem; /*  24px */
  --space-6: 2rem; /*  32px */
  --space-7: 2.5rem; /*  40px */
  --space-8: 3.5rem; /*  56px */
  --space-9: 4.5rem; /*  72px */
  --space-10: 6rem; /*  96px */
  --space-11: 7rem; /* 112px */

  /* ── Type scale ──────────────────────────────────────────── */
  /* 2026-08-08：整条阶梯换成参考站的实测值（见 gg 那份 blog.css）。它没有
     token 系统，字号是一处一个字面量，下面是把它们收拢成的阶梯——右边注的
     是参考站里用这一档的地方，改之前先回去核对。
     注意这是一套**按纯拉丁定的**小字号，中文正文因此从等效 16.8px 掉到
     15px，这是用户明确接受的代价（见 blog-design-direction.md）。 */
  --text-3xs: 0.5625rem; /*  9px — byline 标签 */
  --text-2xs: 0.625rem; /* 10px — byline 日期 */
  --text-xs: 0.75rem; /* 12px — 导航、列表日期、脚注、参考文献 */
  --text-code: 0.8125rem; /* 13px — 代码块、图注 */
  --text-sm: 0.875rem; /* 14px — 列表摘要、表格 */
  --text-base: 0.9375rem; /* 15px — 正文 */
  --text-md: 1rem; /* 16px — 文章摘要（h4）、h3 */
  --text-lg: 1.125rem; /* 18px — 列表标题、正文 h1 */
  --text-xl: 1.625rem; /* 26px — 正文 h2 */
  /* 文章标题。参考站就是浏览器默认的 h1（2em = 32px），没有 clamp——窄屏
     也不缩。这里保留一档 clamp 只为手机上不撑破 700px 的列。 */
  --text-display: clamp(1.75rem, 5vw, 2rem);
  --text-masthead: clamp(3.2rem, 9vw, 6.4rem); /* home nameplate */

  --leading-tight: 1.2; /* h1，参考站显式写的 */
  --leading-snug: 1.35; /* 其余标题 */
  /* **是长度不是比例**，这一条很容易看错：参考站写的是 `html { line-height:
     1.5em }`，em 在 line-height 上是当场求值再**按长度继承**的，所以全站每一
     行都固定占 24px，跟自己多大字号无关——12px 的日期也占 24px。正文那条
     `.article { font-size: 15px; line-height: 1.6em }` 算出来同样是 24px，
     整站其实躺在一个 24px 的基线网格上。
     照抄成无单位的 1.5 会让小字缩成 18px 行高，列表就是这么挤起来的。 */
  --leading-body: 24px;
  --leading-reading: 24px;

  /* 参考站只有 regular / bold 两档（h1–h6 是浏览器默认粗体，.post-date 和
     .post-title 显式 bold）。--w-semibold 因此保留成 700 的别名：全站二十
     多处声明不用逐个改，Latin 侧也不会去合成一个系统字体没有的 600。 */
  --w-regular: 400;
  --w-semibold: 700;
  --w-bold: 700;

  --tracking-tight: 0; /* 参考站不调字距 */
  --tracking-caps: 0.06em; /* uppercase labels */

  /* ── Layout ──────────────────────────────────────────────── */
  /* 视口宽度，**不含**滚动条——铺满视口的东西（两条分隔线、脚注线、|full 图、
     页脚底色）都得用 --vw 而不是 100vw。100vw 按规范含滚动条，用它的元素会
     比文档宽 ~15px，两侧各探出 7px：不产生滚动条（body 的 overflow-x 挡着），
     但文档确实溢出，脚本仍能横向平移那 7px。
     --sbw 由 loading.js 在 <head> 里量出来写进 documentElement；量不到时留在
     0px，此时 --vw 退化成 100vw，也就是这次修复前的行为。 */
  --sbw: 0px;
  --vw: calc(100vw - var(--sbw));

  /* 参考站的 `.wrap { max-width: 700px }`——刊头、正文、导航共用这一个宽度，
     整站只有这一条竖向边界。43.75rem = 700px。
     它按 15px 拉丁定：700 ÷ 15 ≈ 每行 88 字符，比通常说的 65–75 宽一截，
     中文则是每行约 46 字。这是照搬的值，不是重算出来的。 */
  --content: 43.75rem;
  /* 参考站只有 `.wrap` 一个宽度：导航、刊头、正文、图全在 700px 里，没有任何
     东西突破文本列。所以这两档都对齐到 --content——「插图比正文宽 20%」和
     wide shell（62rem）那两级突破随之取消。`![alt|wide]` / `|full` 这些标记
     还在，只是不再有可突破的余量；要恢复分级，把这两个值改回去即可。 */
  --content-figure: var(--content);
  --content-wide: var(--content);
  /* 图片的实际宽度。min() 那半截是「突破到多宽」，外面的 max(100%, …) 是
     「永远不许窄于所在的容器」——两者都必要：窄屏 .site-shell 收的是
     1.75rem 边距，比这里的 2.5rem 小，光看 min() 会算出比正文列还窄的图。
     100% 在自定义属性里是延迟解析的，正好各自相对自己的父容器求值。 */
  --figure-w: max(100%, min(var(--content-figure), calc(var(--vw) - 2.5rem)));
  --figure-w-wide: max(100%, min(var(--content-wide), calc(var(--vw) - 2.5rem)));

  /* ── Radius ──────────────────────────────────────────────────
     圆角只跟着图走（封面、正文插图、包着图的 media--frame）和浮在页面之上
     的搜索卡片，全站别处——包括文章列表卡片——都是直角。 */
  --radius-card: 0.875rem; /* 14px */

  /* ── Motion ──────────────────────────────────────────────────
     浮现的四个参数，全站共用这一套。取值来自 charliedeets.com 实测
     （它的 .fade-in 挂 @keyframes fade-in-up）：位移 8px、0.5s、ease-out。
     **位移小、时长长**是这个手感的全部——把 8px 调大或把 500ms 调短，
     浮现立刻变成滑入，就不是那个东西了。
     --rise-step 是 stagger 的步长，没有出处，压着 500ms 定的：再大就能
     看出是「一个一个来」，而不是「一起浮起来、先后落定」。 */
  --rise-dist: 8px;
  --rise-dur: 500ms;
  --rise-ease: ease-out;
  --rise-step: 60ms;
  /* 交互形变（搜索卡片弹出）用的过冲缓动。参考站的 Menu 用一条 20 段采样的
     linear() 弹簧，峰值 1.06；这里取等效的 cubic-bezier，过冲同样落在 6%
     上下。**只作用在 transform 上**——拿去做 opacity 会在 1 处被裁掉，
     过冲那一段变成一顿停顿。 */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* ── Font stacks ─────────────────────────────────────────────
     2026-08-08：参考站全站一个字体栈 `-apple-system, Helvetica, arial,
     sans-serif`，**没有衬线**，代码是裸 `monospace`。Source 超家族那一套
     （衬线阅读文字 / 无衬线导航）整个退场，--font-serif 保留成 --font-sans
     的别名，只为不去改几十处引用。

     跟参考站唯一的偏差：栈尾挂了 "Noto Sans SC"。Helvetica 没有汉字，不挂
     的话中文会落到 fontconfig 随便挑的一个字体，而这个 face 本来就在项目里
     subset 好了。它排在拉丁字体之后，所以拉丁侧仍然 1:1 走系统 Helvetica。
     size-adjust 同时撤了（见 build.mjs 的 FONT_FACES）：那 94% 是压着
     Source Sans 3 的 cap height 0.66 定的，Helvetica 是 0.717，
     0.875 ÷ 0.717 = 1.22 已经落在 1.23–1.25 的混排舒适区里，不用再压。 */
  --font-sans: -apple-system, Helvetica, Arial, "Noto Sans SC", sans-serif;
  --font-serif: var(--font-sans);
  --font-mono: monospace;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  min-height: 100vh;
  margin: 0;
  /* Backstop only — the full-bleed elements size themselves off --vw and no
     longer overhang. Kept for whatever overflows next (a wide table, a long
     unbroken token), and it has to stay here rather than on html: body's
     overflow propagates to the viewport only while html declares none, and
     that propagation is what suppresses the scroll. `hidden` rather than
     `clip` for the same reason — measured, html+clip lets the page pan again. */
  overflow-x: hidden;
  color: var(--text);
  background: var(--bg);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: var(--leading-body);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body.search-lock {
  overflow: hidden;
}

/* ── Page transitions ──────────────────────────────────────────
   Cross-document view transitions. The chrome (navbar, footer) is
   identical either side of a navigation, so cross-fading the root reads
   as nothing moving; only the content column is named separately and
   given a short fade-and-rise. Purely declarative — no script, nothing
   for the CSP to allow. Browsers without cross-document view
   transitions ignore the at-rule and cut straight to the new page,
   which is the behaviour this site had before. */
@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 180ms;
  animation-timing-function: ease;
}

main#content {
  view-transition-name: page-content;
}

::view-transition-old(page-content) {
  animation: page-fade-out 120ms ease both;
}

/* 站内导航和首屏载入烧的是同一条 rise-in、同一组 token——两者本来就是同
   一件事（内容浮上来），分成两套参数会在「点链接」和「刷新」之间露出手感
   接缝。原来这里是 220ms，快到读不出浮现，只读得出闪了一下。 */
::view-transition-new(page-content) {
  animation: rise-in var(--rise-dur) var(--rise-ease) both;
}

@keyframes page-fade-out {
  to {
    opacity: 0;
  }
}

/* 位移走 `translate` 这个独立属性，**不是 transform**——这条不能改回去：
   正文里的图、嵌入块、封面都是 `left: 50%` + `transform: translateX(-50%)`
   居中的，而这段 keyframes 只写了 from，to 就隐含取元素的基础值。写成
   transform 的话，动画会在 translateY(8px) 和 translateX(-50%) 之间插值，
   图在浮现的同时横着滑半个自己的宽度过去。translate 和 transform 是分开
   合成的，居中那条因此完全不受影响。 */
@keyframes rise-in {
  from {
    opacity: 0;
    translate: 0 var(--rise-dist);
  }
}

/* ── 浮现 ──────────────────────────────────────────────────────
   两种触发烧同一条 rise-in：**载入时按顺序浮起**（下面第一块），和
   **滚动到才浮起**（第二块）。分工按「首屏内 / 首屏外」切——列表卡片前
   六张归载入、第七张起归滚动——所以同一个元素永远只被其中一块选中，不会
   叠两层透明度。六这个数是压着 --content 的 700px 列和 41px 行距估的首屏
   容量；改列宽或卡片间距时这里要跟着回来核。

   **正文段落不参与**，这是刻意的：读到一半还有东西在动是干扰不是质感。
   正文里只有图和代码块浮现——它们本来就是读者会停下来看的东西。

   整节包在 no-preference 里而不是到 reduce 块去关：这样 reduce 下这些规则
   根本不存在，不用担心哪条 !important 没盖住而把内容永久留在 opacity 0。 */
@media (prefers-reduced-motion: no-preference) {
  .home-masthead,
  .home-links,
  .home-plate,
  .cat-nav,
  .blog-list-item:nth-child(-n + 6),
  .article-header h1,
  .article-header .article-lead,
  .article-meta,
  .article-main {
    animation: rise-in var(--rise-dur) var(--rise-ease) both;
    animation-delay: calc(var(--rise-i, 0) * var(--rise-step));
  }

  /* 每页各自的先后。--rise-i 是「第几个落定」，不是毫秒——步长只在
     --rise-step 一处，改节奏不用动下面任何一个数。 */
  .home-plate {
    --rise-i: 1;
  }

  .home-links {
    --rise-i: 2;
  }

  /* 首页状态行没有 --rise-i，也不在上面的选择器里：它由 home-status.js
     逐字打出来，自己就是入场动画，再浮一次会变成两段。 */

  .article-header .article-lead {
    --rise-i: 1;
  }

  .article-meta {
    --rise-i: 2;
  }

  .article-main {
    --rise-i: 3;
  }

  .blog-list-item:nth-child(1) {
    --rise-i: 1;
  }

  .blog-list-item:nth-child(2) {
    --rise-i: 2;
  }

  .blog-list-item:nth-child(3) {
    --rise-i: 3;
  }

  .blog-list-item:nth-child(4) {
    --rise-i: 4;
  }

  .blog-list-item:nth-child(5) {
    --rise-i: 5;
  }

  .blog-list-item:nth-child(6) {
    --rise-i: 6;
  }
}

/* 滚动浮现。scroll-driven animations 做的，**没有 JS**——不用 observer，
   不用给 CSP 加东西，也没有「脚本没跑起来内容就永远看不见」这个类别的
   故障：不支持 animation-timeline 的浏览器整块跳过，元素照常显示。

   有 timeline 时 animation-duration 被忽略，进度完全由 animation-range 映射，
   所以那个 500ms 在这里不生效，rise-ease 仍然作用在两个 keyframe 之间。
   range 的收尾用 cover 而不是 entry：entry 100% 要求元素**整个**进视口，
   比视口还高的代码块永远够不到，会卡在半透明。 */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .blog-list-item:nth-child(n + 7),
    .article-body .media,
    .article-body .code-block {
      animation: rise-in var(--rise-dur) var(--rise-ease) both;
      animation-timeline: view();
      animation-range: entry 0% cover 20%;
    }
  }
}

::selection {
  background: var(--selection);
}

/* 字是墨色、线是科技蓝——参考站那条线是文字本色，这里是刻意的偏离。
   offset 和 thickness 也一起给回来了：浏览器默认的下划线贴着基线，一条彩色
   的线压在那个位置会糊进字里，只有拉开才读得出是「一条蓝线」而不是脏边。
   hover 仍按参考站来：去掉下划线并把字调浅（所以蓝也随之消失）。 */
a {
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--link-line);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

/* 参考站的全局 `h1 { line-height: 1.2em }`。**这条不能漏**：继承下来的行高是
   固定的 24px（见 --leading-body），h1 有 32px，不给它自己的行高就会拿 24px
   的行盒去装 32px 的字，行盒外溢、跟上下文挤在一起。404 和 .page-header 的
   h1 都只靠这条兜着。 */
h1 {
  line-height: var(--leading-tight);
}

p {
  margin: 0 0 var(--space-4);
}

img,
svg {
  display: block;
}

img {
  max-width: 100%;
}

button,
input {
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.skip-link {
  position: fixed;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 500;
  padding: var(--space-1) var(--space-3);
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--text);
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform 150ms ease;
}

.skip-link:focus {
  transform: translateY(0);
}

/* ── Top navbar (blog / code / article / category pages) ────
   Sits at the top of the content column so its edges line up with the
   content below: home + blog on the left, language on the right, with a
   thin light divider and generous vertical breathing room. */
/* 参考站的 `.nav`：三个 12px 灰链接，无下划线，**上下都没有规线**，只有
   15px 的上下外边距。此前那条满幅 3px 墨色横杠（全站最重的一笔）连同
   article-header 那条一起撤了——参考站唯一的规线是刊头上下的两条
   rgba(0,0,0,0.1) 细线，见 .article-header。 */
.topbar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
  /* 参考站是 `.nav { margin: 15px 0 }` 套一个带默认 1em margin 的 <ul>；两者
     折叠成 max(15, 16) = 16，上下各一次。实测导航条上下确实各 16px。 */
  margin: 16px 0;
  line-height: 24px;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
}

/* Baseline, not the default stretch: the active link carries chip padding and
   the home link does not, so stretching leaves the two items with baselines
   11px apart and the mark floating above the word beside it. Aligning on the
   baseline lets the mark's own -0.15em sink do the rest — the row is the same
   arrangement as .topbar-tools, and wants the same rule. */
.topbar-nav {
  display: flex;
  align-items: baseline;
  gap: var(--space-5);
}

.topbar a {
  color: var(--muted);
  text-decoration: none;
}

/* 参考站的导航 hover 是**加粗**，不是加下划线也不是变色。当前页那一项直接
   用同一个粗体标出来——原来那个 --panel 底色的方框胶囊撤了，参考站没有。 */
.topbar a:hover,
.topbar a.active {
  color: var(--muted);
  font-weight: var(--w-bold);
  text-decoration: none;
}

/* Right-hand tool cluster in the topbar: search trigger + language
   switch. The trigger is a <button> only semantically — visually it is
   the same bare text link as everything else in the bar. */
.topbar-tools {
  display: flex;
  align-items: baseline;
  gap: var(--space-5);
}

.search-trigger {
  position: relative;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
}

/* The word used to be the target; the mark alone is ~15px square, under the
   24px minimum and a poor thing to hit on a phone. An absolutely positioned
   pseudo-element grows the hit area past that without entering layout — any
   padding here would move the mark and the language switch beside it. */
.search-trigger::after {
  content: "";
  position: absolute;
  inset: -6px;
}

/* The trigger is the mark alone, so the underline the text tools get would
   draw a stray dash under a wordless box — the colour shift carries the hover,
   same as the brand marks in the bar. */
.search-trigger:hover {
  color: var(--muted);
}

/* Home has no topbar: the same tool cluster sits fixed in the top-right
   corner. */
.home-tools {
  position: fixed;
  top: var(--space-4);
  right: var(--space-4);
  z-index: 20;
  display: flex;
  align-items: baseline;
  gap: var(--space-5);
}

.lang-switch {
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  text-decoration: none;
}

.lang-switch:hover {
  color: var(--muted);
  font-weight: var(--w-bold);
  text-decoration: none;
}

/* ── Inline brand marks ─────────────────────────────────────
   Sized in em so a mark tracks whatever type it sits in (navbar --text-sm,
   home list --text-md) and inherits the link colour through currentColor.
   Wider than 1em: each mark stands alone as a whole link, so it has to hold
   its own next to the text links beside it. Both boxes are ~1.25:1 because
   both viewBoxes are cropped to the ink — matching the ratio is what makes a
   mark fill its box edge to edge instead of letterboxing inside it. */
.nav-icon {
  vertical-align: -0.15em;
}

.icon-tg {
  width: 1.2em;
  height: 1em;
  fill: currentColor;
}

.icon-mail {
  width: 1.25em;
  height: 1em;
}

/* Square like the house, but set under 1em: this is the only mark here whose
   ink is a solid disc, and a full 1em circle beside the plane and the hollow
   envelope reads as the heavy one of the three. 0.95em is the disc paying
   back the ink the others spend on empty space. */
.icon-gh {
  width: 0.95em;
  height: 0.95em;
}

/* Square, and solid where the envelope is hollow — a filled shape carries
   more ink than an outlined one at the same size, so it stays at 1em instead
   of taking the extra height the magnifier needs.
   The sink is a negative margin, not vertical-align: the reset above sets
   svg { display: block }, so these marks never sit in a line box and
   vertical-align has nothing to act on. What actually places them is the flex
   baseline — a block link with no line box baselines on its bottom edge, so
   the mark's bottom lands exactly on the neighbouring word's baseline and the
   whole glyph rides about 2px high of the word's optical centre. Lifting that
   edge with a negative margin is what drops the mark. -0.15em (2.1px at
   --text-sm) lands within 0.35px of centre for both 博客 and Blog. */
.icon-home {
  width: 1em;
  height: 1em;
  margin-bottom: -0.15em;
}

/* Square where the other two are ~1.25:1, so it takes a little more height to
   carry the same visual weight beside them — a 1em circle next to a 1.2em
   plane reads as the smaller mark.
   The extra height has to be paid back in the sink, or the mark rides high
   next to the EN/中 switch beside it — and for the same reason as .icon-home
   below, that sink has to be a negative margin rather than vertical-align.
   A touch more than the house's because this glyph is the taller of the two. */
.icon-search {
  width: 1.05em;
  height: 1.05em;
  margin-bottom: -0.17em;
}

/* These links are the mark alone, so the underline the other nav links get
   would draw a stray dash under a wordless box. The mark moving on hover is
   the affordance instead. */
.topbar-nav a:has(.nav-icon):hover,
.home-links a:has(.nav-icon):hover {
  text-decoration: none;
}

/* A mark is 14–17px of ink sitting in a row where everything else is a word,
   which is a small thing to hit on a phone. The pseudo-element grows each one
   past the 24px minimum without entering layout — padding here would spread
   the row apart. The plane and the envelope were always this size; the house
   replacing a word is what made it the whole cluster. */
.topbar-nav a:has(.nav-icon) {
  position: relative;
}

.topbar-nav a:has(.nav-icon)::after {
  content: "";
  position: absolute;
  inset: -6px;
}

/* Declared as no-preference rather than switched off further down: marks
   moving under the cursor are decoration, and someone who asked for less
   motion should simply never start them. The plane drifts off diagonally
   because that is where a plane goes; the envelope only lifts, so the two
   read as different objects rather than one shared wobble.
   The plane and the envelope are home-page-only, so they are scoped to
   .home-links alone; the house is the bar's only mark. */
@media (prefers-reduced-motion: no-preference) {
  .home-links a:hover .icon-tg {
    animation: tg-float 1.6s ease-in-out infinite;
  }

  .home-links a:hover .icon-mail {
    animation: mail-lift 1.6s ease-in-out infinite;
  }

  /* The mark turns instead of travelling: a disc that slides looks like it
     is falling off the row, and the cat cut out of it is what makes the
     rotation legible at all. Small — past a few degrees the silhouette stops
     reading as a logo and starts reading as a broken one. */
  .home-links a:hover .icon-gh {
    animation: gh-turn 1.6s ease-in-out infinite;
  }

  /* The house goes the other way, and barely: it is the one object here that
     is fixed to the ground, so it settles rather than lifts, at half the
     envelope's travel. A house that floats is a different thing entirely. */
  .topbar-nav a:hover .icon-home {
    animation: home-settle 1.6s ease-in-out infinite;
  }

  @keyframes tg-float {
    0%,
    100% {
      transform: translate(0, 0);
    }

    50% {
      transform: translate(0.1em, -0.18em);
    }
  }

  @keyframes mail-lift {
    0%,
    100% {
      transform: translateY(0);
    }

    50% {
      transform: translateY(-0.16em);
    }
  }

  @keyframes gh-turn {
    0%,
    100% {
      transform: rotate(0deg);
    }

    50% {
      transform: rotate(-7deg);
    }
  }

  @keyframes home-settle {
    0%,
    100% {
      transform: translateY(0);
    }

    50% {
      transform: translateY(0.08em);
    }
  }
}

/* ── Shell ─────────────────────────────────────────────────── */
/* 参考站的 `.wrap` + `.content { margin-bottom: 120px }`。上方**不留 padding**
   ——它那里 `.nav { margin: 15px 0 }` 和 <ul> 默认的 1em margin 会折叠成一个
   16px，padding 参与不了折叠，加上去整块就会往下掉 15px（实测过）。窄屏它把
   边距收到 `body { margin: 0 7px }`，这里沿用站里原有的 calc 写法，多留一点。 */
.site-shell {
  width: min(calc(100% - 2.5rem), var(--content));
  margin: 0 auto;
  padding: 0 0 120px;
}

/* 参考站没有第二级宽度，wide shell 因此退化成和普通 shell 一样宽；
   layout() 传的 `wide: true` 还在，只是不再有视觉效果。 */
.site-shell--wide {
  width: min(calc(100% - 2.5rem), var(--content-wide));
}

/* ── Home ──────────────────────────────────────────────────── */
/* Editorial front page on the wide shell. The nameplate spans the full
   column — a long wordmark can never collide with the hero figure. */
.home-hero {
  padding: var(--space-6) 0 var(--space-7);
  margin-bottom: var(--space-8);
}

.home-masthead {
  margin-bottom: var(--space-7);
}

.home-masthead h1 {
  margin: 0;
  font-size: var(--text-masthead);
  font-weight: var(--w-bold);
  line-height: 0.95;
  letter-spacing: var(--tracking-tight);
}

/* The nameplate is an animated wordmark embedded from media/, sitting on the
   same paper ground as the page (the iframe's own background is set to --bg).
   The accessible name lives in a visually-hidden span; the frame is
   aria-hidden. Sized to the masthead type scale so it reads as the H1. */
.home-nameplate-anim {
  display: block;
}

.home-nameplate-frame {
  display: block;
  width: 100%;
  height: clamp(9rem, 22vw, 15rem);
  border: 0;
  background: transparent;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Contents list: one row of serif links under the plate. align-items is
   baseline, not center: the marks sit a size up from the text links (see
   below), and centering two different type sizes leaves neither sitting on
   the line the other reads from. */
.home-links {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: var(--space-6);
  margin: 0;
  padding-left: 0;
  list-style: none;
}

.home-links li {
  font-size: var(--text-md);
}

.home-links a {
  color: var(--text);
  text-decoration: none;
}

.home-links a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The marks sit side by side on one row of the list — they are a contact
   block, not three more entries in the contents.
   Set a notch *above* the reading size the text links use, not below it: a
   mark is read by its silhouette, and a glyph carries far less of one at a
   given size than a word does at the same size. This font-size is the only
   handle on all three — every box below is in em, so the row scales as one
   and the hover travel scales with it. */
.home-links-marks {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  font-size: var(--text-lg);
}

.home-links-marks a {
  color: var(--soft);
  line-height: 1;
}

.home-links-marks a:hover {
  color: var(--text);
}

/* The plate between the nameplate and the contents list. The image is a print
   that already carries its own cream mount, so it gets no border, no shadow
   and no radius from us — anything added here would read as a second frame
   around the first. Held well under the reading column: at full column width a
   square would outweigh the nameplate, and the page's whole argument is that
   the nameplate is the only loud thing on it.
   No margin-top — the gap above is .home-masthead's margin-bottom, kept in
   one place so the nameplate's breathing room stays a single number. */
.home-plate {
  width: min(100%, 21.25rem);
  margin: 0 0 var(--space-7);
}

.home-plate img {
  display: block;
  width: 100%;
  height: auto;
}

/* ── Home status line ────────────────────────────────────────
   本地日期时钟 + 边缘解析的城市/天气，载入时逐字打出（src/home-status.js）。
   min-height 撑住行高：内容是空文本节点起步的，不然字落位时会把下面的
   东西顶一下。 */
.home-status {
  margin-top: var(--space-9);
}

.home-status-meta {
  /* --leading-body 现在是长度，直接用它撑高，不再乘 1em。 */
  min-height: var(--leading-body);
  margin: 0;
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--w-semibold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

.home-status-meta.is-typing::after {
  content: "";
  display: inline-block;
  width: 0.45em;
  height: 1em;
  margin-left: 0.12em;
  vertical-align: -0.12em;
  background: var(--accent);
  animation: status-caret 1s step-end infinite;
}

@keyframes status-caret {
  50% {
    opacity: 0;
  }
}

/* ── Blog shelf (blog index + category pages) ────────────────
   A flush-left stack, no plates and no rules: title, uppercase meta line,
   standfirst. (The home page and 404 still use the .post-list rows below —
   those are date-rail lists.) */
/* 列表页整块，照参考站的 `#blog { margin: 80px 0 100px }`。之前漏了这条，
   分类行直接顶在导航条下面，整页从上到下都挤在一起。 */
.blog-shell {
  margin: 80px 0 100px;
}

/* 分类行，照参考站的 `ul#tag-nav`：14px 粗体、无下划线、**左对齐**（原来是
   居中的），下方 30px 留白 + <ul> 自带的 1em 下 margin，每项之间 10px。 */
.cat-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) 10px;
  max-width: var(--content);
  /* 30px 是参考站 `ul#tag-nav` 自己的 padding，27px 是补它 <ul> 默认下 margin
     和首条 .post-title 那个 1em 上 margin 折叠后剩下的量——实测对齐的：分类行
     末行到第一个标题，参考站 83px，这里也是 83px。 */
  margin: 0 auto 27px;
  padding-bottom: 30px;
}

.cat-nav a {
  color: var(--text);
  font-size: var(--text-sm);
  font-weight: var(--w-bold);
  text-decoration: none;
}

.cat-nav a:hover {
  color: var(--text);
  text-decoration: underline;
}

.cat-nav a.current {
  color: var(--muted);
}

/* 选中分类时压在列表上方的分类名，只出现在分类页（全部文章那页没有）。
   参考站是「26px 粗体 + 一条满列宽的细线」，尺寸全部按截图折算过来的：
   截图是 5/3 缩放，列宽 1166px÷5/3 = 700px 正好是 --content，分类行、
   文章标题、日期也分别落在 14/18/12px 上，所以这里的 26px 是可信的。
   行高走全局那条 `h1 { line-height: 1.2 }`，别再写一遍。 */
.cat-title {
  max-width: var(--content);
  /* 上边 43px 会和 .cat-nav 的 27px 下 margin 折叠（取大的那个），加上分类行
     自己 30px 的 padding，分类行末行的行盒底到标题行盒顶 = 73px。
     下边 37px 同理和 .blog-list 的 0 折叠，量出来是 36.6px。 */
  margin: 43px auto 37px;
  padding-bottom: 13px;
  border-bottom: 1px solid var(--line-strong);
  font-size: var(--text-xl);
  font-weight: var(--w-bold);
  text-wrap: balance;
}

/* 参考站的 `#blog` 列表。关键是**标题和日期都是 `<p>` 不是标题元素**——所以
   标题带着 `1em`(16px) 的默认上 margin，摘要带着显式的 `margin-bottom: 25px`。
   两者在条目之间折叠成 max(25, 16)=25，但标题那 16px 在条目**内部**也要算，
   一开始我把标题 margin 归零，条目间只剩 25px，整列就挤住了。
   这里用 flex gap 表达：25 + 16 = 41px。 */
.blog-list {
  display: flex;
  flex-direction: column;
  gap: 41px;
  max-width: var(--content);
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

/* line-height 跟着 html 的 1.5 走，不是标题那档 1.35——参考站这里就是一个
   继承了 body 行高的 <p>。 */
.blog-list-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--w-bold);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

.blog-list-title a {
  text-decoration: none;
}

.blog-list-title a:hover {
  color: var(--text);
  text-decoration: underline;
}

/* `.post-date`：12px 大写**粗体**，rgba(0,0,0,0.7)——比正文浅一档但比标签
   重一档，是整个列表里唯一的大写元素。分类名跟着它排在同一行。 */
.blog-list-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
  margin: 4px 0;
  color: var(--soft);
  font-size: var(--text-xs);
  font-weight: var(--w-bold);
  text-transform: uppercase;
}

/* 这一条参考站是写在元素自己身上的 `line-height: 1.5em`，em 相对它自己的
   14px 求值 = 21px，**不是**全站继承下来的那个 24px。所以这里用无单位的
   1.5，不是 --leading-body。 */
.blog-list-dek {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* 汉字字面比拉丁 x-height 高得多，同一个 1.5 在中文里行距明显发紧。 */
:lang(zh) .blog-list-dek {
  line-height: 1.6;
}

/* ── Post list (date + title + dek) ─────────────────────────
   Three columns: date rail, entry, kind tag (mixed lists only). The row
   rule runs the full column width — it is what closes the page on the
   right now that the list column is no longer capped at --content. */
.post-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.post-list li {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--space-4);
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--line);
}

.post-list li:last-child {
  border-bottom: 0;
}

.post-date {
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--w-regular);
  font-variant-numeric: tabular-nums;
}

.post-entry a {
  color: var(--text);
  font-size: var(--text-md);
  font-weight: var(--w-regular);
  text-decoration: none;
}

.post-entry a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Two lines of standfirst, clamped: enough to predict the piece, never
   enough to compete with the title. */
.post-dek {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  max-width: var(--content);
  margin: var(--space-1) 0 0;
  color: var(--soft);
  font-size: var(--text-sm);
  line-height: var(--leading-body);
}

.post-kind {
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.list-empty {
  color: var(--muted);
  font-style: italic;
}

/* ── Page header (collection / archive / tags / about) ─────── */
.page-header {
  margin-bottom: var(--space-6);
}

.page-header h1 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-display);
  font-weight: var(--w-bold);
  text-wrap: balance;
}

.page-header p {
  margin: 0;
  color: var(--soft);
}

.page-meta {
  margin-top: var(--space-2);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/* ── Archive ───────────────────────────────────────────────── */
.archive-year {
  margin-bottom: var(--space-6);
}

.archive-year-num {
  margin: 0 0 var(--space-3);
  font-size: var(--text-md);
  font-weight: var(--w-bold);
}

/* ── Tags index ────────────────────────────────────────────── */
.tag-index {
  margin: 0;
  padding: 0;
  list-style: none;
}

.tag-index li {
  padding: var(--space-1) 0;
}

.tag-index .tag-count {
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.inline-tags {
  margin-top: var(--space-2);
}

.inline-tags a,
.inline-tags span {
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
}

.inline-tags a {
  text-decoration: none;
}

.inline-tags a:hover {
  text-decoration: underline;
}

/* ── Article ───────────────────────────────────────────────── */
/* 参考站的 `.front-matter`：`padding: 20px 0` + 上下各一条 rgba(0,0,0,0.1)。
   关键是它**包着** `.wrap` 而不是被包在里面——所以那两条线是满幅的，文字
   却仍收在 700px 里。这里靠两个 --vw 宽的伪元素画线（站里原本就有这个满幅
   规线的写法），比再套一层 div 省事。
   20px 不在 8pt 阶梯上，是照搬的字面量。 */
.article-header {
  position: relative;
  max-width: var(--content);
  margin: 0 auto;
  padding: 20px 0;
}

.article-header::before,
.article-header::after {
  content: "";
  position: absolute;
  left: 50%;
  width: var(--vw);
  height: 1px;
  transform: translateX(-50%);
  background: var(--line);
}

.article-header::before {
  top: 0;
}

.article-header::after {
  bottom: 0;
}

/* 标题和摘要的上下间距是**浏览器默认的 h1 / h4 margin**（0.67em / 1.33em），
   参考站一条都没覆盖。折算过来 h1 是 21.4px、h4 是 21.3px，相邻时取大的那
   个，所以两处都写成 21px。 */
.article-header h1 {
  margin: 21px 0;
  font-size: var(--text-display);
  font-weight: var(--w-bold);
  line-height: var(--leading-tight);
  text-wrap: balance;
}

/* 参考站这里是个 h4：1em 粗体，行高跟着 html 的 1.5。不是灰的次级文字，
   而是和标题同色的粗体——它承担的是副标题而不是摘要。 */
.article-header .article-lead {
  margin: 21px 0;
  font-size: var(--text-md);
  font-weight: var(--w-bold);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

/* bylines：每个 byline 是「9px 大写灰标签 / 10px 日期」两行，横向排开，
   间距 20px。参考站用的是 float + .clear，这里用 flex 达到同样的结果。 */
.article-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) 20px;
  margin: 0;
}

.article-meta dt {
  color: var(--muted);
  font-size: var(--text-3xs);
  text-transform: uppercase;
}

.article-meta dd {
  margin: 0;
  font-size: var(--text-2xs);
  line-height: 1.6;
}

/* 9px / 10px 是按拉丁小型大写定的；汉字没有大写可转，9px 的「发布于」只剩
   一团灰。中文页各升一档，仍在阶梯内。 */
:lang(zh) .article-meta dt {
  font-size: var(--text-2xs);
}

:lang(zh) .article-meta dd {
  font-size: var(--text-xs);
}

.article-main {
  max-width: var(--content);
  min-width: 0;
  margin-inline: auto;
}


/* The hero takes the same breakout as the figures below it (see
   --content-figure). It has to: leaving the cover at the reading measure
   while every inline figure runs wider makes the article's lead image the
   narrowest picture on the page. */
.entry-cover-article {
  position: relative;
  left: 50%;
  width: var(--figure-w);
  margin: 0 0 var(--space-5);
  transform: translateX(-50%);
}

.entry-cover-article img {
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

/* ── Article body (prose) ──────────────────────────────────── */
/* 参考站的 `.article { font-size: 15px; line-height: 1.6em; margin: 50px 0 }`
   ——刊头和正文之间那 50px 就是这条 margin-top。 */
.article-body {
  margin: 50px 0;
  font-size: var(--text-base);
  line-height: var(--leading-reading);
  overflow-wrap: break-word;
  /* CJK 排印增强，按引擎支持渐进生效：标点挤压（Chrome/Edge）、
     中西文自动四分空、行尾标点悬挂（Safari）。不支持的引擎忽略。 */
  text-spacing-trim: normal;
  text-autospace: normal;
  hanging-punctuation: allow-end;
}

/* Paragraph spacing has to beat the 1.85 leading to register as a break at
   all; at --space-4 the gap between paragraphs was barely twice the gap
   between lines, which is too close to read as a boundary in justified CJK. */
.article-body p {
  margin-bottom: var(--space-5);
  text-indent: 0;
  text-wrap: pretty;
}

/* zh prose is set justified like print; en keeps its ragged right. */
:lang(zh) .article-body p {
  text-align: justify;
}

.article-body p.indent {
  text-indent: 2em;
}

.article-body h1,
.article-body h2,
.article-body h3,
.article-body h4 {
  margin: var(--space-7) 0 var(--space-4);
  font-weight: var(--w-semibold);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  scroll-margin-top: var(--space-5);
  text-wrap: balance;
  clear: both;
}

/* h1 / h3 / h4 参考站一条规则都没写，所以它们就是浏览器默认的相对字号，
   基准是 .article 的 15px：2em / 1.17em / 1em。写成 em 而不是折算成 px，
   跟着 --text-base 走。 */
.article-body h1 {
  font-size: 2em;
}

/* 参考站的 h2 是全站唯一带规线的标题：26px，下方一条 rgba(0,0,0,0.2)，
   `padding: 1em 0 1rem`。margin 归零交给 padding，规线才贴在文字下面而不是
   飘在空白里。 */
.article-body h2 {
  margin: 0 0 var(--space-4);
  padding-top: 1em;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line-strong);
  font-size: var(--text-xl);
}

/* h3 carries the section load in notes that never use h2, but at 1.2× the
   prose it can't do that on size alone. It has only the size and the space
   around it to work with, so keep the margins asymmetric: tighter below
   than above, which reads as belonging to what follows. */
.article-body h3 {
  margin-bottom: var(--space-3);
  font-size: 1.17em;
}

/* 参考站没给正文 h4 任何规则，所以它就是浏览器默认的 1em 粗体。此前那套
   「h4 不是标题而是大写 sans 功能标签」的约定随之作废——那是长在衬线正文
   上的语法，这里全站已经只有一个无衬线字体，大写标签不再跟正文有对比。 */
.article-body h4 {
  margin: var(--space-5) 0 var(--space-3);
  font-size: 1em;
}

.article-body ul,
.article-body ol {
  margin: var(--space-4) 0;
  padding-left: var(--space-5);
}

.article-body li {
  margin: var(--space-1) 0;
}

.article-body li::marker {
  color: var(--muted);
}

.article-body a {
  color: var(--text);
  text-underline-offset: 0.15em;
}

.article-body code {
  padding: 0.1rem 0.35rem;
  background: var(--code-bg);
  font-family: var(--font-mono);
  font-size: 0.86em;
}

/* Code block: the wrapper owns the margins and the language label, the
   <pre> owns the scroll. */
.article-body .code-block {
  position: relative;
  margin: var(--space-5) 0;
}

.article-body .code-block::before {
  content: attr(data-lang);
  position: absolute;
  top: var(--space-2);
  right: var(--space-3);
  z-index: 1;
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--w-semibold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  pointer-events: none;
}

/* 参考站的 `div.highlight`：13px / 1.3em，**上下各一条 rgba(0,0,0,0.2)，
   没有底色也没有左右边框**。 */
.article-body pre {
  margin: var(--space-5) 0;
  padding: var(--space-4) 0;
  overflow: auto;
  border-top: 1px solid var(--line-strong);
  border-bottom: 1px solid var(--line-strong);
  font-family: var(--font-mono);
  font-size: var(--text-code);
  line-height: 1.3;
}

/* Inside the wrapper the margins live on .code-block; the extra top
   padding keeps the label off the first line of code. */
.article-body .code-block pre {
  margin: 0;
  padding-top: var(--space-5);
}

.article-body pre code {
  padding: 0;
  background: none;
  font-size: inherit;
}

/* 参考站原样：`margin: 0; padding: 0 1em; color: #777; border-left: 0.25em
   solid #ddd`。不放大、不倾斜——所以 CJK 那条「中文不用斜体」的例外也没用
   武之地了，但留着，改回斜体时还要它。 */
.article-body blockquote {
  margin: var(--space-5) 0;
  padding: 0 1em;
  border-left: 0.25em solid var(--line-strong);
  color: var(--muted);
}

.article-body blockquote p:last-child {
  margin-bottom: 0;
}

/* CJK has no italics — synthetic obliquing just smears the glyphs. */
:lang(zh) .article-body blockquote {
  font-style: normal;
}

.article-body .callout {
  margin: var(--space-5) 0;
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid var(--text);
  background: var(--code-bg);
}

.article-body .callout p {
  margin: var(--space-2) 0 0;
}

.article-body .callout p:first-child {
  margin-top: 0;
}

.article-body .callout-title {
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--w-semibold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

/* 参考站的正文图片没有边框也没有圆角（`.figure` 是靠上下两条线框住的，
   见下面的 figcaption）。 */
.article-body img {
  height: auto;
  margin: var(--space-5) 0;
}

/* Section break: a short ink dash, flush left — the modernist stand-in
   for a full-width hairline or a centered asterisk. */
/* 通栏细淡规线，不是瑞士那种短粗墨色小节记号——2026-07-17 改成过后者
   （32px / 2px / --text），用户要回细淡的。 */
.article-body hr {
  height: 1px;
  margin: var(--space-7) 0;
  border: 0;
  background: var(--line);
}

.article-body .footnote-ref a {
  padding: 0 0.15em;
  color: var(--text);
}

/* markdown-it 会在脚注块前面吐一个 <hr class="footnotes-sep">。参考站的分隔
   线是画在 `.footnotes` 自己的 border-top 上的（列宽，不满幅），两个都留着
   就会出现「一条满幅线 + 100px 空白 + 一条列宽线」。这里把 hr 收掉，线交给
   下面的 .footnotes。 */
.article-body hr.footnotes-sep {
  display: none;
}

/* 参考站的 `.footnotes { margin-top: 100px; font-size: 12px; border-top:
   1px solid rgba(0,0,0,0.1) }`——上方是 100px 的空白加一条细线。 */
.article-body .footnotes {
  margin-top: 100px;
  padding-left: var(--space-5);
  border-top: 1px solid var(--line);
  font-size: var(--text-xs);
}

.article-body .footnotes li {
  margin: var(--space-1) 0;
}

.article-body .footnote-backref {
  margin-left: var(--space-1);
  text-decoration: none;
}

.article-body .table-wrap {
  overflow-x: auto;
  margin: var(--space-5) 0;
}

/* 参考站原样：14px，上 2px 黑 / 下 1px 黑，表头下一条 1px 黑。
   两处偏离：单元格**左对齐**而不是居中——参考站的表全是短数字，这里的表
   是整句中文，居中一换行就成了锯齿；上下各给 4px，一行单元格恰好占 24px，
   仍落在基线网格上。 */
.article-body table {
  margin: 0 auto;
  border-collapse: collapse;
  border-top: 2px solid black;
  border-bottom: 1px solid black;
  font-size: var(--text-sm);
  line-height: 1.5;
}

.article-body th,
.article-body td {
  padding: 4px 8px;
  border: 0;
  text-align: start;
  vertical-align: top;
}

.article-body th {
  border-bottom: 1px solid black;
}

.article-body ul.task-list {
  padding-left: var(--space-1);
  list-style: none;
}

.article-body .task-item {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
}

.article-body del {
  color: var(--muted);
}

/* 参考站文末没有任何收束记号，所以这个方块只保留 clear——浮动图（|left /
   |right）仍然要靠它闭合，去掉的话最后一段会包到图片旁边。 */
.article-end {
  clear: both;
}

/* ── Rich media figures ────────────────────────────────────── */
/* Figures break out of the reading column by default (see --content-figure).
   The variants below that set their own width — wide, full, left, right, and
   anything inside a media-row — each have to undo the centering transform, so
   keep this rule and their resets in sync. */
.article-body .media {
  position: relative;
  left: 50%;
  width: var(--figure-w);
  margin: var(--space-5) 0;
  transform: translateX(-50%);
}

/* Audio is a control strip, not an image — nothing to see at 48rem, and a
   player stretched past the prose reads as a layout bug. Keep it in column. */
.article-body .media-audio {
  left: auto;
  width: auto;
  transform: none;
}

.article-body .media img {
  margin: 0;
}

.article-body .media video,
.article-body .embed-frame {
  width: 100%;
  border: 1px solid var(--line);
  background: #000;
}

.article-body .media audio {
  width: 100%;
}

.article-body .embed-frame {
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.article-body .embed-frame iframe {
  width: 100%;
  height: 100%;
  border: 0;
}

.article-body .html-embed-frame {
  width: 100%;
}

.article-body .html-embed-frame iframe {
  display: block;
  width: 100%;
  /* Initial height before the embed reports its own (embed-autosize.js then
     sets the exact content height inline, so the text below sits right under
     it). Also the fallback when JS is off. */
  height: 320px;
  border: 0;
}

/* 参考站的 `.figure`：上下各一条 hsla(0,0%,0%,0.2)，20px 内边距，图注 13px
   正文色、上方 15px。带图注的图才有框线——所以这里的框线挂在有 figcaption
   的 figure 上，无图注的图（.figure-no-caption）只有上下留白。 */
.article-body .media:has(figcaption) {
  padding: 20px 0;
  border-top: 1px solid var(--line-strong);
  border-bottom: 1px solid var(--line-strong);
}

.article-body .media figcaption {
  padding-top: 15px;
  font-size: var(--text-code);
  text-align: left;
}

/* ── Figure variants (`![alt|wide]` …) ─────────────────────── */
/* wide: break out symmetrically from the reading column to the shell's
   full width. */
.article-body .media--wide {
  position: relative;
  left: 50%;
  width: var(--figure-w-wide);
  transform: translateX(-50%);
}

/* full: bleed edge-to-edge across the viewport; the caption comes back
   to the shell's width so it stays anchored to the layout. */
.article-body .media--full {
  position: relative;
  left: 50%;
  width: var(--vw);
  transform: translateX(-50%);
}

.article-body .media--full img {
  border-left: 0;
  border-right: 0;
  border-radius: 0;
}

.article-body .media--full figcaption {
  width: min(calc(100% - 2.5rem), var(--content-wide));
  margin-left: auto;
  margin-right: auto;
}

/* left / right: small figure with text running around it. A float can't also
   be pulled to the column's center line, so these undo the breakout — they're
   sized against the prose they wrap into, not against the shell. */
.article-body .media--left,
.article-body .media--right {
  left: auto;
  width: min(45%, 16rem);
  margin-top: var(--space-2);
  margin-bottom: var(--space-4);
  transform: none;
}

.article-body .media--left {
  float: left;
  margin-right: var(--space-5);
}

.article-body .media--right {
  float: right;
  margin-left: var(--space-5);
}

/* half: consecutive figures share one row (grouped at build time). The row is
   what breaks out; the figures inside are grid items, so they go back to
   filling their track — a translated grid item would slide out of its cell. */
.article-body .media-row {
  display: grid;
  position: relative;
  left: 50%;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  width: var(--figure-w);
  margin: var(--space-5) 0;
  transform: translateX(-50%);
  align-items: start;
}

.article-body .media-row .media {
  left: auto;
  width: auto;
  margin: 0;
  transform: none;
}

/* plain: borderless, for diagrams and transparent artwork.
   frame: a paper-tinted mat around screenshots that would otherwise
   bleed into the page ground. */
.article-body .media--plain img,
.article-body .media--frame img {
  border: 0;
}

.article-body .media--frame {
  padding: var(--space-4);
  background: var(--code-bg);
  border-radius: var(--radius-card);
}

/* ── Article footer: tags / prev-next / related ────────────── */
.article-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-6);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
}

.article-tags .tag {
  color: var(--text);
  background: var(--panel);
  padding: 0.15rem 0.55rem;
  text-decoration: none;
}

.article-tags .tag:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ── Footer ────────────────────────────────────────────────── */
.site-footer {
  position: relative;
  margin-top: var(--space-10);
  padding: var(--space-6) 0;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
}

.site-footer::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: var(--vw);
  height: 100%;
  transform: translateX(-50%);
  background: var(--panel);
  z-index: -1;
}

.site-footer p {
  margin: 0 0 var(--space-1);
}

.footer-links {
  margin-top: var(--space-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

/* ── 404 ───────────────────────────────────────────────────── */
.not-found h1 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-display);
  font-weight: var(--w-bold);
}

.not-found-recent {
  margin-top: var(--space-6);
}

/* ── Search overlay ────────────────────────────────────────── */
.search-overlay {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 12vh var(--space-4) var(--space-4);
  background: rgba(34, 32, 28, 0.28);
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease;
}

.search-overlay.open {
  opacity: 1;
  pointer-events: auto;
}

/* A soft floating card rather than a bordered panel: rounded, no outline,
   lifted off the dimmed page by the shadow alone. `overflow: hidden` is what
   keeps the results list from squaring off the bottom corners. */
.search-box {
  width: min(38rem, 100%);
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--bg);
  box-shadow: 0 18px 48px rgb(17 17 17 / 0.2);
  /* 卡片自己再浮一档，比罩层多 100ms 收尾，弹到位时罩层已经黑透了——这点
     错位就是「卡片落在暗底**之上**」而不是「两层一起亮起来」的来源。
     透明度整个交给罩层：这里再来一次会把卡片压成半透明，底下的页面透出来。 */
  transform: translateY(calc(-1 * var(--rise-dist))) scale(0.985);
  transition: transform 260ms var(--ease-spring);
}

.search-overlay.open .search-box {
  transform: none;
}

/* The field reads as one line of type: magnifier, then the placeholder, with
   room around them. It carries no rule of its own — with nothing typed the
   card is the field and nothing else. */
.search-input-wrap {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
}

.search-icon {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  color: var(--soft);
}

.search-input {
  width: 100%;
  border: 0;
  outline: 0;
  background: transparent;
  font-family: var(--font-sans);
  font-size: 1.05rem;
}

.search-input::placeholder {
  color: var(--muted);
}

.search-results {
  max-height: 24rem;
  overflow: auto;
}

/* The divider under the field belongs to the results, so it only exists
   while there are results to divide from. */
.search-results:not(:empty) {
  border-top: 1px solid var(--line);
}

.search-result-item {
  display: block;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
  text-decoration: none;
}

.search-result-item:last-child {
  border-bottom: 0;
}

.search-result-item:hover,
.search-result-item.active {
  background: var(--code-bg);
}

.search-result-item span {
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.search-result-item h4 {
  margin: 0.15rem 0;
  font-size: var(--text-base);
  font-weight: var(--w-bold);
}

.search-result-item p {
  margin: 0;
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.search-result-item mark {
  padding: 0 0.1em;
  background: var(--selection);
  color: inherit;
}

.search-empty {
  padding: var(--space-6) var(--space-4);
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  text-align: center;
}

/* ── Math (KaTeX) ──────────────────────────────────────────── */
.katex-display {
  margin: var(--space-5) 0;
  padding: var(--space-1) 0;
  overflow-x: auto;
  overflow-y: hidden;
}

/* ── Responsive ────────────────────────────────────────────── */
@media (max-width: 600px) {
  body {
    font-size: 17px;
  }

  .site-shell {
    width: calc(100% - 1.75rem);
    padding-top: var(--space-5);
  }

  /* 80px 是按桌面首屏定的，手机上导航条到分类行之间空掉近四分之一屏。 */
  .blog-shell {
    margin: var(--space-8) 0 var(--space-9);
  }

  /* 表格在窄屏里横向滚动，滚到底时最后一列不要贴住边缘。 */
  .article-body .table-wrap {
    padding-bottom: var(--space-1);
  }

  /* The date rail folds above the entry rather than beside it. */
  .post-list li {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
    padding: var(--space-3) 0;
  }

  .post-kind {
    display: none;
  }

  /* Small screens: floats and rows stack into the single column. */
  .article-body .media--left,
  .article-body .media--right {
    float: none;
    width: 100%;
    margin-left: 0;
    margin-right: 0;
  }

  .article-body .media-row {
    grid-template-columns: 1fr;
  }

  /* Too few characters per line for justification to distribute — the
     leftover space lands between glyphs instead. Ragged right reads better. */
  :lang(zh) .article-body p {
    text-align: start;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }

  /* The view-transition pseudo-elements live outside the document tree,
     so the `*` rule above never reaches them — they need their own. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* ── Print ─────────────────────────────────────────────────── */
/* Articles print like the publication they imitate: chrome gone, black
   ink on white paper, masthead and text as they read on screen. */
@media print {
  .skip-link,
  .home-tools,
  .topbar,
  .search-overlay,
  .site-footer {
    display: none !important;
  }

  body {
    background: #fff;
    color: #000;
  }

  .site-shell,
  .site-shell--wide {
    width: auto;
    padding: 0;
  }

  .article-header::after {
    display: none;
  }

  .article-body pre {
    white-space: pre-wrap;
    overflow: visible;
  }

  .article-body a {
    text-decoration: none;
  }

  .article-body .media--wide,
  .article-body .media--full {
    position: static;
    width: 100%;
    margin-left: 0;
    transform: none;
  }
}

/* ── Page loader ───────────────────────────────────────────────
   Full-screen wave shown only when a load runs long (src/loading.js
   reveals it after a grace period and fades it out on `load`). Sound-wave
   path is pre-rendered into the markup; the draw-in is pure CSS. */
.page-loader {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  opacity: 0;
  transition: opacity 0.35s ease;
}
.page-loader.is-visible {
  opacity: 1;
}
.page-loader.is-hidden {
  opacity: 0;
}
.page-loader svg {
  width: min(280px, 70vw);
  height: auto;
  overflow: visible;
  animation: pl-reveal 2s ease-in-out infinite alternate;
}
@keyframes pl-reveal {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}
@media (prefers-reduced-motion: reduce) {
  .page-loader svg {
    animation: none;
    clip-path: none;
  }
}
