/* ==========================================================================
   00 字体：Noto Serif SC 子集（只含标题实际用到的 164 个字符）
   --------------------------------------------------------------------------
   完整字体 24.0 MB，子集后 84.8 KB（0.35%）——中文 Web Font 绝不可整包上线。
   重新生成：npm run font（改了标题文案之后）
   ========================================================================== */
@font-face {
  font-family: "Noto Serif SC";
  font-style: normal;
  font-weight: 400 700; /* 可变字重范围 */
  font-display: swap;
  src: url("fonts/serif-subset.woff2") format("woff2");
  /* 只对标题类字符生效；未覆盖字符自动回落到 --font-serif 兜底链 */
}

/* ==========================================================================
   01 设计令牌 —— 想整体换调性，只改这一段
   ========================================================================== */
:root {
  /* —— 色彩：全站零彩色，除影像外不得出现任何非灰阶值 ——
     六阶沿用设计稿；--line 是实现补充（原设计没有分隔线色）。 */
  --ink: #18181b;         /* 正文 / 深色板块底 */
  --ink-2: #3f3f46;       /* 次级文字 */
  --ink-3: #52525b;       /* 元信息 / 标签 */
  --ink-4: #a1a1aa;       /* 弱化信息 / 占位标签（不承载信息时用） */
  --paper: #f7f7f5;       /* 浅色区块底（比纯白暖一档） */
  --white: #ffffff;       /* 主背景 */
  --line: #e4e4e7;        /* 1px 细线 */

  /* 语义映射 */
  --fg: var(--ink);
  --fg-muted: var(--ink-2);
  --fg-subtle: var(--ink-3);
  /* 小字（12px 及以下的标签 / 元信息 / 编号）：--ink-3 在白底 8.6:1、
     在 --paper 上 8.0:1，都远超 AA。
     ⚠️ --ink-4 只有 2.4:1，**不能用来写字** —— 踩过两次：
       ① 图位占位标签用它 → 2.39:1；
       ② 深色卡上的标签若继承它 → 2.29:1。
     它只能给"不承载信息"的装饰（分隔符、虚线、圆点）。 */
  --fg-faint: var(--ink-3);
  --bg: var(--white);
  --bg-alt: var(--paper);

  /* 反白底（深色板块） */
  --inv-bg: var(--ink);
  --inv-fg: var(--white);
  --inv-muted: #d4d4d8;   /* 深色底上的次级文字，对 #18181B 约 10:1 */
  --inv-line: #3f3f46;
  /* 早先 .tool--ink 引用过 --inv-faint 却从未定义（潜伏 bug）：补上，装饰分隔符用 */
  --inv-faint: #a1a1aa;

  /* —— 字体 ——
     标题走子集化的 Noto Serif SC；正文全系统字体栈，零请求。
     兜底链必须含中文衬线，否则断网时标题会掉到黑体、气质全失。 */
  --font-serif: "Noto Serif SC", "Songti SC", "STSong", "SimSun", "Source Han Serif SC",
    serif;
  --font-sans: "Noto Sans SC", "PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei",
    "Hiragino Sans GB", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* —— 字号阶梯：display 与 body 差 6 倍。内容越少，层级越只能靠字号差建立 ——
     ⚠️ h2 只承担"段落标题"，必须明显低于 h1（62/34）。
     早先取 52，与 h1 只差 1.19 倍 —— 全页 5 个大字（h1 + 4 个 h2 + 邮箱）
     挤在 52–64 这一段里互相抢戏，等于没有层级。 现在 62 : 36 ≈ 1.7。 */
  --fs-display: clamp(44px, 7vw, 96px);
  --fs-h2: clamp(24px, 2.5vw, 36px);
  --fs-h3: 24px;
  --fs-lead: 17px;
  --fs-body: 16px;
  --fs-small: 14px;
  --fs-label: 12px;

  /* —— 栅格与留白：版心比简历站窄（内容少就该收窄），留白约为其 1.8 倍 —— */
  --maxw: 1280px;
  --gutter: clamp(20px, 5vw, 72px);
  --section-y: clamp(72px, 12vh, 160px);
  --header-h: 64px;

  /* —— 圆角：接近 0，这是锐利感的来源 —— */
  --r: 2px;

  /* —— 动效：只留四种动作（浮起 / 影像显现 / 擦除 / 画线）——
     刻意不做视差、打字机、数字计数、跑马灯：那些是简历站"工程感"的语言，
     本站签名元素是影像，动效抢戏就是失败。 */
  --mo-step: 80ms;
  --mo-base: 0.72s;
  --mo-slow: 1.1s;
  --mo-clip: 1.2s;
  /* easeOutQuint：起步快、收尾极慢 */
  --mo-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  /* 更软：收尾几乎不带惯性，用于悬停 */
  --mo-ease-soft: cubic-bezier(0.33, 0.9, 0.4, 1);
  /* 带一点过冲的回弹：只给轻反馈的出场用（toast / 打勾），幅度刻意很小 */
  --mo-ease-pop: cubic-bezier(0.2, 0.9, 0.3, 1.15);

  /* 表面：吸顶导航毛玻璃底 / 卡片跟手光斑 —— Ink 模式各给一份 */
  --bg-glass: rgba(255, 255, 255, 0.92);
  --spot: rgba(24, 24, 27, 0.05);

  color-scheme: light;
}

/* —— Ink 模式（暗色）——
   只换令牌值，组件规则一行不改：墨变浅、纸变深，黑配额跟着反转 ——
   联系区与主推卡从「唯一的深色」翻成「唯一的浅色」。
   色值全部取中性灰（通道差 ≤ 3），守得住 verify [6] 的零彩色断言。
   切换由 main.js 模块 8 完成；防刷新闪白靠 <head> 的内联脚本提前落 data-theme。 */
[data-theme="dark"] {
  --ink: #ededf0;
  --ink-2: #c6c6cb;
  --ink-3: #9d9da3;
  --ink-4: #6a6a70;
  --paper: #1b1b1f;
  --white: #131316;
  --line: #333338;

  --inv-bg: #ececef;
  --inv-fg: #131316;
  --inv-muted: #4c4c52;   /* 对 #ECECEF 约 6.9:1，AA 达标 */
  --inv-line: #c9c9ce;
  --inv-faint: #6a6a70;

  --bg-glass: rgba(19, 19, 22, 0.92);
  --spot: rgba(255, 255, 255, 0.06);

  color-scheme: dark;
}
/* 浅色卡在暗色下换深色光斑（主推卡在暗色下是浅色底） */
[data-theme="dark"] .tool--ink {
  --spot: rgba(24, 24, 27, 0.06);
}

