/*
 * 墨 Ink — Lumo 内置默认主题（第三版，2026-09-14）
 *
 * 设计方向：为中文长文阅读而做，不追随通用 UI 风格。
 *
 * 这一版把前两版整个推翻：前台与后台（Console）改用同一套设计语言。
 * 一个站长白天在 Console 里写稿、晚上在前台读自己的站，不该觉得那是两个产品。
 * 所以三层 token 的结构、中性梯级的取值、圆角的四档、阴影的三档、
 * 按钮与输入框的形态、浮层的骨架，全部与 console/src/styles/tokens.css 对齐。
 *
 * 两处**故意不一致**，各有理由：
 *
 *   1. 材质分野。界面走无衬线（与 Console 同一条字体栈），
 *      但**读者要读的东西走衬线**——刊名、文章标题、正文。
 *      Console 是工作台，前台是读物；把正文也换成无衬线，这个站就没有读物的样子了。
 *      分界线是可执行的一句话：要读的用衬线，要操作的用无衬线。
 *
 *   2. 尺寸上移一档。Console 的界面基准是 14px、按钮 36px 高——那是密集工作区的密度。
 *      前台面对的是随手点进来的访客，故界面基准 15px、可点元素 44px。
 *
 * 配色由站长在后台选**一个**主题色，其余全部派生（见下方 --seal 一节）。
 */

/* ==================================================================
   第 1 层 · Primitive —— 原始值，无语义
   ================================================================== */

:root {
  /*
   * 中性梯级。与 Console 逐值相同（色相 286，彩度 0.001–0.010：
   * 肉眼不可辨的一点冷调，只用来避开纯灰在某些屏上发闷的观感）。
   * 抄值而不是重新取色，是因为「同一套灰」正是这次改版要的东西。
   */
  --n-0: oklch(1 0 0); /* #FFFFFF */
  --n-50: oklch(0.9851 0 0); /* #FAFAFA */
  --n-100: oklch(0.9674 0.0013 286.4); /* #F4F4F5 */
  --n-workspace: oklch(0.9467 0.0027 286.3); /* #EDEDEF 工作区底色 */
  --n-150: oklch(0.9316 0.0027 286.3); /* #E8E8EA */
  --n-200: oklch(0.9073 0.0027 286.3); /* #E0E0E2 */
  --n-300: oklch(0.8554 0.0041 286.3); /* #CFCFD2 */
  --n-400: oklch(0.6979 0.0087 286.2); /* #9D9DA3 */
  --n-500: oklch(0.5088 0.0094 286.1); /* #65656B */
  --n-850: oklch(0.2359 0.0038 286.1); /* #1E1E20 */
  --n-900: oklch(0.2011 0.0039 286); /* #161618 */
  --n-950: oklch(0.1693 0.0041 286); /* #0F0F11 */

  /* 只服务于暗色的几块 */
  --n-dark-inset: oklch(0.1785 0.0041 286); /* #111113 */
  --n-dark-hover: oklch(0.249 0.0056 286); /* #212124 */
  --n-dark-active: oklch(0.2825 0.0073 285.9); /* #29292D */
  --n-dark-line: oklch(0.278 0.0055 286); /* #28282B */
  --n-dark-line-strong: oklch(0.3661 0.0102 285.9); /* #3E3E44 */

  /*
   * 墨（暖调，色相 145）。这是主题名字的由来，也是 Console 里
   * --color-ink-light / --color-ink-dark 的同源值：文字与主按钮都用它。
   * 中性梯级偏冷，墨偏暖——两者放在一起，字看着是「写上去的」而不是「打上去的」。
   */
  --ink-light: oklch(0.2264 0.0073 145.3); /* #1A1D1A */
  --ink-light-hover: oklch(0.3 0.008 145.3); /* #2B2F2B */
  --ink-dark: oklch(0.935 0.0034 145.5); /* #E8EAE8 */
  --ink-dark-hover: oklch(0.97 0.002 145.5); /* #F4F5F4 */

  /*
   * 主题色。**站长能改的只有这一个**，其余（悬停、活动、淡底、暗色下的提亮）
   * 全部由它算出来——见第 2 层的 --seal 一节。默认值与 Console 的强调色同号。
   */
  --theme: #1b3a6b;

  /* 状态三色。与 Console 同值：它们表达的是同一批状态 */
  --ok-600: oklch(0.4902 0.0852 158.4);
  --ok-300: oklch(0.7401 0.106 156.3);
  --warn-600: oklch(0.5078 0.108 73.3);
  --warn-300: oklch(0.7613 0.1248 81.9);
  --danger-600: oklch(0.4693 0.1648 27.6);
  --danger-300: oklch(0.7315 0.1157 23.5);

  /*
   * 圆角四档，与 Console 逐值相同：
   * 控件与卡片 4px、浮层 6px（大一档以表达高度）。
   * 印面 2px：页眉那枚方印头像是它唯一的用处，比控件更方（印是刻出来的，不是做的按钮）。
   */
  --corner-control: 4px;
  --corner-card: 4px;
  --corner-overlay: 6px;
  --corner-seal: 2px;
  --corner-full: 9999px;

  /* 间距：4px 基准的模块化梯级 */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.25rem;
  --s6: 1.5rem;
  --s7: 1.75rem;
  --s8: 2rem;
  --s10: 2.5rem;
  --s12: 3rem;
  --s16: 4rem;

  /* 字号。比 Console 整体上移一档（界面 15px / 正文 17px），理由见文件头 */
  --fs-xs: 0.8125rem; /* 13px 元信息、角标 */
  --fs-sm: 0.875rem; /* 14px 次要说明 */
  --fs-base: 0.9375rem; /* 15px 界面默认 */
  --fs-md: 1rem; /* 16px 输入框 */
  --fs-lg: 1.0625rem; /* 17px 正文 */
  --fs-xl: 1.25rem; /* 20px 卡片与浮层标题 */
  --fs-2xl: 1.5rem; /* 24px 列表项标题、归档标题 */

  --lh-tight: 1.3;
  --lh-ui: 1.6;
  --lh-prose: 1.85;

  /*
   * 字体栈。本主题不依赖任何 Web 字体 CDN——默认主题必须能离线跑。
   * --font-ui 与 Console 的 --typeface-ui 逐字相同。
   * --font-display 是标题槽位，站长开启自托管思源宋体时由 base.html 覆盖。
   */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC",
    sans-serif;
  --font-serif: "Songti SC", "SimSun", "Noto Serif CJK SC", "Source Han Serif SC",
    Georgia, "Times New Roman", serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;
  --font-display: var(--font-serif);

  /* 动效：只做回应用户动作的那一类，时长按位移距离分档（与 Console 同值） */
  --t-fast: 130ms;
  --t-normal: 190ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /*
   * 正文列宽。站长设的是「多少个汉字」，是个**纯数字**（见 base.html 的注入），
   * 在这里乘以正文字号得到真实宽度。
   * 不让站长直接填 em：那样它的基准会随所在元素的字号漂移，
   * 页眉（15px）与正文（17px）上的同一个 36em 不是同一个宽度。
   */
  --measure-ch: 36;
  --measure: calc(var(--measure-ch) * var(--fs-lg));
  /*
   * 页面外壳：正文列 + 卡片左右内边距 + 页面左右留白。
   * 三项都要算进来，因为 box-sizing 是 border-box：少算一项，
   * 卡片里的正文就比站长设的栏宽窄掉那一项。
   */
  --card-pad: var(--s8);
  --gutter: var(--s4);
  --shell: calc(var(--measure) + var(--card-pad) * 2 + var(--gutter) * 2);

  --z-sticky: 20;
}

/* ==================================================================
   第 2 层 · Semantic —— 用途别名。明暗主题在此切换
   ================================================================== */

/*
 * color-scheme 决定表单控件、滚动条与 UA 默认色跟着哪一边走。
 * 三种取值各自对应站长在后台选的那一档。
 */
:root {
  color-scheme: light;
}
:root[data-scheme="auto"] {
  color-scheme: light dark;
}
:root[data-scheme="dark"] {
  color-scheme: dark;
}

:root {
  /* 表面三层：器底 → 面板 → 嵌入区。与 Console 的骨架一致 */
  --chrome: var(--n-workspace); /* 页面底色 */
  --surface: var(--n-0); /* 页眉、页脚、卡片 */
  --surface-raised: var(--n-50); /* 卡片内的次级区块 */
  --surface-hover: var(--n-50);
  --surface-active: var(--n-150);
  --surface-inset: var(--n-100); /* 嵌在卡片里的代码块、行内码 */

  --fg: var(--ink-light); /* 主文字 */
  --fg-muted: var(--n-500); /* 次要文字：说明、时间、分类 */
  /*
   * 最淡的一档。**只用在不承载信息的地方**：禁用态、省略号、锚点符号。
   * 它在纸底上只有 2.7:1，任何读者需要读懂的字都不能用它——
   * 元信息、时间、说明文字一律用上面那档 --fg-muted（5.8:1）。
   * Console 那边同样只拿它做占位符与禁用态，这条纪律是跟着抄过来的。
   */
  --fg-subtle: var(--n-400);

  /* 墨是主行动色：主按钮、置顶角标 */
  --action: var(--ink-light);
  --action-hover: var(--ink-light-hover);
  --action-on: var(--n-0);

  --line: var(--n-200); /* 分隔线与卡片描边 */
  --line-strong: var(--n-300); /* 输入框边框：需要看得出边界 */

  --ok: var(--ok-600);
  --warn: var(--warn-600);
  --danger: var(--danger-600);

  /*
   * 主题色的派生。站长只选 --theme，下面五个全算出来：
   *
   *   --seal        亮色下压到 L≤0.48、暗色下提到 L≥0.72。
   *                 这一条是**对比度保证**：纸底 / 墨底上都至少 4.5:1，
   *                 站长挑了一个浅黄或深紫也不会出现读不清的链接。
   *   --seal-hover  往「当前的字色」混 12%：亮色下字是深的→变深，
   *   --seal-active 往同一个方向混 22%：暗色下字是浅的→变亮。
   *                 一条式子同时管住两种模式，不必各写一遍。
   *   --seal-soft   往「当前的面板色」混，得到淡底（选中项、角标、软提示）。
   *
   * 兜底：相对颜色语法（oklch(from …)）不被支持时，--seal 就是站长填的原值，
   * 页面完整可用，只是少了那层对比度保证。故它先在这里无条件写一遍。
   */
  --seal: var(--theme);
  --seal-hover: color-mix(in oklab, var(--seal), var(--fg) 12%);
  --seal-active: color-mix(in oklab, var(--seal), var(--fg) 22%);
  --seal-soft: color-mix(in oklab, var(--seal) 12%, var(--surface));
  --seal-soft-strong: color-mix(in oklab, var(--seal) 22%, var(--surface));
  --seal-on: var(--n-0);

  --focus-ring: var(--seal);

  /*
   * 阴影三档。明暗差异只落在阴影的**颜色**上，几何形状两边共用一套——
   * 那本来也不该随主题变。
   */
  --shadow-tint: oklch(0.2 0 0 / 0.04);
  --shadow-tint-2: oklch(0.2 0 0 / 0.16);
  --shadow-tint-3: oklch(0.2 0 0 / 0.28);
  --elevation-card: 0 1px 2px 0 var(--shadow-tint);
  --elevation-overlay: 0 16px 40px -12px var(--shadow-tint-3),
    0 2px 8px -2px var(--shadow-tint-2);

  --scrim: oklch(0.2 0 0 / 0.45);
}

/*
 * 暗色。两个入口各写一遍——这是 CSS 的限制，不是复制粘贴的疏忽：
 * 「跟随系统」要包在 @media 里，「站长指定暗色」是个属性选择器，
 * 二者没有能合并的写法。改动时两块必须同时改。
 */
@media (prefers-color-scheme: dark) {
  :root[data-scheme="auto"] {
    --chrome: var(--n-950);
    --surface: var(--n-900);
    --surface-raised: var(--n-850);
    --surface-hover: var(--n-dark-hover);
    --surface-active: var(--n-dark-active);
    --surface-inset: var(--n-dark-inset);

    --fg: var(--ink-dark);
    /* 暗色下次要文字指向 400 而非 500：照搬亮色的值只剩 2.7:1 */
    --fg-muted: var(--n-400);
    --fg-subtle: var(--n-500);

    --action: var(--ink-dark);
    --action-hover: var(--ink-dark-hover);
    --action-on: var(--n-900);

    --line: var(--n-dark-line);
    --line-strong: var(--n-dark-line-strong);

    --ok: var(--ok-300);
    --warn: var(--warn-300);
    --danger: var(--danger-300);

    --seal-on: var(--n-900);

    --shadow-tint: oklch(0 0 0 / 0.3);
    --shadow-tint-2: oklch(0 0 0 / 0.5);
    --shadow-tint-3: oklch(0 0 0 / 0.7);
    --scrim: oklch(0 0 0 / 0.6);
  }
}

:root[data-scheme="dark"] {
  --chrome: var(--n-950);
  --surface: var(--n-900);
  --surface-raised: var(--n-850);
  --surface-hover: var(--n-dark-hover);
  --surface-active: var(--n-dark-active);
  --surface-inset: var(--n-dark-inset);

  --fg: var(--ink-dark);
  --fg-muted: var(--n-400);
  --fg-subtle: var(--n-500);

  --action: var(--ink-dark);
  --action-hover: var(--ink-dark-hover);
  --action-on: var(--n-900);

  --line: var(--n-dark-line);
  --line-strong: var(--n-dark-line-strong);

  --ok: var(--ok-300);
  --warn: var(--warn-300);
  --danger: var(--danger-300);

  --seal-on: var(--n-900);

  --shadow-tint: oklch(0 0 0 / 0.3);
  --shadow-tint-2: oklch(0 0 0 / 0.5);
  --shadow-tint-3: oklch(0 0 0 / 0.7);
  --scrim: oklch(0 0 0 / 0.6);
}

