/* ============================================================
   个人站 · 设计系统 v2
   蓝调纸色 + 清晰文字层级 + 语义色用于阶段标签
   改这里一处，全站跟着变。规范见 docs/01-设计规范.md
   ============================================================ */

/* ---------- 1. 设计令牌 ---------- */
/* 2026-09-22 用户定调：底色用**淡蓝**（我原稿推的是暖米白，用户否掉了）。
   整组灰阶都跟着偏蓝，否则蓝底配纯灰会显脏。 */
:root {
  /* 中性灰阶（浅色）—— 蓝调纸 */
  --bg: #f5f9fd;
  --surface: #ffffff;
  --text: #17202a;
  --text-2: #4c5a68;
  --text-3: #627587;
  --link: #245f99;
  /* 主页英雄区高度。基值用 vh（任何浏览器都懂）；支持 svh 的浏览器由文件末尾的
     @supports 换成 svh —— 手机地址栏收起/展开时 svh 不跳，vh 会跳。
     **不能只写 svh**：不支持它的浏览器会把整条声明作废（自定义属性在"用的时候"才判无效），
     而 `var(--land-height)` 一旦无效就退成 auto —— 主页那 100svh 的整块会塌掉。 */
  --land-height: max(100vh, 620px);
  --border: #e3ebf4;
  --border-strong: #ccd9e7;
  --hover: rgba(30, 64, 110, 0.05);

  /* 阶段语义色 —— 只做小圆点与文字，不做色块 */
  --st-ideas: #a1a1aa;
  --st-planned: #6366f1;
  --st-ongoing: #f59e0b;
  --st-done: #10b981;

  /* 「我的活动」时间线上"计划中"那一条的颜色（2026-09-30 站主指定，取自他的样图 #366d9d）。
     **单独一个令牌**，不直接改 `--st-planned`：那个靛蓝还是 Progress 阶段语义与代码高亮里
     "关键字/变量"的色源，改它会连带改动若干与活动页无关的面。 */
  --act-planned: #366d9d;

  /* 代码高亮 token 色 —— **只从上面四个阶段色里取，不新增颜色**（站的原则：
     "不引入彩色主色，彩色只出现在小面积上"）。再往正文墨色 `--text` 混一点：
     那四个色本来是给 6px 圆点调的，直接当正文色在浅底上偏刺眼、饱和度也太高；
     混 `--text` 还顺带让深浅两个主题自动跟着走，不用各写一份。 */
  --code-com: var(--text-3);                                          /* 注释 = 灰 */
  --code-str: color-mix(in srgb, var(--st-done) 72%, var(--text));    /* 字符串 = 绿 */
  --code-num: color-mix(in srgb, var(--st-ongoing) 70%, var(--text)); /* 数字 = 琥珀 */
  --code-kw:  color-mix(in srgb, var(--st-planned) 88%, var(--text)); /* 关键字 / YAML 键 = 靛蓝 */
  --code-var: color-mix(in srgb, var(--st-planned) 68%, var(--text)); /* 变量 = 淡一档 */
  --code-cmd: color-mix(in srgb, var(--st-planned) 58%, var(--text)); /* 行首命令 = 再淡一档 */
  --code-pun: var(--text-3);                                          /* 标点 = 灰 */

  /* 账号子系统的语义色（阶段 4）—— 和阶段色一样，只做文字与描边，不做色块 */
  --danger: #b42318;
  --ok: #0f7b53;

  /* 尺度 */
  --measure: 680px;
  --radius: 10px;
  --radius-sm: 6px;
  --dur: 160ms;
  --ease: cubic-bezier(0.4, 0, 0.2, 1);

  /* 上栏的底与阴影 —— **单列成令牌**。
     因为"主页收起成的上栏"（.land.is-done）和"真实上栏"（.nav）是两个不同的元素，
     只有共用同一组令牌才能保证看起来是同一个东西。
     曲线最前那层也用这个底色，所以收起后那 64px 条和真实上栏严丝合缝。
     2026-09-22 用户定：以"收起态"为准（比原来更实、且常驻阴影）。 */
  --navbg: #eaf3fc;
  --navshadow: 0 16px 32px -18px rgba(20, 50, 90, 0.30);

  /* 字标（wordmark）的字距 —— **只此一处**，`.land-name`（主页大字）与 `.nav-brand`（上栏）
     共用。2026-09-25：这两处原来各写一个值（-0.035em vs -0.01em），于是收起动画把大字
     等比缩到上栏字号时**宽度差 4.6%**，交接那一瞬间站名会"突然放大一点"。
     判据：等比缩放只能对齐字号，对齐不了字距 —— 两边必须是同一个值。 */
  --wordmark-tracking: -0.035em;

  /* 字体：不引外链，走系统栈（中文优先苹方/微软雅黑） */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", system-ui, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace;
}

/* 深色模式：跟随系统 + 手动切换（手动优先）—— 同样偏蓝，不能只改浅色 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0b1118;
    --surface: #121a23;
    --text: #f0f4f8;
    --text-2: #9aa8b6;
    --text-3: #8b9bab;
    --link: #99caff;
    --border: #1e2833;
    --border-strong: #33404e;
    --hover: rgba(190, 215, 245, 0.06);
    --navbg: #101c29;
    --navshadow: 0 16px 32px -18px rgba(0, 0, 0, 0.55);
    --st-ideas: #71717a;
    --st-planned: #818cf8;
    --st-ongoing: #fbbf24;
    --st-done: #34d399;
    --act-planned: #70abdb;   /* 同一个蓝在深色底上抬亮一档（与 --st-planned 的深浅关系同理） */
    --danger: #ff9b8f;
    --ok: #4ade80;
  }
}
[data-theme="dark"] {
  --bg: #0b1118;
  --surface: #121a23;
  --text: #f0f4f8;
  --text-2: #9aa8b6;
  --text-3: #8b9bab;
  --link: #99caff;
  --border: #1e2833;
  --border-strong: #33404e;
  --hover: rgba(190, 215, 245, 0.06);
  --navbg: #101c29;
  --navshadow: 0 16px 32px -18px rgba(0, 0, 0, 0.55);
  --st-ideas: #71717a;
  --st-planned: #818cf8;
  --st-ongoing: #fbbf24;
  --st-done: #34d399;
  --act-planned: #70abdb;
  --danger: #ff9b8f;
  --ok: #4ade80;
}

/* ---------- 2. 基础 ---------- */
* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }
html { color-scheme: light dark; scrollbar-gutter: stable; }
html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"] { color-scheme: dark; }
button, input { font: inherit; }
:focus-visible { outline: 2px solid var(--link); outline-offset: 4px; }
.skip-link { position: fixed; top: 8px; left: 16px; z-index: 200; padding: 8px 16px; background: var(--surface); border-radius: 6px; transform: translateY(-160%); }
.skip-link:focus { transform: none; }
main:focus { outline: none; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.75;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

::selection { background: var(--text); color: var(--bg); }

/* 内容列：窄一点，读起来才舒服 */
.wrap {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0 24px;
}

/* ---------- 3. 导航 ---------- */
.nav {
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 10;
  /* 底色与阴影用**共用令牌** —— 和"主页收起成的上栏"（.land.is-done）同一组，
     两边才能看起来是一个东西。令牌定义见 :root 里的 --navbg / --navshadow。
     2026-09-22 用户定：以"收起态"为准（比原来更实、且常驻阴影）。 */
  background: var(--navbg);
  box-shadow: var(--navshadow);
  transition: box-shadow 200ms var(--ease), border-color 200ms var(--ease);
}
.nav.is-stuck {
  border-bottom-color: transparent;
  box-shadow: 0 1px 2px rgba(20, 50, 90, 0.05), var(--navshadow);
}
.nav-inner {
  max-width: 1120px;
  margin: 0 auto;
  padding: 14px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.nav-brand { font-weight: 650; letter-spacing: var(--wordmark-tracking); }
.nav-links { display: flex; align-items: center; gap: 4px; }
.nav-link {
  padding: 5px 10px;
  border-radius: var(--radius-sm);
  color: var(--text-2);
  font-size: 14px;
  position: relative;
}
.nav-link:hover { color: var(--text); background: var(--hover); }
.nav-link[aria-current="page"] { color: var(--text); font-weight: 550; }
.nav-link[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 10px; right: 10px; bottom: -15px;
  height: 1px;
  background: var(--text);
}
.nav-toggle {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-2);
  border-radius: var(--radius-sm);
  padding: 4px 9px;
  font-size: 13px;
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.nav-toggle:hover { color: var(--text); border-color: var(--border-strong); }

/* ---------- 4. 页头 ---------- */
.page { padding: 56px 0 96px; }
.hero { padding: 40px 0 8px; }
.hero h1 {
  margin: 0 0 10px;
  font-size: 34px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.hero p { margin: 0; color: var(--text-2); max-width: 46ch; }

.page-title {
  margin: 0 0 6px;
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.015em;
}
.page-desc { margin: 0 0 40px; color: var(--text-2); }

/* ---------- 5. 阶段分区 ---------- */
.stage { margin-bottom: 44px; }
.stage-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
.stage-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  flex: none;
  background: var(--st-ideas);
}
.stage[data-stage="ideas"]   .stage-dot { background: var(--st-ideas); }
.stage[data-stage="planned"] .stage-dot { background: var(--st-planned); }
.stage[data-stage="ongoing"] .stage-dot { background: var(--st-ongoing); }
.stage[data-stage="done"]    .stage-dot { background: var(--st-done); }
.stage-name { margin: 0; font-size: 14px; font-weight: 600; }
.stage-count { color: var(--text-3); font-size: 13px; font-variant-numeric: tabular-nums; }

/* ---------- 6. 条目 ---------- */
.entries { list-style: none; margin: 0; padding: 0; }
.entry {
  display: flex;
  gap: 12px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
  transition: transform var(--dur) var(--ease);
}
.entry:last-child { border-bottom: none; }
.entry:hover { transform: none; }
.entry-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  flex: none;
  margin-top: 9px;
  background: var(--st-ideas);
}
.stage[data-stage="ideas"]   .entry-dot { background: var(--st-ideas); }
.stage[data-stage="planned"] .entry-dot { background: var(--st-planned); }
.stage[data-stage="ongoing"] .entry-dot { background: var(--st-ongoing); }
.stage[data-stage="done"]    .entry-dot { background: var(--st-done); }
.entry-body { min-width: 0; }
.entry-title { font-weight: 600; }
/* 点线引导符（2026-09-24 用户批准）：标题 ······ 日期 拉成一条基线。
   .ld-fill 用 flex 撑开、画一条点状下边框；日期用等宽字，跟标题拉开质感差。 */
.entry-title { display: flex; align-items: baseline; gap: 8px; }
.entry-title > .t { flex: 0 1 auto; min-width: 0; }
.entry-title > .ld-fill {
  flex: 1 1 auto; min-width: 12px; align-self: center;
  border-bottom: 1px dotted var(--border-strong);
}
.entry-title > .ld-date {
  flex: none; font-family: var(--mono); font-size: 12px;
  font-weight: 400; color: var(--text-3); white-space: nowrap;
}
.entry a:hover { text-decoration-thickness: 2px; }
.entry-sum { color: var(--text-2); font-size: 14px; }
.entry-meta {
  color: var(--text-3);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 2px;
}
.entry-id { font-family: var(--mono); font-size: 11.5px; color: var(--text-3); }

/* ---------- 7. 列表（成品 / 教程） ---------- */
.list { list-style: none; margin: 0; padding: 0; }
.list-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  padding: 18px 0;
  border-bottom: 1px solid var(--border);
  transition: transform var(--dur) var(--ease);
}
.list > li:last-child .list-item { border-bottom: none; }
.list-item > span:first-child { min-width: 0; overflow-wrap: anywhere; }
.list-item:hover { transform: translateX(1px); }
.list-title { font-weight: 600; font-size: 15.5px; }
.list-item:hover .list-title { text-decoration: underline; text-underline-offset: 3px; }
.list-sum { display: block; color: var(--text-2); font-size: 14px; margin-top: 2px; }
.list-side {
  color: var(--text-3);
  font-size: 12.5px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  flex: none;
}