/* ==========================================================================
   02 基础
   ========================================================================== */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 16px);
  -webkit-text-size-adjust: 100%;
}

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

/* 注意：全站不写 body { overflow-x: hidden } —— 会把溢出传播到视口、
   破坏 position: sticky 吸顶导航（简历站踩过这个坑）。
   出血元素一律由各自区块自行 overflow: hidden。 */

img {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
}

h1, h2, h3, h4, p, figure, blockquote {
  margin: 0;
}

ul, ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

::selection {
  background: var(--ink);
  color: var(--white);
}

/* 可见焦点环 */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* 深色底上的焦点环:基线环是 --ink,落进深色区(contact / .tool--ink / .skip)
   会隐进同色背景 —— 这些区域换反白环 */
.contact :focus-visible,
.tool--ink:focus-visible,
.tool--ink :focus-visible,
.skip:focus-visible {
  outline-color: var(--inv-fg);
}

/* 跳到主要内容 */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 999;
  padding: 10px 16px;
  background: var(--ink);
  color: var(--white);
  font-size: var(--fs-small);
}
.skip:focus {
  left: 0;
}

/* ==========================================================================
   03 版面骨架
   ========================================================================== */
.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  padding-block: var(--section-y);
  /* 出血 / 裁切由区块自己负责，不要提到 body 上 */
  overflow: hidden;
}

.section--alt {
  background: var(--bg-alt);
}

/* 区块起始的细线：由 JS 画出来（scaleX 0→1）。线是"长出来"的，不做淡入。 */
.section-rule {
  height: 1px;
  background: var(--line);
  transform-origin: left center;
  border: 0;
  margin: 0;
}

/* 编号 + 标题的小标签行 */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

.h2 {
  font-family: var(--font-serif);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
}

.lead {
  font-size: var(--fs-lead);
  color: var(--fg-muted);
  line-height: 1.85;
}

/* ==========================================================================
   04 导航（吸顶）
   ========================================================================== */
.header {
  position: sticky;
  top: 0;
  z-index: 60;
  background: var(--bg-glass);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--line);
  transition: transform 0.32s var(--mo-ease);
}

.header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  height: var(--header-h);
}

.header__brand {
  font-family: var(--font-serif);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: 0.01em;
}

.header__nav {
  display: flex;
  align-items: center;
  gap: 28px;
}

.header__link {
  position: relative;
  font-size: var(--fs-small);
  color: var(--fg-muted);
  transition: color 0.24s var(--mo-ease-soft);
}
.header__link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -4px;
  width: 100%;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  /* 方向感知：基线挂在右端、悬停态换到左端 —— 进场从左长出，退场向右收回 */
  transform-origin: right center;
  transition: transform 0.28s var(--mo-ease-soft);
}
/* hover 全部包在 (hover: hover)：触屏不粘滞 —— 触屏的回应走 :active 按压 */
@media (hover: hover) {
  .header__link:hover {
    color: var(--fg);
  }
  .header__link:hover::after {
    transform: scaleX(1);
    transform-origin: left center;
  }
}

/* scroll-spy：滚到哪一段，对应导航项常亮（aria-current 由 main.js 维护）。
   这是状态不是悬停，所以不进 (hover: hover) —— 触屏上同样成立。 */
.header__link[aria-current] {
  color: var(--fg);
}
.header__link[aria-current]::after {
  transform: scaleX(1);
}

/* 外链胶囊：「简历网站」。全站唯一的胶囊形态。 */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border: 1px solid var(--ink);
  border-radius: 999px;
  font-size: var(--fs-small);
  transition: background 0.24s var(--mo-ease-soft), color 0.24s var(--mo-ease-soft);
}
@media (hover: hover) {
  .pill:hover {
    background: var(--ink);
    color: var(--white);
  }
}

/* Ink 切换：main.js 注入（无 JS 不存在 —— 那时也没法切换，别留死按钮）。
   图标随 [data-theme] 换：亮色显示月亮（去暗色），暗色显示太阳。 */
.theme-toggle {
  display: none;
}
.js .theme-toggle {
  position: relative;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  color: var(--fg-muted);
  transition: color 0.24s var(--mo-ease-soft), border-color 0.24s var(--mo-ease-soft);
}
/* 40 视觉 + 4 = 44 命中热区，与 header__toggle 同一标准 */
.js .theme-toggle::before {
  content: "";
  position: absolute;
  inset: -2px;
}
.theme-toggle .icon-sun {
  display: none;
}
[data-theme="dark"] .theme-toggle .icon-sun {
  display: block;
}
[data-theme="dark"] .theme-toggle .icon-moon {
  display: none;
}
@media (hover: hover) {
  .js .theme-toggle:hover {
    color: var(--fg);
    border-color: var(--ink);
  }
}

/* 滚动进度线：导航收起时只剩它 */
.header__progress {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: 100%;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
}

/* 向下读时收起导航 */
.header.is-hidden {
  transform: translateY(-100%);
}

/* 移动端抽屉按钮 */
.header__toggle {
  display: none;
  /* 40 → 44：拇指操作的推荐最小热区（Apple HIG 44 / Material 48），
     图标本身仍是 18px 的细线，只是热区变大 */
  width: 44px;
  height: 44px;
  place-items: center;
}
.header__toggle span {
  display: block;
  width: 18px;
  height: 1px;
  background: var(--ink);
  position: relative;
}
.header__toggle span::before,
.header__toggle span::after {
  content: "";
  position: absolute;
  left: 0;
  width: 18px;
  height: 1px;
  background: var(--ink);
}
.header__toggle span::before { top: -6px; }
.header__toggle span::after { top: 6px; }

/* ==========================================================================
   05 首屏（前台：一句话 + 一个活样品）
   ========================================================================== */
.hero {
  min-height: 84vh; /* 不写 100vh：避免移动端地址栏引起的跳动 */
  display: flex;
  align-items: center;
  padding-block: clamp(48px, 8vh, 96px);
  position: relative;
  overflow: hidden;
}