/*
 * 主题色的对比度保证。单独关在 @supports 里：相对颜色语法不被支持时
 * 这三条整块失效，上面那句 `--seal: var(--theme)` 原样生效。
 */
@supports (color: oklch(from red l c h)) {
  :root {
    --seal: oklch(from var(--theme) calc(min(l, 0.48)) c h);
  }

  @media (prefers-color-scheme: dark) {
    :root[data-scheme="auto"] {
      --seal: oklch(from var(--theme) calc(max(l, 0.72)) c h);
    }
  }

  :root[data-scheme="dark"] {
    --seal: oklch(from var(--theme) calc(max(l, 0.72)) c h);
  }
}

/* ==================================================================
   标签色
   ================================================================== */
/*
 * 标签的底色 / 文字 / 描边只给**明度与彩度**，色相由每枚标签自己的 --tag-h 提供
 * （见 .tag）。这样一屏标签可以有十几二十种颜色，而对比度始终是同一个数：
 * 明度定死，变的只有色相。
 *
 * 三处入口各写一遍是 CSS 的限制、不是复制粘贴的疏忽：「跟随系统」要包在 @media 里，
 * 「站长指定暗色」是个属性选择器。改动时三块必须同时改（与上面 --seal 一套同理）。
 */
:root {
  /* 模板没给色相时的兜底：主题蓝那一带，总比一条黑标签好 */
  --tag-h: 250;
  --tag-bg-l: 0.94;
  --tag-bg-c: 0.05;
  --tag-bg-hover-l: 0.9;
  --tag-fg-l: 0.42;
  --tag-fg-c: 0.105;
  --tag-line-l: 0.87;
  --tag-line-c: 0.06;
}

@media (prefers-color-scheme: dark) {
  :root[data-scheme="auto"] {
    --tag-bg-l: 0.3;
    --tag-bg-c: 0.05;
    --tag-bg-hover-l: 0.35;
    --tag-fg-l: 0.8;
    --tag-fg-c: 0.09;
    --tag-line-l: 0.38;
    --tag-line-c: 0.055;
  }
}

:root[data-scheme="dark"] {
  --tag-bg-l: 0.3;
  --tag-bg-c: 0.05;
  --tag-bg-hover-l: 0.35;
  --tag-fg-l: 0.8;
  --tag-fg-c: 0.09;
  --tag-line-l: 0.38;
  --tag-line-c: 0.055;
}

/* ==================================================================
   基础
   ================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  /*
   * 滚动条的位置一直留着。弹窗打开时 <html> 会被设成 overflow: hidden，
   * 滚动条随之消失——不留位的话整页会在那一瞬间往右跳十几个像素。
   */
  scrollbar-gutter: stable;
}

/*
 * 整页是一列 flex，正文区吃掉多余的高度——页脚因此永远贴在视口底部，
 * 而不是吊在内容后面、下面露出一片空的工作区底色。
 * 登录页、404、空状态这三处内容最短，问题也最明显。
 */
body {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  margin: 0;
  background: var(--chrome);
  color: var(--fg);
  font: var(--fs-base) / var(--lh-ui) var(--font-ui);
  font-feature-settings: "kern" 1;
}

img,
svg,
video {
  max-width: 100%;
  height: auto;
}

a {
  color: var(--seal);
  text-decoration: none;
  transition: color var(--t-fast) var(--ease-out);
}

a:hover {
  color: var(--seal-hover);
}

/*
 * 焦点环。全站唯一写法，任何地方都不得用 outline: none 抹掉它。
 * WCAG 2.2：至少 2px 周长与 3:1 状态对比，不用 box-shadow 假装。
 */
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--corner-control);
}

:where(input, textarea, select, button) {
  font: inherit;
  color: inherit;
}

:where(button, [role="button"], a, label[for], select, summary):not(:disabled) {
  cursor: pointer;
}

::selection {
  background: var(--seal-soft-strong);
}

* {
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}

/* 数字一律等宽：列表里的日期、篇数、页码要能对齐 */
.tabular,
.post-item-meta time,
.archive-count,
.page-numbers,
.count,
.notfound-code {
  font-variant-numeric: tabular-nums;
}

/* 键盘用户的第一站：仅在聚焦时出现 */
.skip-link {
  position: absolute;
  top: var(--s2);
  left: -9999px;
  z-index: 50;
  padding: var(--s2) var(--s4);
  border-radius: var(--corner-control);
  background: var(--action);
  color: var(--action-on);
  box-shadow: var(--elevation-overlay);
}

.skip-link:focus {
  left: var(--s4);
  color: var(--action-on);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ==================================================================
   骨架 —— 白页眉 / 浅灰工作区 / 白卡片 / 白页脚
   ================================================================== */
/*
 * 这正是 Console 的骨架：白色的页头把工作区框出来，卡片浮在浅灰上。
 * 前台没有侧栏，其余一一对应。
 */

.wrap {
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.main {
  flex: 1;
  padding-block: var(--s6) var(--s16);
}

/* ---------- 页眉 ---------- */
/*
 * 吸顶。Console 的页头一直在那里，前台也是——长文读到一半想换个分类，
 * 不该先滚回顶部。最小高度取 3.5rem，与 Console 的 --page-header-height 同值。
 */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.site-header .wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s5);
  min-height: 3.5rem;
  padding-block: var(--s2);
}

.site-id {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  min-width: 0;
}

/*
 * 刊名走衬线。这是文件头那条分界线的应用：刊名是读者要读的东西，
 * 不是一个可操作的控件——它和文章标题、正文属于同一侧。
 */
.site-title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 600;
  color: var(--fg);
  letter-spacing: 0.01em;
}

.site-title:hover {
  color: var(--fg);
}

.site-logo {
  display: block;
  max-height: 1.75rem;
  width: auto;
}

.site-subtitle {
  margin: 0;
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}

/*
 * 窄屏页眉。
 *
 * 四个菜单项加两个账户入口在 390px 上排不下，换行是必然的；
 * 要管的是**换行之后的次序**：默认换行会把「登录 / 注册」甩到第二行，
 * 第一行剩下刊名和半截导航，看着像页眉坏了。
 *
 * 所以这里明确指派：第一行是刊名（左）与账户入口（右）——这两样任何时候都要在；
 * 导航整条落到第二行。
 *
 * 导航本身仍然允许换行，不做成横向滚动条：滚动容器会把二级菜单裁掉，
 * 而二级菜单在这个宽度上是内嵌展开的，展开一次就得横竖都能看见。
 */
@media (max-width: 48rem) {
  .site-subtitle {
    display: none;
  }

  .site-header .wrap {
    gap: var(--s2) var(--s3);
  }

  .site-nav {
    order: 3;
    width: 100%;
    margin-inline-start: 0;
  }

  .site-tools {
    margin-inline-start: auto;
  }
}

/* ---------- 页脚 ---------- */
.site-footer {
  border-top: 1px solid var(--line);
  background: var(--surface);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}

.site-footer .wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2) var(--s6);
  padding-block: var(--s6);
}

.site-footer p {
  margin: 0;
}

.site-footer a {
  color: var(--fg-muted);
}

.site-footer a:hover {
  color: var(--seal);
}

.footer-links {
  display: flex;
  gap: var(--s4);
}

/* ==================================================================
   卡片 —— 内容区里的每一块
   ================================================================== */
/*
 * 与 Console 的 Card 同一套：白底、1px 描边、极轻的一层托底阴影、4px 圆角。
 * 卡片之间靠器底的浅灰分开，卡片内部靠分隔线分区；卡片里不再套卡片。
 *
 * 「什么是一张卡片」只在这一处定义，下面各块只补自己的内边距与内部排版。
 * 本主题没有 .card 这个类：前台的每一块都有自己的名字（文章、列表、评论…），
 * 让它们各自挂一个通用类只是多一层要同步的东西。
 */
.card,
.site-intro,
.post-list,
.article,
.archive-header,
.related,
.archive-months,
.notfound-suggest,
.notfound-head,
.tag-cloud,
.comments,
.empty-state,
.auth-panel,
.account-meta {
  border: 1px solid var(--line);
  border-radius: var(--corner-card);
  background: var(--surface);
  box-shadow: var(--elevation-card);
}

/* 卡片里的分区标题：一条线加一行小字，与 Console 的 CardHeader 同调 */
.section-title {
  margin: 0 0 var(--s4);
  color: var(--fg-muted);
  font-size: var(--fs-base);
  font-weight: 600;
}

/* 窄屏收内边距：2rem 在 390px 上会把正文挤成一条 */
@media (max-width: 40rem) {
  :root {
    --card-pad: var(--s4);
  }
}

/* ==================================================================
   控件 —— 按钮 / 输入框 / 角标 / 提示条
   ================================================================== */
/*
 * 与 Console 的 Button、Input、Badge、Alert 一一对应，只把尺寸放大一档：
 * 前台的可点元素做到 44px 高（触屏的最小目标），Console 的 36px 是工作区密度。
 */

/*
 * Lucide 图标（partials/icon.html）的统一尺寸。
 * 宽高写死，不靠 SVG 上的属性：全局那条 `svg { height: auto }` 会让它
 * 在 flex 行里被容器拉高，一个 16px 的图标能长到跟按钮一样高。
 */
.icon {
  flex: none;
  width: 1rem;
  height: 1rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: 2.75rem;
  padding-inline: var(--s4);
  border: 1px solid transparent;
  border-radius: var(--corner-control);
  background: none;
  font-size: var(--fs-base);
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease-out),
    background-color var(--t-fast) var(--ease-out),
    border-color var(--t-fast) var(--ease-out);
}

/* 主行动：墨底白字，暗色下反转。一个页面里最多一个 */
.btn-primary {
  background: var(--action);
  color: var(--action-on);
}

.btn-primary:hover {
  background: var(--action-hover);
  color: var(--action-on);
}

/* 次行动：白底描边 */
.btn-secondary {
  border-color: var(--line-strong);
  background: var(--surface);
  color: var(--fg);
}

.btn-secondary:hover {
  background: var(--surface-hover);
  color: var(--fg);
}

/* 行内操作：无边无底，只在悬停时浮出 */
.btn-ghost {
  color: var(--fg-muted);
}

.btn-ghost:hover {
  background: var(--surface-active);
  color: var(--fg);
}

.btn-sm {
  min-height: 2.25rem;
  padding-inline: var(--s3);
  font-size: var(--fs-sm);
}

.btn[disabled],
.btn[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.55;
}

/*
 * 文本输入。与 Console 的 Input 同一套：1px 描边、4px 圆角、面板底色，
 * 悬停时边框加重、聚焦时换成主题色、aria-invalid 时换成错误色。
 *
 * 「视觉上的错误」由 aria-invalid 驱动，而不是各处手写一个红边框类——
 * 这样它与「语义上的错误」永远同步，读屏软件和眼睛看到的是同一件事。
 */
.field-input,
.auth-form input:not([type="hidden"]),
.auth-form textarea,
.comment-form :is(input, textarea),
.search-form input {
  width: 100%;
  min-width: 0;
  min-height: 2.75rem;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line-strong);
  border-radius: var(--corner-control);
  background: var(--surface);
  color: var(--fg);
  font-size: var(--fs-md);
  line-height: 1.5;
  transition: border-color var(--t-fast) var(--ease-out);
}

:is(.field-input, .auth-form input, .auth-form textarea, .comment-form :is(input, textarea), .search-form input)::placeholder {
  color: var(--fg-muted);
}

:is(.field-input, .auth-form input, .auth-form textarea, .comment-form :is(input, textarea), .search-form input):hover {
  border-color: var(--fg-subtle);
}

:is(.field-input, .auth-form input, .auth-form textarea, .comment-form :is(input, textarea), .search-form input):focus-visible {
  border-color: var(--seal);
}

:is(.field-input, .auth-form input, .auth-form textarea, .comment-form input)[aria-invalid="true"] {
  border-color: var(--danger);
}

textarea.field-input,
.auth-form textarea,
.comment-form textarea {
  resize: vertical;
  line-height: 1.7;
}

/*
 * 字段行：标签在上、控件在下、说明与错误在最下。
 * 选择器带上模板里既有的类名（.auth-field / .comment-form-row / .auth-hint …），
 * 这样第三方主题照抄一段现成的表单标记也能直接套上这套样式，
 * 不必先学会一套新类名。
 */
.field-label,
.auth-field > label,
.comment-form-row > label {
  display: block;
  margin-bottom: var(--s2);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}

.field-hint,
.auth-hint,
.comment-form .hint {
  display: block;
  margin-top: var(--s1);
  color: var(--fg-muted);
  font-size: var(--fs-xs);
  line-height: 1.5;
}

.field-error,
.auth-error {
  display: block;
  margin-top: var(--s1);
  color: var(--danger);
  font-size: var(--fs-xs);
  line-height: 1.5;
}

/*
 * 角标（Console 的 Badge）。淡底 + 同色文字，不描边。
 * 颜色不单独表意：每一个角标里都有字。
 */