/* ---------- 8. 归档册 ---------- */
.archive-year {
  margin: 0 0 28px;
  font-size: 30px;
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.archive-month {
  margin: 32px 0 2px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-3);
  font-family: var(--mono);
  letter-spacing: 0.02em;
}
.archive .entry-dot { background: var(--text-3); }

/* ---------- 9. 按钮 / 表单 ----------
   尺度统一（2026-09-29）：**表单控件一律 44px 高 / 14px 字**（用户：「很多地方对齐不齐、
   高度不统一」，实测账户页同一页曾有 45/42/40 三种高与 14/13.5/13/12 四种字号）。
   高度写成显式值而不是靠 padding 凑：`.input` 原来靠 padding 撑出 45px，而 `.btn` 是 42px，
   两者放同一行就"底边齐、顶边差 3px"。上栏控件（.nav-link/.nav-theme/.nav-avatar 34px）
   不在这条规则范围内 —— 那是 §8 的上栏不变量，smoke 有断言。 */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  border-radius: var(--radius-sm);
  padding: 10px 16px;
  font-size: 14px;
  line-height: 22px;
  font-family: inherit;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}
.btn-primary { background: var(--text); color: var(--bg); }
.btn-primary:hover { opacity: 0.88; }
.btn-ghost { background: transparent; border-color: var(--border); color: var(--text-2); }
.btn-ghost:hover { border-color: var(--border-strong); color: var(--text); }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 28px;
}
.field { margin-bottom: 16px; }
.field label { display: block; font-size: 13px; color: var(--text-2); margin-bottom: 6px; }
.input {
  width: 100%;
  min-height: 44px;                 /* 与 .btn 同高：同一行的底边与顶边都要齐 */
  padding: 10px 12px;
  font: inherit;
  font-size: 14px;
  line-height: 22px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.input:focus {
  outline: none;
  border-color: var(--border-strong);
  box-shadow: 0 0 0 3px var(--hover);
}
.hint { color: var(--text-3); font-size: 12.5px; margin-top: 6px; }

/* ---------- 10. 正文排版（教程 / 成品详情） ---------- */
.prose { max-width: var(--measure); font-size: 16px; line-height: 1.85; overflow-wrap: anywhere; }
.prose a, .page-desc a, .entry-meta a, .main > .small a { color: var(--link); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.prose a:hover, .page-desc a:hover { text-decoration-thickness: 2px; }
.table-scroll { max-width: 100%; overflow-x: auto; margin-bottom: 20px; }
.table-scroll table { min-width: 440px; margin-bottom: 0; }
.prose blockquote > :last-child { margin-bottom: 0; }
.btn:disabled, .input:disabled { opacity: .65; cursor: not-allowed; }
.prose h2 {
  margin: 40px 0 12px;
  font-size: 20px;
  font-weight: 650;
  letter-spacing: -0.01em;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.prose h3 { margin: 28px 0 8px; font-size: 16px; font-weight: 600; }
.prose p { margin: 0 0 14px; }
.prose ul, .prose ol { margin: 0 0 14px; padding-left: 22px; }
.prose li { margin-bottom: 4px; }
.prose code {
  font-family: var(--mono);
  font-size: 0.875em;
  background: var(--hover);
  padding: 1px 5px;
  border-radius: 4px;
}
.prose pre {
  /* 2026-09-24 用户批准：柔和底色 + 圆角（原来是白底 + 1px 边框，像块硬纸）。
     2026-09-26：底色与圆角挪到外壳 `.code-block` 上（那个块的右上角要放控件），
     这里只管排版与横向滚动。 */
  position: relative;
  background: none;
  border: 0;
  border-radius: 0;
  margin: 0;
  padding: 12px 16px 14px;
  overflow-x: auto;
  font-size: 13px;
  line-height: 1.6;
}
.prose pre code { background: none; padding: 0; font-size: inherit; }

/* ---------- 代码块外壳：右上角「语言名 ↔ 复制按钮」 ----------
   结构照 VitePress —— `docs/01` §8 记着"代码块圆角 + 顶部标签栏 + 右上角语言名"这条
   就是从它那借鉴的（2026-09-26 按用户要求"参考别人的实现"又对了一遍它的源码）。
   两个控件占**同一个角、不并存**：平时是弱化的语言名，悬停 / 键盘聚焦时语言名淡出、按钮淡入，
   永远不打架。按钮必须是 `pre` 的**兄弟** —— 放进 `pre` 里，手动选中整块会把按钮文字一起带走。 */
.code-block {
  position: relative;
  margin: 0 0 14px;
  background: var(--hover);
  border-radius: var(--radius);
}
.code-lang {
  position: absolute; top: 9px; right: 12px; z-index: 2;
  font-family: var(--mono); font-size: 11px; letter-spacing: .04em;
  color: var(--text-3); pointer-events: none;
  transition: opacity .16s var(--ease);
}
.code-copy {
  position: absolute; top: 7px; right: 9px; z-index: 3;
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text-2);
  /* 隐藏时**同时关掉命中**：只写 opacity:0 的话按钮仍然可点，
     在代码块右上角误点一下就会静默复制（VitePress 就是这个行为，我们补上）。
     `pointer-events` 不影响键盘 Tab 聚焦，所以无障碍不受影响。 */
  opacity: 0; pointer-events: none; cursor: pointer;
  transition: opacity .16s var(--ease), color .16s var(--ease), border-color .16s var(--ease);
}
.code-copy:hover { color: var(--text); border-color: var(--border-strong); }
.code-copy svg { width: 14px; height: 14px; }
.code-copy .icon-check { display: none; }
.code-copy.is-copied { opacity: 1; pointer-events: auto; color: var(--st-done); border-color: var(--border-strong); }
.code-copy.is-copied .icon-copy { display: none; }
.code-copy.is-copied .icon-check { display: block; }
.code-block:hover .code-copy,
.code-copy:focus-visible { opacity: 1; pointer-events: auto; }
.code-block:hover .code-lang,
.code-copy:focus-visible ~ .code-lang { opacity: 0; }
/* 复制成功的播报位（视觉上是按钮左边滑出的小药丸）。用**真元素**而不是 `::before` ——
   伪元素进不了无障碍树，读屏用户就听不到"已复制"。 */
.code-copied {
  position: absolute; top: 7px; right: 40px; z-index: 3;
  display: none; align-items: center; height: 26px; padding: 0 9px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text-2);
  font-size: 11px; white-space: nowrap;
}
.code-copied:not(:empty) { display: inline-flex; }
/* 触摸屏没有 hover：按钮常显、语言名让位 —— 否则根本按不到（VitePress 这点没做，我们补上） */
@media (hover: none) {
  .code-copy { opacity: 1; pointer-events: auto; }
  .code-lang { opacity: 0; }
}

/* 高亮 token。**颜色只来自上面那几个 --code-*，也就是四个阶段色**，不新增颜色。 */
.prose .tok-com { color: var(--code-com); }
.prose .tok-str { color: var(--code-str); }
.prose .tok-num { color: var(--code-num); }
.prose .tok-kw  { color: var(--code-kw); }
.prose .tok-key { color: var(--code-kw); }
.prose .tok-var { color: var(--code-var); }
.prose .tok-cmd { color: var(--code-cmd); }
.prose .tok-pun { color: var(--code-pun); }
.prose blockquote {
  margin: 0 0 14px;
  padding: 2px 0 2px 16px;
  border-left: 2px solid var(--border-strong);
  color: var(--text-2);
}
/* 语义提示块（2026-09-24 用户批准）：正文写 `> [!NOTE]` / `[!TIP]` / `[!WARN]`。
   标记词由渲染器吃掉，这里用 ::before 画出来 —— 所以正文里只写标记，不用重复写 "NOTE"。 */
.prose blockquote.callout {
  padding: 12px 16px;
  border-left-width: 3px;
  border-radius: 0 var(--radius) var(--radius) 0;
}
.prose blockquote.callout > p:first-child::before {
  font-weight: 700; letter-spacing: .06em; margin-right: .5em;
}
.prose blockquote.callout.is-note { border-left-color: #4f6bed; background: color-mix(in srgb, #4f6bed 8%, transparent); }
.prose blockquote.callout.is-note > p:first-child::before { content: "NOTE"; color: #3b53c4; }
.prose blockquote.callout.is-tip  { border-left-color: #15803d; background: color-mix(in srgb, #15803d 8%, transparent); }
.prose blockquote.callout.is-tip > p:first-child::before { content: "TIP"; color: #15803d; }
.prose blockquote.callout.is-warn { border-left-color: #b45309; background: color-mix(in srgb, #b45309 10%, transparent); }
.prose blockquote.callout.is-warn > p:first-child::before { content: "WARN"; color: #b45309; }
[data-theme="dark"] .prose blockquote.callout.is-note { border-left-color: #8fa8ff; background: color-mix(in srgb, #8fa8ff 12%, transparent); }
[data-theme="dark"] .prose blockquote.callout.is-note > p:first-child::before { color: #8fa8ff; }
[data-theme="dark"] .prose blockquote.callout.is-tip  { border-left-color: #6ee7a8; background: color-mix(in srgb, #6ee7a8 12%, transparent); }
[data-theme="dark"] .prose blockquote.callout.is-tip > p:first-child::before { color: #6ee7a8; }
[data-theme="dark"] .prose blockquote.callout.is-warn { border-left-color: #fbbf24; background: color-mix(in srgb, #fbbf24 12%, transparent); }
[data-theme="dark"] .prose blockquote.callout.is-warn > p:first-child::before { color: #fbbf24; }
.prose table { width: 100%; border-collapse: collapse; margin: 0 0 16px; font-size: 14px; }
.prose th, .prose td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); }
.prose th { color: var(--text-2); font-weight: 600; font-size: 13px; }
.prose hr { border: none; border-top: 1px solid var(--border); margin: 32px 0; }
/* 外链图（徽章）加载失败时由脚本打上这个类自己收起来 —— 破图 + alt 文字是"坏掉的样子"，
   不是"没有这张图"。判据与理由见 `layout.php` 那段注释；只对外链生效。 */
.is-img-failed { display: none !important; }

/* ---------- 11. 工具类 / 页脚 ---------- */
.muted { color: var(--text-3); }
.small { font-size: 13px; }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.mt-32 { margin-top: 32px; }
.mt-48 { margin-top: 48px; }

.footer {
  border-top: 1px solid var(--border);
  padding: 28px 0 48px;
  color: var(--text-3);
  font-size: 13px;
}
.footer-inner {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0 24px;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
/* 备案号（内容来自 site.yml 的 site.icp，空着不渲染）—— 管局要求放在首页正下方并链接工信部。
   **用 --text-2 不用 --text-3**：实测对比度 --text-3 浅色只有 4.24（低于 WCAG AA 4.5），
   而备案号是"必须清晰可见"的东西，藏着反而可能被挑；--text-2 浅色 6.31 / 深色 7.09。 */
.footer-icp { color: var(--text-2); text-decoration: none; }
.footer-icp:hover { color: var(--link); text-decoration: underline; text-underline-offset: 3px; }

/* 首页那一枚：绝对定位在 hero 底部（落在最前那层曲线的实心带上）。
   **不参与布局** —— 所以 hero / 曲线 / FLIP 的几何一个字节都没动。 */
.land-icp {
  position: absolute; left: 0; right: 0; bottom: 16px;
  z-index: 3; text-align: center; font-size: 12px;
  pointer-events: none;              /* 容器不吃点击，免得挡住曲线区域的交互 */
}
.land-icp a {
  pointer-events: auto;              /* 但链接本身要能点 */
  color: var(--text-2); text-decoration: none;
}
.land-icp a:hover { color: var(--link); text-decoration: underline; text-underline-offset: 3px; }

/* ---------- 12. 样张专用（真实站不用） ---------- */
.spec {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  padding: 20px 24px 28px;
  margin: 40px 0;
}
.spec-label {
  display: inline-block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--text-3);
  border: 1px solid var(--border);
  border-radius: 99px;
  padding: 2px 9px;
  margin-bottom: 16px;
}
.swatches { display: flex; flex-wrap: wrap; gap: 10px; }
.swatch { width: 104px; }
.swatch-chip {
  height: 44px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}
.swatch-name { font-size: 11px; font-family: var(--mono); color: var(--text-3); margin-top: 5px; }

/* ---------- 13. 外壳：左侧分级目录 + 右侧内容 ---------- */
.shell {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 24px;
  display: grid;
  grid-template-columns: 224px minmax(0, 1fr);
  gap: 56px;
  align-items: start;
}

/* 左侧目录：吸顶 + 独立滚动 */
.side {
  position: sticky;
  top: var(--navh, 64px);
  /* 老浏览器不懂 svh → 第二条整条作废，留下上面那条 vh 的（真机上地址栏一动会跳一点，
     但至少有正确高度；只有 svh 会让 max-height 无效、侧栏高度退成 auto）。 */
  max-height: calc(100vh - var(--navh, 64px));
  max-height: calc(100svh - var(--navh, 64px));
  overflow-y: auto;
  padding: 30px 0 48px;
  scrollbar-width: thin;
}
.side::-webkit-scrollbar { width: 6px; }
.side::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }

.side-group { margin-bottom: 18px; }
.side-label {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}
.side-list, .side-sub, .side-sub2 { list-style: none; margin: 0; padding: 0; }
.side-sub { margin-top: 1px; }
.side-sub2 { margin-top: 1px; }

/* 相邻行之间留 2px —— 否则上下两行的高亮会糊成一片。
   用 `.side li` 而不是 `+ li`：跨列表的相邻行（如「已完成」→「归档」）也要拿到间距。 */
.side li { margin-top: 2px; }
.side ul > li:first-child { margin-top: 0; }

.side-link {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px 5px 28px;      /* 一级：与折叠箭头的槽位对齐 */
  border-radius: var(--radius-sm);
  font-size: 14px;
  color: var(--text-2);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.side-link:hover { background: var(--hover); color: var(--text); }
.side-link.is-active { background: var(--hover); color: var(--text); font-weight: 550; }

/* 缩进与高亮一起内缩：层级越深，整块（含高亮）越往右 —— 相邻层级不会糊在一起 */
.side-link[data-lv="2"] { margin-left: 14px; font-size: 13.5px; }
.side-link[data-lv="3"] { margin-left: 28px; font-size: 13px; color: var(--text-3); }
/* 最深一级（当前只有归档册的年份）选中时**不铺背景**，只留字体变化。
   理由：它上面已经有两层高亮（页面行 + 归档行），再叠一块背景就成了三层，
   而且它本身没有折叠箭头，铺底的盒子左边一大截是空槽。 */
.side-link[data-lv="3"].is-active {
  background: transparent;
  color: var(--text);
}

.side-n {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.side-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  flex: none;
  background: var(--st-ideas);
}
.side-dot[data-stage="ideas"]   { background: var(--st-ideas); }
.side-dot[data-stage="planned"] { background: var(--st-planned); }
.side-dot[data-stage="ongoing"] { background: var(--st-ongoing); }
.side-dot[data-stage="done"]    { background: var(--st-done); }

.side-divider { height: 1px; background: var(--border); margin: 16px 10px; }

/* 右侧内容：不再需要自己居中，由 grid 管 */
.main { padding: 44px 0 96px; min-width: 0; }
.directory-toggle, .directory-heading, .directory-backdrop { display: none; }
.main .hero { padding-top: 0; }

/* 窄屏：目录折到上面 */
@media (max-width: 940px) {
  .shell { grid-template-columns: 1fr; gap: 8px; }
  .js .directory-toggle {
    position: fixed; right: max(20px, env(safe-area-inset-right)); bottom: calc(20px + env(safe-area-inset-bottom)); z-index: 45;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
    width: 58px; height: 58px; padding: 0; border-radius: 50%;
    background: var(--navbg); color: var(--text); border: 1px solid var(--border-strong);
    box-shadow: 0 4px 18px rgba(20,50,90,.18); cursor: pointer; font-size: 12px;
    transition: transform 180ms var(--ease), box-shadow 180ms var(--ease);
  }
  .directory-toggle:hover { transform: translateY(-2px); }
  .directory-toggle[aria-expanded="true"] { visibility: hidden; }
  .js .side {
    display: block; position: fixed; z-index: 50; top: auto;
    bottom: calc(88px + env(safe-area-inset-bottom)); left: 16px; right: 16px;
    max-height: min(70vh, 640px); max-height: min(70svh, 640px); padding: 0 14px 18px;
    background: var(--surface); border: 1px solid var(--border); border-radius: 18px;
    box-shadow: 0 16px 56px rgba(15,35,60,.2); overscroll-behavior: contain;
    visibility: hidden; opacity: 0; transform: translateY(12px) scale(.98); transform-origin: bottom right;
    transition: opacity 180ms var(--ease), transform 180ms var(--ease), visibility 180ms;
  }
  .js .side.is-open { visibility: visible; opacity: 1; transform: none; }
  .js .directory-heading { display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 1; padding: 12px 0; background: color-mix(in srgb, var(--surface) 72%, transparent); backdrop-filter: blur(16px) saturate(140%); -webkit-backdrop-filter: blur(16px) saturate(140%); border-bottom: 1px solid var(--glass-border); margin-bottom: 12px; font-size: 14px; font-weight: 600; }
  .directory-close { color: var(--text-2); background: var(--hover); border: 0; border-radius: 8px; min-height: 40px; padding: 0 12px; cursor: pointer; }
  .js .directory-backdrop { display: block; position: fixed; inset: 0; z-index: 40; background: rgba(12,25,42,.20); backdrop-filter: blur(9px) saturate(125%); -webkit-backdrop-filter: blur(9px) saturate(125%); opacity: 0; visibility: hidden; transition: opacity 180ms var(--ease), visibility 180ms; }
  .directory-open .directory-backdrop { opacity: 1; visibility: visible; }
  body.directory-open { overflow: hidden; }
  .side-toggle { min-height: 32px; }
  .side-link { min-height: 40px; }
  .footer { padding-bottom: calc(96px + env(safe-area-inset-bottom)); }
  .main { padding-top: 24px; }
  .side {
    position: static;
    max-height: none;
    padding: 20px 0 16px;
    border-bottom: 1px solid var(--border);
  }
}

/* 锚点跳转时给顶栏留空间（左侧目录点阶段/章节用） */
.stage, .prose h2, .prose h3, .archive-month, #main-content { scroll-margin-top: calc(var(--navh, 64px) + 16px); }

/* ---------- 14. 侧栏展开 / 收起 ---------- */
/* 整行（箭头 + 文字）是 hover / 选中的单位 */
.side-row {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 6px;
  border-radius: var(--radius-sm);
  transition: background var(--dur) var(--ease);
}
.side-row[data-lv="2"] { margin-left: 14px; }
.side-row:hover { background: var(--hover); }
/* 选中态画在**整行**上（含箭头），不再只画在链接上 */
.side-row.is-active { background: var(--hover); }

/* 一级分类标题（Progress / Projects / Tutorials）：**无 hover 高亮** */
.side-row.side-group-row:hover,
.side-row.side-group-row.is-active { background: transparent; }

.side-row .side-label { flex: 1; min-width: 0; padding: 6px 0; }
.side-row .side-label a { color: inherit; }
.side-row .side-label a:hover { color: var(--text-2); }

/* 行内链接不自己铺背景，否则叠成两层、比别的行深 */
.side-row .side-link,
.side-row .side-link:hover,
.side-row .side-link.is-active {
  flex: 1;
  min-width: 0;
  padding: 5px 8px 5px 0;
  border-radius: 0;
  background: transparent;
}

/* 箭头在文字**前面** */
.side-toggle {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px; height: 20px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text-3);
  cursor: pointer;
  transition: color var(--dur) var(--ease);
}
.side-toggle:hover { color: var(--text); }

/* 箭头旋转的过渡**也只在"刚点过"的窗口里存在**（理由同下面 .side-collapsible）：
   常驻的话，页面加载 / 侧栏被主页过场重建时按记忆值恢复折叠态，箭头会当场转一下 ——
   看起来就像"我没点它，那个三角又自己播了一次动画"。 */
.side-chev { transition: none; }
.side-row.is-animating .side-chev { transition: transform var(--dur) var(--ease); }
.side-toggle[aria-expanded="false"] .side-chev { transform: rotate(-90deg); }

/* 折叠用 grid-template-rows 过渡：平滑，且只动这一块自身的高度。
   **过渡只在"刚点过这个开关"的那 260ms 里存在**（`.is-animating`）——
   因为 grid 里 `grid-template-rows` 的计算值是**用出来的 px**（不是 `1fr`）：
   侧栏别处一展开、滚动条一出现、文字一重排，这个 px 就跟着变，
   挂在常驻过渡上会被重新触发 —— 看着就像"我没点它，它自己又播放了一次展开动画"。
   顺带一个好处：加载时按 localStorage 恢复折叠态变成瞬时的，首屏不会再闪一下动画。 */
.side-collapsible { display: grid; grid-template-rows: 1fr; }
.side-collapsible.is-animating { transition: grid-template-rows 180ms var(--ease); }
.side-collapsible[data-collapsed="true"] { grid-template-rows: 0fr; }
.side-collapsible-inner { overflow: hidden; min-height: 0; }

.side-empty { padding: 5px 10px; font-size: 13px; color: var(--text-3); }
.side-node { position: relative; }

/* 无侧栏的独立页（登录） */
.bare {
  max-width: 420px;
  margin: 0 auto;
  padding: 72px 24px 96px;
}

/* ============================================================
   15. 主页（大落地页）
   ------------------------------------------------------------
   结构：天空渐变 + 层叠有机曲线 + 大字名字 + 签名 + 三个入口。
   点任一入口 → 整块向上收成上栏，然后跳转。
   收起的**终帧要和真实上栏重合**，所以下面用了一个不可见的
   「幽灵上栏」来量目标位置（见 layout.php 的 render_landing）。
   ============================================================ */

.land {
  position: relative;
  z-index: 1;
  /* 曲线下弹在面板内部发生；裁掉上方和下边界外的波形，避免覆盖正文。 */
  overflow: hidden;
  /* 用 height 不用 min-height：收起时要真的压到上栏高度，
     min-height 会被内容撑住（实测只从 818 掉到 703，等于没收）。 */
  height: var(--land-height);
  display: flex;
  flex-direction: column;
  /* 蓝调天空 → 页面底色 */
  background: linear-gradient(180deg, #cfe3f8 0%, #e3eefb 42%, var(--bg) 100%);
  /* 「往上收」的关键：下边界必须**看得见**。
     只有一条边线还不够，加一层向下的阴影才有"这是一张浮起来的纸"的感觉。 */
  border-bottom: 1px solid var(--border-strong);
  transition: border-bottom-color 200ms var(--ease);
  box-shadow: 0 16px 32px -18px rgba(20, 50, 90, 0.30);
}

/* 内容裁剪层：面板变矮时由它把内容裁掉，曲线在外面不受影响 */
.land-clip {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
[data-theme="dark"] .land {
  background: linear-gradient(180deg, #0e1a28 0%, #0c141e 45%, var(--bg) 100%);
}

/* 层叠有机曲线：贴在底部，三层，越靠前越浅（借 Josh Comeau） */
/* 层叠有机曲线：贴在底部，三层，**越靠下越在上层**（前遮后）。
   三层各自是一个 <svg>，好让位移用 px 而不是 SVG 用户单位。 */
.land-curves {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  width: 100%;
  /* 带子高度 = 波形画布的高度（三层的 svg 都是 100%）。手机档在下面覆盖成 clamp()。 */
  --curve-band-h: 50vh;
  /* **收尾时要往上让出多少**（curveDip 末段用它，见那段注释）。
     三道波纹的**曲线最低点**离带子底边的高度 = 带高的 15.75%（= 63/400，由 land_wave_path
     的形状定死）。面板收到上栏高（--navh）时，上栏那条取样的正是带子底部 --navh 这么高：
       · 带子 ≥ 400px（桌面 50vh 通常够）⇒ 曲线最低点本来就在上栏之上，让 0px（**桌面一个像素不动**）；
       · 带子 < 400px（手机档 clamp(280px,42svh,400px) 常落在 280..400）⇒ 曲线会探进上栏
         十几像素，正是"手机端没收回去、桌面端收回了"的全部原因 ⇒ 按差额让出去。
     extra 是给手机档留的一点余量（曲线最低点是个尖点，正好压线会留一丝）。 */
  --curve-retract-extra: 0px;
  --curve-retract: max(0px, calc(var(--navh, 63px) + var(--curve-retract-extra) - 0.1575 * var(--curve-band-h)));
  height: var(--curve-band-h);
  min-height: 240px;
  pointer-events: none;
  /* 横向要裁掉（svg 宽 300%，不裁会出横向滚动条），但**纵向不能裁**
     —— 下弹要弹到边界以外。
     注意：`overflow-x: hidden` 会把 overflow-y 强制变成 auto，两边都裁；
     `clip-path` 只裁视觉、**裁不掉可滚动溢出**（实测文档宽 2339 > 视口 1278，照样出滚动条）。
     只有 `overflow-x: clip` + `overflow-y: visible` 这个组合能"只裁横向"。 */
  /* **两个方向都要裁。** 原来是 `overflow-x: clip; overflow-y: visible` ——
     理由是"下弹要弹到边界以外才看得见"。但实测（像素）证明那是错的：
     下弹时曲线会**从面板下边界溢出去**盖住下面的内容，最下那层 c1 的实色
     （亮度 192）正好落在边界下方 —— 用户看到的就是"动画中的阴影比静态时深"，
     而且它还把"下边界那条线"盖掉了，违背了本来的设计意图。
     改成两个方向都裁：下弹在面板内部照样看得见（波形整体下移 56px），
     但不会越过边界。 */
  overflow: clip;
}
/* 外层：静止往复（translateX）。左右各留 7% 余量，往复时不露元素的边。
   代价是波形横向被拉宽 14% —— 比"周期化"造成的折角/尖峰/平台小得多。 */
.land-curves .sway {
  position: absolute;
  bottom: 0;
  left: -7%;
  width: 114%;
  height: 100%;
  will-change: transform;
}
/* 内层：上收时的下弹（translateY）。**必须和外层分开** ——
   两者都是 transform，放同一个元素上时，切动画那一瞬间 transform 会从当前值
   跳到新动画的初值，看起来就是"上收时波形瞬间弹回去"。 */
.land-curves svg {
  display: block;
  width: 100%;
  height: 100%;
  will-change: transform;
}
/* 填充**必须不透明** —— 半透明会互相透出来，遮不住，"前遮后"就没了。
   越靠前越浅（雾感纵深），最前那层接近页面底色，和下方内容自然接上。 */
.land-curves .c1 { fill: #a6c6e6; }   /* 最后面 */
.land-curves .c2 { fill: #c8dcf2; }   /* 中间 */
.land-curves .c3 { fill: var(--navbg); }   /* 最前（最上层）= 上栏底色 */
[data-theme="dark"] .land-curves .c1 { fill: #14293f; }
[data-theme="dark"] .land-curves .c2 { fill: #1b3450; }


/* 让"压住"这件事被看出来：前面的山脊在后面那层上投一条影。
   用 drop-shadow 不用 box-shadow —— 前者跟着**波形轮廓**走，后者是矩形的影。
   只给 c2 / c3 加：最后面那层（c1）上面是天空，给它加影会像发光。 */
.land-curves .l2 { filter: drop-shadow(0 -3px 7px rgba(20, 50, 90, 0.13)); }
.land-curves .l3 { filter: drop-shadow(0 -4px 9px rgba(20, 50, 90, 0.16)); }
[data-theme="dark"] .land-curves .l2 { filter: drop-shadow(0 -3px 7px rgba(0, 0, 0, 0.45)); }
[data-theme="dark"] .land-curves .l3 { filter: drop-shadow(0 -4px 9px rgba(0, 0, 0, 0.55)); }

/* 静止状态：三层一起**向右**缓慢往复，速度各不相同（视差：后面慢、前面快），
   并且**起始相位依次错开**（-0s / -3s / -6s），所以三层不是同进同退 —— 错位看得出来。
   再叠一个每层随机的 --drift-jitter（±1.5s），每次打开相位都不同。

   **为什么是往复（alternate）而不是无限循环**：
   无限循环要求波形是周期函数，而原波形两端曲率方向相反（c1 起点 +0.0029、终点 −0.006）。
   三条路都试过 —— 只对齐端点留折角、傅里叶拟合抹平曲线、五次埃尔米特鼓尖峰 ——
   都会在接缝附近留下肉眼可见的怪东西。往复不需要周期性，形状因此完全保真。 */
/* **三层都必须 `animation-fill-mode: backwards`** —— 这是"刷新后最上面那条曲线抽搐一下"的根因。
   抖动量 `--drift-jitter` 是 `landing_script()` 写上去的，而它写在**动画已经创建之后**：
   脚本开头那句 `ghostNav.getBoundingClientRect()` 会强制一次样式+布局，CSS 动画就在那一刻定下了
   startTime。于是抖动值为**正**时，`animation-delay` 变成正值 → 动画进入 delay 阶段；
   而 fill-mode 默认是 `none`，delay 阶段元素退回**基础样式（没有 transform）**，
   delay 一过又瞬间跳到 0% 的 `translateX(-5%)`。
   实测（本机 Chromium，视口 1386）：l1 抖动 **+0.99s** 时，currentTime **989ms** 处 `transform: none`、
   **990ms** 处直接变成 **-78.16px**（元素宽 1563.2 × 5%）—— 一帧跳 78px，就是那次"抽搐"。
   只有 l1 会中招：它的 delay 就是抖动本身（±1.5s，一半概率为正），
   而 l2/l3 的 delay 是 `抖动-3s` / `抖动-6s`，恒为负 → 一直在 active 阶段。
   加上 `backwards` 之后，delay 阶段显示 0% 关键帧（-5%），与 delay=0 时**同一位置**，跳变消失。 */
.land-curves .l1 { animation: curveSway 16s linear infinite alternate; animation-delay: calc(var(--drift-jitter, 0s)); animation-fill-mode: backwards; }
.land-curves .l2 { animation: curveSway 12s linear infinite alternate; animation-delay: calc(var(--drift-jitter, 0s) - 3s); animation-fill-mode: backwards; }
.land-curves .l3 { animation: curveSway 9s  linear infinite alternate; animation-delay: calc(var(--drift-jitter, 0s) - 6s); animation-fill-mode: backwards; }
@keyframes curveSway {
  /* **幅度必须小于元素左右留的余量**，否则元素边界会被推进画面（用户实测看到了边界）。
     元素宽 114%（左右各留 7% 容器宽），所以幅度上限是 7/1.14 = 6.14% 元素宽 —— 取 5% 留安全边。
     ±5% 元素宽 = ±5.7% 容器宽 ≈ ±72px；一个来回 144px，最快那层 9s → 约 16px/s。 */
  from { transform: translateX(-5%); }
  to   { transform: translateX(5%); }
}

/* 内容层：和上栏共用同一套内边距，收起时才能严丝合缝 */
.land-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  min-height: var(--land-height);
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 24px;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;          /* 整块居中 */
}

/* 操作行：只放右上角的登录 / 深浅切换。
   高度对齐真实上栏 —— 收起时名字要落到同一水平线上。 */
/* 2026-09-23 起这一行放的是「灯泡 + 登录」或「灯泡 + 头像」，控件顺序与统一高度见 §17。 */
.land-acts-row {
  align-self: stretch;
  display: flex;
  /* **必须 flex-start。** 这一行 `min-height: 55px` + `padding: 14px 0 0` → 内容盒 41px；
     默认的 `stretch` 会把 34px 的控件在 41px 里居中 → 顶边落在 14 + 3.5 = **17.5px**。
     而上栏 `.nav-inner`（padding 14px、盒子正好 34px）是 **14px**。
     于是收起动画交接的那一刻，右上角控件会**凭空上跳 3.5px**（2026-09-25 用户报的
     "上栏文字和头像有突然放大一点的跳变" 里的位移部分）。flex-start 之后两处都在 14px。 */
  align-items: flex-start;
  justify-content: flex-end;
  padding: 14px 0 0;
  min-height: 55px;
}
.land-acts { display: flex; align-items: center; gap: 4px; flex: none; }

/* 居中区：大字名字 + 一行签名。
   align-self:stretch 让这一块占满宽度，里面的 text-align:center 才管用
   （父级是 align-items:center，不撑开的话子块会缩到内容宽度）。 */
.land-hero {
  align-self: stretch;
  text-align: center;
  margin-top: clamp(28px, 7vh, 84px);
}
.land-name {
  margin: 0;
  /* inline-block 让它的盒子贴合文字本身 —— FLIP 量的是"文字在哪儿"，
     块级元素会撑满整行，量出来就偏了。 */
  display: inline-block;
  font-size: clamp(46px, 8.6vw, 124px);
  font-weight: 700;
  line-height: 1.04;
  letter-spacing: var(--wordmark-tracking);   /* 与上栏 `.nav-brand` 共用 —— FLIP 才对得齐 */
  transform-origin: top left;
}
/* 名字本身是通往首页的链接；收起成上栏后点它会把过场倒放回去 */
.land-name-link { color: inherit; }
.land.is-done .land-name-link { cursor: pointer; }

.land-sign {
  margin: 18px 0 0;
  color: var(--text-2);
  font-size: 16px;
}

/* 三个并排的圆角方形按钮 */
.land-cards {
  display: flex;
  gap: 16px;
  justify-content: center;
  width: 100%;
  max-width: 496px;
  margin: clamp(34px, 8vh, 76px) 0 0;
  padding: 0 0 clamp(150px, 26vh, 300px);   /* 底部给曲线带让位 */
}
.land-card {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  gap: 18px;
  padding: 22px;
  border: 1px solid var(--border);
  border-radius: 18px;
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  box-shadow: 0 1px 2px rgba(20, 50, 90, 0.04);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.land-card:hover {
  transform: translateY(-3px);
  border-color: var(--border-strong);
  background: var(--surface);
  box-shadow: 0 2px 4px rgba(20, 50, 90, 0.05), 0 14px 28px -14px rgba(20, 50, 90, 0.22);
}
.land-card-t {
  font-size: 13px;
  font-weight: 550;
  letter-spacing: .02em;
}
/* 4 张卡（只有亲密账户会看到 Activities 那张）：宽一档，四张才留得住方形与内边距。
   窄屏那档换 2×2（见 ≤720px 里那条）—— 320 宽硬排四张会把图标和字挤出去。 */
.land-cards.is-4 { max-width: 640px; }
.land-card-icon { width: 54px; height: 54px; color: var(--link); flex: none; }
.land-card-icon .icon-tint { fill: var(--link); fill-opacity: .08; stroke: none; }
.land-card:hover .land-card-t { text-decoration: underline; text-underline-offset: 4px; }
.land-card-s { color: var(--text-3); font-size: 13px; }
.land-card-foot {
  margin-top: 20px;
  display: flex;
  align-items: baseline;
  gap: 8px;
  color: var(--text-3);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.land-card-go {
  margin-left: auto;
  transition: transform var(--dur) var(--ease), color var(--dur) var(--ease);
}
.land-card:hover .land-card-go { color: var(--text); transform: translateX(3px); }

/* 幽灵上栏：和真实上栏一模一样的标记，但不可见，只用来量落点 */
.land-ghost {
  position: absolute;
  top: 0; left: 0; right: 0;
  visibility: hidden;
  pointer-events: none;
  z-index: -1;
}
/* 幽灵里的上栏要 static —— 真上栏是 sticky，套在绝对定位的幽灵里量出来的高度会不对 */
.land-ghost .nav { position: static; }

/* ---------- 收起动画 ----------
   要的是「整页像一张纸往上收」：收的过程中**下边界是一条看得见的线**，
   这条线往上走，最后停在上栏底部，变成上栏与正文的分界线。

   所以三件事缺一不可：
   ① 面板底下要有**可见的边**（border + 阴影）—— 否则曲线最后一层就是页面底色，
      和下面糊在一起，根本看不出有边界在往上走；
   ② 动画 height（不是 min-height）—— 内容撑得住 700px 时 min-height 压不下去；
   ③ **不要**再叠一层"淡出成底色"的遮罩 —— 那会把边界糊掉。

   全部动画统一 1100ms 一条时间轴。
   错位用 **animation-delay**（三层同一份关键帧，只把开始时间推后一点），
   不是三份不同的关键帧 —— 后者幅度和节奏都不一样，看起来像三个独立动画。
   调试钩子 ?anim= 会读每个元素**当前**的计算 delay 再减去 t，所以不会抹掉错位。 */
.land.is-leaving { pointer-events: none; }

/* 时间轴：0–20% 三层曲线依次下弹；**下弹到底的那一瞬间面板就开始收**。
   关键是中间不留停顿 —— 原来面板保持到 55%、而下弹 48% 就回位了，
   中间空了一段静止，看起来就成了"先回弹、停一下、再往上收"两段动作。

   总时长 1300ms、收缩段占 1040ms，是**故意慢**的：
   原来 900ms 里收缩只占 700ms、加速段约 140ms，一闪就过去，看不出"加速上收"的过程。 */
.land.is-leaving { animation: landSquash 1300ms both; }
@keyframes landSquash {
  /* 起步用强一点 ease-in（.5,0,.2,1）：前段慢、看得见在加速，中段快，尾段收 */
  0%, 20% {
    height: var(--land-height);
    border-bottom-color: var(--border-strong);
    animation-timing-function: cubic-bezier(.5, 0, .2, 1);
  }
  /* **收到上栏高度就停，不再冲过头再回弹。**
     用户 2026-09-25：正向这一下反向运动是画蛇添足 —— 开头三层曲线的下潜
     （0→20% 往下 56px）已经有过一次反向运动了，收尾再弹一下等于第二次。
     边框颜色必须**在动画里收敛**到上栏那一档：原来只给 is-done 加了过渡，
     结果是动画全程深色（--border-strong）、最后一刻才变浅，
     用户看到的就是"动画中和静态时的阴影（其实是边框）不一样"。 */
  90%     { height: var(--navh, 55px); border-bottom-color: var(--border); }
  100%    { height: var(--navh, 55px); border-bottom-color: var(--border); }
}

/* 三层曲线：**同一份关键帧**，只把开始时间依次推后 70ms。
   幅度一样、节奏一样，所以整体趋势看着是一条；错位只体现在"谁先动"。

   回位（20%→62%）比面板的收缩（20%→90%）**慢**，而且两者同时开始 ——
   所以是"回位顺便被面板带着往上收"，不是回位完了再收。
   由于三层各自错开 70/140ms，后排开始收的时候前排还在回位，重叠感更明显。 */
.land.is-leaving .land-curves svg {
  animation-name: curveDip;
  animation-duration: 1300ms;
  animation-timing-function: linear;   /* 分段缓动写在关键帧里 */
  animation-iteration-count: 1;
  animation-fill-mode: both;           /* both：延迟期间也保持 0% 的状态 */
  animation-delay: 0ms;
  will-change: transform;
  /* 末段"收回去"时，svg 的盒子往上让了一个上栏高，**盒子底下那 63px 是空的** ——
     由 `.curve-fill`（y=319 往下那块、颜色与各自波纹相同）补上。它画在 viewBox 之外，
     所以要 `overflow: visible` 才看得见（倒放那一侧本来就有这条）。 */
  overflow: visible;
}
/* 选择器必须带上 .land-curves 和 .lN —— 否则被上面那条（0,3,1）压过，
   delay 全清成 0，下弹的错位就没了。注意下弹在 svg 上（.lN 是外层 .sway）。 */
.land.is-leaving .land-curves .l1 svg { animation-delay: 0ms; }
.land.is-leaving .land-curves .l2 svg { animation-delay: 70ms; }
.land.is-leaving .land-curves .l3 svg { animation-delay: 140ms; }

@keyframes curveDip {
  /* 下弹：从静止加速、到底减速（自然的下落感） */
  0%   { transform: translateY(0);    animation-timing-function: cubic-bezier(.45, 0, .55, 1); }
  /* 回弹：起步是零速，和面板收缩的起步接得上，中间没有速度突变 */
  20%  { transform: translateY(56px); animation-timing-function: cubic-bezier(.4, 0, .3, 1); }
  62%  { transform: translateY(0); animation-timing-function: cubic-bezier(.4, 0, .2, 1); }
  /* **末段：让它把这一段走完。**
     面板收到 63px 时，上栏那一条取样的正是带子底部那 --navh 这么高；而三道波纹的曲线
     最低点在带内的高度是带高的 15.75%（= 63/400，由 land_wave_path 的形状定死）：
       · 带子 ≥ 400px（桌面 50vh）⇒ 曲线最低点本来就在上栏之上，--curve-retract = 0
         —— **桌面端一个像素都没动**（它本来就是"收回去"的）；
       · 带子 < 400px（手机档 clamp(280px,42svh,400px)）⇒ 曲线最低点探进上栏十几像素，
         相对运动却在 62% 就停在 0 了 ⇒ 上栏里留着一小截波浪，is-done 一摘就跳变。
     让它按同一方向把这个差额走完（--curve-retract，见 .land-curves），三道曲线就退到
     上栏之上；带子底部空出来那一段由已有的 `.curve-fill` 补齐（最前面那块 = --navbg
     = 上栏底色）⇒ 最后一帧的上栏那一条与收完后的静态上栏逐像素相接，跳变归零。 */
  100% { transform: translateY(calc(-1 * var(--curve-retract, 0px))); }
}

/* 名字：FLIP 到幽灵上栏里品牌名的位置（--dx/--dy/--s 由 JS 现算）。
   **关键帧结构必须和 landSquash 一模一样**（0/20% → 90% → 100%、同样的 easing）：
   之前面板是 20%→90%→100%、字是 20%→100%，面板 90% 就走完了字还没到，
   全程字都落后于面板，看着就是"字和上收错位"。 */
.land.is-leaving .land-name { animation: nameToNav 1300ms both; }
@keyframes nameToNav {
  0%, 20% {
    transform: translate(0, 0) scale(1);
    animation-timing-function: cubic-bezier(.5, 0, .2, 1);
  }
  /* 与面板一致：不再过冲，直接落到上栏品牌名的位置 */
  90%   { transform: translate(var(--dx, 0), var(--dy, 0)) scale(var(--s, .2)); }
  100%  { transform: translate(var(--dx, 0), var(--dy, 0)) scale(var(--s, .2)); }
}

/* 签名与入口按钮：先淡出并轻微上移，不要和名字抢注意力。
   **备案号（`.land-icp`）也在这条列表里** —— 它同样是 `.land` 的直接子元素（与 `.land-curves` 同类），
   原来漏了它，hero 收到 63px 后它就正好落进上栏中间。
   2026-09-28 用户实测报的："上栏收回去后还有备案号？这东西不该动画一开始的时候就淡出吗" ——
   量到 `?anim=200` 时它在 (686,747)、`?anim=1200` 时跑到 (686,36)，即收回后的上栏里。
   没配号码时那段 HTML 压根不渲染，所以这个洞一直没露出来。 */
.land.is-leaving .land-sign,
.land.is-leaving .land-cards,
.land.is-leaving .land-icp {
  animation: landFadeOut 240ms var(--ease) forwards;
}
@keyframes landFadeOut {
  0%   { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-10px); }
}

/* 收完之后主页就转成吸顶上栏（和真实上栏同一位置，所以看不出接缝）。
   **必须自己保持收起的终态**：收起动画结束后 is-leaving 会被摘掉
   （它带 pointer-events:none，留着名字点不动），高度和名字的 FLIP 变换得由这里接住，
   否则摘掉的那一瞬间会跳回展开态。 */
.land.is-done {
  position: sticky;
  top: 0;
  z-index: 10;
  height: var(--navh, 55px);
  background: transparent;
  /* **边框和阴影不要摘掉，只把颜色收敛到上栏那一档。**
     原来这里写的是 box-shadow: none; border-bottom: none —— 收起动画结束时
     下边框从 --border-strong(深 rgb(204,217,231)) 直接消失、换成真上栏的
     --border(浅 rgb(227,235,244))，阴影也摘掉再重画一遍。
     用户实测："动画中的阴影和停下来的阴影有很明显的跳变"。
     保留 + 抑制里面那份真上栏自己的边影，就全程只有一套，颜色还能过渡。 */
  border-bottom-color: var(--border);
  box-shadow: var(--navshadow);
}
/* 真上栏自己不再画边和影（hero 已经在画了），否则叠两层 */
.land.is-done .land-ghost .nav {
  border-bottom-color: transparent;
  box-shadow: none;
}
.land.is-done .land-name {
  transform: translate(var(--dx, 0), var(--dy, 0)) scale(var(--s, .2));
}
/* **收起完成后直接显示"真实上栏那份标记"（幽灵），把主页那份内容藏起来。**
   这是这个功能的关键：我原先维护了两份 DOM（大面板 vs 真上栏）再手工对齐样式，
   而行高/字重/盒模型总有细微差别（实测名字盒子 16px vs 26px、顶 24 vs 18），
   所以永远对不齐。改成**复用同一份 DOM** 之后，样式不可能不一致，
   名字的位置、字号、链接功能也全都天然正确。
   FLIP 只是过渡过程中的近似，最后一帧由这个切换接管。 */
.land.is-done .land-ghost {
  visibility: visible;
  pointer-events: auto;
  z-index: 2;
}
.land.is-done .land-inner { visibility: hidden; }

/* 收回后曲线必须收口。`.land-curves` 是 `bottom:0; height:50vh`，而 `.land` 是
   `overflow: visible`（下弹要弹到边界外）—— 面板收到 55px 后曲线会**从上方溢出来**，
   用户实测报过"收回的上栏上还有曲线的一小节"。
   is-done 是在动画**结束后**才加的，所以不影响下弹动画；服务端渲染的收回态（内容页）也走这条。 */
.land.is-done .land-curves { visibility: hidden; }
/* 备案号同理（同一次实测）：收完之后彻底藏起来 —— 与服务端渲染的收回态对齐
   （内容页 `land_hero(true)` 压根不渲染它，两边行为至此一致）。 */
.land.is-done .land-icp { visibility: hidden; }

/* 抓回来插在主页下面的正文。不设样式 —— 它就是个普通流内块，
   主页一收，它自然被从下面拉上来。 */
.land-next { position: relative; }

/* ---------- 返回：下边界牵引三层曲线向下展开 ----------
   借鉴正向的“静止 → 加速拉开 → 跟随收拢”。
   **2026-09-25 用户验收通过后，旧的“进度差 ×240px + 18px 分层回弹”机制已删除**
   （连同 @property --return-edge / --return-wave / --return-rebound 一起）——
   它三层振幅不同源（被撕到 99px）、末尾还有速度脉冲。现在怎么做见下面 curveFollowDown 的注释。 */
.land.is-returning { animation: landGrow 1300ms cubic-bezier(.5, 0, .2, 1) both; }
/* 正向有 56px 下拉（**没有过冲** —— 2026-09-25 起）；返回边界越终点 20px。
   **展开段要和正向的收起段同长**（2026-09-25 逐帧实测）：
   正向 800→63 走 737px 用 **780ms**（260→1040ms）；倒放原来把同样的 737px 摊在 **1040ms** 上
   （80% 才到终点），慢 33%，读起来就是"拖"。现在 60%（780ms）走完、78% 落定。 */
@keyframes landGrow {
  0% { height: var(--navh, 55px); border-bottom-color: var(--border); }
  60% { height: calc(var(--land-height) + 20px); border-bottom-color: var(--border-strong); animation-timing-function: cubic-bezier(.3, 0, .5, 1); }
  78% { height: var(--land-height); border-bottom-color: var(--border-strong); }
  100% { height: var(--land-height); border-bottom-color: var(--border-strong); }
}
/* **必须用 both 填充**：只用 forwards 的话，动画启动前那一帧没有回填，
   元素显示基础样式（无 transform）—— 倒放一开始大字会先跳到自然位置再回来，
   用户看到的就是"闪一下"。both = backwards + forwards，启动前就保持倒放起点。 */
.land.is-returning .land-name { animation: nameFromNav 1300ms cubic-bezier(.5, 0, .2, 1) both; }
/* **关键帧结构必须和 landGrow 一模一样**（0/60% → 78% → 100%、同样的缓动）——
   和上面 nameToNav 那条同一个道理：面板和字各走各的节奏，全程就会互相错位。
   实测过一次：正向曾经面板 20%→90%→100%、字 20%→100%，结果字全程落后于面板。 */
@keyframes nameFromNav {
  0% { transform: translate(var(--dx, 0), var(--dy, 0)) scale(var(--s, .2)); }
  60% { transform: translate(calc(var(--dx, 0px) * -.008), calc(var(--dy, 0px) * -.008)) scale(1.008); animation-timing-function: cubic-bezier(.3, 0, .5, 1); }
  78% { transform: translate(0, 0) scale(1); }
  100% { transform: translate(0, 0) scale(1); }
}
/* ---------- 返回时三层曲线：**和正向 curveDip 完全同一套做法** ----------
   **同一份关键帧、同一个振幅，只错开开始时间**（错位用 animation-delay，不用三份不同关键帧）。

   为什么换掉旧机制（2026-09-25 逐帧实测 + 冻帧核对）：
   旧机制把相对偏移算成 `(--return-wave - --return-edge) × 240px` —— 边界进度是三层**共享**的，
   只有 wave 各自延迟，于是**三层的峰值振幅互不相同**：实测 −135.3 / −88.6 / −36.5px。
   屏幕上的样子：三层被拉开到 **99px**、浮在半空，底下垫着一块纯色填充 ——
   读起来是"带子被撕开"，不是"同一个动作依次发生"。正向三层都是同一个 56px，
   错位只体现在"谁先动"（实测层间最大 32px，三层始终紧贴成一条带）。
   另外那个 240px 系数 + 18px 分层回弹本来就是"待视觉验收"的草案（docs/03 §5），从未被接受。

   新轨迹：**和正向 `curveDip` 逐帧同构** —— 0%→20% 建立滞后、20%→62% 平滑追上、62%→100% 静止，
   **两段都是长的缓动段**，振幅取正向同一个 **56px**，错位只靠 delay。
   **不要再写成"先不动、最后一截猛追"。** 前一版就是那样（滞后写成常量 −72px，再用 208ms 短段追平），
   实测**绝对速度**在 600→650ms 从 560 **反弹到 780 px/s** —— 面板正在减速、曲线却在加速，
   用户看到的就是"最后一段突然加速"。正向没有这个问题：它两段都在动、都是长的缓动段
   （260ms 下潜 + 546ms 回位），绝对速度只有一个平缓的峰。
   **判据：抄正向要连"两段都在动、每段都够长"一起抄，只抄错位会得到一个末尾的速度脉冲。** */
.land.is-returning .land-curves svg {
  animation-name: curveFollowDown;
  animation-duration: 1040ms;
  animation-timing-function: linear;   /* 分段缓动写在关键帧里，和 curveDip 一致 */
  animation-iteration-count: 1;
  animation-fill-mode: both;
  animation-direction: normal;
  animation-delay: 0ms;
  overflow: visible;
}
@keyframes curveFollowDown {
  /* **起点必须正好接上正向收尾的那一帧**：收起演完时曲线已经"收回"到上栏之上一个
     上栏高（见 curveDip 末段）。倒放若从 0 起跑，点进主页的第一帧就会凭空冒出深色
     波浪（同一个跳变，只是挪到了反向的开头）。所以这里同样从 -(--navh) 起跑，
     再按原来的轨迹（20% 滞后 → 62% 追平 → 静止）落回原位。 */
  0%   { transform: translateY(calc(-1 * var(--curve-retract, 0px))); animation-timing-function: cubic-bezier(.45, 0, .55, 1); }
  20%  { transform: translateY(-56px); animation-timing-function: cubic-bezier(.4, 0, .3, 1); }
  /* 平滑追上：长段缓动，不是短促 snap */
  62%  { transform: translateY(0); }
  100% { transform: translateY(0); }
}
.land.is-returning .land-curves .l1 svg { animation-delay: 240ms; }
.land.is-returning .land-curves .l2 svg { animation-delay: 160ms; }
.land.is-returning .land-curves .l3 svg { animation-delay: 80ms; }

/* 旧机制（相对偏移 = (--return-wave - --return-edge) × 240px，另叠 18px 分层回弹）
   已于 2026-09-25 用户验收新轨迹后删除 —— 连同三个 @property 声明一起。
   要翻旧账看 git：本文件在 HEAD~1 与更早的提交里都有完整那一段。 */
.land-curves .curve-fill { display: none; }
/* 倒放：曲线往上让过（滞后段）时补带子底部；收起：末段"收回去"让出上栏那一条时补 */
.land.is-returning .land-curves .curve-fill,
.land.is-leaving .land-curves .curve-fill { display: block; }
/* 边界 0ms → 下层 80ms → 中层 160ms → 上层 240ms。
   延伸填充只补曲线下方，不改原始控制点；外层 clip 仍封住边界，避免露出背景缝。
   1040ms + 最大延迟 240ms = 1280ms，小于外层清理时刻 1300ms。
   （旧机制那句"不支持数值自定义属性动画的浏览器不加 motion-progress"随机制一起删了 ——
   现在的关键帧只用 SVG 的 transform，所有浏览器都一样，没有特性分支。） */
/* 签名先出现，入口随后跟上；both 保证等待阶段透明，避免闪一下。 */
.land.is-returning .land-sign {
  animation: landFadeIn 360ms var(--ease) 300ms both;
}
.land.is-returning .land-cards { animation: landFadeIn 420ms var(--ease) 420ms both; }
/* 备案号跟着卡片一起淡入（同一次实测的另一半：倒放回主页时它也得回来，不能凭空冒出来） */
.land.is-returning .land-icp { animation: landFadeIn 420ms var(--ease) 420ms both; }
@keyframes landFadeIn {
  0%   { opacity: 0; transform: translateY(-10px); }
  100% { opacity: 1; transform: translateY(0); }
}
/* 插入的正文淡出（面板展开会把它顶出视口，淡出只是保险） */
.land-next.is-going { animation: landNextOut 1300ms var(--ease) forwards; }
@keyframes landNextOut { to { opacity: 0; } }

/* ============================================================
   跨文档共享元素过渡（View Transitions）
   ------------------------------------------------------------
   解决的是：**刷新后仍能播放逆向动画**。

   刷新后的内容页上没有主页那套脚本（landing_script 只加载在 `/`），
   点站名只能走真实跳转 —— 而真实跳转 JS 是做不了形变的。
   别人的标准做法：给两个页面里"同一个东西"起**同名**的 view-transition-name，
   由**浏览器**把它从 A 位置形变到 B 位置，跟 JS 在不在场无关。
   https://daverupert.com/2023/05/getting-started-view-transitions/

   ⚠️ 同一页面里同名元素**只能有一个**：出现两个浏览器会放弃整个过渡、
   退化成普通淡入淡出。所以主页幽灵上栏里那份站名必须排除（见下面第三条）。

   ⚠️ 这与主页那套手工过场**不冲突**：@view-transition 只管**跨文档**跳转；
   主页点入口走的是 pushState（不是跳转），仍然由手工动画负责。
   ============================================================ */
@view-transition { navigation: auto; }

/* 主页：大字名字 */
.land .land-name { view-transition-name: brand; }
/* 内容页：真上栏的站名 */
.nav .nav-brand { view-transition-name: brand; }
/* 主页幽灵上栏那份排除 —— 必须写在上面两条**之后**（特异性相同，靠顺序取胜） */
.land-ghost .nav-brand { view-transition-name: none; }

/* 站名用默认形变（浏览器自己插值位置与大小）—— 那正是"大字 ↔ 站名"的 morph */
::view-transition-group(brand) {
  animation-duration: 620ms;
  animation-timing-function: cubic-bezier(.45, 0, .2, 1);
}

/* 页面本体：旧页轻微上移淡出、新页从下方顶上来 —— 和手工收起的方向感一致 */
::view-transition-old(root) { animation: vtPageOut 460ms cubic-bezier(.45, 0, .2, 1) both; }
::view-transition-new(root) { animation: vtPageIn 460ms cubic-bezier(.45, 0, .2, 1) both; }
@keyframes vtPageOut { to { opacity: 0; transform: translateY(-12px); } }
@keyframes vtPageIn { from { opacity: 0; transform: translateY(18px); } }

/* 曲线是 svg 快照，按图片处理会被拉伸 —— 裁切而不是拉伸 */
::view-transition-old(root), ::view-transition-new(root) { object-fit: cover; overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(*), ::view-transition-new(*), ::view-transition-group(*) {
    animation-duration: 1ms !important;
  }
}
/* 内容页播倒放时，把抓回来的 hero 当**覆盖层**盖住正文。
   这样"刷新后点站名"和"主页点名字"跑的是**同一套 CSS 动画**，观感一模一样。 */
.land.is-overlay {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
}
/* 调试用：把动画停在某一时刻（?anim= / ?back=），便于截图逐帧核对 */
.land.is-leaving.is-frozen,
.land.is-leaving.is-frozen *,
.land.is-returning.is-frozen,
.land.is-returning.is-frozen * {
  animation-play-state: paused !important;
}

@media (prefers-reduced-motion: reduce) {
  html:not([data-motion-preview]) .land.is-leaving,
  html:not([data-motion-preview]) .land.is-leaving * { animation-duration: 1ms !important; }
  /* 静止漂移也停掉 */
  html:not([data-motion-preview]) .land-curves svg { animation: none !important; }
}

/* 窄屏：名字收一点，三个按钮改成竖排（横排会挤成一团） */
@media (max-width: 720px) {
  :root { --land-height: max(100vh, 720px); }
  .land-hero { margin-top: 32px; }
  .land-name { font-size: clamp(38px, 12vw, 64px); }
  .land-cards {
    flex-direction: row;
    max-width: 360px;
    gap: 10px;
    margin-top: 40px;
    padding-bottom: 32px;
  }
  /* 4 张（亲密账户）：窄屏横排四张会把 40px 图标 + 12px 字挤出去，改成 2×2 方块；
     3 张（游客）走上面那条老样子，一行排开。 */
  .land-cards.is-4 {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    max-width: 300px;
    gap: 12px;
  }
  .land-card { padding: 14px 8px; gap: 12px; border-radius: 16px; }
  .land-card-icon { width: 40px; height: 40px; }
  /* 手机上的小字（2026-09-30）：原来是 11px，≤360px 那档还压到 10.5px —— 手机上读不动。
     基准是 13px，窄屏收到 12px 就够（卡片三张并排，12px 的 "Tutorials" 在 320 宽下仍放得下）。 */
  .land-card-t { font-size: 12px; }
  .land-card-foot { margin-top: 12px; }
  /* 手机只裁取宽波形的中段，避免把全部波峰挤进窄视口。
     带子高度同时写进 `--curve-band-h`（收尾让出的量按它算，见 .land-curves 那条注释）——
     **自定义属性不能靠"写两条"兜底**（#80 踩过），所以 `svh` 那一档放在文件末尾的
     `@supports (height: 100svh)` 里单独覆盖。 */
  .land-curves {
    --curve-band-h: clamp(280px, 42vh, 400px);
    --curve-retract-extra: 4px;   /* 曲线最低点是个尖点，正好压线会留一丝 */
    height: var(--curve-band-h);
    min-height: 0;
  }
  .land-curves svg { width: 180%; margin-left: -40%; }
  .land-card { background: color-mix(in srgb, var(--surface) 90%, transparent); }
  .nav-inner { gap: 12px; }
  .page-title { font-size: 25px; line-height: 1.4; overflow-wrap: anywhere; }
  .page-desc { margin-bottom: 28px; }
}

.land.is-loading .land-card { cursor: progress; opacity: .65; }
.land.is-loading .land-card:focus { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0s !important; }
  html:not([data-motion-preview]) .land .sway,
  html:not([data-motion-preview]) .land.is-returning,
  html:not([data-motion-preview]) .land.is-returning *,
  html:not([data-motion-preview]) .land-next.is-going { animation: none !important; }
  .land-card:hover, .list-item:hover { transform: none; }
}

/* ============================================================
   16. 账号子系统（阶段 4）
   ------------------------------------------------------------
   只服务 /login、/invite/<token>、/account 三个页面。
   **纯新增**：不改上面任何一条既有规则；颜色一律走 :root / [data-theme] 的令牌
   （--danger / --ok 是这一版新加的，同样三处都写了）。
   前端本来是冻结的 —— 这一节存在的唯一理由是"没有它，登录失败的错误提示看不见"。
   ============================================================ */

.auth-card { max-width: 420px; }
.auth-submit { width: 100%; justify-content: center; }

.auth-alert {
  max-width: 420px;
  margin: 0 0 20px;
  padding: 11px 14px;
  font-size: 14px;
  line-height: 1.6;
  border: 1px solid var(--danger);
  border-radius: var(--radius-sm);
  color: var(--danger);
}
.auth-alert-ok { border-color: var(--ok); color: var(--ok); }

/* 账号页里的表格：正文列宽不设上限，窄屏横向滚动而不是把表格压烂 */
.auth-prose { max-width: none; }
.auth-prose h2:first-child { margin-top: 0; }
.auth-scroll { overflow-x: auto; }
.auth-scroll table { min-width: 620px; }
/* 单元格不折行 —— 日期和状态一折行，整张表就碎成一列一列的；宽度交给 .auth-scroll 横向滚动 */
.auth-scroll th, .auth-scroll td { white-space: nowrap; }
/* 「给谁」列例外：备注可以折行，但必须给足最小宽度，否则会被挤成竖排碎字 */
.auth-scroll th:nth-child(2), .auth-scroll td:nth-child(2) {
  white-space: normal;
  min-width: 110px;
  max-width: 240px;
  overflow-wrap: anywhere;
}
/* 撤回按钮也不能折成两行（尺寸统一交给基准 .btn，这里只管不换行） */
.auth-scroll .btn { white-space: nowrap; }

/* 新建邀请码：明文只显示这一次，所以给一块显眼的框 */
.auth-code {
  max-width: 680px;
  margin: 0 0 36px;
  padding: 18px 20px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
}
.auth-code-warn { margin: 0 0 16px; font-size: 13.5px; color: var(--danger); }
.auth-code-label { margin: 0 0 4px; font-size: 12.5px; color: var(--text-3); }
.auth-code-value,
.auth-code-url {
  display: block;
  font-family: var(--mono);
  overflow-wrap: anywhere;
  margin: 0 0 16px;
}
.auth-code-value { font-size: 17px; letter-spacing: 0.04em; color: var(--text); }
.auth-code-url { font-size: 13.5px; color: var(--text-2); margin-bottom: 0; }
/* 一键复制邀请文案（2026-09-30）：只给一个按钮，不摆预览框 —— 文案是写好的整段话，
   按钮上就写着它叫"邀请文案"。反馈沿用 `.code-copied`（真元素 + role=status）。 */
.auth-code-acts { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 18px 0 0; }
.auth-code-acts .btn svg { width: 17px; height: 17px; flex: none; margin-right: -2px; }
.auth-code-acts .code-copied { color: var(--link); }

/* ============================================================
   17. 上栏右侧控件：灯泡主题切换 + 头像菜单（2026-09-23 用户要求）
   ------------------------------------------------------------
   纯新增（`.nav-links` / `.land-acts` 那两行最终没有改动）。
   参考：灯泡表示"当前状态" + 提示"点下去变成什么"（WAKU 的太阳/月亮那套）；
   菜单按钮的 aria 三件套按 W3C APG Menu Button Pattern。
   ============================================================ */

/* **上栏三个控件统一高度** —— 这一条是这次改动的关键。
   `.nav-link`（登录）的自然高度是 34.5px：padding 5+5 加 14px 文字的行盒 24.5。
   灯泡和头像按钮如果各写各的高度，就会和它不一样高（用户 2026-09-23 一眼看出"按钮高度不一样"）；
   写 35px 还会把上栏顶高 0.5px、`--navh` 从 63 变 64。

   取 34px：整数（亚像素不会把边框渲染糊），三处一模一样高；
   14 + 34 + 14 = 62，加上 `.nav` 那条发丝边仍四舍五入到 **63** —— `--navh` 一个像素没动。
   （`.nav-link` 的行盒比内容盒高 0.5px，`overflow` 是 visible，看不见也不会裁。）
   两处容器（`.nav-links` 与 `.land-acts`）都靠 `align-items: center` 居中，不用 stretch ——
   stretch 在"只有图标按钮、没有文字链接"时没有参照高度，头像会塌回圆的 28px。 */
.nav-link, .nav-theme, .nav-avatar { height: 34px; }

/* 触控目标（2026-09-30）：这两个图标按钮实测只有 35×34，低于 44×44 的手机惯例。
   **不动盒子** —— `--navh` 是按 14 + 34 + 14 = 63 定死的（JS 现量上栏写进 `--navh`），
   把按钮撑大会推着整站上栏高度、乃至主页收起态的几何一起走。
   改用伪元素把**可点面积**撑到 44×44：视觉一个像素不变，手指点得准。
   代价：与相邻按钮的重叠区只有 4px 那道缝（主题 ↔ 头像），缝归后者（DOM 靠后）。 */
.nav-theme, .nav-avatar { position: relative; }
.nav-theme::after, .nav-avatar::after {
  content: ""; position: absolute; top: -5px; bottom: -5px; left: -5px; right: -5px;
}

/* 灯泡按钮：原来是文字「切换深浅」的 72×32，现在是图标按钮。
   两个图标都渲染，用 [data-theme] 决定显示哪个（亮 = 浅色，灭 = 深色）。
   高度由上面那条 `.nav-link, .nav-theme, .nav-avatar { height: 34px }` 统一给。 */
.nav-theme {
  width: 35px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.nav-theme:hover { color: var(--text); border-color: var(--border-strong); }
.nav-theme .theme-icon { width: 18px; height: 18px; }
.nav-theme .theme-icon-dark { display: none; }
[data-theme="dark"] .nav-theme .theme-icon-light { display: none; }
[data-theme="dark"] .nav-theme .theme-icon-dark { display: block; }

/* 头像按钮：35px 宽 + 里面 28px 的圆；高度同样由上面那条统一规则给。 */
.nav-avatar {
  width: 35px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 50%;
  cursor: pointer;
}
.nav-avatar-letter {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--text);
  color: var(--bg);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1;
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.nav-avatar:hover .nav-avatar-letter,
.nav-avatar[aria-expanded="true"] .nav-avatar-letter { box-shadow: 0 0 0 3px var(--hover); }
.nav-avatar[aria-expanded="true"] .nav-avatar-letter { transform: scale(1.05); }

/* 站主的真头像：28px 圆。图和首字母那颗**同一尺寸、同一悬停反馈**，
   所以换过之后按钮的位置与手感都不变。 */
.nav-avatar-img {
  display: block;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.nav-avatar:hover .nav-avatar-img,
.nav-avatar[aria-expanded="true"] .nav-avatar-img { box-shadow: 0 0 0 3px var(--hover); }
.nav-avatar[aria-expanded="true"] .nav-avatar-img { transform: scale(1.05); }

/* 下拉菜单：挂在 <body> 末尾、fixed 定位，坐标由 JS 现算。
   为什么不在上栏里：上栏住在 .land 里，而 .land 有 overflow: hidden（红线），会被裁掉。 */
.account-menu {
  position: fixed;
  z-index: 80;
  min-width: 196px;
  padding: 6px;
  /* 2026-09-24 改毛玻璃：它是浮在正文之上的浮层，背后就是页面内容，
     换成玻璃材质能明显看出层次（和 .land-card / .main 用同一组 --glass 令牌）。 */
  border: 1px solid var(--glass-border);
  border-radius: 14px;
  background: var(--glass);
  box-shadow: 0 12px 32px -12px rgba(20, 50, 90, 0.28), 0 2px 8px -4px rgba(20, 50, 90, 0.12);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
  animation: accountMenuIn 140ms var(--ease);
}
.account-menu[hidden] { display: none; }
[data-theme="dark"] .account-menu {
  box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.6), 0 2px 8px -4px rgba(0, 0, 0, 0.4);
}
@keyframes accountMenuIn {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
.account-menu-head { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px 6px; }
.account-menu-name { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.account-menu-role { font-size: 12.5px; color: var(--text-3); }
.account-menu-sep { height: 1px; margin: 4px 6px; background: var(--border); }
.account-menu-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  color: var(--text-2);
  font-size: 14px;
}
.account-menu-item:hover,
.account-menu-item:focus { background: var(--hover); color: var(--text); }
.account-menu-hint { font-size: 12px; color: var(--text-3); }
@media (prefers-reduced-motion: reduce) { .account-menu { animation: none; } }

/* ---------- 16.1 管理列表（邀请码 / 账号，2026-09-23）----------
   每行一个 `<details>`：摘要行放一眼能扫完的信息，展开是这一行的全部管理动作。
   为什么不用表格：正文列只有 680px，再加一列"管理"就把表格压碎了；
   而每行要放一个编辑表单加两三个动作，表格里根本放不下。
   `<details>` 零依赖、不需要 JS、也不用另开路由，展开后整行宽度都归它用。 */
.mg-list { list-style: none; margin: 0 0 8px; padding: 0; }
.mg-item { border-bottom: 1px solid var(--border); }
.mg-item > details > summary {
  display: block;
  padding: 12px 0;
  cursor: pointer;
  list-style: none;
}
.mg-item > details > summary::-webkit-details-marker { display: none; }
.mg-item > details[open] > summary { border-bottom: 1px dashed var(--border); }
.mg-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.mg-code { font-family: var(--mono); font-size: 13.5px; color: var(--text); }
.mg-note { font-size: 14px; color: var(--text-2); min-width: 0; overflow-wrap: anywhere; }
.mg-state { margin-left: auto; font-size: 12.5px; color: var(--text-3); }
.mg-state.is-ok { color: var(--ok); }
.mg-state.is-used { color: var(--link); }
.mg-open { font-size: 12.5px; color: var(--text-3); }
.mg-item > details[open] .mg-open { color: var(--text); }
.mg-sub { display: block; margin-top: 3px; font-size: 12.5px; color: var(--text-3); }
/* 账号行的名字用了「邀请备注」时，用户名退到副行：用等宽体，一眼看出那是登录名 */
.mg-username { font-family: var(--mono); }
.mg-body { display: flex; flex-direction: column; gap: 10px; padding: 4px 0 16px; }
.mg-form, .mg-act { margin: 0; }
.mg-form { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.mg-fields { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.mg-f { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--text-3); }
/* 内联编辑表单的输入也用同一套尺度（原来缩到 40px/13.5px，与生成表单的 45px 打架） */
.mg-f .hint { margin-top: 0; }
.mg-note-line { margin: 0; font-size: 12.5px; color: var(--text-3); }
.mg-danger { color: var(--danger); border-color: var(--danger); }
.mg-danger:hover { color: var(--danger); border-color: var(--danger); }

/* ============================================================ 18. 标签 chip（2026-09-23 新增）
   为什么追加在文件末尾而不是补进 §16.1 的 .mg-* 那一块：本项目规矩是"样式新增一律
   追加到文件末尾"，而且 §16.1 上面就是曲线那条红线区域（里面有一个孤立的 CRLF），
   动它会把无关的字节卷进 diff。
   用在 /account：邀请码行显示"发放时打的标签"，账号行显示"这个账号现在的标签"；
   虚线那种是"未打标签"的占位（只有账号行会给，邀请码行没标签就不占位）。 */
.mg-tags { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 5px; min-width: 0; }
.mg-tag {
  display: inline-block; padding: 1px 8px; border: 1px solid var(--glass-border);
  border-radius: 999px; background: var(--hover); color: var(--text-2);
  font-size: 11px; line-height: 1.7; white-space: nowrap;
}
.mg-tag.is-none { border-style: dashed; background: none; color: var(--text-3); }

/* ============================================================ 19. 任务列表（2026-09-27 新增）
   用在正文里的 `- [ ]` / `- [x]`（成品页 README、教程都走 md()）。DOM 与类名**照 GitHub 那一套**
   （`.contains-task-list` / `.task-list-item` / `.task-list-item-checkbox`）——
   用户对这件事的原话是「参考别人的实现」，类名是那套实现里唯一能逐字对照的部分。
   为什么不用原生复选框：`input[type=checkbox]` 的默认外观在深色主题下发白、各浏览器也不一致，
   所以 `appearance: none` 自己画 —— 但**只用本站令牌**，不新增颜色：
   勾选态填充 `--link`、对勾用 `--bg`（浅色主题是深蓝底 + 近白勾，深色主题是浅蓝底 + 近黑勾，
   两边都高对比，且跟着主题自动走）。
   为什么追加在文件末尾而不是补进 §10：本项目规矩是"样式新增一律追加到文件末尾"，
   而且 §10 那片区域旁边就是曲线红线，动它会把无关字节卷进 diff。
   选择器带 `.prose` 与前缀类是为了压过 §10 的 `.prose ul` / `.prose li`（否则列表符号与左内距不生效）。 */
.prose ul.contains-task-list, .prose ol.contains-task-list { list-style: none; padding-left: 4px; }
.prose li.task-list-item { margin-bottom: 4px; }
.task-list-item-checkbox {
  appearance: none; -webkit-appearance: none;
  width: 15px; height: 15px; margin: 0 7px 0 0; vertical-align: -2px;
  border: 1.5px solid var(--border-strong); border-radius: 4px;
  background: var(--surface); position: relative; cursor: default;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.task-list-item-checkbox:checked { background: var(--link); border-color: var(--link); }
.task-list-item-checkbox:checked::after {
  content: ""; position: absolute; left: 4px; top: 1px;
  width: 4px; height: 8px; border: solid var(--bg); border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

/* ---------- 退出登录确认卡（2026-09-29 新增） ----------
   用户：「退出登录页面也没动画，且做的太丑了」。原来只有一行字 + 一个裸按钮；
   现在与登录页同一族：**谁在登录** + 主行动 + 回主页，都用既有令牌，不引新颜色。 */
.auth-confirm { display: flex; flex-direction: column; gap: 14px; }
.auth-who {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg);
}
.auth-who-avatar {
  flex: 0 0 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: 12px; background: var(--hover);
  font: 600 16px var(--mono); color: var(--text-2);
}
.auth-who-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.auth-who-text strong { font-size: 15px; font-weight: 600; overflow-wrap: anywhere; }
.auth-who-sub { font-size: 12.5px; color: var(--text-3); }

/* ---------- 带触摸屏的设备：状态反馈留着，位移不要（2026-09-30，小米 pad 实测） ----------
   用户朋友用小米 pad（Android，外接鼠标）访问，**指到哪哪就消失**。看下来是 GPU 瓦片内存被榨干：
   指针每动一下，hover 的 `transform` 就把元素推进一个新的合成层，而这类设备上本来就叠着
   大面毛玻璃 + 三团带 `will-change` 的光雾。位移只是装饰，边框/底色/阴影的悬停反馈都保留。
   R52（2026-10-15）：条件从 `(any-pointer: coarse)` 收窄为 `(pointer: coarse)` —— 前者只要设备带触摸屏
   就命中（带触摸屏的笔记本也被误伤，鼠标 hover 的位移全没了）；后者问的是**主要输入**能不能精确指，
   pad 那种"主输入是触摸、外接鼠标只是辅助"的设备仍然是 `(pointer: coarse)`，保护照旧。 */
@media (pointer: coarse), (hover: none) {
  .land-card:hover { transform: none; }
  .land-card:hover .land-card-go { transform: none; }
  .list-item:hover { transform: none; }
  .directory-toggle:hover { transform: none; }
}

/* ---------- svh 的渐进增强（2026-09-30） ----------
   上面所有 `svh` 的用法都先给了 `vh` 的等价值，这里再把它们升成 `svh`。
   为什么必须成对写：不支持 `svh` 的浏览器（iOS < 15.4 / Android WebView < 108 /
   部分 App 内置浏览器）会把带 `svh` 的那条声明整个作废 ——
   对 `height: var(--land-height)` 这种用法，作废的后果是 `auto`，主页那整块会塌。 */
@supports (height: 100svh) {
  :root { --land-height: max(100svh, 620px); }
  @media (max-width: 720px) { :root { --land-height: max(100svh, 720px); } }
  /* 波形带子同理：手机档的高度要跟 `svh` 一起升，否则"收尾让出多少"会按 `vh` 多让几像素 */
  @media (max-width: 720px) {
    .land-curves { --curve-band-h: clamp(280px, 42svh, 400px); height: var(--curve-band-h); }
  }
}

/* ============================================================
   R1（2026-10-20）二级站入口：主页四张主卡正下方的一条横向轻卡
   ------------------------------------------------------------
   纯新增：选择器全部收在 `.land-sites` / `.land-site` 里，上面任何一条既有规则都没有改
   —— 所以四张主卡的坐标、尺寸、间距与原动效一个像素都不动。

   结构：入口是 `.land-cards` 的**跨行 flex 项**（flex-basis 100%），
   于是它天然跟四张卡一起居中、一起收盘、一起淡出，不必另立一层容器
   （另立容器就得搬 `.land-cards` 的 padding，那会动到既有几何）。

   预留：`.land-cards` 的 padding-bottom 本来是给曲线带让位的（clamp(150px,26vh,300px)）。
   入口占掉「间距 + 自身高度」，就**等量**从那段预留里扣掉 ——
   于是面板总高与改前一致：既不会被 `.land-clip`（overflow:hidden）裁掉，
   也不会把 ICP 或曲线带压下去。1440×900 实测：234px → 132px，内容底边仍是 752px。

   将来多一个二级站：只改 `land_sites()` 那个列表。桌面并排、放不下自动换行，
   窄屏一列，单卡高度不变。
   ============================================================ */
.land-cards.has-sites {
  --land-site-gap: 30px;   /* 主卡与入口之间（设计：28–32px） */
  --land-site-h: 72px;     /* 入口卡高度（设计：约 72px） */
  --land-site-dur: 200ms;  /* 设计要 180–220ms：比 --dur(160ms) 略缓，配轻卡的手感 */
  flex-wrap: wrap;         /* 只有带入口的首页才换行；四张卡本身仍是一行 */
  row-gap: 0;              /* 行间距交给下面的 margin-top，才能精确控成 30px（不叠 gap） */
  padding-bottom: max(64px, calc(clamp(150px, 26vh, 300px) - var(--land-site-gap) - var(--land-site-h)));
}
.land-sites {
  flex: 1 1 100%;          /* 独占一整行 ⇒ 永远落在主卡下方 */
  display: flex;
  flex-wrap: wrap;         /* 将来多张：并排；放不下自己换行 */
  justify-content: center;
  gap: 12px;
  margin-top: var(--land-site-gap, 30px);
  min-width: 0;
}
.land-site {
  /* 一档 300–320px：单张时撑到 320（居中）；将来两张并排时各占一半、仍落在这一档；
     容器真放不下就换行。**不为"以后可能有的站"另造配置系统**。
     注意游客态容器只有 496px（四张主卡是 640px），两张 300 + 12 间距在 496 里放不下 ——
     那时会换行成竖排，这是容器宽度决定的，不是 bug。 */
  flex: 1 1 300px;
  max-width: 320px;
  min-width: 0;
  height: var(--land-site-h, 72px);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 14px;
  border: 1px solid var(--border);
  border-radius: 16px;
  /* 柔和半透明表面：不做黑块、不加厚壳；深色靠同一组令牌自然适配 */
  background: color-mix(in srgb, var(--surface) 58%, transparent);
  box-shadow: 0 1px 2px rgba(20, 50, 90, 0.05);
  transition: transform var(--land-site-dur, 200ms) var(--ease),
              box-shadow var(--land-site-dur, 200ms) var(--ease),
              border-color var(--land-site-dur, 200ms) var(--ease),
              background var(--land-site-dur, 200ms) var(--ease);
}
.land-site:hover {
  transform: translateY(-2px);
  border-color: var(--border-strong);
  background: color-mix(in srgb, var(--surface) 84%, transparent);
  box-shadow: 0 2px 4px rgba(20, 50, 90, 0.06), 0 12px 22px -12px rgba(20, 50, 90, 0.20);
}
/* 整张卡就是一个 a，键盘焦点必须留得下来 */
.land-site:focus-visible { outline: 2px solid var(--link); outline-offset: 3px; }
.land-site-img {
  width: 46px; height: 46px; flex: none;
  border-radius: 9px;
  object-fit: cover;
  /* 图没取到时也是一块安静的浅底，不是空洞 */
  background: color-mix(in srgb, var(--text) 6%, transparent);
}
.land-site-txt { display: flex; flex-direction: column; min-width: 0; }
.land-site-t {
  font-size: 14px; font-weight: 600; letter-spacing: .01em;
  color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.land-site-s {
  margin-top: 2px;
  font-size: 12px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.land-site-go {
  width: 16px; height: 16px; flex: none; margin-left: auto;
  color: var(--text-3);
  transition: transform var(--land-site-dur, 200ms) var(--ease), color var(--land-site-dur, 200ms) var(--ease);
}
.land-site:hover .land-site-go { color: var(--text); transform: translate(2px, -2px); }

@media (max-width: 720px) {
  /* 窄屏一列；卡不超容器，上下都不贴边。间距与桌面同值（设计 28–32px）：
     预留的扣减是按同一个变量算的，所以改间距只挪入口、不增总高。 */
  .land-site { flex: 1 1 100%; max-width: 320px; }
  /* 4 张卡在窄屏是 grid（2×2）：入口必须跨满整行，否则会被塞进半格 */
  .land-cards.is-4.has-sites > .land-sites { grid-column: 1 / -1; }
  /* R4 修：上面 `.land-cards.has-sites` 的 `row-gap: 0` 会盖掉 2×2 那条
     `.land-cards.is-4 { gap: 12px }` 里的**行**间距，两排卡上下贴死。只在这一档补回来。
     补回来之后 grid 行距 12 + 入口 margin-top 变成 12+30=42 —— 所以入口那条同时减 12，
     让"入口离最后一排卡"仍是 30px。横向间距、卡尺寸、原有 mobile 布局一个都没动。 */
  .land-cards.is-4.has-sites { row-gap: 12px; }
  .land-cards.is-4.has-sites > .land-sites { margin-top: calc(var(--land-site-gap, 30px) - 12px); }
}

/* 减少动态偏好：和既有 `.land-card:hover` 同一条约定 —— 位移也一起停掉 */
@media (prefers-reduced-motion: reduce) {
  .land-site:hover { transform: none; }
  .land-site:hover .land-site-go { transform: none; }
}

/* ============================================================
   R3（2026-10-21）入口变成可增删的列表之后：多到一屏放不下时不许被永远裁掉
   ------------------------------------------------------------
   为什么不能靠调高度：`.land` 的 `height: var(--land-height)` 是收起动效的地基 ——
   `@keyframes landSquash` / `landGrow` 直接写死这个值，改高度会让动画起手跳一下。
   所以只在**有入口**时把内容层变成滚动容器：放得下就与改前一模一样（没有滚动条、
   也没有多余的滚动范围），放不下就滚出来看。

   实测（1386×800，4 张主卡）：第 1 行入口顶 522、每行 72 + 12 间距，
   第 3 行底 762 仍在面板内，第 4 行（774–846）就出界了 —— 那正是这条规则要接住的情形。

   `.land-inner` 的那个 1px 是 `.land` 的 1px 下边框造成的（面板 800 / 内容层 799）：
   不扣掉它，只有 1 条入口时也会挂一条几乎拖不动的滚动条。
   ============================================================ */
.land.has-sites .land-inner { min-height: calc(var(--land-height) - 1px); }
.land.has-sites .land-clip {
  overflow-y: auto;
  overscroll-behavior: contain;               /* 滚到底不要带着整页跳 */
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--text) 22%, transparent) transparent;
}
/* 收起 / 倒放期间恢复裁剪：动效中途不该出现滚动条，也不该能滚 */
.land.has-sites.is-leaving .land-clip,
.land.has-sites.is-returning .land-clip,
.land.has-sites.is-done .land-clip { overflow-y: hidden; }
/* 两条以上：底部那段"给曲线带让位"的大留白换成最小值（40px）。
   不算这一步会白白多出 40–80px 的滚动尾巴 —— 内容明明全在面板里，滚动条却出来了
   （实测：访客 3 条、1200×800 时，留 106px → 内容 858 > 面板 799，留 40px → 792 刚好）。
   一条时保持原样 —— 当前这一条的视觉与 R1/R2 逐像素一致。
   真放不下（比如 390 宽 + 4 条）时滚动条才该出现，那是它该起的作用。 */
.land.has-sites-multi .land-cards { padding-bottom: 40px; }
/* 没设头图时的方形占位：与 `.land-site-img` 同尺寸。span 要显式 block 才吃宽高 */
.land-site-img-empty {
  display: block;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 7%, transparent);
}