.hero__inner {
  display: grid;
  grid-template-columns: 6fr 5fr;
  gap: clamp(32px, 5vw, 72px);
  align-items: center;
  width: 100%;
}

.hero__title {
  font-family: var(--font-serif);
  font-size: clamp(34px, 4.6vw, 62px);
  font-weight: 600;
  line-height: 1.22;
  letter-spacing: -0.02em;
}

.hero__sub {
  margin-top: 24px;
  max-width: 40ch;
  font-size: var(--fs-lead);
  color: var(--fg-muted);
  line-height: 1.9;
}

.hero__cta {
  margin-top: 36px;
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
}

/* ── 按钮 ──
   只有一种实心样式。首屏刻意只放一个 CTA（前台的动线要单一，
   "看看有什么"一个动作就够了），所以不需要描边变体。 */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 13px 26px;
  border-radius: var(--r);
  font-size: var(--fs-small);
  letter-spacing: 0.02em;
  background: var(--ink);
  color: var(--white);
  /* 磁性吸附：位移全走变量，CSS 默认 0；main.js 模块 9 在精确指针下写入（±4px 内） */
  translate: var(--magx, 0) var(--magy, 0);
  transition: transform 0.26s var(--mo-ease-soft), translate 0.22s var(--mo-ease-soft);
}
/* 箭头与 .tool__go 同一套 hover 语言：位移量、时长、缓动完全一致 */
.btn svg {
  transition: translate 0.26s var(--mo-ease-soft);
}
@media (hover: hover) {
  .btn:hover {
    transform: translateY(-1px);
  }
  .btn:hover svg {
    translate: 4px 0;
  }
}
/* 按压：从悬停位落回基线之下，80ms（--mo-step）内回应；松手按常规时长回弹 ——
   与"浮起"入场互为镜像。触屏没有 hover，按压就是它"操作发生了"的回应。 */
.btn:active {
  transform: translateY(1px);
  transition-duration: var(--mo-step);
}

/* ── 滚动提示 ──
   首屏底部的一根呼吸线，纯装饰（aria-hidden）。只在「首屏确实是整屏」时
   才有意义：矮屏 / 窄屏 / 打印一律不出现。减少动效时由全局规则停成静线。 */
.hero__cue {
  position: absolute;
  left: 50%;
  bottom: 18px;
  translate: -50% 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.hero__cueText {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.3em;
  color: var(--fg-faint);
}
.hero__cueLine {
  position: relative;
  width: 1px;
  height: 34px;
  overflow: hidden;
  background: var(--line);
}
.hero__cueLine::after {
  content: "";
  position: absolute;
  left: 0;
  top: -100%;
  width: 1px;
  height: 100%;
  background: var(--ink);
  animation: cue-fall 2.2s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
@keyframes cue-fall {
  0% { top: -100%; }
  60%, 100% { top: 100%; }
}
@media (max-width: 600px), (max-height: 640px) {
  .hero__cue {
    display: none;
  }
}

/* ==========================================================================
   06 首屏演示：番茄钟「活样品」
   --------------------------------------------------------------------------
   这不是给访客用的工具，是橱窗里演示的样品 —— 它自己循环播放，
   不需要任何人操作。一段动的演示比一张静态图强得多：
   图片只能证明"我做过一个番茄钟"，动的演示能证明"它是活的"。

   纯 CSS 驱动（零 JS 计时）：节奏由 --cycle / --focus / --break 控制（秒）。
   ========================================================================== */
.demo {
  position: relative;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: 0 20px 44px -22px rgba(24, 24, 27, 0.22);
  overflow: hidden;
}

.demo__chrome {
  position: relative; /* 暂停按钮的定位基准 */
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--paper);
}
.demo__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ink-4);
}
.demo__label {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  color: var(--fg-faint);
}

.demo__body {
  display: flex;
  align-items: center;
  gap: clamp(18px, 3vw, 28px);
  padding: clamp(22px, 3vw, 30px);
}

/* ── 进度环 ── */
.demo__dial {
  position: relative;
  width: clamp(108px, 13vw, 132px);
  aspect-ratio: 1;
  flex: none;
}
.demo__ring {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg); /* 让进度从 12 点方向起跑 */
}
.demo__track {
  fill: none;
  stroke: var(--line);
  stroke-width: 6;
}
.demo__arc {
  fill: none;
  stroke: var(--ink);
  stroke-width: 6;
  stroke-linecap: round;
  /* 周长 = 2πr = 2π×52 ≈ 326.7 */
  stroke-dasharray: 326.7;
  stroke-dashoffset: 0;
  animation: demo-arc var(--cycle, 12s) linear infinite;
}
@keyframes demo-arc {
  0%     { stroke-dashoffset: 0; }
  66.7%  { stroke-dashoffset: 326.7; } /* 专注段：满环走空（8s / 12s = 66.7%） */
  100%   { stroke-dashoffset: 0; }    /* 休息段：回填（空→满），与下一轮 0% 无缝衔接 */
}

/* ── 数字倒数 ── */
.demo__num {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  font-family: var(--font-mono);
  font-size: clamp(17px, 2.1vw, 21px);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}
/* 专注段四个数字轮流显示，合起来读作"从 25:00 倒数到 00:03"；
   休息段（66.7% 起）由 .demo__num-rest 常显"05:00" —— 表盘任何时刻都有字。
   ⚠️ 兜底原则：**第一帧必须是可见的基线**，只有后来的几个才靠动画藏起来。
   早先写成"全部 opacity:0，靠动画点亮"，实测动画一旦失效（不支持的浏览器、
   animation:none、样式表被阻断）整个环里是**空的** —— 实测 0/4 可见。
   现在反过来：默认只有第一个可见，动画负责隐藏它、并轮流点亮后面的。
   这样动画失效时回落成"静止但完整可读"，而不是一块空白。
   ⚠️ 相位算术（踩过）：专注段 8s 要放 4 个数字 = 每格 16.67%，
   可见窗口必须 < 16.67%，否则会盖到下一格。早先写成 16.9%，
   每格都溢出 0.23%，末尾那个还会压到休息数字上（实测重叠两帧）。 */