.badge {
  display: inline-flex;
  align-items: center;
  height: 1.375rem;
  padding-inline: var(--s2);
  border-radius: var(--corner-control);
  background: var(--seal-soft);
  color: var(--seal);
  font-size: var(--fs-xs);
  font-weight: 500;
  white-space: nowrap;
}

.badge-neutral {
  background: var(--surface-active);
  color: var(--fg-muted);
}

/*
 * 提示条（Console 的 Alert）。淡底 + 左侧 3px 竖线 + 同色文字。
 * 成功走主题色，失败走错误色；两种都带文字，不靠颜色单独表意。
 */
.notice {
  margin: 0 0 var(--s4);
  padding: var(--s3) var(--s4);
  border-left: 3px solid var(--seal);
  border-radius: var(--corner-control);
  background: var(--seal-soft);
  color: var(--fg);
  font-size: var(--fs-sm);
}

.notice[data-kind="error"] {
  border-left-color: var(--danger);
  background: color-mix(in oklab, var(--danger) 10%, var(--surface));
}

/* ==================================================================
   主导航
   ================================================================== */
/*
 * 一级项是链接或「点击展开」的按钮，两者视觉完全一致——用户不该需要分辨
 * 「这一个能点、那一个是链接」，它们都是主导航里的一项。
 *
 * 二级菜单**默认不隐藏**（模板里不写 hidden），由 nav.js 在初始化时收起。
 * 没有 JS 时它就是一列内嵌展开的链接，页面依然完整可用。
 */

.site-nav {
  margin-inline-start: auto;
  min-width: 0;
}

.site-nav > ul,
.site-nav-sub {
  display: flex;
  flex-wrap: wrap;
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-nav > ul {
  gap: var(--s1);
}

/* 导航项。与 Console 侧栏的条目同一套：圆角、悬停浮出一块底色 */
.site-nav a,
.site-nav-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: 2.25rem;
  padding-inline: var(--s3);
  border: 0;
  border-radius: var(--corner-control);
  background: none;
  color: var(--fg-muted);
  font: inherit;
  font-size: var(--fs-base);
  transition: color var(--t-fast) var(--ease-out),
    background-color var(--t-fast) var(--ease-out);
}

.site-nav a:hover,
.site-nav-toggle:hover {
  background: var(--surface-active);
  color: var(--fg);
}

.site-nav-chevron {
  color: var(--fg-muted);
  transition: transform var(--t-fast) var(--ease-out);
}

.site-nav-toggle[aria-expanded="true"] {
  background: var(--surface-active);
  color: var(--fg);
}

.site-nav-toggle[aria-expanded="true"] .site-nav-chevron {
  transform: rotate(180deg);
}

/*
 * 二级菜单的默认形态是**内嵌展开**，也是没有 JS 时的形态：
 * 子项左内缩并带一条 1px 竖线。
 * 绝对定位的下拉只在 nav.js 盖过 data-nav="js" 之后才启用（见下面的媒体查询）；
 * 少了那个开关，没有 JS 时桌面上的下拉照样生效，会浮在正文之上把文章盖掉——
 * 那不是降级，那是一个看起来坏掉的页面。
 */
.site-nav-sub {
  flex-direction: column;
  gap: var(--s1);
  width: 100%;
  margin-top: var(--s1);
  padding-left: var(--s3);
  border-left: 1px solid var(--line);
  font-size: var(--fs-sm);
  animation: nav-sub-in var(--t-fast) var(--ease-out);
  transition: opacity var(--t-fast) var(--ease-out),
    transform var(--t-fast) var(--ease-out);
}

.site-nav-sub[hidden] {
  display: none;
}

.site-nav-sub.is-closing {
  opacity: 0;
  transform: translateY(-2px);
}

