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

/* ---------- 1. 设计令牌 ---------- */
/* 2026-09-22 用户定调：底色用**淡蓝**（我原稿推的是暖米白，用户否掉了）。
   整组灰阶都跟着偏蓝，否则蓝底配纯灰会显脏。 */
:root {
  /* 中性灰阶（浅色）—— 蓝调纸 */
  --bg: #f5f9fd;
  --surface: #ffffff;
  --text: #17202a;
  --text-2: #4c5a68;
  --text-3: #627587;
  --link: #245f99;
  --land-height: max(100svh, 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;

  /* 代码高亮 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;
    --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;
  --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. 按钮 / 表单 ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border-radius: var(--radius-sm);
  padding: 8px 14px;
  font-size: 14px;
  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%;
  padding: 9px 12px;
  font: inherit;
  font-size: 14px;
  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);
  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: 11px;
    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(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%;
  height: 50vh;
  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;
}
.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;
}
/* 选择器必须带上 .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); }
  100% { transform: translateY(0); }
}

/* 名字：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 {
  /* 建立滞后：边界先向下走，曲线被落在后面（这一段面板在快速展开，曲线正进入视野） */
  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); }
  /* 平滑追上：长段缓动，不是短促 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 { 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(100svh, 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;
  }
  .land-card { padding: 14px 8px; gap: 12px; border-radius: 16px; }
  .land-card-icon { width: 40px; height: 40px; }
  .land-card-t { font-size: 11px; }
  .land-card-foot { margin-top: 12px; }
  .land-curves { height: clamp(280px, 42svh, 400px); 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;
}
/* 撤回按钮也不能折成两行 */
.auth-scroll .btn { padding: 4px 10px; font-size: 13px; 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; }

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

/* 灯泡按钮：原来是文字「切换深浅」的 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-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: 12.5px; color: var(--text-3); }
.mg-f .input { padding: 6px 10px; font-size: 13.5px; }
.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);
}