.demo__num > span {
  grid-area: 1 / 1;
  opacity: 0;
  animation: demo-num var(--cycle, 12s) steps(1, end) infinite;
}
/* 基线：首个数字常亮（无动画时它就是在显示的那个） */
.demo__num > span:nth-child(1) {
  opacity: 1;
  animation-delay: 0s;
}
/* 四个槽位等分专注段：0 / 16.67% / 33.33% / 50%（= 0s / 2s / 4s / 6s） */
.demo__num > span:nth-child(2) { animation-delay: calc(var(--cycle, 12s) * 0.1667); }
.demo__num > span:nth-child(3) { animation-delay: calc(var(--cycle, 12s) * 0.3333); }
.demo__num > span:nth-child(4) { animation-delay: calc(var(--cycle, 12s) * 0.5); }
@keyframes demo-num {
  0%    { opacity: 1; }
  16.6% { opacity: 1; }
  16.7% { opacity: 0; }
  100%  { opacity: 0; }
}
/* 休息段：表盘常显休息时长。它没有 delay（从 0s 起跑），
   靠关键帧前 66.7% 保持隐藏；动画失效时随其余数字一起隐掉，
   表盘回落成"25:00 待开始"，不空白。 */
.demo__num > span.demo__num-rest {
  animation-name: demo-num-rest;
}
@keyframes demo-num-rest {
  0%    { opacity: 0; }
  66.6% { opacity: 0; }
  66.7% { opacity: 1; }
  100%  { opacity: 1; }
}

/* ── 右侧状态 ── */
.demo__meta {
  min-width: 0;
}
.demo__state {
  position: relative;
  height: 1.6em;
  font-family: var(--font-serif);
  font-size: clamp(15px, 1.7vw, 18px);
  font-weight: 600;
}
.demo__state > span {
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: demo-state var(--cycle, 12s) steps(1, end) infinite;
}
/* 基线：首个状态常亮（动画失效时显示"专注中"）。
   选择器必须写到与 `.demo__state > span` 同等特异性（0,2,0 vs 0,1,1 不够），
   否则那条 opacity:0 会赢 —— 实测踩过：写成 `.demo__state-focus` 时
   动画一失效两个状态都不见，标签位置是空的。 */
.demo__state > span.demo__state-focus {
  opacity: 1;
  animation-delay: 0s;
}
/* 休息状态：与 .demo__num-rest 同一套解法 —— delay 0 + 关键帧控相位。
   ⚠️ 早先用 animation-delay: var(--focus) 偏移相位，是错的：
   delay 只偏移**第一轮**，12s 后 focus 进入第二轮又可见，而 break 还停在
   第一轮的可见段 → 实测 12–15.9s 两个标签同时 opacity:1，绝对定位叠成
   「专注中」+「休息」糊字，每周期约 3.8s（占 31%）。
   现在两条关键帧互补：focus 可见 0–66.6%，break 可见 66.7–100%。 */
.demo__state > span.demo__state-break {
  animation-name: demo-state-break;
  animation-delay: 0s;
}
@keyframes demo-state-break {
  0%    { opacity: 0; }
  66.6% { opacity: 0; }
  66.7% { opacity: 1; }
  100%  { opacity: 1; }
}
@keyframes demo-state {
  0%    { opacity: 1; }
  66.6% { opacity: 1; }
  66.7% { opacity: 0; }
  100%  { opacity: 0; }
}

.demo__hint {
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--fg-faint);
}

/* 四个周期小格：跑完一个亮一个，暗示"这是第几个番茄" */
.demo__bars {
  margin-top: 14px;
  display: flex;
  gap: 6px;
}
.demo__bars > span {
  width: 22px;
  height: 3px;
  background: var(--line);
  border-radius: 2px;
  animation: demo-bar var(--cycle, 12s) steps(1, end) infinite;
}
/* 基线：第一格常亮（动画失效时也有"跑起来过"的暗示） */
.demo__bars > span:nth-child(1) {
  background: var(--ink);
  animation-delay: 0s;
}
.demo__bars > span:nth-child(2) { animation-delay: calc(var(--cycle, 12s) * 0.17); }
.demo__bars > span:nth-child(3) { animation-delay: calc(var(--cycle, 12s) * 0.34); }
.demo__bars > span:nth-child(4) { animation-delay: calc(var(--cycle, 12s) * 0.51); }
@keyframes demo-bar {
  0%    { background: var(--ink); }
  16.9% { background: var(--ink); }
  17%   { background: var(--line); }
  100%  { background: var(--line); }
}

/* ── 暂停 / 继续（橱窗样品也可以戳）──
   按钮由 main.js 注入：无 JS 时不存在 —— 那时演示本就不可暂停，诚实。
   绝对定位在 chrome 条右端，不改变卡片的既有节奏；
   视觉 28px，::before 把命中热区撑到 44px（与 header__toggle 同一标准）。
   注入时 main.js 会摘掉 .demo 的 role="img"、给视觉内脏补 aria-hidden，
   并给 chrome 标 has-pause 让 "LIVE DEMO" 挪出按钮的位置。 */
.demo__pause {
  position: absolute;
  top: 50%;
  right: 6px;
  translate: 0 -50%;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--paper);
  color: var(--fg-faint);
  transition: color 0.24s var(--mo-ease-soft), border-color 0.24s var(--mo-ease-soft);
}
.demo__pause::before {
  content: "";
  position: absolute;
  inset: -8px;
}
.demo__chrome.has-pause .demo__label {
  margin-right: 28px;
}
@media (hover: hover) {
  .demo__pause:hover {
    color: var(--fg);
    border-color: var(--ink);
  }
}
.demo__icon-play {
  display: none;
}
.demo__pause.is-paused .demo__icon-pause {
  display: none;
}
.demo__pause.is-paused .demo__icon-play {
  display: block;
}
/* 暂停只停表针：play-state 对这些元素之外的东西没有副作用，
   也不碰「第一帧即基线」的兜底（基线元素此刻本来就显示着） */
.js .demo.is-paused :is(.demo__arc, .demo__num > span, .demo__state > span, .demo__bars > span) {
  animation-play-state: paused;
}