@keyframes nav-sub-in {
  from {
    opacity: 0;
    transform: translateY(-2px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/*
 * 桌面（>640px）且 JS 已就位：浮层下拉。
 * 形态照 Console 的 DropdownMenu —— 6px 圆角、1px 描边、浮层阴影。
 * 这与第二版相反：那时它借页眉的分隔线当上边框、刻意不加阴影，
 * 是「把纸翻开一角」的隐喻。隐喻连同整版设计一起作废了，
 * 现在它就是一块浮在内容之上的面板，所以该有浮起来的样子。
 */
@media (min-width: 41rem) {
  .site-nav[data-nav="js"] > ul > li {
    position: relative;
  }

  .site-nav[data-nav="js"] .site-nav-sub {
    position: absolute;
    top: calc(100% + var(--s1));
    left: 0;
    z-index: var(--z-sticky);
    display: flex;
    width: auto;
    min-width: 11rem;
    margin-top: 0;
    padding: var(--s1);
    border: 1px solid var(--line);
    border-left: 1px solid var(--line);
    border-radius: var(--corner-overlay);
    background: var(--surface);
    box-shadow: var(--elevation-overlay);
  }

  /*
   * 这条必须在这里再声明一次。上面那条选择器的特异性是（0,3,0），
   * 高过块外那条 `.site-nav-sub[hidden]` 的（0,2,0）——少了它，
   * 脚本设的 hidden 在桌面上完全失效，二级菜单一直摊开着收不起来。
   */
  .site-nav[data-nav="js"] .site-nav-sub[hidden] {
    display: none;
  }

  .site-nav[data-nav="js"] .site-nav-sub a {
    display: flex;
    width: 100%;
  }
}

/* ---------- 账户入口 ---------- */
/*
 * 页眉右端的一组工具：账户入口 + 配色切换。
 *
 * 账户菜单（绝对定位，right: 0）的基准是里层的 .site-account 而不是这一层：
 * 菜单该从**头像**底下长出来，而不是从切换按钮底下——按钮点开的是配色，不是菜单。
 */
.site-tools {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* relative 是账户菜单的定位基准：菜单绝对定位在这块的右下角 */
.site-account {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/*
 * 已登录：一枚方印。
 *
 * 第二版这里就是方印，中途改成过圆形（理由是 Console 那条「人用圆、物用方角」）。
 * 改回方印：那条规矩是 **Console** 的，前台该讲的是主题自己的话——
 * 站名与导航都是方正的宋体骨架，一个灰圆头像挂在这一行的右端，它是唯一的柔物。
 * 印面用主题色、首字用印色，与文章页那枚印章插图同源。
 *
 * 尺寸取 2rem：与导航的字号、切换按钮同级，四个角的方与宋体的横细竖粗是同一套语言。
 */
/*
 * 头像在模板里是 <a>，被 account-menu.js 换成 <button>（见那里的说明）。
 * 两种标签共用这一条，故按钮的默认边框、内边距与底色都要清掉——
 * 全局重置只统一了字体与颜色，没有碰这三样。
 */
.site-avatar {
  display: inline-flex;
  padding: 0;
  border: 0;
  border-radius: var(--corner-seal);
  background: none;
}

/*
 * 与头像同尺寸、同圆角、同一行的方角族，但它是**描边**而不是实底：
 * 一枚实心印 + 一枚实心按钮会变成两块一样的色块，主次就没了。
 * 描边按钮悬停时描边与图标转主题色，说明它是可点的，不必再配底色。
 *
 * 没有 JavaScript 时它一直是 hidden（模板里给的），所以要先声明 hidden 赢过 display：
 * 全局的 [hidden] 规则特异性不够，会被下面这条 display: inline-flex 盖掉
 * （二级菜单撞过同一件事，见 .site-nav-sub[hidden]）。
 */
.scheme-toggle[hidden] {
  display: none;
}

.scheme-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--corner-seal);
  background: none;
  color: var(--fg-muted);
  transition: color var(--t-fast) var(--ease-out),
    border-color var(--t-fast) var(--ease-out),
    background-color var(--t-fast) var(--ease-out);
}

.scheme-toggle:hover {
  border-color: var(--seal);
  background: var(--seal-soft);
  color: var(--seal);
}

/*
 * 三态各有自己的图标，三枚都渲染进 DOM，显隐**只由 CSS 决定**：
 * <html data-scheme-pref> 是唯一的状态源，脚本只改那一个属性与按钮的 aria-label。
 * 换 DOM 的话每次切换都要重建一个 SVG，而那正是「切换时抖一下」的来源。
 *
 * 没有 data-scheme-pref（脚本还没跑或没 JS）时三枚都不显示 —— 这正是想要的：
 * 按钮那时还是 hidden，图标不该先冒出来。
 */
.scheme-icon {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transform: scale(0.6) rotate(-30deg);
  transition: opacity var(--t-fast) var(--ease-out),
    transform var(--t-fast) var(--ease-out);
}

:root[data-scheme-pref="auto"] .scheme-icon[data-scheme-icon="auto"],
:root[data-scheme-pref="light"] .scheme-icon[data-scheme-icon="light"],
:root[data-scheme-pref="dark"] .scheme-icon[data-scheme-icon="dark"] {
  opacity: 1;
  transform: none;
}

/*
 * 切换主题时的圆形揭示（View Transitions）。
 *
 * 默认的交叉淡入在这件事上不好看：整页同时淡一次，像屏幕闪了一下。
 * 改成从按钮位置扩散的一个圆 —— 变化有个来处，眼睛知道刚才发生了什么。
 * 三段覆盖是必需的：默认那条 view-transition 会同时跑交叉淡入与新视图动画，
 * 不关掉的话圆形揭示会被叠上一层淡入，边缘发虚。
 */
::view-transition-image-pair(root) {
  isolation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
  display: block;
}

/*
 * 动效总闸（站长在主题设置里关掉「页面动效」）与访客系统的「减少动态效果」。
 *
 * 前者是站长的选择，在本文件里以 [data-motion="off"] 表达；
 * 后者是访客的选择，只有媒体查询问得到。两者都只关动画，不关功能：
 * 切换照常发生，只是瞬间到位。
 */
[data-motion="off"] .scheme-icon {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .scheme-icon {
    transition: none;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/*
 * 账户菜单。骨架与二级菜单的桌面下拉、与 Console 的 DropdownMenu 逐项对齐：
 * 6px 圆角、1px 描边、面板底色、浮层阴影。
 *
 * 右对齐（right: 0）而不是左对齐：它挂在页眉最右端的头像上，
 * 左对齐会让面板整个探出视口右边。
 */
.account-menu {
  position: absolute;
  top: calc(100% + var(--s2));
  right: 0;
  z-index: var(--z-sticky);
  display: flex;
  flex-direction: column;
  width: 14rem;
  /* 窄屏上不许超出页面左右留白：--gutter 两侧各一份 */
  max-width: calc(100vw - var(--gutter) * 2);
  padding: var(--s1);
  border: 1px solid var(--line);
  border-radius: var(--corner-overlay);
  background: var(--surface);
  box-shadow: var(--elevation-overlay);
  animation: account-menu-in var(--t-fast) var(--ease-out);
  transition: opacity var(--t-fast) var(--ease-out),
    transform var(--t-fast) var(--ease-out);
}

/*
 * 这一条不能省，理由与 .site-nav-sub[hidden] 那条相同：
 * 上面那条给了 display: flex，而作者样式赢过 UA 那条 [hidden] { display: none }，
 * 少了它菜单会从页面加载起就一直摊在页眉底下，而且点头像也收不起来。
 */
.account-menu[hidden] {
  display: none;
}

.account-menu.is-closing {
  opacity: 0;
  transform: translateY(-2px);
}

@keyframes account-menu-in {
  from {
    opacity: 0;
    transform: translateY(-2px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* 顶部一行是「你是谁」：显示名一行，用户名一行。与 Console 账号菜单的 Label 同调 */
.account-menu-head {
  padding: var(--s2) var(--s3) var(--s3);
  border-bottom: 1px solid var(--line);
}

.account-menu-name,
.account-menu-id {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-menu-name {
  color: var(--fg);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.account-menu-id {
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}

.account-menu-list {
  margin: var(--s1) 0 0;
  padding: 0;
  list-style: none;
}

/* 菜单项：图标 + 文字一行。链接与退出登录那个按钮共用这一条 */
.account-menu :is(a, button) {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  min-height: 2.25rem;
  padding: 0 var(--s3);
  border: 0;
  border-radius: var(--corner-control);
  background: none;
  color: var(--fg);
  font-size: var(--fs-sm);
  text-align: start;
  transition: background-color var(--t-fast) var(--ease-out),
    color var(--t-fast) var(--ease-out);
}

.account-menu :is(a, button):hover {
  background: var(--surface-active);
  color: var(--fg);
}

.account-menu .icon {
  flex: none;
  color: var(--fg-muted);
}

.account-menu :is(a, button):hover .icon {
  color: var(--fg);
}

/* 底栏：管理后台与退出登录。一条分隔线把「去别处」与「上面这些」分开 */
.account-menu-foot {
  margin-top: var(--s1);
  padding-top: var(--s1);
  border-top: 1px solid var(--line);
}

.account-menu-logout {
  margin: 0;
}

/* 退出登录是这一列里唯一会改变状态的一项，故悬停时给它危险色 */
.account-menu-logout button:hover {
  color: var(--danger);
}

.account-menu-logout button:hover .icon {
  color: var(--danger);
}

/*
 * 印面。首字版与上传图版共用这一条尺寸与圆角，只有底色不同：
 * 首字是印面（主题色底 + 印色字），上传图自己带画面，底色不显。
 */
.site-avatar img,
.avatar-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  border-radius: var(--corner-seal);
  background: var(--seal);
  color: var(--seal-on);
  font-size: var(--fs-sm);
  font-weight: 500;
  object-fit: cover;
  user-select: none;
}

/* 悬停把印面压深一档，与按钮的悬停同一条规则：能点的东西要动一下 */
.site-avatar:hover img,
.site-avatar:hover .avatar-fallback {
  background-color: var(--seal-hover);
}

/* ==================================================================
   首页与列表
   ================================================================== */

.site-intro {
  margin: 0 0 var(--s4);
  padding: var(--s5) var(--card-pad);
  color: var(--fg-muted);
  font-size: var(--fs-lg);
  line-height: 1.7;
}

/*
 * 列表是一张卡片，每篇文章是其中一行——这是 Console 列表页的基本单位。
 * 第二版刻意反对卡片（「像目录而不是商品网格」），这一版把那条推翻：
 * 一张卡片里排十行，仍然是目录；十张卡片排成一列才是商品网格。
 */
.post-list {
  margin: 0;
  padding: 0;
  list-style: none;
  /* 行的悬停底色要被卡片的圆角剪住，否则首尾两行会从圆角里探出方角 */
  overflow: hidden;
}

.post-item {
  display: flex;
  gap: var(--s4);
  padding: var(--s5) var(--card-pad);
  transition: background-color var(--t-fast) var(--ease-out);
}

.post-item + .post-item {
  border-top: 1px solid var(--line);
}

.post-item:hover {
  background: var(--surface-hover);
}

.post-item-body {
  min-width: 0;
  flex: 1;
}

/* 封面缩略图。Console 的实体行把缩略图放在左侧固定宽度的一列里 */
.post-cover {
  display: block;
  flex: none;
  width: 7.5rem;
  border-radius: var(--corner-control);
  overflow: hidden;
}

.post-cover img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

@media (max-width: 34rem) {
  .post-item {
    flex-direction: column;
  }

  .post-cover {
    width: 100%;
  }
}

/*
 * 标题是普通的内联流，不是 flex：「置顶」角标要跟在标题最后一个字后面，
 * 随文字一起换行。flex 的话它会被当成第二个项目，标题一满就单独掉到下一行，
 * 看着像一个跟标题无关的孤立标签——窄屏上每一条都是这样。
 */
.post-item-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 600;
  line-height: 1.45;
  letter-spacing: 0.01em;
}

.post-item-title a {
  color: var(--fg);
}

.post-item-title a:hover {
  color: var(--seal);
}

.pin {
  display: inline-block;
  margin-left: var(--s2);
  padding: 0.1em var(--s2);
  vertical-align: 0.15em;
  border-radius: var(--corner-control);
  background: var(--surface-active);
  color: var(--fg-muted);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 500;
}

/*
 * 元信息用空格分隔而不是中点（· ）：中点连接的 A · B · C 是模板套壳的标志，
 * 而且中文里点号本身有含义，拿它当分隔符会读成顿号。
 */
.post-item-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
  margin: var(--s2) 0 0;
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}

.post-item-meta a {
  color: var(--fg-muted);
}

/*
 * 摘要最多两行。列表是目录，一条目录项被六行摘要撑开之后，
 * 一屏就只剩两篇文章——窄屏上尤其明显。想读全文的人会点进去。
 */
.post-item-excerpt {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  margin: var(--s3) 0 0;
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  line-height: 1.7;
}

.empty {
  margin: 0;
  color: var(--fg-muted);
}

/* ==================================================================
   文章页
   ================================================================== */
/*
 * 整篇文章是一张卡片：封面铺满卡片顶端，标题与元信息在上，正文在下。
 *
 * 第二版把元信息挂在正文列的左外侧（「本主题唯一的破格处」）。
 * 那条栏与 Console 的任何东西都对不上，随旧设计一起去掉；
 * 元信息回到标题下方一行，仍由 appearance.showMeta 控制显示与否。
 */
.article {
  padding: var(--card-pad);
}

/* 封面铺满卡片顶端：用负边距抵掉卡片内边距，只留上两角的圆角 */
.article-cover {
  margin: calc(var(--card-pad) * -1) calc(var(--card-pad) * -1) var(--s6);
  overflow: hidden;
  border-radius: calc(var(--corner-card) - 1px) calc(var(--corner-card) - 1px) 0 0;
}

.article-cover img {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 1;
  object-fit: cover;
}

.article-header {
  margin-bottom: var(--s6);
}

.article-title {
  margin: 0;
  font-family: var(--font-display);
  /* 标题随视口缩放，但有上下限：纯 vw 在超宽屏上会失控 */
  font-size: clamp(1.625rem, 1.2rem + 1.8vw, 2.25rem);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.01em;
  text-wrap: balance;
}

.article-lede {
  margin: var(--s3) 0 0;
  color: var(--fg-muted);
  font-size: var(--fs-lg);
  line-height: 1.7;
}

/* 元信息：一行会换行的键值对，底下一条线把它与正文分开 */
.article-aside {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s6);
  margin-bottom: var(--s6);
  padding-bottom: var(--s4);
  border-bottom: 1px solid var(--line);
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}

.aside-row {
  display: flex;
  gap: var(--s2);
}

.aside-key {
  flex: none;
  color: var(--fg-muted);
}

.aside-row a {
  color: var(--fg-muted);
}

.aside-row a:hover {
  color: var(--seal);
}

/* 文末的标签与作者 */
.article-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin: var(--s10) 0 0;
}

/*
 * 文末的动作区：目前只有收藏一个按钮，右边跟一行收藏数。
 *
 * 不做成吸在屏幕边上的悬浮条：那是社交产品的做法，会一直挡着正文。
 * 读完抬手就能够到的位置，对一个「读完之后才会做的动作」已经足够。
 */
.article-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  margin-top: var(--s8);
}

.favorite-toggle .icon {
  color: var(--fg-muted);
  transition: color var(--t-fast) var(--ease-out),
    fill var(--t-fast) var(--ease-out);
}

/*
 * 已收藏：书签填实、换成主题色，按钮本身落一层主题色的淡底。
 * 只靠填不填实是不够的（那要求人记住约定），文字同时从「收藏」变成「已收藏」，
 * 由 favorite.js 换（见那里的 render）。
 *
 * fill 写在 CSS 里而不是模板里：图标的 fill="none" 是表现属性，
 * 而任何一条 CSS 规则都赢过它——两种状态因此只需要一份 SVG。
 */
.favorite-toggle[aria-pressed="true"] {
  border-color: var(--seal);
  background: var(--seal-soft);
  color: var(--seal);
}

.favorite-toggle[aria-pressed="true"] .icon {
  color: var(--seal);
  fill: var(--seal);
}

.favorite-count {
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.article-author {
  display: flex;
  gap: var(--s4);
  align-items: center;
  margin-top: var(--s8);
  padding-top: var(--s6);
  border-top: 1px solid var(--line);
}

.avatar {
  flex: none;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--corner-full);
  background: var(--surface-active);
  object-fit: cover;
}

.avatar-lg {
  width: 3.5rem;
  height: 3.5rem;
}

.author-name {
  margin: 0;
  font-weight: 600;
}

.author-name a {
  color: var(--fg);
}

.author-name a:hover {
  color: var(--seal);
}

.author-bio {
  margin: var(--s1) 0 0;
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}

/* ==================================================================
   正文排版
   ================================================================== */
/*
 * 这里是整个前台唯一「读物」的部分：衬线、17px、行高 1.85。
 * 与 Console 编辑器里的 .prose-editor 是同一种排版意图——
 * 那边写的东西，在这里读起来应当是同一个样子。
 */
.prose {
  max-width: var(--measure);
  color: var(--fg);
  font-family: var(--font-serif);
  font-size: var(--fs-lg);
  line-height: var(--lh-prose);
  overflow-wrap: anywhere;
}

.prose > * + * {
  margin-top: var(--s6);
}

.prose h2,
.prose h3,
.prose h4 {
  margin-top: var(--s10);
  margin-bottom: var(--s4);
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.01em;
  scroll-margin-top: 4.5rem; /* 吸顶页眉的高度，锚点跳转时别被盖住 */
}

.prose h2 {
  font-size: 1.5rem;
}

.prose h3 {
  font-size: 1.25rem;
}

.prose h4 {
  font-size: 1.0625rem;
}

.prose p {
  margin: 0;
}

.prose ul,
.prose ol {
  margin: 0;
  padding-left: 1.5em;
}

.prose li + li {
  margin-top: var(--s2);
}

.prose a {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  text-decoration-color: color-mix(in oklab, var(--seal) 40%, transparent);
}

.prose a:hover {
  text-decoration-color: currentColor;
}

/* 引文：一条主题色竖线 + 淡底，与提示条同一种语言 */
.prose blockquote {
  margin: var(--s8) 0;
  padding: var(--s4) var(--s5);
  border-left: 3px solid var(--seal);
  border-radius: var(--corner-control);
  background: var(--surface-raised);
  color: var(--fg-muted);
}

.prose blockquote p + p {
  margin-top: var(--s4);
}

.prose img {
  border-radius: var(--corner-control);
}

.prose hr {
  margin: var(--s10) 0;
  border: 0;
  border-top: 1px solid var(--line);
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-ui);
  font-size: var(--fs-base);
}

.prose :is(th, td) {
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}

.prose th {
  background: var(--surface-raised);
  font-weight: 600;
}

.prose code {
  padding: 0.1em 0.35em;
  border-radius: 3px;
  background: var(--surface-active);
  font-family: var(--font-mono);
  font-size: 0.875em;
}

.prose :is(h2, h3, h4) > a[href^="#"] {
  color: var(--fg-subtle);
  text-decoration: none;
}

/* ==================================================================
   代码块
   ================================================================== */
/*
 * 结构由服务端给出（internal/content/highlight.go）：<figure.code-block> +
 * <pre.code-lines>，每行是 <span.line><span.cl>…。行号**不是 DOM 文本**，
 * 由 CSS 计数器生成——读者选中复制时不会把行号一并带走。
 *
 * 外壳与 Console 编辑器里的 pre 同一套：嵌入底色、1px 描边、4px 圆角。
 */
:root {
  --code-gutter: 2.75em;
  --code-gap: var(--s5);
  --code-pad-block: var(--s4);
  --code-pad-inline: var(--s5);
  /* 收起态的高度上限：约 22 行，超出即折起 */
  --code-collapse-h: 24rem;

  /*
   * 语法着色。四种颜料各管一类记号，与主题色无关——
   * 站长把主题色改成橙的那天，字符串不该跟着变成橙的。
   *
   * 这里用 light-dark() 一次写完明暗，而上方的主 token 是双写的。
   * 区别在失效后果：主 token 失效页面会塌，着色失效只是代码退回墨色、照样能读，
   * 所以这一小片值得用更简洁的写法换一点兼容面。
   */
  --code-gutter-fg: light-dark(#6b7280, #8b8f99);
  --code-comment: light-dark(#4a6b52, #8fa192);
  --code-keyword: light-dark(#1b3a6b, #8fb0f0);
  --code-string: light-dark(#9c3122, #e5938a);
  --code-number: light-dark(#7a4a1e, #d9ab7a);
  --code-row: color-mix(in oklab, var(--fg) 5%, transparent);
}

/* 兜底：没有走服务端包装的裸 <pre>（第三方主题或历史内容）仍要有像样的排版 */
.prose pre {
  margin: var(--s8) 0;
  padding: var(--code-pad-block) var(--code-pad-inline);
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--corner-control);
  background: var(--surface-inset);
  font-family: var(--font-mono);
  font-size: 0.875rem;
  line-height: 1.7;
  tab-size: 2;
}

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

.code-block {
  position: relative;
  margin: var(--s8) 0;
  border: 1px solid var(--line);
  border-radius: var(--corner-control);
  background: var(--surface-inset);
}

.code-block .code-lines {
  margin: 0;
  padding: var(--code-pad-block) 0 var(--code-pad-block) var(--code-pad-inline);
  overflow-x: auto;
  border: 0;
  background: none;
  border-radius: inherit;
}

.code-lines code {
  display: block;
  width: max-content;
  min-width: 100%;
  padding: 0;
  background: none;
  font-size: inherit;
  counter-reset: line;
}

/*
 * 每行是一条两列网格：行号列 + 代码列。
 * 用网格而不是绝对定位，是为了让自动换行后的续行自动对齐到代码列——
 * 那样换行开关才真的能用，而不是把缩进搞成一团。
 */
.code-lines .line {
  display: grid;
  grid-template-columns: var(--code-gutter) minmax(0, 1fr);
  column-gap: var(--code-gap);
  padding-right: var(--code-pad-inline);
}

.code-lines .line::before {
  content: counter(line);
  counter-increment: line;
  padding-right: var(--s3);
  border-right: 1px solid var(--line);
  color: var(--code-gutter-fg);
  text-align: right;
  /* 行号是辅助信息，不该被选中、也不该被读屏逐行念出来 */
  user-select: none;
  -webkit-user-select: none;
}

.code-lines .line:hover {
  background: var(--code-row);
}

/* 自动换行：网格列不变，续行自然落在代码列里 */
.code-block.is-wrapped .code-lines code {
  width: auto;
}

.code-block.is-wrapped .code-lines {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/*
 * 展开 / 收起：长代码块默认折到 --code-collapse-h，底部用遮罩淡出。
 * 收起态由 code.js 测量高度后决定，短代码块不显示展开按钮。
 */
.code-block.is-collapsed .code-lines {
  max-height: var(--code-collapse-h);
  overflow-y: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 4rem), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 4rem), transparent);
}

/* ---------- 代码块工具条 ---------- */
/*
 * 三个按钮（换行 / 复制 / 展开）默认隐身，鼠标进入代码块才出现。
 * 但「只在悬停时可见」对触屏与键盘是死路，故：
 *   - :focus-within 时同样显示（键盘用户 Tab 得进来）；
 *   - 指针没有悬停能力的设备（触屏）常显。
 */
.code-tools {
  position: absolute;
  top: var(--s2);
  right: var(--s2);
  z-index: 2;
  display: flex;
  gap: var(--s1);
  padding: var(--s1);
  border: 1px solid var(--line);
  border-radius: var(--corner-control);
  background: var(--surface);
  box-shadow: var(--elevation-card);
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease-out);
}

.code-block:hover .code-tools,
.code-block:focus-within .code-tools {
  opacity: 1;
}

@media (hover: none) {
  .code-tools {
    opacity: 1;
  }
}

.code-tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: var(--corner-control);
  background: none;
  color: var(--fg-muted);
  transition: color var(--t-fast) var(--ease-out),
    background-color var(--t-fast) var(--ease-out);
}

/* 触屏与窄屏给足 44px 的命中区 */
@media (hover: none), (max-width: 40rem) {
  .code-tool {
    width: 2.75rem;
    height: 2.75rem;
  }
}

/*
 * hidden 必须显式处理：.code-tool 的 display 是作者样式，
 * 会盖过浏览器默认表里的 [hidden]{display:none}——不加这条，
 * 「展开按钮对短代码块隐藏」会失效，每个代码块都顶着一个按不动的展开键。
 */
.code-tool[hidden] {
  display: none;
}

.code-tool:hover {
  background: var(--surface-active);
  color: var(--fg);
}

.code-tool svg {
  width: 1.125rem;
  height: 1.125rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* 复制成功的对勾与常态图标互斥显示 */
.code-tool .icon-done {
  display: none;
}

.code-tool.is-done {
  color: var(--ok);
}

.code-tool.is-done .icon-idle {
  display: none;
}

.code-tool.is-done .icon-done {
  display: block;
}

.code-tool-status {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- 语法着色 ---------- */
.code-lines .c,
.code-lines .ch,
.code-lines .cm,
.code-lines .c1,
.code-lines .cs,
.code-lines .cp,
.code-lines .cpf,
.code-lines .sd {
  color: var(--code-comment);
}

.code-lines .k,
.code-lines .kc,
.code-lines .kd,
.code-lines .kn,
.code-lines .kp,
.code-lines .kr {
  color: var(--code-keyword);
  font-weight: 600;
}

.code-lines .kt {
  color: var(--code-number);
}

.code-lines .s,
.code-lines .sa,
.code-lines .sb,
.code-lines .sc,
.code-lines .dl,
.code-lines .sd,
.code-lines .s1,
.code-lines .s2,
.code-lines .se,
.code-lines .sh,
.code-lines .si,
.code-lines .sx,
.code-lines .sr,
.code-lines .ss {
  color: var(--code-string);
}

.code-lines .m,
.code-lines .mb,
.code-lines .mf,
.code-lines .mh,
.code-lines .mi,
.code-lines .mo,
.code-lines .il {
  color: var(--code-number);
}

/*
 * 运算符与标点刻意不另上色，保持墨色。
 * 它们在代码里密度最高，一旦着色，一段普通的 PHP 会花成一张电路图。
 */
.code-lines .na,
.code-lines .nt {
  color: var(--code-keyword);
}

/* 函数名与类名不动色相，靠字重区分 */
.code-lines .nf,
.code-lines .fm,
.code-lines .nc,
.code-lines .nn,
.code-lines .nd,
.code-lines .ne {
  font-weight: 600;
}

/* diff 的增删行：只做极轻的底色，不做整行反白 */
.code-lines .gi {
  background: color-mix(in oklab, var(--ok) 16%, transparent);
}

.code-lines .gd {
  background: color-mix(in oklab, var(--danger) 16%, transparent);
}

.code-lines .gh,
.code-lines .gu {
  color: var(--code-keyword);
  font-weight: 600;
}

/* chroma 的 err 默认是红底白字，太吵；这里退成一条下划波浪 */
.code-lines .err {
  color: var(--danger);
  text-decoration: underline wavy currentColor;
  text-decoration-thickness: 1px;
}

/* ---------- 正文里的图与题注 ---------- */
.prose img,
.prose video {
  display: block;
  border-radius: var(--corner-control);
}

.prose figure {
  margin: var(--s8) 0;
}

.prose figcaption {
  margin-top: var(--s2);
  color: var(--fg-muted);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  text-align: center;
}

/* ==================================================================
   标签
   ================================================================== */
/*
 * 描边式角标（Console 的 Badge outline 变体）。标签可以带站长指定的颜色，
 * 那时描边与文字都跟着换——底色不换，一排彩色块会盖过文章标题。
 */
/*
 * 标签：尺寸、形状、文字居中全站统一，一枚一色。
 *
 * 这三处曾经各不相同——列表里是 4px 圆角的小角标、标签云按热度分五档字号、
 * 首页模块里是胶囊。现在合成**一条**规则：高、圆角、字号、内边距都写在这里，
 * 别处不再覆盖（.cloud--lg 只剩间距）。
 *
 * 颜色由 --tag-h（色相）拼出来，**明度与彩度固定**。固定感知亮度是
 * 「每枚颜色不同、但每一枚都读得清」的唯一稳法：只变色相时，对比度在各色相之间
 * 最多漂 ±9%；而 HSL 那种「固定 L 只变 H」的做法，黄与蓝的实际亮度能差一个数量级。
 * 色相从哪来、站长设的颜色怎么参与，见 finder.go 的 tagHue。
 */
.tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 1.75rem;
  padding-inline: var(--s3);
  border: 1px solid oklch(var(--tag-line-l) var(--tag-line-c) var(--tag-h));
  border-radius: var(--corner-control);
  background: oklch(var(--tag-bg-l) var(--tag-bg-c) var(--tag-h));
  color: oklch(var(--tag-fg-l) var(--tag-fg-c) var(--tag-h));
  font-size: var(--fs-xs);
  /* 中文没有西文的升部降部，靠 align-items 居中常偏下 1px 左右，故把行盒压平 */
  line-height: 1;
  white-space: nowrap;
  transition: background-color var(--t-fast) var(--ease-out),
    border-color var(--t-fast) var(--ease-out);
}

.tag:hover {
  background: oklch(var(--tag-bg-hover-l) var(--tag-bg-c) var(--tag-h));
  border-color: oklch(var(--tag-fg-l) var(--tag-fg-c) var(--tag-h));
}

/* 标签云只调间距：尺寸与形状与别处的标签完全一致 */
.cloud {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
  margin: 0;
}

/* ==================================================================
   归档页头（分类 / 标签 / 作者 / 月份）
   ================================================================== */
.archive-header {
  margin-bottom: var(--s4);
  padding: var(--card-pad);
}

.has-seal,
.author-header {
  display: flex;
  gap: var(--s5);
  align-items: flex-start;
}

/*
 * 「分类」「标签」这类限定词用小字号常规字重，不用 tracked-out 全大写 eyebrow——
 * 后者对中文没有意义（中文没有大小写），在拉丁文本上也是模板套壳的标志。
 */
.archive-kind {
  margin: 0 0 var(--s1);
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}

.archive-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 600;
  line-height: var(--lh-tight);
}

.archive-desc {
  margin: var(--s2) 0 0;
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}

.archive-count {
  margin: var(--s2) 0 0;
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}

/* ==================================================================
   相关文章、归档月份、404 的推荐列表
   ================================================================== */
/*
 * 三处共用一套：一张卡片，每条一行，右端是日期或篇数。
 * 这与首页文章列表是同一个「卡片 + 实体行」的单位，只是行更矮。
 */
.related,
.archive-months,
.notfound-suggest,
.tag-cloud {
  margin-top: var(--s4);
  padding: var(--card-pad);
}

.related-list,
.archive-months ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.related-list li,
.archive-months li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s4);
  padding-block: var(--s2);
}

.related-list li + li,
.archive-months li + li {
  border-top: 1px solid var(--line);
}

.related-list a,
.archive-months a {
  color: var(--fg);
  font-size: var(--fs-base);
}

.related-list a:hover,
.archive-months a:hover {
  color: var(--seal);
}

.related-list time,
.archive-months .count {
  flex: none;
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}

/* ==================================================================
   翻页
   ================================================================== */
/*
 * 与 Console 的 Pagination 同一套：上一页 / 页码 / 下一页，
 * 页码是等宽的方块按钮，当前页用主题色淡底标出。
 */
.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s4);
}

