@font-face{font-family:"Source Serif 4";font-style:normal;font-weight:400;font-display:swap;src:url("/assets/fonts/source-serif-4-400.71cf4d3d.woff2") format("woff2");unicode-range:U+0000-00FF,U+0100-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-20BF,U+2100-2138,U+2190-21FF,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Source Serif 4";font-style:italic;font-weight:400;font-display:swap;src:url("/assets/fonts/source-serif-4-400i.99ba0e49.woff2") format("woff2");unicode-range:U+0000-00FF,U+0100-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-20BF,U+2100-2138,U+2190-21FF,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Source Serif 4";font-style:normal;font-weight:600;font-display:swap;src:url("/assets/fonts/source-serif-4-600.1bec29ba.woff2") format("woff2");unicode-range:U+0000-00FF,U+0100-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-20BF,U+2100-2138,U+2190-21FF,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Source Serif 4";font-style:normal;font-weight:700;font-display:swap;src:url("/assets/fonts/source-serif-4-700.0d5cf4e6.woff2") format("woff2");unicode-range:U+0000-00FF,U+0100-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-20BF,U+2100-2138,U+2190-21FF,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Noto Serif SC";font-style:normal;font-weight:400;font-display:swap;src:url("/assets/fonts/noto-serif-sc-400.f90f02a9.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;size-adjust:93%}
@font-face{font-family:"Noto Serif SC";font-style:normal;font-weight:600;font-display:swap;src:url("/assets/fonts/noto-serif-sc-600.e1b263d9.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;size-adjust:93%}
@font-face{font-family:"Noto Serif SC";font-style:normal;font-weight:700;font-display:swap;src:url("/assets/fonts/noto-serif-sc-700.ce375b60.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;size-adjust:93%}
@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:400;font-display:swap;src:url("/assets/fonts/source-sans-3-400.40748b10.woff2") format("woff2");unicode-range:U+0000-00FF,U+0100-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-20BF,U+2100-2138,U+2190-21FF,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:600;font-display:swap;src:url("/assets/fonts/source-sans-3-600.d54c8346.woff2") format("woff2");unicode-range:U+0000-00FF,U+0100-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-20BF,U+2100-2138,U+2190-21FF,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:700;font-display:swap;src:url("/assets/fonts/source-sans-3-700.e49d0b18.woff2") format("woff2");unicode-range:U+0000-00FF,U+0100-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-20BF,U+2100-2138,U+2190-21FF,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Noto Sans SC";font-style:normal;font-weight:400;font-display:swap;src:url("/assets/fonts/noto-sans-sc-400.4df4d7f9.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;size-adjust:94%}
@font-face{font-family:"Noto Sans SC";font-style:normal;font-weight:600;font-display:swap;src:url("/assets/fonts/noto-sans-sc-400.4df4d7f9.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;size-adjust:94%}
@font-face{font-family:"Noto Sans SC";font-style:normal;font-weight:700;font-display:swap;src:url("/assets/fonts/noto-sans-sc-700.c9a51eda.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;size-adjust:94%}
@font-face{font-family:"Source Code Pro";font-style:normal;font-weight:400;font-display:swap;src:url("/assets/fonts/source-code-pro-400.a315ac57.woff2") format("woff2");unicode-range:U+0000-00FF,U+0100-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-20BF,U+2100-2138,U+2190-21FF,U+2212,U+2215,U+FEFF,U+FFFD}
/* ============================================================
   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: the slow-load wave, the caret on the home status
   line, and the page transition. The one exception is the
   Telegram and Email marks moving under the cursor — decoration,
   kept deliberately, and gated on prefers-reduced-motion.

   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 ──────────────────────────────────────────
     国际现代主义配色：纯白纸、墨黑字、零彩度灰阶，加一个科技蓝。原来那
     套暖色（Anthropic 系 #FBF9F5 / 陶土 #D87756，中性 40°、陶土 20° 的构
     造式）整套换掉——国际主义的立场就是不带体温的中性，所以中性色不再
     「往暖边偏一点」，而是 r = g = b，彩度恒等于 0。要加新档位只加明度，
     别引入任何色偏：偏一格，那格就会跟蓝抢「这里有颜色」的注意力。

     **蓝只做线和面，不做字**，这是这套配色唯一的用色规则。给蓝的是：h3 竖
     线、rail 竖线、blockquote 竖线、文末方块、链接下划线、focus ring、状态
     行光标、ONGOING 那类状态方框的底、拖选高亮。不给蓝的是**任何字**——
     链接也是墨色 + 蓝下划线。理由是一段话里几个蓝词会把阅读打散，而下划线
     贴着基线，不进正文的灰度。列表 marker 是数字和符号，按字算，走 --muted。

     没有第二个色相可借，--line 和 --panel 只能靠明度分开（暖配色里这两个
     明度几乎同级，全靠中性/陶土的色相差在分），所以 --panel 比旧的 wash
     压深了一档，对 bg 从 1.30 到 1.37。

     三级文字（--text / --soft / --muted）+ 两级描边 + 蓝两档；面
     性底色三个（--code-bg / --panel / --selection）。文字色的验收线仍是
     WCAG AA 的 4.5:1，因为这几个色全用在 --text-xs / --text-sm 的功能性
     小字上；约束条件不是 --bg，而是各自实际压着的最暗面色：
       --muted 落到 --panel（.blog-card-link:hover 下的日期/分类）→ 5.11
       --text 落到 --panel（tag）→ 13.77
       --bg 落到 --accent（ONGOING 方框，反白）→ 5.00
       --muted 落到 --selection（最浅的可被拖选文字就是它）→ 5.57
     动任何一个面色，都要回来重算压在它上面的前景色。 */
  --bg: #ffffff;
  --text: #111111;
  --soft: #3a3a3a;
  --muted: #595959;
  --line: #e3e3e3;
  --line-strong: #c9c9c9;
  /* 科技蓝，取 IBM Blue 60——Carbon 本来就是长在瑞士栅格上的系统，这个蓝
     放进这套骨架不用调。它只出现在线和面上（用色规则见上），所以唯一的对
     比约束来自 ONGOING 那类实心方框：--bg 压在它上面是 5.00，够 --text-xs
     的 AA。要换深浅先重算这一条。 */
  --accent: #0f62fe;
  /* 链接下划线。链接的字是墨色，这条线是它唯一的标识，所以要比 --line 那档
     规线重不少：3.01:1，跟暖配色时代那条陶土下划线同强度。hover 时整条换成
     --accent（更深一档）——字不动，动线。改这个值前先确认不低于 2.5:1。 */
  --link-line: #6b93ea;
  /* 中性面色：tag、active pill、封面图底板。实心状态方框走 --accent，不走
     这个。它同时是 --muted 和 --text 的背景，再压深就先压穿 --muted 的 4.5。 */
  --panel: #dcdcdc;
  --code-bg: #f4f4f4;
  /* 拖选高亮。是面，所以可以是蓝的。它承任何被拖选的文字，最浅的是
     --muted，所以对 --bg 不能超过 1.53（现在 1.26）。 */
  --selection: #d9e6ff;

  /* ── 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 ──────────────────────────────────────────── */
  --text-xs: 0.75rem; /* uppercase labels */
  --text-sm: 0.875rem; /* nav, dates, footer, tags */
  /* CJK 走 size-adjust 缩到 93/94%（见 build.mjs 的 FONT_FACES），这里按其
     倒数上调，中文正文的绝对大小保持不变，拉丁则回到 Source Serif 4 的
     x-height（0.475 em，偏小）本来就需要的字号。两者动其一都要重算。 */
  --text-base: 1.13rem; /* reading text */
  /* Code sits a notch under the prose rather than two: Source Code Pro's
     x-height runs taller than Source Serif 4's, so 0.95rem reads level
     with 1.13rem serif instead of receding the way --text-sm did. */
  --text-code: 0.95rem;
  --text-md: 1.35rem; /* lead, h3, section headings */
  --text-lg: 1.8rem; /* h2 */
  --text-xl: 2rem; /* h1 in body */
  --text-2xl: clamp(2.25rem, 5vw, 3.3rem); /* page titles */
  --text-display: clamp(2.75rem, 7vw, 5.25rem); /* article titles */
  --text-masthead: clamp(3.2rem, 9vw, 6.4rem); /* home nameplate */

  --leading-tight: 1.2; /* display */
  --leading-snug: 1.35; /* headings */
  --leading-body: 1.75; /* interface text */
  --leading-reading: 1.85; /* article prose */

  /* 只加载这三档字重；要用新档位先去 build.mjs 的 FONT_FACES 和
     subset-fonts.mjs 的 FACES 里加上对应 subset 并重跑脚本，
     否则浏览器会伪造字重。 */
  --w-regular: 400;
  --w-semibold: 600;
  --w-bold: 700;

  --tracking-tight: -0.01em; /* large serif titles */
  --tracking-caps: 0.06em; /* uppercase labels */

  /* ── Layout ──────────────────────────────────────────────── */
  /* 正文列宽 = 阅读行长。按中文定：40rem ÷ 16.8px（--text-base 经 CJK
     size-adjust 后的实际字号）≈ 38 字/行，英文同宽约 70 字符，两边都在
     舒适区内。动 --text-base 或 size-adjust 都要回来重算这个值。 */
  --content: 40rem;
  --content-wide: 62rem;

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

  /* ── Font stacks ─────────────────────────────────────────────
     全套同族：思源宋体/黑体（Source Han）与 Source Serif 4 /
     Source Sans 3 / Source Code Pro 是 Adobe+Google 配对设计的
     超家族，x 高度和灰度互相咬合。阅读文字走衬线，导航、元信息
     等功能性文字走无衬线，代码走 Source Code Pro。 */
  --font-serif: "Source Serif 4", "Noto Serif SC", Georgia, "Songti SC",
    serif;
  --font-sans: "Source Sans 3", "Noto Sans SC", "Helvetica Neue", Arial,
    sans-serif;
  --font-mono: "Source Code Pro", ui-monospace, "SF Mono", "Menlo",
    "Consolas", monospace;
}

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

html {
  scroll-behavior: smooth;
}

body {
  min-height: 100vh;
  margin: 0;
  overflow-x: hidden;
  color: var(--text);
  background: var(--bg);
  font-family: var(--font-serif);
  font-size: 18px;
  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;
}

::view-transition-new(page-content) {
  animation: page-rise-in 220ms ease both;
}

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

@keyframes page-rise-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

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

a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--link-line);
}

/* Hover moves the rule, never the type. */
a:hover {
  text-decoration-color: var(--accent);
}

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. */
.topbar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
  margin: 0 0 var(--space-9);
  padding-bottom: var(--space-5);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
}

/* Full-bleed divider lines: the content stays in the centered column, but the
   rule under the navbar / article title block spans the whole viewport
   width. (article-body used to get one too, but it always lands after
   whatever the last child is — the last paragraph, or now the footnotes
   list — which reads as a stray extra line, so it's been dropped.) */
.topbar,
.article-header {
  position: relative;
}

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

/* The navbar's full-bleed rule is the one heavy Swiss stroke on every
   page; all other dividers stay hairlines. The article title rule matches
   it — a second, equally confident stroke closing the header block. */
.topbar::after,
.article-header::after {
  height: 3px;
  background: var(--text);
}

/* 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(--soft);
  text-decoration: none;
}

.topbar a:hover {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.topbar a.active {
  padding: 0.15rem 0.6rem;
  background: var(--panel);
  color: var(--text);
  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(--soft);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
}

/* 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(--soft);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  text-decoration: none;
}

.lang-switch:hover {
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ── 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, 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 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 home-settle {
    0%,
    100% {
      transform: translateY(0);
    }

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

/* ── Shell ─────────────────────────────────────────────────── */
.site-shell {
  width: min(calc(100% - 2.5rem), var(--content));
  margin: 0 auto;
  padding: var(--space-6) 0 var(--space-11);
}

/* Article and list pages: wider shell so the meta rail + main grid fits. */
.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: a plain stack of serif links under the nameplate. */
.home-links {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  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 two marks sit side by side on one row of the list, and a notch under
   the reading size the text links use — they are a contact block, not two
   more entries in the contents. */
.home-links-marks {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-1);
  font-size: var(--text-base);
}

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

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

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

.home-status-meta {
  min-height: calc(1em * 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 card grid rather than a rule-separated list: cover plate on top,
   then date / title / category stacked in the card body. The category
   filter is a dropdown above the shelf, so the left rail is gone and the
   grid gets the full wide shell. (The home page and 404 still use the
   .post-list rows below — those are mixed lists with no covers.) */
/* Category row above the shelf: every category laid out at once, centered,
   in the page's serif so it reads as a line of text rather than a control.
   The current one drops to --muted instead of gaining a box — the row keeps
   the same shape on every category page. */
.cat-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3) var(--space-6);
  margin-bottom: var(--space-8);
}

.cat-nav a {
  color: var(--text);
  font-size: var(--text-base);
  text-decoration: none;
}

.cat-nav a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

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

.blog-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.blog-card {
  display: flex;
}

.blog-card-link {
  display: flex;
  flex-direction: column;
  width: 100%;
  /* Not for a corner clip — the 3:2 plate below crops taller covers. */
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--code-bg);
  color: inherit;
  text-decoration: none;
}

.blog-card-link:hover {
  border-color: var(--line-strong);
  background: var(--panel);
}

/* 3:2 plate, cropped — cover art is authored at assorted ratios and the
   grid only reads as a shelf if every card's plate is the same height. */
.blog-card-visual {
  aspect-ratio: 3 / 2;
  background: var(--panel);
}

.blog-card-visual img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.blog-card-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-5);
  padding: var(--space-5);
}

.blog-card-date {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

.blog-card-title {
  margin: 0;
  font-size: var(--text-md);
  font-weight: var(--w-regular);
  line-height: var(--leading-snug);
  text-wrap: pretty;
}

.blog-card-cat {
  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;
}

/* ── 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-2xl);
  font-weight: var(--w-bold);
  letter-spacing: var(--tracking-tight);
  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 ───────────────────────────────────────────────── */
/* Centered masthead under the heavy navbar rule: oversized serif title,
   uppercase meta line beneath it, then the reading column centered under
   both. */
.article-header {
  margin-bottom: var(--space-8);
  padding-bottom: var(--space-7);
  text-align: center;
}

/* Capped to the reading measure so long zh titles wrap into a compact
   centered block instead of sprawling across the full wide shell. */
.article-header h1 {
  max-width: var(--content);
  margin: 0 auto;
  font-size: var(--text-display);
  font-weight: var(--w-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}

/* Section, date and links, set as one centered line under the title. */
.article-meta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  margin-top: var(--space-4);
  color: var(--soft);
  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;
}

/* inline-block keeps the separator out of the preceding link's underline. */
.article-meta > :not(:first-child)::before {
  content: "·";
  display: inline-block;
  margin-right: var(--space-3);
  color: var(--line-strong);
}

.article-meta a {
  color: var(--text);
  font-size: var(--text-sm);
  text-decoration: none;
}

.article-meta a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

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

.entry-cover-article {
  margin: 0 0 var(--space-5);
}

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

/* ── Article body (prose) ──────────────────────────────────── */
.article-body {
  font-size: var(--text-base);
  line-height: var(--leading-reading);
  padding-bottom: var(--space-8);
  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;
}

.article-body h1 {
  font-size: var(--text-xl);
}
.article-body h2 {
  margin-top: var(--space-8);
  font-size: var(--text-lg);
}

/* 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 — and going bigger would collide
   with h2. It gets a hanging terracotta stroke instead: the same mark as
   the rail border and the end square, in the gutter beside the column, so
   the heading text stays flush with the prose. Tighter below than above,
   so it reads as belonging to what follows. */
.article-body h3 {
  position: relative;
  margin-bottom: var(--space-3);
  font-size: var(--text-md);
}

.article-body h3::before {
  content: "";
  position: absolute;
  top: 0.3em;
  left: calc(-1 * var(--space-4));
  width: 3px;
  height: 0.85em;
  background: var(--accent);
}
/* h4 is not a serif heading but a functional label — the same uppercase
   sans grammar as the rail and table headers. Sub-steps inside a numbered
   section ("Standard", "任务一 | …") read as specimen labels instead of
   bold text drowning in the prose. */
.article-body h4 {
  margin: var(--space-6) 0 var(--space-3);
  color: var(--soft);
  font-family: var(--font-sans);
  /* Level with the prose, not under it: as a sub-step label it was the
     only heading on the page set smaller than the text it introduced.
     The sans face, caps and tracking already separate it from h3. */
  font-size: var(--text-base);
  font-weight: var(--w-semibold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.article-body .article-lead {
  margin: 0 0 var(--space-6);
  color: var(--soft);
  font-size: var(--text-md);
  font-weight: var(--w-semibold);
  line-height: var(--leading-body);
}

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

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

/* Markers are glyphs, so they stay out of the blue and sit in the grey
   text scale — the rules and blocks around them carry the accent. */
.article-body li::marker {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.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;
}

.article-body pre {
  margin: var(--space-5) 0;
  padding: var(--space-4);
  overflow: auto;
  background: var(--code-bg);
  border: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--text-code);
  line-height: 1.6;
}

/* 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;
}

.article-body blockquote {
  margin: var(--space-5) 0;
  padding: 0.1rem 0 0.1rem var(--space-4);
  border-left: 3px solid var(--accent);
  color: var(--soft);
  font-size: var(--text-md);
  font-style: italic;
}

.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;
}

.article-body img {
  height: auto;
  margin: var(--space-5) 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

/* 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);
}

.article-body hr.footnotes-sep {
  position: relative;
  left: 50%;
  width: 100vw;
  height: 1px;
  transform: translateX(-50%);
  margin: var(--space-7) 0 var(--space-6);
  background: var(--line);
}

.article-body .footnotes {
  padding-left: var(--space-5);
  font-size: var(--text-sm);
  color: var(--soft);
}

.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;
}

/* Modernist data table: a heavy rule on top, hairlines between rows,
   no vertical borders, no fills — structure over decoration. */
.article-body table {
  width: 100%;
  border-collapse: collapse;
  border-top: 2px solid var(--text);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

.article-body th,
.article-body td {
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}

.article-body th {
  border-bottom: 1px solid var(--line-strong);
  color: var(--soft);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--w-semibold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.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);
}

/* Editorial end mark: a small blue square closes the piece. */
.article-end {
  width: var(--space-2);
  height: var(--space-2);
  margin-top: var(--space-6);
  background: var(--accent);
  clear: both;
}

/* ── Rich media figures ────────────────────────────────────── */
.article-body .media {
  margin: var(--space-5) 0;
}

.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;
}

.article-body .media figcaption {
  margin-top: var(--space-2);
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  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: min(var(--content-wide), calc(100vw - 2.5rem));
  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: 100vw;
  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. */
.article-body .media--left,
.article-body .media--right {
  width: min(45%, 16rem);
  margin-top: var(--space-2);
  margin-bottom: var(--space-4);
}

.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). */
.article-body .media-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  margin: var(--space-5) 0;
  align-items: start;
}

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

/* 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: 100vw;
  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);
}

/* 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);
  }

  /* 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;
  }

  /* No gutter left to hang the h3 stroke in — indent the heading instead. */
  .article-body h3 {
    padding-left: var(--space-4);
  }

  .article-body h3::before {
    left: 0;
  }
}

@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;
  }
}