/* chrome 三个点轻轻呼吸 —— 橱窗的「通电」感。no-preference 才生效 */
@media (prefers-reduced-motion: no-preference) {
  .demo__dot {
    animation: dot-breathe 3.2s ease-in-out infinite;
  }
  .demo__dot:nth-child(2) { animation-delay: 0.5s; }
  .demo__dot:nth-child(3) { animation-delay: 1s; }
}
@keyframes dot-breathe {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* ==========================================================================
   07 小玩意（主角）：入口墙
   --------------------------------------------------------------------------
   卡片不是"作品展示"，是"入口"。所以：
     - 可点的卡片有明确的动作词（进去玩 →），不是被动的标题
     - 没做的卡片与做好的**版面完全一致**，只换标记，不会看起来"缺一块"
   ========================================================================== */
.tools__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}
.tools__headText {
  min-width: 0;
}
.tools__sub {
  margin-top: 18px;
  max-width: 46ch;
}

/* 图例：能用 / 在做了 */
.legend {
  display: flex;
  gap: 18px;
  font-size: var(--fs-small);
  color: var(--fg-muted);
}
.legend__item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.legend__mark {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink);
}
.legend__mark--wip {
  background: transparent;
  border: 1px solid var(--ink-4);
}

.tools__grid {
  margin-top: 52px;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(16px, 2vw, 24px);
}

.tools__cell {
  display: flex;
}

/* 奇数张卡片时，最后一行只剩一张，右侧留一整格空洞（3 张就会发生）。
   让落单的那张居中、宽度仍是半栏 —— 读起来是"收尾"，不是"没加载完"。
   只在两列布局生效；≤900px 已是单列，不需要也不该居中。 */
@media (min-width: 901px) {
  .tools__cell:last-child:nth-child(odd) {
    grid-column: 1 / -1;
    justify-self: center;
    width: calc(50% - clamp(16px, 2vw, 24px) / 2);
  }
}

/* ── 工具卡 ──
   倾斜 / 抬升 / 按压全走变量（--tx / --ty / --lift）：CSS 默认全 0，
   指针效果由 main.js 模块 9 在精确指针下写入 —— 没有 JS 时就是一张安静的卡。 */
.tool {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 100%;
  padding: clamp(22px, 2.6vw, 30px);
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--white);
  transform: perspective(900px) rotateX(var(--tx, 0deg)) rotateY(var(--ty, 0deg))
    translateY(var(--lift, 0px));
  transition: border-color 0.28s var(--mo-ease-soft), transform 0.28s var(--mo-ease-soft),
    box-shadow 0.28s var(--mo-ease-soft), scale 0.26s var(--mo-ease-soft);
}
/* 跟手光斑：一层极淡的灰晕（--spot 随主题/深浅卡换值）。
   挂 .js 作用域 —— 无 JS 没有指针效果，也不留一条悬空的 opacity:0 */
.js a.tool::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    240px circle at var(--mx, 50%) var(--my, 50%),
    var(--spot),
    transparent 65%
  );
  transition: opacity 0.3s var(--mo-ease-soft);
}

/* 可点的卡片：悬停时抬一下、边框加深、箭头右移。
   反馈只落在合成器属性上（transform / box-shadow / translate）——
   早先动的是 .tool__go 的 gap，那是布局属性，过渡时文字会跟着挪。 */
@media (hover: hover) {
  a.tool:hover {
    --lift: -2px;
    border-color: var(--ink);
    box-shadow: 0 16px 32px -20px rgba(24, 24, 27, 0.28);
  }
  a.tool:hover::after {
    opacity: 1;
  }
  a.tool:hover .tool__go svg {
    translate: 4px 0;
  }
}
/* 按压：同 btn —— 落回基线 + 阴影收紧，80ms 内回应；触屏靠它得到反馈。
   scale 走独立属性，不再动 transform（那里归倾斜/抬升管） */
a.tool:active {
  --lift: 0px;
  scale: 0.99;
  box-shadow: 0 8px 18px -14px rgba(24, 24, 27, 0.3);
  transition-duration: var(--mo-step);
}

/* 主推卡：深色。全站黑色有配额，这是唯一大面积用黑的地方 */
.tool--ink {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--inv-fg);
  --spot: rgba(255, 255, 255, 0.1); /* 深色卡上换亮色光斑 */
}
.tool--ink .tool__desc {
  color: var(--inv-muted);
}
/* ⚠️ 深色卡上的标签必须换成反白底专用色。
   基类的 --fg-faint（#52525B）落在 #18181B 上只有 2.29:1，
   远低于 AA 4.5:1 —— 实测被 audit 抓出来。
   深色底上的小字一律用 --inv-muted（#D4D4D8，约 10:1）。 */
.tool--ink .tool__tags {
  color: var(--inv-muted);
}
.tool--ink .tool__badge--live {
  background: var(--white);
  color: var(--ink);
}
.tool--ink .tool__tag::before {
  color: var(--inv-faint);
}
.tool--ink .tool__go {
  color: var(--inv-fg);
}

/* 未完成的卡片：不虚化、不降透明度（那会读成"坏了"），
   只把标题变灰、边框改虚线 */
.tool--wip {
  background: transparent;
  border-style: dashed;
}
.tool--wip .tool__name {
  color: var(--fg-muted);
}

.tool__head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.tool__name {
  font-family: var(--font-serif);
  font-size: clamp(19px, 2.2vw, 23px);
  font-weight: 600;
}

.tool__badge {
  flex: none;
  padding: 3px 9px;
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
}
.tool__badge--live {
  background: var(--ink);
  color: var(--white);
}
.tool__badge--wip {
  border: 1px solid var(--line);
  color: var(--fg-faint);
}

.tool__desc {
  font-size: var(--fs-small);
  color: var(--fg-muted);
  line-height: 1.85;
}

.tool__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 0;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  color: var(--fg-faint);
  text-transform: uppercase;
}
/* 标签是 <span>（不能用 <li>：卡片可能是 <a>，<a> 里放 <li> 会被浏览器重排 DOM）。
   content 的第二个值 `/ ""` 是给读屏的替代文本：分隔符是装饰，不该念出来。 */
.tool__tag + .tool__tag::before {
  content: "/" / "";
  margin-inline: 7px;
  color: var(--ink-4);
}