.page-prev,
.page-next {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding-inline: var(--s3);
  border: 1px solid var(--line-strong);
  border-radius: var(--corner-control);
  background: var(--surface);
  color: var(--fg);
  font-size: var(--fs-sm);
  transition: background-color var(--t-fast) var(--ease-out);
}

.page-prev:hover,
.page-next:hover {
  background: var(--surface-hover);
  color: var(--fg);
}

.page-numbers {
  display: flex;
  align-items: center;
  gap: var(--s1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.page-numbers :is(a, span) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  height: 2.25rem;
  padding-inline: var(--s1);
  border-radius: var(--corner-control);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  transition: background-color var(--t-fast) var(--ease-out);
}

.page-numbers a:hover {
  background: var(--surface-active);
  color: var(--fg);
}

.page-current {
  background: var(--seal-soft);
  color: var(--seal);
  font-weight: 600;
}

.page-gap {
  color: var(--fg-subtle);
}

/* 到头了的那一端：留在原位但明确不可点，不让页面的两端忽长忽短 */
.is-disabled {
  border-color: var(--line);
  color: var(--fg-subtle);
  cursor: not-allowed;
}

/* ==================================================================
   搜索
   ================================================================== */
.search-form {
  display: flex;
  gap: var(--s2);
  margin-top: var(--s4);
}

.search-form input {
  flex: 1;
  min-width: 0;
}

/* ==================================================================
   评论
   ================================================================== */
.comments {
  margin-top: var(--s4);
  padding: var(--card-pad);
}

.comments-title {
  margin: 0 0 var(--s5);
  font-size: var(--fs-xl);
  font-weight: 600;
}

.comment-list,
.comment-replies {
  margin: 0;
  padding: 0;
  list-style: none;
}

.comment {
  padding-block: var(--s4);
}

.comment + .comment {
  border-top: 1px solid var(--line);
}

.comment-replies {
  margin-top: var(--s3);
  padding-left: var(--s5);
  border-left: 1px solid var(--line);
}

.comment-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-bottom: var(--s2);
}

.comment-author {
  color: var(--fg);
  font-size: var(--fs-base);
  font-weight: 600;
}

a.comment-author:hover {
  color: var(--seal);
}

.comment-date {
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}

.comment-body {
  color: var(--fg);
  font-size: var(--fs-base);
  line-height: 1.75;
  overflow-wrap: anywhere;
}

.comment-body p {
  margin: 0;
}

.comment-body p + p {
  margin-top: var(--s3);
}

.comment-form {
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
}

/*
 * 已登录时顶替「称呼 / 邮箱 / 主页」三行的那一句。
 *
 * 它要回答的是「凭什么不用填名字」，所以把名字写出来，而不是只说一句「已登录」。
 * 名字用正文色加半档字重与周围的次要色拉开——那是这一行里唯一的信息。
 */
.comment-identity {
  margin: 0 0 var(--s4);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}
.comment-identity-name {
  color: var(--fg);
  font-weight: 500;
}

/*
 * 只收登录用户的评论时，表单原来的位置上留这一句。
 * 上边框照搬 .comment-form 的，好让它和表单占同一个位置、同一条分界线——
 * 它就是那张表单此刻的形态。
 */
.comment-locked {
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}
.comment-locked a {
  color: var(--seal);
}

/* 标签始终可见，不用 placeholder 代替：填完字后 placeholder 就消失了。
   行距归标签与说明自己的外边距管（见上方 .field-label / .field-hint） */
.comment-form-row {
  margin: 0 0 var(--s4);
}

/* 蜜罐：对人不可见，对读屏软件也不可见，只有机器人会填 */
.comment-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.comment-form-actions {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin: 0;
}

.comment-status {
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}

.comment-status[data-state="error"] {
  color: var(--danger);
}

/* ==================================================================
   印 —— 本主题的插图系统
   ================================================================== */
/*
 * 一枚方印、一个字。空状态盖「待」，搜索无果盖「无」，404 盖「空」，
 * 归档页头盖名字的第一个字。
 *
 * 第二版的印是实心底、白字、逆时针歪 5°（「盖章从来不是水平的」）。
 * 这一版把它摆正、换成主题色淡底 + 主题色的字：Console 里的图标块就是这个样子，
 * 而一个歪着的方块在一页全是正交矩形的界面里会显得是渲染出了错。
 */
.seal {
  display: block;
  flex: none;
  width: 3rem;
  height: 3rem;
}

.seal rect {
  fill: var(--seal-soft-strong);
}

.seal text {
  fill: var(--seal-color, var(--seal));
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 600;
}

/* 空状态是行动的邀请，不是道歉：给一枚印、一句话、一个出口 */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s4);
  padding: var(--s16) var(--card-pad);
  text-align: center;
}

/* ==================================================================
   阅读进度
   ================================================================== */
/*
 * 一条 2px 的主题色线，贴在视口顶部、压在吸顶页眉的上边框上。
 * 初始 scaleX(0)：没有 JS 时它就是不存在。
 */
.reading-progress {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: calc(var(--z-sticky) + 1);
  height: 2px;
  background: var(--seal);
  transform: scaleX(0);
  transform-origin: 0 50%;
  pointer-events: none;
}

/* ==================================================================
   404
   ================================================================== */
.notfound-head {
  display: flex;
  gap: var(--s5);
  align-items: flex-start;
  padding: var(--card-pad);
}

.notfound-head .seal {
  width: 3.5rem;
  height: 3.5rem;
}

.notfound-code {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 1.8rem + 3vw, 3.5rem);
  font-weight: 600;
  line-height: 1;
  color: var(--fg-muted);
}

.notfound-text {
  margin: var(--s3) 0 0;
  color: var(--fg-muted);
}

.notfound-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-top: var(--s4);
}

/* ==================================================================
   账户页与表单页（登录 / 注册 / 找回密码 / 重置密码 / 账户）
   ================================================================== */
/*
 * 五页共用一套版面：一张居中的卡片，宽 26rem。
 *
 * 第二版这里是「在纸上落款」——没有卡片、没有阴影、左对齐不居中，
 * 输入框只有底部一条线。整套隐喻随主题一起作废：登录表单在 Console 里
 * 是一张居中的卡片，前台就该是同一张。
 */
.auth {
  max-width: 26rem;
  margin-inline: auto;
}

.auth-title {
  margin: 0 0 var(--s4);
  font-size: var(--fs-xl);
  font-weight: 600;
}

.auth-panel {
  padding: var(--card-pad);
}

/* 站长写的注册说明。它是页面的一部分而不是一次失败，故不套提示条 */
.auth-intro {
  margin: 0 0 var(--s4);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  line-height: 1.7;
}

.auth-field {
  margin: 0;
}

.auth-field + .auth-field {
  margin-top: var(--s4);
}

/* 输入框与「显示密码」共处的一层，供那个按钮定位 */
.field-control {
  position: relative;
  display: block;
}

.field-control .field-input {
  padding-right: 2.75rem;
}

/*
 * 密码可见切换。由 auth.js 注入（没有 JS 时不该出现一个点了没反应的按钮），
 * 嵌在输入框右端——与 Console 搜索框里的清除键同一个位置、同一种做法。
 */
.field-reveal {
  position: absolute;
  top: 50%;
  right: var(--s1);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: var(--corner-control);
  background: none;
  color: var(--fg-muted);
  transform: translateY(-50%);
  transition: color var(--t-fast) var(--ease-out),
    background-color var(--t-fast) var(--ease-out);
}

.field-reveal:hover {
  background: var(--surface-active);
  color: var(--fg);
}

.field-reveal svg {
  width: 1.125rem;
  height: 1.125rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.auth-actions {
  margin: var(--s6) 0 0;
}

/*
 * 提交键占满整行。Console 的浮层底栏是靠右排的，那是因为它那些弹窗普遍
 * 「取消 + 确认」成对出现；这里只有一个动作，整行就是它的宽度。
 */
.auth-actions .btn {
  width: 100%;
}

/* 支线出口（忘记密码 / 还没有账户 / 返回登录）：一条线之下的一行小字 */
.auth-alt {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  margin: var(--s5) 0 0;
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
  font-size: var(--fs-sm);
}

/* ---------- 账户页 ---------- */
.account-meta {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin-bottom: var(--s4);
  padding: var(--s5) var(--card-pad);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}

.account-meta .aside-row {
  gap: var(--s4);
}

.account-meta .aside-key {
  min-width: 3em;
}

.account-section-title {
  margin: 0 0 var(--s4);
  font-size: var(--fs-lg);
  font-weight: 600;
}

.account-logout {
  margin-top: var(--s4);
}

/* ---------- 个人中心（账户页） ---------- */
/*
 * 一页三块：形象（封面 + 方印 + 昵称 + 简介）、编辑资料、账号本身。
 *
 * 它比正文宽（.wrap--wide）：两栏排版，正文那 36 个汉字的栏宽在这里只会把表单挤成一条。
 */
.account {
  max-width: 64rem;
  margin-inline: auto;
}

/*
 * 形象区是一张卡片，但封面的两条边要贴着卡片的圆角，
 * 所以 overflow: hidden 由这一层给——**子元素自己裁不掉父级的圆角**。
 */
.account-hero {
  overflow: hidden;
  margin-bottom: var(--s6);
  border: 1px solid var(--line);
  border-radius: var(--corner-card);
  background: var(--surface);
  box-shadow: var(--elevation-card);
}

/*
 * 封面带。没有封面时它仍然占位：一片印色淡底 + 一道下边线，
 * 与文章页的默认封面同一种处理（那里也是「没有图」而不是「少一块」）。
 *
 * 高度用 clamp 而不是 aspect-ratio：aspect-ratio 配 max-height 会把**宽度**也缩掉
 * （宽高比与上限一起算，宽度只好让步），于是封面带在宽屏上两边各空出一截。
 */
.account-banner {
  height: clamp(7rem, 18vw, 12rem);
  border-bottom: 1px solid var(--line);
  background: linear-gradient(
    135deg,
    var(--seal-soft),
    color-mix(in oklab, var(--seal) 6%, var(--surface))
  );
}

.account-banner img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* 方印头像：比页眉那枚大一圈（3.5rem），压在封面的下沿上 */
.account-id {
  display: flex;
  align-items: flex-start;
  gap: var(--s5);
  padding: 0 var(--card-pad) var(--card-pad);
}

.account-seal {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 4.5rem;
  height: 4.5rem;
  margin-top: calc(var(--s7) * -1);
  border: 3px solid var(--surface);
  border-radius: var(--corner-seal);
  background: var(--seal);
  color: var(--seal-on);
  font-size: var(--fs-xl);
  font-weight: 500;
  object-fit: cover;
  user-select: none;
}

.account-seal img {
  width: 100%;
  height: 100%;
  border-radius: calc(var(--corner-seal) - 1px);
  object-fit: cover;
}

.account-id-text {
  min-width: 0;
  padding-top: var(--s4);
}

.account-name {
  margin: 0;
  font-size: var(--fs-2xl);
  font-weight: 600;
  line-height: 1.3;
}

.account-handle {
  margin: var(--s1) 0 0;
  color: var(--fg-subtle);
  font-size: var(--fs-sm);
}

.account-bio {
  max-width: 34em;
  margin: var(--s3) 0 0;
  color: var(--fg-muted);
  line-height: 1.75;
  overflow-wrap: anywhere;
}

.account-bio.is-empty {
  color: var(--fg-subtle);
}

/*
 * 两栏：左边改资料（宽），右边是账号本身（窄）。
 * 断点按容器所在的视口给——这一页没有容器查询的必要，整页只有这一处两栏。
 */
.account-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  gap: var(--s6);
  align-items: start;
}

.account-main,
.account-block {
  padding: var(--card-pad);
  border: 1px solid var(--line);
  border-radius: var(--corner-card);
  background: var(--surface);
  box-shadow: var(--elevation-card);
}

.account-side {
  display: flex;
  flex-direction: column;
  gap: var(--s5);
}

/*
 * 提交键在这一页不铺满整行。
 * 那条「按钮占满整行」的规则是给登录 / 注册页写的——那页只有一个动作，
 * 整行就是它的宽度；这里的卡片有六百多像素宽，一条贯穿的实心条子会把版面压住。
 */
.account .auth-actions .btn {
  width: auto;
}

/* 账号信息里的行不带卡片自己的内边距（它已经在 .account-block 里了） */
.account-block .account-meta {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
}

.account-link {
  margin: var(--s4) 0 0;
  font-size: var(--fs-sm);
}

/* ---------- 图片字段（头像 / 封面） ---------- */

.account-media + .account-media {
  margin-top: var(--s5);
}

.account-media-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
  align-items: baseline;
  margin-bottom: var(--s2);
}

.account-media-label {
  font-size: var(--fs-sm);
  font-weight: 500;
}

.account-media-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
}

.account-media-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--line);
  border-radius: var(--corner-seal);
  background: var(--seal);
  color: var(--seal-on);
  font-size: var(--fs-md);
  overflow: hidden;
}

.account-media-thumb.is-wide {
  width: 5rem;
  height: 2.75rem;
  background: var(--surface-inset);
}

.account-media-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * 文件输入框不套自绘的假按钮：原生控件自带「未选择文件 / 文件名」这句话，
 * 而那正是用户选完文件后最需要确认的一件事。只把浏览器自带的按钮涂成站内的样子。
 */
.account-media input[type="file"] {
  /* 清掉 .auth-form 给文本输入框的那套框：文件框自己带一个按钮，
     再套一层输入框的边框就成了框里套框 */
  flex: 1 1 12rem;
  width: auto;
  min-width: 0;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}

.account-media input[type="file"]::file-selector-button {
  margin-inline-end: var(--s3);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line-strong);
  border-radius: var(--corner-control);
  background: var(--surface);
  color: var(--fg);
  font: inherit;
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease-out),
    color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}

.account-media input[type="file"]::file-selector-button:hover {
  border-color: var(--seal);
  background: var(--seal-soft);
  color: var(--seal);
}

.account-remove {
  display: flex;
  gap: var(--s2);
  align-items: center;
  margin-top: var(--s2);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  cursor: pointer;
}

.account-remove input[type="checkbox"] {
  /* 复选框在 .auth-form 里会被当成文本输入框（那条规则给所有非 hidden 的 input
     套上整宽与 2.75rem 的最小高度），所以这里要连同尺寸一起复位。
     勾选色跟主题色走，与 Console 的复选框同一种做法。 */
  flex: none;
  width: 1rem;
  height: 1rem;
  min-height: 0;
  margin: 0;
  padding: 0;
  accent-color: var(--seal);
}

/* 窄屏：两栏并一栏，右栏（账号与密码）落到下面 —— 那是「改完资料再看」
   的顺序，比把它顶到上面更顺 */
@media (max-width: 56rem) {
  .account-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .account-id {
    gap: var(--s4);
  }
}

/* ==================================================================
   账户弹窗
   ================================================================== */
/*
 * 与 Console 的浮层逐项对齐（console/src/components/ui/dialog.tsx）：
 * 6px 圆角、1px 描边、面板底色、浮层阴影，结构是「标题栏 / 主体 / 动作区」。
 *
 * 用原生 <dialog> 而不是自绘的一层 div：焦点被困在弹窗里、背景 inert、
 * Esc 关闭、关闭后焦点还给触发它的链接——这几件事由浏览器提供，
 * 自己实现每一个都容易漏掉一处。
 *
 * `display` **只能写在 [open] 上**。
 * 浏览器自带的 `dialog:not([open]) { display: none }` 来自 UA 样式表，
 * 而作者样式无条件赢过 UA 样式——在 `.auth-dialog` 上直接写 display: flex，
 * 等于把那条隐藏规则整个抹掉，于是没打开的弹窗常驻在页面左上角：
 * 一个空壳子摊在正文之上，点它也没有反应。
 *
 * 这条踩过一次（2026-09-14），所以有 TestBuiltinThemeDialogStaysHidden
 * 盯着：那条测试读的就是这份 CSS。
 */
.auth-dialog[open] {
  display: flex;
  flex-direction: column;
}

.auth-dialog {
  width: min(32rem, calc(100vw - var(--s8)));
  max-height: calc(100dvh - var(--s12));
  margin: auto;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--corner-overlay);
  background: var(--surface);
  color: var(--fg);
  box-shadow: var(--elevation-overlay);
}

.auth-dialog::backdrop {
  background: var(--scrim);
}

.auth-dialog[open] {
  animation: auth-dialog-in var(--t-normal) var(--ease-out);
}

.auth-dialog[open]::backdrop {
  animation: auth-backdrop-in var(--t-normal) var(--ease-out);
}

/* 与 Console 的 panel-in 同一条：淡入 + 极轻微放大 */
@keyframes auth-dialog-in {
  from {
    opacity: 0;
    scale: 0.985;
  }
  to {
    opacity: 1;
    scale: 1;
  }
}

@keyframes auth-backdrop-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/*
 * 标题栏：标题在左，关闭在右，底下一条线。
 * 标题是当前这一步的名字（登录 / 创建账户 / 重置密码），由 auth.js 从面板上取。
 */
.auth-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  padding: var(--s3) var(--s3) var(--s3) var(--s5);
  border-bottom: 1px solid var(--line);
}

.auth-dialog-title {
  margin: 0;
  font-size: var(--fs-xl);
  font-weight: 600;
}

/* 关闭键：Console 的 ghost 图标按钮，2rem 见方 */
.auth-dialog-close {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: var(--corner-control);
  background: none;
  color: var(--fg-muted);
  transition: color var(--t-fast) var(--ease-out),
    background-color var(--t-fast) var(--ease-out);
}

.auth-dialog-close:hover {
  background: var(--surface-active);
  color: var(--fg);
}