/* 动作词：卡片底部，是"入口"感的来源 */
.tool__go {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: auto;
  padding-top: 4px;
  font-size: var(--fs-small);
  font-weight: 600;
}
/* 悬停时动的只有箭头自身（translate，合成器属性）——间距固定，文字纹丝不动 */
.tool__go svg {
  transition: translate 0.26s var(--mo-ease-soft);
}
.tool__go--off {
  color: var(--fg-faint);
  font-weight: 400;
}

.tools__count {
  margin-top: 28px;
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
  color: var(--fg-faint);
}

/* ==========================================================================
   08 关于（前台只需"保留一点介绍"）
   ========================================================================== */
.about__head {
  margin-top: 8px;
}
.about__body {
  margin-top: 44px;
  display: grid;
  /* 左栏收窄到接近正文自身的行宽（.about__p 是 52ch ≈ 468px）：
     原来 7fr 给了 677px —— 文字只占 468，中间空 200 多，再叠上 72 的栏距，
     右栏被推到 x≈890，两栏看上去像两个不相干的区块。 */
  grid-template-columns: minmax(0, 480px) minmax(0, 1fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
}

.about__lead {
  font-family: var(--font-serif);
  /* 必须低于 h2（36），否则 26–30 这一段会和段落标题糊在一起 */
  font-size: clamp(19px, 2.1vw, 26px);
  font-weight: 600;
  line-height: 1.45;
  letter-spacing: -0.01em;
}
.about__p {
  margin-top: 20px;
  max-width: 52ch;
  font-size: var(--fs-body);
  color: var(--fg-muted);
  line-height: 1.95;
}

.about__keywords {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 0;
  font-size: var(--fs-small);
  color: var(--fg-muted);
}
/* `/ ""`：分隔符是装饰，替换文本置空，读屏不再把"·"念进关键词里 */
.about__keywords li + li::before {
  content: "·" / "";
  margin-inline: 12px;
  color: var(--ink-4);
}

/* 互联网入口（GitHub 等）：比关键词重一档 —— 它是「去哪儿找我」的答案，
   不是可有可无的标签。下划线只画在 handle 上，像一条可以点的地址。 */
.about__links {
  margin-top: 24px;
  display: grid;
  gap: 12px;
  justify-items: start;
}
.about__link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  /* 热区 44：文字只有 27 高，竖向内边距补足可点区域（触屏容错） */
  min-height: 44px;
  font-size: var(--fs-small);
  color: var(--fg-muted);
}
.about__linkLabel {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.about__linkHandle {
  color: var(--fg);
  padding-bottom: 1px;
  border-bottom: 1px solid var(--line);
  transition: border-color 0.26s var(--mo-ease-soft);
}
.about__link svg {
  color: var(--fg-faint);
  transition: color 0.26s var(--mo-ease-soft);
}
@media (hover: hover) {
  .about__link:hover .about__linkHandle {
    border-color: var(--ink);
  }
  .about__link:hover svg {
    color: var(--fg);
  }
}

.about__resume {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 26px;
  padding-bottom: 3px;
  font-size: var(--fs-small);
  color: var(--fg);
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size 0.36s var(--mo-ease-soft);
}
@media (hover: hover) {
  .about__resume:hover {
    background-size: 100% 1px;
  }
}

/* ==========================================================================
   09 联系（深色板块 —— 全站唯一的深色，黑色有配额）
   ========================================================================== */
.contact {
  background: var(--inv-bg);
  color: var(--inv-fg);
}
.contact .eyebrow {
  color: var(--inv-muted);
}
.contact__note {
  margin-top: 20px;
  /* 全站其他段落都在 21–30 字/行，这里原来是 67 字/行（1136px 通栏）——
     最正式的深色版块反而行长最松。收到 40em 后这句正好一行；
     再窄（34em）会在第二行剩「迎。」这种孤字，比长行更难看。
     text-wrap: pretty 兜住窄屏换行，不支持的环境忽略即可。 */
  max-width: 40em;
  text-wrap: pretty;
  color: var(--inv-muted);
  font-size: var(--fs-lead);
}

.contact__email {
  display: inline-block;
  margin-top: 44px;
  font-family: var(--font-serif);
  /* 上限必须低于 h1（62）—— 邮箱是"动作"，不该比页面的标题还响。 */
  font-size: clamp(28px, 4.4vw, 56px);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
  word-break: break-all;
  position: relative;
  padding-bottom: 8px;
}
/* 下划线由伪元素向上长粗 —— 改 border-width 会把下面的链接整体顶下去 */
.contact__email::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--inv-fg);
  transition: height 0.3s var(--mo-ease-soft);
}
@media (hover: hover) {
  .contact__email:hover::after {
    height: 3px;
  }
}

/* 复制邮箱的图标按钮：由 main.js 注入（无 JS 不存在，不留死按钮）。
   按钮无文字 —— 读屏走 aria-label，也保住了 audit 的「无 JS 文本量一致」。
   反白线框 + 悬停提亮，与社交链接同一套 hover 语言。 */
.contact__copy {
  display: flex;
  align-items: center;
  justify-content: center;
  width: fit-content;
  margin-top: 20px;
  padding: 8px;
  border: 1px solid var(--inv-line);
  border-radius: var(--r);
  color: var(--inv-muted);
  transition: color 0.24s var(--mo-ease-soft), border-color 0.24s var(--mo-ease-soft);
}
@media (hover: hover) {
  .contact__copy:hover {
    color: var(--inv-fg);
    border-color: var(--inv-fg);
  }
}

/* 打勾确认（toast 之外的第二个通道）：复制成功后图标换 1.6s 再换回来。
   切换走 display —— 不产生文本、不碰 opacity，audit 的文本量对账不受影响。 */
.contact__copyCheck {
  display: none;
}
.contact__copy.is-copied .contact__copyIcon {
  display: none;
}
.contact__copy.is-copied .contact__copyCheck {
  display: block;
  animation: copy-pop 0.24s var(--mo-ease-pop);
}
@keyframes copy-pop {
  from { scale: 0.5; }
  to   { scale: 1; }
}

/* ── 留言板（EchoThread 托管评论 —— 本站唯一的第三方嵌入）────────────────
   主题适配只走它公开的接口：data-* 属性 + --et-* 变量。
   绝不碰内部类名（.et-widget 之类）—— 那是它的私有实现，人家一升级就碎。

   这块落在深色联系区里：data-theme="dark" 打底，
   下面再把背景/描边/文字压回本站令牌，保证和周围浑然一体。 */