.auth-dialog-close svg {
  width: 1.125rem;
  height: 1.125rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* 主体。表单长过视口时在这里滚，标题栏与关闭键留在原处 */
.auth-dialog-body {
  min-height: 0;
  flex: 1;
  overflow-y: auto;
  padding: var(--s4) var(--s5);
}

/*
 * 弹窗里的面板不再是卡片：弹窗本身已经是那张卡片，
 * 里面再套一层描边与阴影就成了卡片里的卡片。
 */
.auth-dialog .auth-panel {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}

/*
 * 提交那一行在弹窗里画成贴底的动作区：一条分隔线撑满弹窗宽度，按钮撑满整行。
 * DOM 没动——它仍是表单里的那个 <p>，只是这里让它看起来像 Console 浮层的底栏。
 *
 * 没有把按钮真搬进第三块底栏，是因为主体装的是服务端产出的整张表单，
 * 提交按钮就在它里面；搬出去就得在两处维护同一张表单，
 * 而漏改的那一次表现为「线上登录偶发失败」。
 */
.auth-dialog .auth-actions {
  margin: var(--s5) calc(var(--s5) * -1) 0;
  padding: var(--s3) var(--s5) 0;
  border-top: 1px solid var(--line);
}

/* 支线出口在弹窗里不再自带分隔线：上面那条动作区已经画过一条 */
.auth-dialog .auth-alt {
  margin-top: var(--s3);
  padding-top: 0;
  border-top: 0;
}

/* 取页面面板的那一小段时间。留着空面板会像坏掉了 */
.auth-dialog-loading {
  margin: 0;
  color: var(--fg-muted);
  font-size: var(--fs-sm);
}

/*
 * 打开弹窗时锁住背后的滚动。
 * <dialog> 只做到「背景不可交互」，滚轮照样能把背后的文章滚走——
 * 人在填表单，身后的页面自己动起来，是这一层最容易被忽略的破绽。
 */
html.auth-open {
  overflow: hidden;
}

/* 窄屏：弹窗几乎贴边，内边距跟着收 */
@media (max-width: 30rem) {
  .auth-dialog {
    width: calc(100vw - var(--s4));
  }

  .auth-dialog-head {
    padding-left: var(--s4);
  }

  .auth-dialog-body {
    padding: var(--s4);
  }

  .auth-dialog .auth-actions {
    margin-inline: calc(var(--s4) * -1);
    padding-inline: var(--s4);
  }
}

/* ==================================================================
   动效与打印
   ================================================================== */
/*
 * 全站只有回应用户动作的那一类过渡。入场动画由 motion.js 另行负责，
 * 并受主题设置里的「页面动效」总闸控制。
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

@media print {
  .site-header,
  .site-footer,
  .comments,
  .pagination,
  .related,
  .reading-progress,
  .skip-link {
    display: none;
  }

  body {
    background: #fff;
    color: #000;
    font-size: 11pt;
  }

  /* 纸上不需要卡片：去掉描边、阴影与圆角，内容直接落在页面上 */
  .article,
  .post-list,
  .archive-header,
  .auth-panel,
  .card {
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding-inline: 0;
  }

  .wrap {
    max-width: none;
    padding-inline: 0;
  }

  /* 打印时把链接地址显式写出来，否则纸面上的链接等于失效 */
  .prose a::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
    color: #555;
  }
}

/* ==================================================================
   首页模块与列表版式（2026-09-15）
   ------------------------------------------------------------------
   这一节是新加的：首页从「一篇文章列表」变成了「若干模块自上而下」，
   列表页也从单栏变成了「文章 + 侧栏」，于是多了一套容器与卡片的样式。

   两条贯穿始终的规矩：

   1. **模块之间靠间距分层，不靠边框**。八个模块各自加一圈边框，
      首页会变成一摞盒子；模块之间留出空白，再用一条细线把标题与它的内容连起来就够了。

   2. **卡片是链接，里面不再套完整的链接**。分类名在卡片上是纯文字，
      标题才是链接——一个卡片里两个链接，触屏上点哪儿都要试一次。
   ================================================================== */

:root {
  /*
   * 侧栏宽度：有一个下限，再跟着正文栏宽往上走。
   *
   * 原先是写死的 17rem。两个毛病：默认太窄，卡片扣掉内边距只剩十三个汉字，
   * 文章标题基本都要折两行；而且站长把正文栏宽从 36 字调到 48 字之后它纹丝不动，
   * 页面越宽，侧栏看起来越像一条缝。
   *
   * 系数取 0.45 而不是 1：侧栏放的是标题、标签与评论摘要，不是长文，
   * 没有理由跟着正文等比例长。下限 20rem 保证 36 字那档也有十六七个字宽。
   *
   * 加宽只让外壳跟着变宽，不挤主列——主列的宽度由 --shell 定，与这里无关。
   */
  --sidebar-w: max(20rem, calc(var(--measure) * 0.45));
  /*
   * 带侧栏页面（首页、列表页、挂了侧栏的文章页）的外壳：正文外壳 + 侧栏 + 两者的间距。
   *
   * 原先写死成 min(72rem, 100%)。写死的后果是站长在「外观」里把正文栏宽从 36 字
   * 调到 44 字之后，文章页跟着变宽，分类页纹丝不动——同一个站的两种页面对不上，
   * 而「正文栏宽」这个设置看起来只管了一半的页面。现在两处都从同一个式子推出来。
   *
   * 首页没有侧栏，用同一个宽度是为了让它与列表页左右边界对齐：
   * 从首页点进分类页，页面不该整体横向跳一下。
   */
  --shell-wide: min(calc(var(--shell) + var(--sidebar-w) + var(--s6)), 100%);
}

.wrap--wide {
  max-width: var(--shell-wide);
}

/*
 * 页眉与页脚跟最宽的那种页面对齐。
 *
 * 它们原先用 --shell（正文外壳）：那时文章页是单栏，两者正好对齐，代价是
 * 首页与列表页比它宽一截，刊名于是悬在内容左边缘往里一百多像素的位置。
 * 文章页也挂上侧栏之后，再用 --shell 就连那一种对齐都保不住。
 *
 * 改成跟 --shell-wide 走：带侧栏的页面严丝合缝；站长关掉侧栏时是页眉比正文宽，
 * 那是常见做法，比刊名缩在正文里侧好。
 */
.site-header .wrap,
.site-footer .wrap {
  max-width: var(--shell-wide);
}

/* 卡片比例：由「列表与卡片」里的设置给到 html 上 */
[data-cover-ratio="4/3"] .cards--grid .card-cover {
  aspect-ratio: 4 / 3;
}
[data-cover-ratio="1/1"] .cards--grid .card-cover {
  aspect-ratio: 1 / 1;
}

/* ---------- 模块的外壳 ---------- */

.home-module + .home-module {
  margin-top: var(--s10, 2.5rem);
}

.module-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s4);
  margin-bottom: var(--s5);
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--line);
}

.module-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.module-more {
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  text-decoration: none;
  white-space: nowrap;
}
.module-more:hover {
  color: var(--seal);
}

.home-intro {
  margin-bottom: var(--s10, 2.5rem);
}
.home-intro-title {
  margin: 0 0 var(--s2);
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 1.2rem + 1.6vw, 2.75rem);
  font-weight: 600;
  letter-spacing: -0.02em;
}
.home-intro-desc {
  margin: 0;
  max-width: 46ch;
  color: var(--fg-muted);
  font-size: var(--fs-md);
  line-height: var(--lh-ui);
}

/* ---------- 卡片网格与瀑布流 ---------- */

.cards {
  display: grid;
  grid-template-columns: repeat(var(--columns, 3), minmax(0, 1fr));
  gap: var(--s5);
}

/*
 * 瀑布流用 CSS 多列而不是 grid：多列的每一列高度不齐，正是瀑布流要的效果，
 * 而 grid 的行是对齐的。代价是条目按列而非按行流动（第一列排完才排第二列），
 * 动态追加内容时观感会跳——列表页是分页的，不存在这个问题。
 */
.cards--masonry {
  display: block;
  columns: var(--columns, 3);
  column-gap: var(--s5);
}
.cards--masonry .card {
  break-inside: avoid;
  margin-bottom: var(--s5);
}

.card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
  transition:
    transform var(--t-normal) var(--ease-out),
    box-shadow var(--t-normal) var(--ease-out);
}
.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--elevation-overlay);
}

.card-cover {
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--surface-inset);
}
.cards--masonry .card-cover {
  aspect-ratio: auto;
}
.card-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--t-normal) var(--ease-out);
}
.card:hover .card-cover img {
  transform: scale(1.03);
}

.card-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s5);
}

.card-terms {
  margin: 0;
  color: var(--seal);
  font-size: var(--fs-xs);
  letter-spacing: 0.02em;
}

.card-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 600;
  line-height: 1.45;
}
.card-title a {
  color: inherit;
  text-decoration: none;
}
.card-title a:hover {
  color: var(--seal);
}

.card-excerpt {
  margin: 0;
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-ui);
  /* 行数由模块或列表设置给（data-excerpt），默认两行 */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
[data-excerpt="none"] .card-excerpt {
  display: none;
}
[data-excerpt="one"] .card-excerpt {
  -webkit-line-clamp: 1;
  line-clamp: 1;
}
[data-excerpt="three"] .card-excerpt {
  -webkit-line-clamp: 3;
  line-clamp: 3;
}

.card-meta {
  display: flex;
  gap: var(--s3);
  margin: auto 0 0;
  padding-top: var(--s2);
  color: var(--fg-subtle);
  font-size: var(--fs-xs);
}

/* ---------- 列表方式（一行一篇） ---------- */

.post-list--dense .post-item {
  padding: var(--s4) var(--s5);
}
.post-list--dense .post-item-title {
  font-size: var(--fs-lg);
}

/* ---------- 轮播 ---------- */

.module-carousel .module-head {
  border-bottom: 0;
  padding-bottom: 0;
  margin-bottom: var(--s3);
}

.carousel {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--corner-card);
  background: var(--surface);
  box-shadow: var(--elevation-card);
}

.carousel-track {
  display: flex;
  margin: 0;
  padding: 0;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  list-style: none;
}
.carousel-track::-webkit-scrollbar {
  display: none;
}

.carousel-slide {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: start;
  aspect-ratio: 21 / 9;
  background: var(--surface-inset);
}
.carousel-slide > a {
  display: block;
  height: 100%;
  color: #fff;
  text-decoration: none;
}
.carousel-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * 文字压在图上，必须自己保证对比度：图是站长上传的，底色无从预料，
 * 故用一层自下而上的黑到透明的渐变兜底，而不是只给文字加阴影。
 */
.carousel-caption {
  position: absolute;
  inset: auto 0 0;
  padding: var(--s8) var(--s6) var(--s6);
  background: linear-gradient(
    to top,
    oklch(0.12 0 0 / 0.9),
    oklch(0.12 0 0 / 0.62) 58%,
    transparent
  );
}
/*
 * 没有封面时不给高度：21:9 的框里只放两行字，会变成首页顶部一大块空白。
 * 新站点的文章通常还没有配图，这不该让首页看起来像坏了。
 */
.carousel-slide.no-cover {
  aspect-ratio: auto;
}
.carousel-slide.no-cover > a {
  color: var(--fg);
}
.carousel-slide.no-cover .carousel-caption {
  position: static;
  padding: var(--s6);
  background: none;
  color: var(--fg);
}
.carousel-terms {
  margin: 0 0 var(--s1);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  opacity: 0.9;
}
.carousel-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1rem + 1vw, 2rem);
  font-weight: 600;
  line-height: 1.35;
  text-wrap: balance;
}
.carousel-excerpt {
  margin: var(--s2) 0 0;
  max-width: 60ch;
  font-size: var(--fs-sm);
  line-height: var(--lh-ui);
  opacity: 0.86;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.carousel-meta {
  margin: var(--s2) 0 0;
  font-size: var(--fs-xs);
  opacity: 0.8;
}

.carousel-nav {
  position: absolute;
  top: 50%;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border: 0;
  border-radius: var(--corner-full);
  background: oklch(0.15 0 0 / 0.45);
  color: #fff;
  cursor: pointer;
  transform: translateY(-50%);
  opacity: 0;
  transition:
    background var(--t-fast) var(--ease-out),
    opacity var(--t-fast) var(--ease-out);
}
.carousel:hover .carousel-nav,
.carousel-nav:focus-visible {
  opacity: 1;
}
.carousel-nav:hover {
  background: oklch(0.15 0 0 / 0.7);
}
.carousel-nav[data-carousel-prev] {
  left: var(--s3);
}
.carousel-nav[data-carousel-next] {
  right: var(--s3);
}

.carousel-dots {
  position: absolute;
  right: var(--s5);
  bottom: var(--s3);
  display: flex;
  gap: var(--s2);
}
.carousel-dot {
  width: 0.375rem;
  height: 0.375rem;
  padding: 0;
  border: 0;
  border-radius: var(--corner-full);
  background: oklch(1 0 0 / 0.5);
  cursor: pointer;
  transition:
    width var(--t-normal) var(--ease-out),
    background var(--t-fast) var(--ease-out);
}
.carousel-dot[aria-current="true"] {
  width: 1.125rem;
  background: #fff;
}

/* ---------- 公告条 ---------- */

.notice-bar {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin: 0;
  padding: var(--s3) var(--s5);
  border: 1px solid var(--line);
  border-left: 3px solid var(--seal);
  border-radius: var(--corner-card);
  background: var(--seal-soft);
  font-size: var(--fs-sm);
  line-height: var(--lh-ui);
}
.notice-tag {
  flex: none;
  color: var(--seal);
  font-weight: 600;
  letter-spacing: 0.06em;
}

/* ---------- 分类卡片 ---------- */

.cat-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--s4);
}

.cat-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--corner-card);
  background: var(--surface);
  overflow: hidden;
  text-decoration: none;
  transition:
    border-color var(--t-fast) var(--ease-out),
    transform var(--t-normal) var(--ease-out);
}
.cat-card:hover {
  border-color: var(--seal);
  transform: translateY(-2px);
}
.cat-cover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.16;
}
.cat-name {
  color: var(--fg);
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
}
.cat-count,
.cat-desc {
  color: var(--fg-muted);
  font-size: var(--fs-xs);
}
.cat-desc {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* ---------- 排行榜（热门） ---------- */

.rank-list {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: rank;
}
.rank-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s4);
  padding: var(--s2) 0;
  counter-increment: rank;
}
.rank-list li + li {
  border-top: 1px solid var(--line);
}
.rank-list li::before {
  content: counter(rank);
  flex: none;
  width: 1.5rem;
  color: var(--fg-subtle);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}
.rank-list li:nth-child(-n + 3)::before {
  color: var(--seal);
  font-weight: 600;
}
.rank-list a {
  flex: 1;
  min-width: 0;
  color: var(--fg);
  overflow: hidden;
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rank-list a:hover {
  color: var(--seal);
}
.rank-list--lg a {
  font-size: var(--fs-md);
}
.rank-meta {
  flex: none;
  color: var(--fg-subtle);
  font-size: var(--fs-xs);
}

/* ---------- 标签云（首页模块里的放大版） ---------- */

/* 标签云（首页模块）：间距略松一档，标签本身与别处一模一样 */
.cloud--lg {
  gap: var(--s3);
}

/*
 * 标签上的计数。跟着标签的文字色走（opacity 而不是换一个灰），
 * 否则一枚暖色的标签里会嵌进一个冷灰的数字。
 */
.tag-count {
  margin-inline-start: var(--s2);
  font-size: 0.85em;
  opacity: 0.75;
}

/* ---------- 友情链接 ---------- */

.link-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.link-list a {
  display: inline-block;
  padding: var(--s2) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--corner-card);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  text-decoration: none;
  transition:
    border-color var(--t-fast) var(--ease-out),
    color var(--t-fast) var(--ease-out);
}
.link-list a:hover {
  border-color: var(--seal);
  color: var(--seal);
}

/* ---------- 列表页 + 侧栏 ---------- */

.list-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--sidebar-w);
  gap: var(--s6);
  align-items: start;
}
.list-layout.no-aside {
  grid-template-columns: minmax(0, 1fr);
}

.list-aside {
  display: flex;
  flex-direction: column;
  gap: var(--s5);
}

/*
 * 跟随滚动是个开关（sidebar.sticky），不是默认。
 * 小组件堆到比一屏还高时，sticky 会把超出视口的那几个永久顶在屏幕外面——
 * 站长加满十个小组件的那天，最下面的两个就再也滚不到了。
 */
.list-aside.is-sticky {
  position: sticky;
  top: calc(3.5rem + var(--s4));
}

.side-block {
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--corner-card);
  background: var(--surface);
  box-shadow: var(--elevation-card);
}

.side-title {
  margin: 0 0 var(--s3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
  color: var(--fg);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.side-text {
  margin: 0 0 var(--s3);
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-ui);
}
.side-links {
  display: flex;
  gap: var(--s4);
  margin: 0;
  font-size: var(--fs-xs);
}
.side-links a {
  color: var(--fg-muted);
  text-decoration: none;
}
.side-links a:hover {
  color: var(--seal);
}

.side-comments {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.side-comment-meta {
  margin: 0 0 var(--s1);
  color: var(--fg-subtle);
  font-size: var(--fs-xs);
  line-height: 1.5;
}
.side-comment-meta a {
  color: var(--fg-muted);
  text-decoration: none;
}
.side-comment-meta a:hover {
  color: var(--seal);
}
.side-comment-author {
  color: var(--fg);
  font-weight: 600;
}
.side-comment-body {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  margin: 0;
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  line-height: 1.6;
  overflow: hidden;
}

/* ---------- 文章页：目录与翻页 ---------- */

.article-toc {
  margin: var(--s6) 0;
  padding: var(--s4) var(--s5);
  border: 1px solid var(--line);
  border-left: 3px solid var(--seal);
  border-radius: var(--corner-card);
  background: var(--surface-raised);
}
.toc-title {
  margin: 0 0 var(--s2);
  color: var(--fg-muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
}
.toc-list {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-sm);
}
.toc-item a {
  display: block;
  margin-left: calc(-1 * var(--s3) - 2px);
  padding: var(--s1) 0 var(--s1) var(--s3);
  border-left: 2px solid transparent;
  color: var(--fg-muted);
  text-decoration: none;
  transition:
    color var(--t-fast) var(--ease-out),
    border-color var(--t-fast) var(--ease-out);
}
.toc-item.level-3 a {
  padding-left: var(--s6);
  font-size: var(--fs-xs);
}
.toc-item a:hover {
  color: var(--fg);
}
.toc-item a[aria-current="true"] {
  border-left-color: var(--seal);
  color: var(--seal);
}

.article-adjacent {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4);
  margin-top: var(--s6);
}
.adjacent-item {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: var(--s4) var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--corner-card);
  background: var(--surface);
  text-decoration: none;
  transition: border-color var(--t-fast) var(--ease-out);
}
.adjacent-item:hover {
  border-color: var(--seal);
}
.adjacent-item.is-empty {
  border-style: dashed;
  background: none;
}
.adjacent-next {
  text-align: right;
}
.adjacent-label {
  color: var(--fg-subtle);
  font-size: var(--fs-xs);
}
.adjacent-title {
  color: var(--fg);
  font-family: var(--font-display);
  font-size: var(--fs-base);
  font-weight: 600;
  line-height: 1.5;
}

/* ---------- 页脚 ---------- */

.footer-text {
  margin: 0 0 var(--s4);
  max-width: 60ch;
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-ui);
}
.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s4);
}
.footer-icp {
  margin: 0;
  color: var(--fg-subtle);
  font-size: var(--fs-xs);
}
.footer-icp a {
  color: inherit;
  text-decoration: none;
}
.footer-icp a:hover {
  color: var(--seal);
}
.footer-links .social {
  display: inline-flex;
  align-items: center;
  color: var(--fg-muted);
}
.footer-links .social:hover {
  color: var(--seal);
}

/* ---------- 侧栏小组件 ---------- */

/*
 * 小组件的内容排在 17rem 减去两侧内边距、约 13 个汉字宽的一列里。
 * 这个宽度决定了这一节所有的取舍：标题最多两行，日期另起一行，
 * 数量靠右对齐——没有哪一行还能塞下第二样东西。
 */

.side-posts {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: side-rank;
}

.side-post {
  padding: var(--s3) 0;
  counter-increment: side-rank;
}
.side-post:first-child {
  padding-top: 0;
}
.side-post:last-child {
  padding-bottom: 0;
}
.side-post + .side-post {
  border-top: 1px solid var(--line);
}

.side-post-title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  color: var(--fg);
  font-size: var(--fs-sm);
  line-height: var(--lh-ui);
  text-decoration: none;
}
.side-post-title:hover {
  color: var(--seal);
}

.side-post-date {
  display: block;
  margin-top: var(--s1);
  color: var(--fg-subtle);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

/*
 * 序号只给热门榜：那里的 1、2、3 是按评论数排出来的名次，本身是信息。
 * 最新文章按时间排，编号只会让人以为那也是排名。
 */
.side-posts.is-ranked .side-post {
  display: grid;
  grid-template-columns: 1.25rem minmax(0, 1fr);
  column-gap: var(--s2);
}
.side-posts.is-ranked .side-post::before {
  content: counter(side-rank);
  grid-row: 1;
  color: var(--fg-subtle);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: var(--lh-ui);
}
.side-posts.is-ranked .side-post > * {
  grid-column: 2;
}

/* 缩略图：图占满两行，标题与日期在右侧叠起来 */
.side-posts.is-thumb .side-post {
  display: grid;
  grid-template-columns: 3.25rem minmax(0, 1fr);
  column-gap: var(--s3);
  align-items: start;
}
.side-posts.is-thumb .side-post-title,
.side-posts.is-thumb .side-post-date {
  grid-column: 2;
}
/* 名次与缩略图同时开时让名次退场：两样都摆上，右边只剩七八个字的标题 */
.side-posts.is-thumb.is-ranked .side-post::before {
  display: none;
}

.side-post-thumb {
  grid-area: 1 / 1 / span 2 / 2;
  overflow: hidden;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--corner-card);
}
.side-post-thumb img,
.side-post-thumb .seal {
  display: block;
  width: 100%;
  height: 100%;
}
.side-post-thumb img {
  object-fit: cover;
}

/* 分类、归档、友链：左边名字右边数量，一行一个 */
.side-terms {
  margin: 0;
  padding: 0;
  list-style: none;
}
.side-terms li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s2) 0;
}
.side-terms li + li {
  border-top: 1px solid var(--line);
}
.side-terms li:first-child {
  padding-top: 0;
}
.side-terms li:last-child {
  padding-bottom: 0;
}
.side-terms a {
  min-width: 0;
  overflow: hidden;
  color: var(--fg);
  font-size: var(--fs-sm);
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.side-terms a:hover {
  color: var(--seal);
}

/* 数字右对齐要等宽字形，否则「9 篇」与「12 篇」的个位对不齐 */
.side-count {
  flex: none;
  color: var(--fg-subtle);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

/* 友链没有数量，条目之间也不必画线：它们是一组同类项，不是一张清单 */
.side-terms--plain li {
  padding: var(--s1) 0;
}
.side-terms--plain li + li {
  border-top: 0;
}

/*
 * 搜索：一条横线，一枚印。
 *
 * 原先这里是「浏览器原生输入框 + 实心按钮」。前台的输入框样式是按类名挂的
 * （见 .field-input 那一组），侧栏这个不在名单里，于是露出了浏览器默认控件——
 * 一个灰底圆角框，跟它旁边的方印与衬线标题隔着一个世界。
 *
 * 没有把它补进那份名单，是因为标准输入框在这里依然是「哪个网站都长这样」的东西。
 * 侧栏通篇只有这一个可操作的控件，那就让它说主题自己的话：横线是纸，提交键是印。
 * 印不是为这里新画的，它本来就在空状态、404 与归档页头上用着，这次只是用在动作上。
 */
.side-search {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line-strong);
  transition: border-color var(--t-fast) var(--ease-out);
}

/*
 * 焦点落进来时整条线变色并加粗一像素。
 * 加粗是留给分不清蓝与灰的人的第二个信号——焦点不能只靠颜色说话。
 * 用 box-shadow 叠出那一像素，改 border-width 会让整行跳一下。
 */
.side-search:focus-within {
  border-bottom-color: var(--seal);
  box-shadow: 0 1px 0 0 var(--seal);
}

.side-search input {
  flex: 1;
  min-width: 0;
  padding: var(--s1) 0;
  border: 0;
  background: none;
  color: var(--fg);
  font-family: inherit;
  font-size: var(--fs-base);
  line-height: 1.6;
  appearance: none;
}
.side-search input::placeholder {
  color: var(--fg-subtle);
}
/* 焦点由整条底线表达，输入框自己不必再画第二个框 */
.side-search input:focus {
  outline: none;
}
/* Chrome 给 type=search 配的那个叉：它和印挨在一起，是两个动作挤在一处 */
.side-search input::-webkit-search-cancel-button {
  display: none;
}

/* 印占 2.25rem，按钮撑到 2.75rem——手指点的是按钮，眼睛看的是印 */
.side-search-seal {
  display: grid;
  flex: none;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.side-search-seal .seal {
  width: 2.25rem;
  height: 2.25rem;
  opacity: 0.7;
  transition: opacity var(--t-fast) var(--ease-out);
}
/* 光标悬上去、或者光标正在输入框里时，印落实 */
.side-search-seal:hover .seal,
.side-search:focus-within .seal {
  opacity: 1;
}
.side-search-seal:focus-visible {
  border-radius: var(--corner-seal);
  outline: 2px solid var(--seal);
  outline-offset: 2px;
}

/* 自定义内容：站长自己写的 HTML，只负责把它约束在侧栏的字号与宽度里 */
.side-html {
  color: var(--fg-muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-ui);
  overflow-wrap: anywhere;
}
.side-html > :first-child {
  margin-top: 0;
}
.side-html > :last-child {
  margin-bottom: 0;
}
.side-html img {
  max-width: 100%;
  height: auto;
  border-radius: var(--corner-card);
}
.side-html a {
  color: var(--seal);
}

/* ---------- 窄屏 ---------- */

@media (max-width: 68rem) {
  .list-layout {
    grid-template-columns: minmax(0, 1fr);
  }
  /*
   * 单栏之后侧栏在正文底下，再跟随滚动就会粘住半个屏幕。
   * 选择器要带上 .is-sticky：只写 .list-aside 压不过上面那条同样带类名的规则。
   */
  .list-aside.is-sticky {
    position: static;
  }
  .cards--grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .cards--masonry {
    columns: 2;
  }
  .carousel-slide {
    aspect-ratio: 16 / 9;
  }
}

@media (max-width: 44rem) {
  .cards--grid,
  .cards--masonry {
    grid-template-columns: minmax(0, 1fr);
    columns: 1;
  }
  .carousel-slide {
    aspect-ratio: 4 / 3;
  }
  .carousel-nav {
    display: none;
  }
  .carousel-caption {
    padding: var(--s6) var(--s4) var(--s4);
  }
  .article-adjacent {
    grid-template-columns: minmax(0, 1fr);
  }
}