.guestbook {
  margin-top: clamp(64px, 10vh, 104px);
}

/* 区块起始线：深色区里必须换 --inv-line，直接用 --line 会白得刺眼 */
.guestbook .section-rule {
  background: var(--inv-line);
}

.guestbook__note {
  margin-top: 16px;
  max-width: 52ch;
  font-size: var(--fs-small);
  line-height: 1.9;
  color: var(--inv-muted);
}

/* 挂载点：脚本没到 / 被拦时版面不塌，也不会先闪一大块空白 */
.guestbook__mount {
  margin-top: 34px;
  min-height: 96px;
}

/* 没配 key 时的静态说明：虚线与首页「在做了」的卡片同一套语言 */
.guestbook__empty {
  margin-top: 30px;
  padding: 20px 22px;
  border: 1px dashed var(--inv-line);
  border-radius: var(--r);
  font-size: var(--fs-small);
  color: var(--inv-muted);
}

/* EchoThread 只管颜色与圆角，字体继承本站 */
#echothread {
  --et-color-bg: var(--inv-bg);
  --et-color-surface: #232327;
  --et-color-border: var(--inv-line);
  --et-color-text: var(--inv-fg);
  --et-color-muted: var(--inv-muted);
  --et-color-accent: var(--inv-fg);
  --et-color-accent-hover: var(--inv-muted);
  --et-radius: var(--r);
  --et-font: inherit;
  --et-font-size: var(--fs-small);
}

/* ==========================================================================
   10 页脚
   ========================================================================== */
.footer {
  background: var(--bg-alt);
  padding-block: 40px;
  border-top: 1px solid var(--line);
}
.footer__inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 12px;
  font-size: var(--fs-small);
  color: var(--fg-muted);
}

/* 本地时间（main.js 模块 10）：走 data-* + 伪元素渲染 ——
   文本不进 DOM，audit 的「无 JS 文本量一致」对账天然无感 */
.footer__time {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
  color: var(--fg-faint);
}
.footer__time::after {
  content: attr(data-tz) "\00a0\00a0" attr(data-time);
}

/* 返回顶部（main.js 注入，无 JS 不存在）：滚过 600px 浮现 */
.js .to-top {
  position: fixed;
  right: clamp(16px, 3vw, 28px);
  bottom: clamp(16px, 3vh, 28px);
  z-index: 70;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: var(--r);
  background: var(--ink);
  color: var(--white);
  box-shadow: 0 16px 32px -20px rgba(24, 24, 27, 0.4);
  opacity: 0;
  translate: 0 12px;
  pointer-events: none;
  /* opacity:0 挡不住键盘 Tab —— 隐形时连焦点能力一起收走 */
  visibility: hidden;
  transition: opacity 0.3s var(--mo-ease-soft), translate 0.3s var(--mo-ease-soft),
    visibility 0.3s;
}
.js .to-top.is-show {
  opacity: 1;
  translate: 0 0;
  pointer-events: auto;
  visibility: visible;
}
@media (hover: hover) {
  .js .to-top:hover {
    translate: 0 -2px;
  }
}

/* 换主题的 380ms 里给全站一个颜色过渡，平时绝不挂着（会拖累入场动效） */
.js.theme-fade,
.js.theme-fade *,
.js.theme-fade *::before,
.js.theme-fade *::after {
  transition: background-color 0.32s var(--mo-ease-soft), color 0.32s var(--mo-ease-soft),
    border-color 0.32s var(--mo-ease-soft) !important;
}

/* ==========================================================================
   11 Toast（全站反馈通道）
   --------------------------------------------------------------------------
   白底 + 1px 墨线：浅色区靠边框立住，深色联系区靠底色反差立住。
   无 JS：display:none，连空壳都不留（容器永远是空的，文本量不受影响）。
   ========================================================================== */
.toast {
  display: none;
}
.js .toast {
  position: fixed;
  left: 50%;
  bottom: clamp(20px, 4vh, 40px);
  z-index: 80;
  display: block;
  padding: 10px 18px 15px;
  background: var(--white);
  border: 1px solid var(--ink);
  border-radius: var(--r);
  box-shadow: 0 16px 32px -20px rgba(24, 24, 27, 0.28);
  font-size: var(--fs-small);
  color: var(--fg);
  opacity: 0;
  translate: -50% 12px;
  pointer-events: none;
  transition: opacity 0.28s var(--mo-ease-soft), translate 0.28s var(--mo-ease-soft);
}
/* 自动消失进度条：与 JS 的 2600ms 计时同长，由 WAAPI 每次弹出时重放 */
.js .toast__bar {
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 5px;
  height: 2px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
}
.js .toast.is-show {
  opacity: 1;
  translate: -50% 0;
  /* 过冲只该发生在进场的那个状态：pop 缓动挂在这里，
     退场回落到基线的减速曲线，收得干脆 */
  transition: opacity 0.28s var(--mo-ease-soft), translate 0.32s var(--mo-ease-pop);
}

/* ==========================================================================
   12 动效（四种动作，全部挂在 .js 下 —— 没有 JS 时页面完全静止但内容一字不少）
   ========================================================================== */
/* 初始态只在 .js 下生效；无 JS 时元素本来就是终态 */
.js .reveal {
  opacity: 0;
  translate: 0 16px;
  transition: opacity var(--mo-base) var(--mo-ease),
    translate var(--mo-base) var(--mo-ease);
  transition-delay: calc(var(--i, 0) * var(--mo-step));
}
/* 触发单位是「区块」：.is-in 由 JS 加在 .hero / .section 上，组内元素按各自的
   --i × 80ms 错开，排成固定时间线（线 → 标题 → 正文 → 卡片波浪）。
   逐元素触发的话，快速滚动会看到"半成型"的波浪。 */
.js .is-in .reveal {
  opacity: 1;
  translate: 0 0;
}
/* 逐元素置终态（2.6s 安全网 / audit 探针强制）时不再按时间线错开 ——
   直接显形。这条必须写在组选择器之后：两者特异性相同，后者按出现顺序赢。 */
.js .reveal.is-in {
  opacity: 1;
  translate: 0 0;
  transition-delay: 0s;
}


/* 擦除：大标题从上刷下。
   用 clip-path 而不是包一层遮罩 —— 遮罩必须 overflow: hidden，
   会把 g / y / @ 这类下伸的字形切掉。 */
.js .reveal--clip {
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--mo-clip) var(--mo-ease);
  transition-delay: calc(var(--i, 0) * var(--mo-step));
}
.js .is-in .reveal--clip {
  clip-path: inset(0 0 0 0);
}
.js .reveal--clip.is-in {
  clip-path: inset(0 0 0 0);
  transition-delay: 0s;
}

/* 逐行升起（hero 标题专用）：外层 .hl 是裁切遮罩，内层 .hl__in 从下方浮上。
   与 reveal--clip 的整块擦除同一族，行与行再按 --i × 80ms 错开 ——
   两行标题依次落座。只动 translate，不碰 opacity（verify [3] 无感）。 */
.hl {
  display: block;
  overflow: hidden;
  /* 行高 1.22 的裁切框可能切到字形下缘：留一点缓冲再用负边距抵平 */
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
}
.js .hl__in {
  display: block;
  translate: 0 112%;
  transition: translate var(--mo-clip) var(--mo-ease);
  transition-delay: calc(var(--i, 0) * var(--mo-step));
}
.js .is-in .hl__in {
  translate: 0 0;
}

/* 画线：线是"长出来"的，所以不做淡入 */
.js .rule-draw {
  transform: scaleX(0);
  transition: transform var(--mo-slow) var(--mo-ease);
}
.js .is-in .rule-draw {
  transform: scaleX(1);
}
.js .rule-draw.is-in {
  transform: scaleX(1);
  transition-delay: 0s;
}

/* —— 跨文档衔接：首页 ↔ 小玩意子站整页淡换 ——
   Chrome 126+ / Safari 18.2+ 支持；跨文档过渡要求两侧文档都声明，
   三个子站仓库（pomodoro / bomb-cats / racing-game）的样式里已各加同一条。
   不支持的环境自动回落成普通跳转：零 JS、零兜底成本。
   ⚠️ 子站的 deploy 分支要同步带上这条，线上才接得住。 */
@view-transition {
  navigation: auto;
}

/* —— 三条安全边界 —— */
/* 边界 2：减少动效时把初始态直接改成终态。
   只压时长的话，元素会永远停在 opacity: 0 上不出现。 */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  .js .reveal,
  .js .reveal--clip,
  .js .hl__in,
  .js .rule-draw {
    opacity: 1;
    translate: none;
    scale: 1;
    clip-path: none;
    transform: none;
    transition: none;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* 跨文档淡换的伪元素不在 * 树上，单独关 */
  ::view-transition-group(*),
  ::view-transition-image-pair(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* ==========================================================================
   13 响应式
   ========================================================================== */
@media (max-width: 1200px) {
  .hero__inner,
  .about__body {
    grid-template-columns: 1fr;
  }
  .hero__inner {
    gap: 40px;
  }
  /* 单栏后演示不要撑满，避免过大 */
  .hero__demo {
    max-width: 520px;
  }
}

@media (max-width: 900px) {
  :root {
    --header-h: 60px;
    --section-y: clamp(64px, 9vh, 104px);
  }

  /* 导航转抽屉 */
  .header__nav {
    position: absolute;
    /* 手指在面板上滑动时，不让它把背后的页面一起带走 ——
       这是"锁整页滚动"的精确替代：只挡滚动穿透，不拿走页面能力，
       键盘用户仍能滚、焦点也不会被困在锁死的页面上。 */
    overscroll-behavior: contain;
    inset: var(--header-h) 0 auto 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: 8px var(--gutter) 20px;
    background: var(--white);
    border-bottom: 1px solid var(--line);
    display: none;
  }
  .header__nav.is-open {
    display: flex;
  }
  .header__nav .header__link,
  .header__nav .pill {
    padding-block: 12px;
  }
  .header__nav .pill {
    margin-top: 8px;
  }
  .header__toggle {
    display: grid;
  }

  /* 工具墙：两列 → 单列 */
  .tools__grid {
    grid-template-columns: 1fr;
  }
  .tools__head {
    align-items: flex-start;
    flex-direction: column;
  }
}

@media (max-width: 600px) {
  :root {
    --gutter: 20px;
    --fs-body: 15px;
  }
  .hero {
    min-height: auto;
  }
  .hero__cta {
    flex-direction: column;
    align-items: stretch;
  }
  .btn {
    justify-content: center;
  }
  /* 演示：环与文字竖排，避免横向挤压 */
  .demo__body {
    flex-direction: column;
    align-items: flex-start;
    gap: 20px;
  }
  .legend {
    gap: 14px;
  }
}

/* 减少动效时：演示停在第一帧。
   注意是"停住"而不是"删掉" —— 删掉的话那块版面会塌。 */
@media (prefers-reduced-motion: reduce) {
  .demo__arc,
  .demo__num > span,
  .demo__state > span,
  .demo__bars > span {
    animation: none !important;
    opacity: 1;
  }
  .demo__num > span:not(:first-child),
  .demo__state-break {
    opacity: 0;
  }
  .demo__arc {
    stroke-dashoffset: 0;
  }
  .demo__bars > span {
    background: var(--line);
  }
  .demo__bars > span:first-child {
    background: var(--ink);
  }
}

/* ==========================================================================
   14 打印（边界 3：全局复位必须含 opacity / translate / clip-path）
   ========================================================================== */
@media print {
  *,
  *::before,
  *::after {
    background: transparent !important;
    color: #000 !important;
    box-shadow: none !important;
    text-shadow: none !important;
    /* 复位动效的三类属性，否则"没滚到过"的内容会印成空白 */
    opacity: 1 !important;
    translate: none !important;
    scale: 1 !important;
    clip-path: none !important;
    transform: none !important;
    transition: none !important;
    animation: none !important;
  }
  .header,
  .header__progress,
  .skip,
  .demo,
  .guestbook,
  .toast,
  .theme-toggle,
  .to-top,
  .hero__cue {
    display: none !important;
  }
  .section {
    padding-block: 24px !important;
    break-inside: avoid;
  }
  .contact {
    border-top: 1px solid #000;
  }

}
