/* layout: nav, hero, section chrome, add button */
    /* ── 环境光斑 ── */
    /* 🔴 三层合一（2026-09-24 二轮，iOS「每个页面放大都崩」）
       ────────────────────────────────────────────────────────────────
       原先这里是 `.ambient-bg` + `::before` + `::after` **三个**各自独立光栅化的层：
       元素自身 390×844（11.3 MB）、::before 因为 `inset: -12% -8% 0` 反而做到
       452×945（14.68 MB）、::after 242×523（4.34 MB）—— 合计 19 MB @1×，
       pinch-zoom 放大 3× 就是 **171 MB**，而且是四条路由**完全相同**的一份开销。
       而它们其实是可以叠成一张背景图的：只要把伪元素的框用 vw/vh 精确表达出来，
       `radial-gradient(at 58% 12%)` 在两种写法里解析到的仍是同一个点。

       几何换算（视口 = 100vw × 100vh）：
         ::before  inset: -12% -8% 0  ⇒ 盒 = 116vw × 112vh，位于 (-8vw, -12vh)
         ::after   bottom:-30vh right:-14vw w:62vw h:62vh ⇒ 盒位于 (52vw, 68vh)
       绘制顺序（背景层里**越靠前越在上**）＝ 原堆叠顺序的反序：
         原：网格(元素底) → ::before(顶光) → ::after(底光，最上)
         新：底光 → 顶光 → 四个网格
       观感差异：两个无限动画（glow-breathe 24s / float-glow 26s）随之取消 ——
       它们的 `0%, 100%` 关键帧就是 opacity:1 / translate(0,0) scale(1)，
       也就是现在的静止态，所以页面固定显示动画的**起始帧**；
       中途幅度不过 ±18% 不透明度、±2vw 位移，且底部光斑源码注释本就写着
       「几乎不可见」。 */
    .ambient-bg {
      position: fixed; inset: 0; z-index: -1; pointer-events: none;
      /* 手账网格：32px 细格（蓝相浅灰）+ 每 160px 一根主格，
         固定视口 → 像方格纸垫在内容之下，滚动时不产生摩尔纹抖动 */
      background-image:
        radial-gradient(ellipse at center, var(--glow-bottom) 0%, transparent 70%),
        radial-gradient(circle 820px at 58% 12%,
                        var(--glow-top) 0%, var(--glow-top-2) 40%, transparent 72%),
        linear-gradient(to right,  var(--grid-line)       0px, var(--grid-line)       1px, transparent 1px),
        linear-gradient(to bottom, var(--grid-line)       0px, var(--grid-line)       1px, transparent 1px),
        linear-gradient(to right,  var(--grid-line-major) 0px, var(--grid-line-major) 1px, transparent 1px),
        linear-gradient(to bottom, var(--grid-line-major) 0px, var(--grid-line-major) 1px, transparent 1px);
      background-size:
        62vw 62vh,
        116vw 112vh,
        32px 32px, 32px 32px, 160px 160px, 160px 160px;
      background-position:
        52vw 68vh,
        -8vw -12vh,
        0 0, 0 0, 0 0, 0 0;
      background-repeat:
        no-repeat, no-repeat,
        repeat, repeat, repeat, repeat;
    }
    /* （原 `.ambient-bg::before` 顶光 / `::after` 底光 / `@keyframes glow-breathe` /
       `@keyframes float-glow` 于 2026-09-24 二轮**合并进上面那个元素的 background**：
       三层 → 一层，四个路由共省 171 MB @放大3×。几何与绘制顺序换算见上方注释；
       该处的 `@media (prefers-reduced-motion)` 覆盖也随之失效，一并删除。） */

    /* 颗粒质感 */
    /* 🔴 噪声由「内联 SVG feTurbulence」改为**静态位图切片**（2026-09-24 二轮，iOS「放大就崩」）
       ────────────────────────────────────────────────────────────────
       feTurbulence(baseFrequency 0.9 / numOctaves 4) 是**运行时执行**的滤镜：
       原来它挂在 `position: fixed` 全屏层的背景上（background-size: 200px），
       于是 pinch-zoom **每换一档倍率**，WebKit 都要按新倍率把这段分形噪声重算一遍
       （DPR3 × 放大3× ⇒ 单块切片 600×600 设备像素 × 4 个八度）。
       这是四条路由**唯一**一处每页都在跑的重滤镜，高度可疑。
       烘成位图后只剩一次位图平铺，缩放时零滤镜开销。
       切片用 playwright 按 SVG 原样的 256 视框 1:1 烘出（omitBackground 保住
       feTurbulence 自带的 alpha 噪声），再量化到 64 色 —— 实测 alpha 均值
       127.53 → 127.05（差 0.48/255），在 2.8% 不透明度下等于 0.005% 的亮度差。
       噪声**特征尺寸与原来完全一致**（256 单位本来就映射到 200 CSS px）。 */
    .grain {
      position: fixed; inset: 0; z-index: 9999; pointer-events: none; opacity: 0.028;
      background-image: url('../grain-256.png');
      background-repeat: repeat; background-size: 200px 200px;
    }
    /* 🔴 暗色下不再用 mix-blend-mode（2026-09-24 二轮，iOS「放大就崩」）
       `.grain` 是 `position: fixed; inset: 0` 的全屏层（z-index 9999），
       挂上 `mix-blend-mode: screen` 会让整个视口进入一个混合组 ——
       iOS pinch-zoom 要按新倍率重算这一整组，是四条路由共有的固定开销
       （11.3 MB @1× ⇒ 101.7 MB @放大3×）。
       改成普通混合后要**把亮度补回来**：screen 是提亮，普通混合是往噪声中灰混。
       实测拟合（grain_opacity_fit.mjs，拿改动前实拍图当亮度基准）：
         opacity 0.030 → 全图均值差 −1.25/255
         opacity 0.045 → −0.47
         opacity 0.055 → +0.30     ⇒ 线性插值零点 ≈ 0.0505
       取 0.05（残差约 −0.09/255，即 0.03%，比原来的视觉差还小）。 */
    html[data-theme="dark"] .grain { opacity: 0.05; }

    /* （原「阅读进度条」.scroll-progress 的整套样式于 2026-09-21 收尾时删除：
       该元素在 ㉒ 之后就不再存在于任何页面，五处规则 —— 本体 + 两段 @supports +
       @keyframes scroll-grow + 减弱动效/打印里的两条 —— 全都是零匹配的死代码。
       如需恢复，从项目根 abobb-site-original/ 对应文件里取回。） */

    .container { max-width: 1160px; margin: 0 auto; padding: 0 2rem; }

    /* ── 导航 ── */
    .nav {
      position: fixed; top: 0; left: 0; right: 0; z-index: 100;
      padding: 1.05rem 0;
      transition: padding 0.45s var(--ease-out);
    }
    .nav-inner {
      display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
      max-width: 1160px; margin: 0 auto; padding: 0 2rem;
      border: 1px solid transparent; border-radius: 999px;
      transition: max-width 0.5s var(--ease-out), padding 0.5s var(--ease-out),
                  background 0.45s var(--ease-out), border-color 0.45s var(--ease-out),
                  box-shadow 0.45s var(--ease-out), backdrop-filter 0.45s var(--ease-out);
    }
    /* 滚动态：收成一枚悬浮胶囊 */
    .nav.scrolled { padding: 0.5rem 0; }
    .nav.scrolled .nav-inner {
      max-width: 920px;
      padding: 0.38rem 1.5rem;
      background: var(--nav-bg);
      border-color: var(--line-strong);
      backdrop-filter: blur(22px) saturate(1.7);
      -webkit-backdrop-filter: blur(22px) saturate(1.7);
      box-shadow: var(--shadow-card), inset 0 1px 0 color-mix(in srgb, #fff 58%, transparent);
    }
    html[data-theme="dark"] .nav.scrolled .nav-inner {
      box-shadow: var(--shadow-card), inset 0 1px 0 rgba(255,255,255,0.055);
    }
    .nav-logo {
      font-family: var(--font-display); font-size: 1.42rem; font-weight: 600;
      color: var(--ink); text-decoration: none; letter-spacing: -0.025em;
      display: inline-flex; align-items: center; gap: 0.55rem;
      transition: opacity 0.2s var(--ease-out);
    }
    .nav-logo::before {
      content: ''; width: 7px; height: 7px; border-radius: 50%;
      background: linear-gradient(135deg, var(--blue-600), var(--blue-400));
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue-500) 12%, transparent);
      transition: box-shadow 0.3s var(--ease-out), transform 0.3s var(--ease-spring);
    }
    .nav-logo:hover { opacity: 0.72; }
    .nav-logo:hover::before { transform: scale(1.18); box-shadow: 0 0 0 5px color-mix(in srgb, var(--blue-400) 20%, transparent); }

    .nav-actions { display: flex; align-items: center; gap: 1rem; }
    .nav-links { display: flex; gap: 0.25rem; list-style: none; }
    /* ⚠️ 导航中文标签（作品/旅途/启发）每个都只有两个字：缺 nowrap 时，一旦 nav-inner
       变窄、flex 把 <a> 压到不足两字宽，文字就会逐字换行成 2 行竖排，把导航条撑成
       两倍高（2026-09-23 实测：≤404px 宽的窗口必现）。这里在 base 就锁死不断行，
       窄屏只靠下面的断点缩字号与内边距来让位。 */
    .nav-links a {
      display: inline-flex; align-items: center; white-space: nowrap;
      padding: 0.44rem 0.92rem; border-radius: 999px;
      font-size: 0.89rem; font-weight: 470; color: var(--ink-muted); text-decoration: none;
      letter-spacing: 0.012em;
      transition: color 0.22s var(--ease-out), background-color 0.22s var(--ease-out), box-shadow 0.22s var(--ease-out);
    }
    .nav-links a:hover { color: var(--ink); background: var(--surface-3); }
    .nav-links a.active {
      color: var(--blue-700); font-weight: 560;
      background: var(--blue-50);
      box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blue-500) 15%, transparent);
    }
    html[data-theme="dark"] .nav-links a.active { color: var(--blue-700); background: var(--blue-50); }

    .nav-search,
    .theme-toggle,
    .lang-toggle {
      width: 38px; height: 38px; border-radius: 50%; border: 1px solid var(--line-strong);
      background: var(--control-bg); color: var(--ink-muted);
      display: inline-flex; align-items: center; justify-content: center;
      cursor: pointer;
      transition: color var(--t-fast), border-color var(--t-fast), background-color var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast);
    }
    .nav-search:hover,
    .theme-toggle:hover,
    .lang-toggle:hover {
      color: var(--blue-700);
      border-color: color-mix(in srgb, var(--blue-500) 36%, transparent);
      background: var(--control-hover);
      transform: translateY(-1.5px);
      box-shadow: var(--shadow-sm);
    }
    .nav-search:active,
    .theme-toggle:active,
    .lang-toggle:active { transform: translateY(0) scale(0.95); }
    .nav-search:focus-visible,
    .theme-toggle:focus-visible,
    .lang-toggle:focus-visible { box-shadow: var(--ring); border-color: transparent; }
    .nav-search svg { width: 19px; height: 19px; stroke-width: 1.85; }
    .theme-toggle svg { width: 17.5px; height: 17.5px; stroke-width: 1.85; }
    .nav-search svg,
    .theme-toggle svg,
    .lang-toggle .lang-text {
      transition: transform 0.5s var(--ease-spring);
      will-change: transform;
    }
    .theme-toggle:hover svg { transform: rotate(-18deg); }
    .nav-search:hover svg { transform: scale(1.14); }
    .lang-toggle:hover .lang-text { transform: translateY(-1.5px); }
    .lang-toggle {
      min-width: 38px; flex: 0 0 38px; padding: 0;
      font-family: var(--font-body); font-size: 0.84rem; font-weight: 650;
      letter-spacing: 0.02em;
    }
    .lang-toggle .lang-text {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      line-height: 1;
    }
    .lang-toggle .lang-text.is-zh {
      font-size: 0.94rem;
      font-weight: 600;
      letter-spacing: 0;
    }

    /* ── Hero ── */
    /* 2026-10-27：标题回到四行后整块高 421px，1440×900 时 CTA 底 790 还在首屏内，
       但 1280×800 / 1024×768 会被挤出 16~50px。改成按视口高度收紧上下留白
       （900 高时 ≈6.2rem/3.4rem，800 高时 ≈5.5rem/3rem），比固定 7rem/4.5rem 更稳。 */
    .hero { min-height: 90vh; display: flex; align-items: center; padding: clamp(5rem, 11vh, 7rem) 0 clamp(3rem, 6vh, 4.5rem); position: relative; }
    .hero .container { width: 100%; }
    .hero-shell {
      display: grid;
      grid-template-columns: minmax(0, 1.05fr) minmax(360px, 560px);
      align-items: start;
      justify-content: space-between;
      gap: clamp(2rem, 3.5vw, 4rem);
    }
    /* 2026-10-27：成为容器查询的参照，标题字号按它的宽度反推（见 .hero-title） */
    .hero-content { max-width: 660px; container-type: inline-size; }

    /* 眉标：胶囊徽章 + 状态圆点。
       2026-10-27：外面多包了一层 `.hero-eyebrow-wrap`（左侧 24px 工匠印章），
       行内边距与底部间距都交给 wrap 管，胶囊自身不再带 margin —— 否则印章
       和胶囊之间会被 margin 顶出高低差。 */
    .hero-eyebrow-wrap {
      display: inline-flex; align-items: center; gap: 0.6rem;
      margin-bottom: 1.7rem;
    }
    /* 工匠小印章：作者身份在这里低调背书（原大画幅头像是本页 C 位，已让给标本展台） */
    /* 2026-10-27：眉标旁那个 24px 圆形头像印章随双层展台一起下线，
       .author-stamp 的三条规则同时删除（HTML 里已无此节点）。 */
    .hero-eyebrow {
      display: inline-flex; align-items: center; gap: 0.55rem;
      padding: 0.42rem 0.92rem 0.42rem 0.78rem;
      border-radius: 999px;
      background: var(--surface);
      border: 1px solid var(--line-strong);
      box-shadow: var(--shadow-xs);
      font-size: 0.7rem; font-weight: 620;
      letter-spacing: 0.15em; text-transform: uppercase;
      color: var(--ink-muted);
    }
    .hero-eyebrow::before {
      content: ''; width: 6px; height: 6px; border-radius: 50%;
      background: var(--blue-500); flex: 0 0 auto;
      animation: pulse-dot 2.8s var(--ease-inout) infinite;
    }
    @keyframes pulse-dot {
      0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue-500) 22%, transparent); }
      50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--blue-500) 0%, transparent); }
    }

    .hero-title {
      font-family: var(--font-display);
      /* 2026-10-27 ⑦：四行断句（Engineering / with restraint / crafted for the /
         quiet everyday），字号重新按容器反推。最长行 "quiet everyday" = 6.33em，
         11.5cqw 让它占 660px 栏的 ~72%（76px 级）—— 第一版给到 14cqw/92px 显得太压，
         用户要求收小。上限 4.8rem 给单列（≤1024 容器 960）兜底，否则会算到 110px。 */
      font-size: clamp(2.2rem, 11.5cqw, 4.8rem);
      font-weight: 600; line-height: 1.08; letter-spacing: -0.032em;
      color: var(--ink); margin-bottom: 1.75rem;
      /* 原来是 700px：中文两行最宽只要 425px，收窄是为了让整块渐变贴住文字。
         现在揭示的 `clip-path` 挂在 h1 自己身上（见 motion.css ⑲），盒子一旦比文字窄，
         单列布局下第二行会被直接裁断（1024 视口实测 "crafted for the quiet ev…"）。
         所以放开到容器宽度，让盒子永远包住 nowrap 的文字。 */
      max-width: 100%;
      text-wrap: balance;
    }
    .hero-title > .hero-title-line { display: block; white-space: nowrap; }
    /* 首屏两行是写死的 span（无 .hero-title-line 类）：同样一行一句，绝不折行。
       窄屏在 responsive.css 里放回 normal —— 那时字号由 vw 给，折行才不会溢出。 */
    .hero-title > span { display: block; white-space: nowrap; }

    /* 末行渐变：深蓝 → 浅蓝 */
    /* 每一行的 span 都要渐变（2026-10-27 标题拆成两行后，只给 last-of-type
       会让第一行落回父级的 transparent → 直接看不见）。 */
    .hero-title > span,
    .hero-title > .hero-title-line:last-child {
      /* 渐变框收紧到文字宽度：渐变正好铺满文字，也不会因超出而被裁掉 */
      width: fit-content;
      background: linear-gradient(100deg, var(--blue-600) 0%, var(--blue-500) 46%, var(--blue-400) 100%);
      -webkit-background-clip: text; background-clip: text;
      -webkit-text-fill-color: transparent; color: transparent;
      padding-bottom: 0.06em;
    }
    /* 关键字：斜体衬线 + 手写下划线 */
    .hero-title > span .ink-hl,
    .hero-title > .hero-title-line > .ink-hl {
      font-style: italic; font-weight: 500;
      letter-spacing: -0.01em;
      -webkit-text-fill-color: var(--blue-600); color: var(--blue-600);
      padding-bottom: 0.1em;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8.6C23 3.2 45 2.6 61 5.1 77 7.6 99 6.9 118 3.1' fill='none' stroke='%233563d6' stroke-width='3.1' stroke-linecap='round'/%3E%3C/svg%3E");
      background-repeat: no-repeat;
      background-position: 0 100%;
      background-size: 100% 0.3em;
      /* ⑳ 落笔时机：等标题的揭示彻底结束再画波浪线，中间留一点空档。
         时间线（同一坐标系，从页面加载算起）：
           h1 整体淡入      0.12s → 0.67s（容器 .reveal，现在只管 opacity）
           四行逐层揭示      0.26s → 1.37s（每行 0.78s，层间错峰 0.11s）
           波浪线落笔        1.50s → 2.40s   ← 改到这里
         2026-10-27 ⑧ 之前是「整块揭示 0.30→1.22、落笔 1.40」；改回逐行后最晚一行
         1.37s 才落位，落笔必须跟着后移到 1.50s（留 0.13s 空档，与原来的 0.18s 同级），
         否则波浪线会在第 4 行还在揭示的时候就开画。
         取值经三轮反馈：原始 0.5s（与揭示重叠，太挤）→ 1.6s（嫌长）→ 1.5s（还嫌长）
         → 1.4s（整块版）。逐行版沿用 1.5s 是因为起点本身后移了，绝对节奏没变慢。
         中英文同刻落笔，切换语言时节奏一致。 */
      animation: ink-draw 0.9s 1.5s var(--ease-out) both;
    }
    @keyframes ink-draw { from { background-size: 0% 0.3em; } to { background-size: 100% 0.3em; } }
    html[data-theme="dark"] .hero-title > span .ink-hl,
    html[data-theme="dark"] .hero-title > .hero-title-line > .ink-hl {
      -webkit-text-fill-color: var(--blue-900); color: var(--blue-900);
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8.6C23 3.2 45 2.6 61 5.1 77 7.6 99 6.9 118 3.1' fill='none' stroke='%2383b0f4' stroke-width='3.1' stroke-linecap='round'/%3E%3C/svg%3E");
    }

    /* ㉒ 中文标题排版二次迭代（2026-09-18）——针对「中文像套模板、英文有设计感」的落差。
       全部先实测（playwright + CDP CSS.getPlatformFontsForNode）再动刀，三条硬结论：
         · 中文实际渲染的是 Noto Serif SC 的**细字面 + 浏览器合成加粗**，不是真字重
           （wght 400 与 500 墨量完全相同 46,210；600/700 只是叠加合成粗体），
           合成粗体把宋体的「横细竖粗」对比抹平 → 这就是大字号发虚的真凶；
         · 英文标题 4 行 h1 = 334px，中文 2 行只有 169px（又矮又宽）；
         · 「换字体族名」在这条链路上是无效动作（Noto Serif SC 本来就在声明里）。
       六处一起改（只作用在中文，英文标题一行不动）：
       (a) 真实 700 字面（HeroSerif 自托管子集，见上方 @font-face 说明）；
       (b) 行高 1.05 → 1.30：底纹网格从行间透出来，不再糊成一堵墙；
       (c) 字距 -0.032em → -0.02em；
       (d) 蓝渐变铺满三行：与英文同源同参数，结构与英文完全对齐；
       (e) 关键词「有温度」换手写毛笔楷 + 实色，承担英文里 warmth 斜体的角色；
       (f) 「AI」留在展示字体族内、字重与中文齐平，缩到 0.88em + 两侧微白。 */
    html:not([lang="en"]) .hero-title {
      font-family: 'Playfair Display', 'HeroSerif', 'Noto Serif SC', 'Songti SC', 'STSong', serif;
      font-weight: 700;
      line-height: 1.30;
      letter-spacing: -0.02em;
    }
    /* (d) 中文三行的基色 —— ㉟（2026-09-21 第四轮）从「实色」改回「蓝渐变」。
       演进路线：㉒(d) 蓝渐变（跨度 22，末端比重音还亮 → 重音糊）
                 → ㉛ 实色冷墨 → ㉞ 实色靛蓝（重音跳出来了，但整块是死的）
                 → ㉟ 蓝渐变窄跨度版（本条）：两端 ΔL* 9.8，整段压在重音之下。
       参数与取舍的完整数据见文件上方 :root 的 ㉟ 注释，这里只说三件事：
         · **逐行铺而非整块铺**：三行各是 block，各铺一条同参数渐变。100deg 几乎水平，
           三行同相位铺出来视觉上就是连续的一道，不会出现「每行重新起色」；
         · **宽度取 auto**（不是 fit-content）：三行渐变盒等宽 → 相位一致。
           若按各自文字宽度收紧，同一个颜色在三行会落在不同百分比上，反而更碎；
         · `-webkit-text-fill-color: transparent` 既是渐变生效的必要条件，
           也是「文字隐形」的开关 —— 改回实色时必须连它一起改。 */
    /* 2026-09-21 收尾：本块原有的 `background-image: var(--title-grad)` 与
       `-webkit-text-fill-color/color: transparent` 已被下方 ㊴/㊶ 那条同名选择器
       （特异性 (0,3,1) > (0,3,0)）用 `background-image: none` + 同款 transparent 全覆盖，
       故只删剩这两条。二者都不是废话：`width: auto` 用来压掉上方 `:last-child`
       的 `width: fit-content`（(0,3,0)）—— 三行渐变盒等宽是 ㉟ 的前提；
       `background-clip: text` 是「改回逐行铺渐变」时的前置声明，一行成本，故意保留。 */
    html:not([lang="en"]) .hero-title > .hero-title-line {
      width: auto;
      -webkit-background-clip: text; background-clip: text;
    }
    /* (e) 关键词「有温度」：手写毛笔楷 + 实色（从渐变里跳出来）+ 波浪线。
       结构与英文的 warmth（衬线斜体 + 实色 + 波浪线）同构 —— 中文没有斜体传统，
       由「毛笔手写体」承担英文斜体的角色。不用 font-style:italic：
       汉字没有斜体，浏览器只能做几何斜切，大字号下一眼廉价。
       2026-09-21 收尾：本处原有的一份 `html:not([lang="en"]) .hero-title .ink-hl`
       声明块与下方 ㉘ 的同选择器块**逐项重复且值还不同**（字距 0.02em vs 0）；
       ㉘ 文档顺序在后、实际生效 → 本块除 animation-delay 外全是死声明。已全部并入 ㉘。 */
    /* 暗色重音取「英文 warmth 同一色」—— 实测（playwright 读 computedStyle）两版对照：
              中文「有温度」曾是 rgb(131,176,244) = blue-500（与波浪线描边同色）
              英文「warmth」= rgb(230,238,253) = blue-900（近白）
       两者本应同构（都是「从蓝渐变里跳出来的实色关键词」），中文却一路是蓝的 ——
       关键词掉进标题渐变同一色域，焦点直接消失，用户反馈「深色下有温度应该是白色的」。
       ⚠️ 2026-09-21 收尾：这条暗色声明**原来在文件里写了两遍**（本处 + 下方 ㊸ 那个块），
       选择器与值完全相同；文档顺序在后、真正生效的是 ㊸ 那一份。此处仅保留来龙去脉，
       声明本体已删 —— **改暗色重音颜色只需要改 ㊸ 一处**。 */
    /* (f) 「AI」= Source Serif 4 SemiBold（2026-09-21 第三轮，㊲）
       —— 为什么换族见上方「AISerif」@font-face 注释，这里只记落地值：
         font-family     AISerif（子集只含 A / I，1.6KB）
         font-size       0.95em → 1.05em。Source Serif 的 cap height 是 0.67em
                         （Cormorant 只有 0.63em），要让 AI 的大写顶到与中文同一
                         水平线，字号必须回补一档。1.05em 时 AI 的 cap = 0.70em，
                         约为中文字面高（0.86em）的 82% —— **故意比中文略矮**：
                         西文大写与汉字混排时撑满是新手做法，会顶破行感、更显突兀，
                         差 15~20% 才是「融进去但还能认出来」的区间。
         font-weight     600（静态实例，无 wght 轴；写这个值只为命中唯一那一档）
         letter-spacing  0.02em → 0.005em。Cormorant 字面窄，靠字距松口气；
                         Source Serif 字面本就宽，再给 0.02em 会像「两个字分开站」。
         margin          0.08em → 0.05em。A 左侧与中文之间的气口 —— Source Serif
                         的侧边距（side bearing）比 Cormorant 宽，外边距得往回收。
       ⚠️ 三行主色的渐变不会漏到 AI 上 —— 它没有自己的 color/text-fill，
       继承父行的 `-webkit-text-fill-color: transparent`，跟着同一道渐变走。 */
    .hero-title .ai-tech {
      font-family: 'AISerif', 'Playfair Display', serif;
      font-size: 1.05em; font-weight: 600; letter-spacing: 0.005em; margin: 0 0.05em;
    }
    /* ㊶ 原来的「三行错开揭示」（0.30 / 0.42 / 0.54 三条 animation-delay）**已删除**。
       它们挂在行元素上，而行元素自己不画文字（文字色来自父元素 h1 的渐变背景绘制），
       所以从 ㊴ 起就是三条空转的声明。现在揭示统一由 `.hero-title` 承载、整块一起走，
       与英文逐值一致。理由与实测见下方 ㊶。 */

    /* ㉘ 中文标题第四轮（2026-09-18）—— 上一轮**走偏到「过度手写化」**，本轮收回：
         ① 主标题整段换成圆钝手写体 → 低龄感 / 小红书手账感，工程师气质荡然无存；
         ② 对照英文版的做法：**大面积严肃工整的衬线体 + 极小面积的一处手写笔触**，
           整幅仍是同一道蓝渐变。高级感来自「大面积正统印刷体」这个底座，
           手写只是底座上的一道笔触 —— 反过来，整页都会变成草稿。
       故中文侧收回成同一结构（本轮推翻上一轮的 (d)(e) 两条）：
         · 主标题（三行 + AI）→ 正统宋体**真 700 字面**（HeroSerif 自托管子集）。
           700 是**真实字面**，不是浏览器合成加粗 —— 合成粗体只做整体横向加粗，
           会把宋体的「横细竖粗」抹平（实测见上方 @font-face 注释）；
         · 颜色**整体维持蓝色渐变** —— ⚠️ 这条已被 ㉛（2026-09-21）**推翻**：
           第四轮当时试过「三行炭黑 + 只有关键词上蓝」，但只改了一处、且保留着
           整行的蓝渐变打底，用户看后判定「整体还是蓝色渐变过渡」；
           第五轮（㉙）把关键词改成实色后，问题反而更清楚了 ——
           三行全蓝 + 关键词也蓝 = 处处是重点，「有温度」永远跳不出来。
           故 ㉛ 把三行统一改成沉稳冷墨（--title-base），**唯一的一处彩色**只留给
           「有温度」。㉒(d) 的蓝渐变随之撤掉（见上方那条改写后的注释）。
           → ㉞ 把这条冷墨换成了靛蓝（去掉黑色），→ ㉟ 又把实色换回**蓝渐变**
           （用户点名要「和英文一样」）。三次调整的差别全在「跨度」上，见 :root 的 ㉟ 注释；
         · 只有「有温度」三字换手写：山海**最细档**（角色名仍是 InkBrush）。
           焦点靠四样东西立起来 —— 笔迹不同 / 重量级差（0.64x vs 正文 1.00x）/
           波浪线 / 实色（**不是**跟着渐变走，见下方 ㉙ 第五轮修正）；
         · 描述文字 → 回标准系统无衬线（var(--font-body)）——
           **大段正文用手写体是界面设计大忌**：既难读，又把整页拉成草稿。
       ⚠️ 2026-09-21 收尾：这里原有一份
       `html:not([lang="en"]) .hero-title { font-family …; font-weight: 700;
       letter-spacing: -0.02em; }`，与上方 ㉒(a)(c) 那份**逐值相同**（只少了
       line-height），纯属同选择器写两遍。已删除，字号/字重/字距的唯一来源是 ㉒(a)(c)。 */
    /* 中文侧渐变现在有**三处来源**：更早那条 `.hero-title > .hero-title-line:last-child`
       给末行铺的、㉒(d) 给每行铺的、以及 ㉟/㊱ 的 `--title-grad`。
       **下面那条特异性最高**（html:not([lang]) 是 (0,3,1)，压得住 :last-child 的 (0,3,0)），
       所以前两条留着也不会打架 —— 但**改色一律改 :root 的 --title-grad**，别去改它们。
       留个提醒：这几条都带 `-webkit-text-fill-color: transparent`，
       那是渐变生效的必要条件 —— 谁要改回实色，必须连 text-fill-color 一起改，
       否则整行文字会隐形。 */
    /* 「有温度」：唯一保留手写的地方。**这是该选择器唯一的字形 / 间距 / 时机来源**
       （2026-09-21 收尾把原 ㉒(e) 那份重复声明并了进来）。四个要点 ——
       ① font-weight 必须显式写 400：手写体只有单字重，写 700 会让浏览器叠一层
          **合成粗体**，笔锋会被直接糊平（比宋体更致命）；
       ② font-size 回 1em：山海的字面偏大（实测墨迹高 0.88–0.97em），放大就会顶破行盒；
          再把 line-height 钉成 1 —— inline 盒不参与行高计算，三行才等距；
       ③ letter-spacing 0（㉒(e) 原写 0.02em，被本条覆盖，收尾时已删）；
       ④ padding-bottom 0.22em：给「度」的下伸笔画（实测 -0.135em）与波浪线留间隙 ——
          宋体只需 0.1em，手写体的下伸深得多。
       ⑤ animation-delay 1.4s：㊶ 起中英逐值统一，原在 ㉒(e)，已并入本块。
       ⚠️ 本块**不写颜色**：实色声明在下文 ㉙（亮）与 ㊸（暗）两条里 —— 它们文档顺序
       更靠后、权重也更高，写在这里会被压掉（曾因此出现「改了上面、页面纹丝不动」）。 */
    html:not([lang="en"]) .hero-title .ink-hl {
      font-family: 'InkBrush', 'HeroSerif', 'Noto Serif SC', 'Songti SC', serif;
      font-style: normal;
      font-weight: 400;
      font-size: 1em;
      line-height: 1;
      letter-spacing: 0;
      padding-bottom: 0.22em;
      animation-delay: 1.5s;
    }
    /* ㊵ 重音的字形必须从父级文字遮罩里「摘出去」（2026-09-21 第九轮）。
       ── 用户反馈：「有温度 你只留一个图层啊，两个图层都留下成什么了」「我文字的
          动画去哪里了，而且波浪线是最后加载的」。三句话指向同一个 bug。

       【根因】㊴ 把渐变从「逐行铺」挪到了父级 `.hero-title` 上，父级靠
       `background-clip: text` 把渐变裁成**全标题所有字形的形状**——注意：遮罩里
       包含了**后代**（`.ink-hl`）的字形，因为 Chromium 收集遮罩时会连普通行内
       后代的文字一起收。于是「有温度」被上了两遍色：
         ① 父级遮罩 → 这三个字的字形，色 = 该处渐变值（暗蓝 blue-700 附近）
         ② `.ink-hl` 自己的 `-webkit-text-fill-color` → blue-400（亮蓝）
       两遍本身还能勉强叠齐，真正致命的是**①不在 line 3 的子树里**：
       `.hero-title-line` 的入场揭示用的是 `clip-path: inset(...)`（见文件下方
       hero-line-up），clip-path 只裁**元素自己和它的后代**，而①是父元素 h1 的
       背景绘制 → **完全不受 clip-path 管辖**。实测（把 line 3 钉在揭示动画起点、
       完全裁掉后截图）：`的日常` 已被裁没，**`有温度` 三个字仍整片可见**（暗蓝）。
       用户看到的就是：这三个字「没有入场动画，一开始就在」，动画跑完后又因为
       两遍色的位置差了几个亚像素而叠成**双影** —— 「两个图层」。
       【修法】让这三个字的字形不再进父级遮罩：把 em 里的文字套一层
       `display: inline-block` 的 span。原子行内盒是**独立的行内格式化上下文**，
       父级收集 `background-clip:text` 遮罩时不会下钻进去（实测遮罩里的字形像素
       8155 → 0）。于是 ① 整条消失：「有温度」只剩自己那一遍色，重新落回 line 3
       的子树 → **重新被入场动画正常裁切**，双影也随之不可能发生。
       ⚠️ 为什么不让 em 自己 `display: inline-block`：那样 em 的内容盒由
       `line-height` 决定（1em），而 inline 时内容盒是字体自身的字面框（InkBrush
       实测 ≈1.11em）—— 盒高差 0.11em 会把**波浪线**整体上提 8px（波浪线铺在
       em 的 padding box 底部）。实测两种写法：套内层 span 的墨迹外接框/波浪线
       与改前**逐像素相同**；改 em 自身则波浪线下移 1 设备像素。取内层 span。 */
    html:not([lang="en"]) .hero-title .ink-hl > .ink-hl-in { display: inline-block; }
    /* ㉙ 关键词「有温度」的颜色（2026-09-18 第五轮）—— 撤回上一轮的 transparent，
       改成**与英文 warmth 逐值同构的实色重音**。
       起因（用户对照截屏）：英文侧 warmth 是实色近白、其余三行是蓝渐变，
       一眼就是「两个颜色」；中文侧「有温度」却被 transparent 绑死在父行的渐变上，
       跟两边的「的日常」同一个色域，焦点整个消失。
       颜色取值的改动轨迹：
         ㉞ 亮色 var(--blue-600) → **var(--blue-500) = #3563d6**（基色换靛蓝后必须提一档）
         ㊱ 亮色 var(--blue-500) → **var(--blue-400) = #6089e7**（基色渐变末端已到
            L*45，重音必须再让一档才有落差；见 :root 的 ㊱ 推算）
         暗色原为 #ffffff（英文侧是 var(--blue-900) 的近白，手写细笔只有
           正文 0.64x 的墨量，故取满白补回对比度，肉眼与英文的近白几乎一致）
           → **㊸ 已改为 var(--blue-900)，与英文 warmth 逐值统一**，理由见下方 ㊸。
         ⚠️ 亮色两轮「提档」都是被基色逼出来的，不是审美选择 —— 重音与紧邻字的
         ΔL* 必须 ≥ ~15（㉝ 立的硬约束）。当前实测：亮色 vs「的日常」≈ +24，
         暗色（blue-900）vs「的日常」≈ +17（㊸ 已实测复核）。
         **下一次若再放宽基色，重音就会先糊。**
       注意 ㉘ 的 transparent 必须撤掉：父行是 `background-clip: text`，
       子元素填 transparent 就会「跟着渐变走」，这三个字等于没有重音。
       两个选择器都要写：暗色那条权重比亮色那条高（多一个 [data-theme]），
       但亮色那条文档顺序在前 —— 所以亮色规则**只作用于亮色**，不必再加 :not。
       ⚠️ 改颜色只改这里。文件上方 ㉒(e) 那个同名块里**没有**颜色声明（已标注）。 */
    /* ㊷（2026-09-21 第十一轮）：亮色重音从 blue-400 **回到 blue-600**。
       用户把中英两版首屏并排比对后说「把细节也对齐一下」——量出来中英两边
       「重音的明暗角色」正好相反：
         英文 warmth   = blue-600 = 该行渐变**最沉**的那一档，是整题的「墨心」；
         中文 有温度   = blue-400 = 整题**最亮**的那一档，读起来是「褪色」而不是「重音」。
       现在中英同号（blue-600 = #274cae），波浪线同步（见下方那条 stroke）。
       实测（playwright 元素截图 → 笔画核心色中位数），改后：
         有温度 #294eae ｜ 紧邻「的」#416dda ｜ 行尾「常」#567fe1
       重音比「的」深 1 档、比「常」深 2 档 —— 与英文 warmth(#2e4da7) vs for(#304fab)
       同一种「同色系、只深一档、靠字形和波浪线立焦点」的关系。
       ⚠️ 这条与上面 ㉙/㊱ 的历史结论并不冲突：㊱ 当时取 blue-400 是被**旧的深色基色
       （blue-700→500）**逼出来的；㊷ 把基色整体提到英文同阶（blue-400→600）之后，
       重音就该回到同阶的 blue-600。**基色与重音是同一条推演的产物，永远一起改。** */
    html:not([lang="en"]) .hero-title .ink-hl {
      -webkit-text-fill-color: var(--blue-600);
      color: var(--blue-600);
    }
    /* ㊱ 中文的波浪线跟着重音走，保持「字 + 线 = 一个完整记号」（㉞ 的原始设计意图）。
       ㊷ 起描边色 = 重音色 = #274cae。**只作用在浅色中文**：
         暗色中文线 = #83b0f4（与英文共用那条）、英文线 = #3563d6，两头都不动。 */
    html:not([data-theme="dark"]):not([lang="en"]) .hero-title .ink-hl {
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8.6C23 3.2 45 2.6 61 5.1 77 7.6 99 6.9 118 3.1' fill='none' stroke='%23274cae' stroke-width='3.1' stroke-linecap='round'/%3E%3C/svg%3E");
    }
    /* ㊸（2026-09-21 第十二轮）暗色重音对齐英文：`#ffffff` → `var(--blue-900)`。
       用户原话：「黑暗模式下 warmth 颜色和有温度颜色不一样，统一成 warmth 的颜色」。
       实测（线上 computedStyle + 元素像素双证）：暗色下
         英文 warmth  fill = rgb(230,238,253) = var(--blue-900) #e6eefd（L* 93.9）
         中文 有温度  fill = rgb(255,255,255)（L* 100.0）
       差 6.1 个 L*。来源是 ㉙ 的旧理由：手写细笔墨量只有正文 0.64x，当初用「满白」
       把对比度补回来。但并排看就是**两个白** —— ㊷ 已经把亮色重音统一到 blue-600 了，
       暗色这一处是最后一个「不统一」。
       ΔL* 代价（元素截图像素实测的墨心色，不是纸上推算）：
         改前 有温度 #ffffff vs 紧邻「的日常」#9dbff5 → ΔL* = 23.3
         改后 有温度 #e6eefd vs 紧邻「的日常」#9dbff5 → ΔL* = 17.2
       两个值都 ≥ ㉝ 的 15 软约束 —— 所以这次**并没有**牺牲重音的落差，
       只是把「多出来的 6 个 L*」还给统一性。降 6 个 L* 换来中英同色，值。
       （注意别拿 `--blue-900` 的标称值去算：暗色渐变端点 #a9c8f9 是 46% 处的
       理论值，实测落在「有温度」那一带的基色是 #9dbff5，比端点更沉。）
       波浪线（stroke）两边**本来就已经同值** #83b0f4，无需动。
       ⚠️ 这是「同一处重音、两个 block 各写一遍」的历史遗留：上面 ㉒(e) 那个同名块
       也有一条暗色声明，两处必须同改（本条文档顺序在后、实际生效）。 */
    html[data-theme="dark"]:not([lang="en"]) .hero-title .ink-hl {
      -webkit-text-fill-color: var(--blue-900);
      color: var(--blue-900);
    }
    /* ㊴ 整块对角线过渡（2026-09-21 第八轮）—— 撤掉了 ㊳ 的「左下角柔光」。
       用户原话：「我说错了，应该是右下角的文字图层从常到用画一条对角线然后过渡，
       不要包括有温度」。于是载体从「字下面的一团光」换成**整块标题的一道斜向渐变**：
       起点在末行末尾的「常」（右下），终点在首行开头的「用」（左上），沿 315deg 扫过；
       「有温度」三个字有独立实色（见 ㉙ 那两条），天然被排除在这道渐变之外。
       ⚠️ 两个实现要点：
         ① **渐变必须挪到父元素上**，不能再逐行铺。逐行铺时每条渐变都以「行盒」为
            自己的坐标系，三行节奏一致却**彼此独立**，拼不出跨行的斜线；只有让
            `.hero-title` 自己铺一道、三行文字继承 `-webkit-text-fill-color: transparent`
            透出来，才是一条从「常」贯到「用」的完整对角线。下面行元素那句
            `background-image: none` 就是为这个服务的。
         ② **`width: fit-content` 是几何前提**。h1 默认撑满 .hero-content 的 660px，
            而最长行只有 425px —— 背景盒比文字宽 55%，对角线会被抻平、颜色变化全挤在
            左侧一小段里。收紧到文字外接框后，315deg 的两端正好落在「常」和「用」上。
            实测：h1 宽 660 → 425，`.hero-desc` 的 y 一动不动（579，改前改后同值）。
       方向（㊷ 已改）：**左上沉、右下亮** —— 与英文标题「左沉右亮」同向。
       ㊴ 当时是「左上亮、右下沉」（用户在 A/B 对照图里选定 B 档，B = 用 #315cc8 /
       常 #203e8f）；㊷ 把色序与档位一起改成英文同阶后，采样实测
       「用」= #2c53b8（≈blue-600，最沉）、「常」= #567fe1（≈blue-400，最亮）——
       正好是 B 的镜像，也就是当初 A/B 里的 A 档观感。
       **为什么推翻用户选过的 B**：B 是在「只看中文」的 A/B 里选的，当时没有英文参照；
       把中英首屏并排看，B 的受光方向与英文正好相反（英文每一行都是左沉右亮），
       切换语言时整块标题的明暗梯度会翻面 —— 这正是「把细节也对齐」要消掉的那一处。
       若哪天想换回 B，**把 :root 那条 --title-grad 的色序倒过来即可**（同时要把
       亮色重音改回 blue-400 / 描边改回 #6089e7，否则重音会糊进基色）。 */
    html:not([lang="en"]) .hero-title {
      width: fit-content;
      background-image: var(--title-grad);
      -webkit-background-clip: text; background-clip: text;
      -webkit-text-fill-color: transparent; color: transparent;
    }
    /* 三行不再各铺各的：整块的渐变已由父元素提供，行元素只负责「把颜色让出去」。
       ⚠️ 这句同时压掉文件上方 `.hero-title > .hero-title-line:last-child` 那条末行渐变
       （特异性 (0,3,1) > (0,3,0)），所以末行不会又冒出一道德蓝。
       ⚠️ 别把 `-webkit-text-fill-color: transparent` 删掉 —— 那是渐变生效的必要条件。 */
    html:not([lang="en"]) .hero-title > .hero-title-line {
      background-image: none;
      -webkit-text-fill-color: transparent; color: transparent;
    }
    /* AI：原本在这里又把字重拉回 700（㉒(f) 的结论），㉜ 换族后已删除 ——
       字重统一由 `.hero-title .ai-tech` 那条给 600。留着这条会让 AISerif 落到
       700 档，把「压细到与中文同档」的目的全部抵消。
       ⚠️ 换 AISerif 的 src 时注意：新字体若无 wght 轴，700 会由浏览器**合成加粗**，
       笔画会被整体糊宽。换族细节见上方 @font-face 注释。 */
    /* 描述文字（1.08rem / 17px）：回标准系统无衬线。手写体只服务标题里那三个字。 */
    html:not([lang="en"]) .hero-desc {
      font-family: var(--font-body);
      font-weight: 400;
      letter-spacing: 0;
    }
    .hero-desc {
      font-size: 1.08rem; line-height: 1.78; color: var(--ink-muted);
      font-weight: 400; max-width: 33rem;
      margin-left: 0.85rem;
      text-wrap: pretty;
      position: relative;
    }
    .hero-desc::before {
      content: ''; position: absolute; left: -0.85rem; top: 0.42em;
      width: 2px; height: calc(100% - 0.84em); border-radius: 2px;
      background: linear-gradient(180deg, var(--blue-400), var(--blue-300) 60%, transparent);
      opacity: 0.9;
    }

    /* ── 2026-10-27：首屏文案固定英文，中文界面不再套用「中文分行」那套排版 ──
       上面那组 html:not([lang="en"]) 规则（㉒/㉙/㊱/㊲/㊴…）全是按「中文两行/三行
       标题」调的：700 字重、1.30 行高、-0.02em 字距、重音换成手写毛笔楷 InkBrush。
       文案改成英文后它们全部不适用 —— 实测中文界面下 h1 从 334px 涨到 412px
       （多折一行），warmth 变成毛笔楷。这里用**同特异性 + 写在后面**把值钉回英文版，
       不动上面那些带历史注释的规则（它们仍准确描述中文结构；将来若要回中文，
       删掉这一块即可恢复）。 */
    html:not([lang="en"]) .hero-title {
      font-weight: 600;
      line-height: 1.08;
      letter-spacing: -0.032em;
      /* 2026-10-27 ⑨：还必须把 ㊴ 那道「跨行对角线渐变」关掉。
         它铺在 h1 上、靠 background-clip:text 直接画字形 —— 但父级的背景绘制
         不属于任何一行的子树，**不受行级 clip-path 管辖**（就是 ㊶ 记过的那个坑）。
         逐行揭示上线后实测中文界面：0.26s 时四行已经全亮、没有任何层次，并且与
         行内渐变叠成双影（/tmp/zh-300.png）。
         关掉之后中英两版同构：颜色只由行内的 `.hero-title > span` 渐变提供，
         「上色」和「被裁」重新落在同一个元素上。
         `width: fit-content` 当初是为那道对角线收紧盒子的，一并撤回默认（block 撑满），
         否则盒宽 467px 与行宽不一致，将来加行级样式容易踩到。 */
      width: auto;
      background-image: none;
      -webkit-text-fill-color: currentColor;
    }
    html:not([lang="en"]) .hero-title .ink-hl {
      font-family: var(--font-display);
      font-style: italic;
      font-weight: 500;
      letter-spacing: -0.01em;
      line-height: inherit;
      padding-bottom: 0.1em;   /* 与英文 em 一致，只留波浪线的地儿 */
    }

    /* 首屏动作区：主胶囊按钮 + 次级文字动作 */
    .hero-actions {
      display: flex; align-items: center; gap: 0.85rem; flex-wrap: wrap;
      margin: 2.35rem 0 0 0.85rem;
      -webkit-user-select: none; user-select: none;
    }
    .btn-solid {
      position: relative; overflow: hidden;
      display: inline-flex; align-items: center; gap: 0.6rem;
      height: 50px; padding: 0 1.45rem; border-radius: 999px;
      border: none; cursor: pointer; text-decoration: none;
      /* ㉚ 走统一令牌：明暗两套渐变由 --grad-solid 各自给出，这里不再写死 */
      background: var(--grad-solid);
      color: #fff; font-family: var(--font-body);
      font-size: 0.93rem; font-weight: 560; letter-spacing: 0.012em;
      box-shadow: 0 10px 24px -12px rgba(20,40,70,0.22),
                  inset 0 1px 0 rgba(255,255,255,0.3);
      transition: transform 0.36s var(--ease-spring), box-shadow 0.36s var(--ease-out);
    }
    .btn-solid:hover {
      transform: translateY(-2.5px);
      box-shadow: 0 16px 36px -14px rgba(20,40,70,0.28),
                  inset 0 1px 0 rgba(255,255,255,0.34);
    }
    .btn-solid:active { transform: translateY(-0.5px) scale(0.99); }
    .btn-solid:focus-visible { outline: none; box-shadow: var(--ring), 0 10px 24px -12px rgba(20,40,70,0.22); }
    .btn-solid svg { width: 16px; height: 16px; stroke-width: 2.1; transition: transform 0.36s var(--ease-spring); }
    .btn-solid:hover svg { transform: translate(2px, -2px); }
    .btn-solid::after {
      content: ''; position: absolute; inset: 0; pointer-events: none;
      background: linear-gradient(100deg, transparent 32%, rgba(255,255,255,0.3) 50%, transparent 68%);
      transform: translateX(-130%);
    }
    .btn-solid:hover::after { animation: btn-shine 0.9s var(--ease-out); }
    @keyframes btn-shine { to { transform: translateX(130%); } }

    .btn-line {
      display: inline-flex; align-items: center; gap: 0.5rem;
      height: 50px; padding: 0 1.3rem; border-radius: 999px;
      border: 1px solid var(--line-strong);
      background: var(--surface);
      color: var(--ink-muted); text-decoration: none;
      font-size: 0.93rem; font-weight: 500;
      box-shadow: var(--shadow-xs);
      transition: color 0.3s var(--ease-out), border-color 0.3s var(--ease-out),
                  background-color 0.3s var(--ease-out), transform 0.36s var(--ease-spring), box-shadow 0.36s var(--ease-out);
    }
    .btn-line:hover {
      color: var(--blue-700);
      border-color: color-mix(in srgb, var(--blue-500) 42%, transparent);
      background: var(--blue-50);
      transform: translateY(-2.5px);
      box-shadow: var(--shadow-card);
    }
    .btn-line:focus-visible { outline: none; box-shadow: var(--ring-blue); }

    /* 首屏滚动提示 —— ⚠️ 该组件已按用户反馈**下线**（2026-09-21 收尾时合并）：
       原 DOM（下方 hero 里的 `.hero-scroll` div）仍保留在页面中，纯 CSS 隐藏；
       定位 / 字号 / 滚动点动画全套样式一并保留，只为「改 display 即可复活」。
       原来控制隐藏的声明有三处（本节 + ⑧ 节 + 窄屏媒体查询里的同值 `display: none`），
       已收成这一处 —— **隐藏开关只看这里**。 */
    .hero-scroll {
      display: none;             /* ← 复活只需把这里改回 flex */
      position: absolute; left: 50%; bottom: 1.5rem; transform: translateX(-50%);
      flex-direction: column; align-items: center; gap: 0.6rem;
      font-family: var(--font-mono); font-size: 0.58rem; font-weight: 500;
      letter-spacing: 0.26em; text-transform: uppercase; color: var(--ink-faint);
      pointer-events: none;
    }
    .hero-scroll i {
      display: block; width: 1px; height: 44px; position: relative;
      background: linear-gradient(180deg, var(--line-bold), transparent);
    }
    .hero-scroll i::after {
      content: ''; position: absolute; left: -1.5px; top: 0;
      width: 4px; height: 4px; border-radius: 50%;
      background: linear-gradient(135deg, var(--blue-600), var(--blue-400));
      animation: scroll-dot 2.6s var(--ease-inout) infinite;
    }
    @keyframes scroll-dot {
      0%   { transform: translateY(-2px); opacity: 0; }
      18%  { opacity: 1; }
      82%  { opacity: 1; }
      100% { transform: translateY(44px); opacity: 0; }
    }

    /* ── Hero 右侧：Harbor Jelly 数字标本展台（2026-10-27 首屏改版） ──
       原「拍立得头像 + 扇形叠卡」整体下线，作者身份收缩到左侧 24px 印章。
       结构：.specimen-stage（玻璃底托）> .specimen-vitrine（方形视窗，内嵌 iframe）
             + .specimen-plate（等宽字标牌）
       ⚠️ 几处刻意的取舍：
         · 视窗用 aspect-ratio:1/1 固定比例，iframe 载入前后都不会引起 CLS；
         · iframe 内已摘掉滚轮缩放（见 assets/demos/harbor-jelly.html 的注释），
           否则访客在展台上滚不动页面；
         · 手机上 iframe 里的 canvas 是 touch-action:pan-y，竖滑照样滚页面，
           只有横向拖动才搅水。 */
    /* ── Hero 右侧：Harbor & Launch 双层 WebGPU 标本叠放展台 ──
       大长方形 macOS 风格视窗。卡片支持前后洗牌对调（is-front / is-back）。 */
    .hero-visual {
      position: relative;
      justify-self: end;
      width: min(48vw, 550px);
      min-width: 340px;
      height: 460px;
      margin-top: 67px; /* 顶部与 Engineering 的大写首字顶部对齐 */
      margin-right: 0.5rem;
      pointer-events: auto !important;
      z-index: 10;
    }

    .specimen-deck {
      position: relative;
      width: 100%;
      height: 100%;
    }

    /* 视窗卡片 */
    .specimen-window {
      position: absolute;
      left: 0;
      top: 0;
      width: calc(100% - 32px);
      border-radius: var(--radius-lg, 1.15rem);
      border: 1px solid var(--line-strong);
      background: linear-gradient(175deg,
        color-mix(in srgb, var(--surface) 94%, transparent),
        color-mix(in srgb, var(--surface-3) 86%, transparent));
      backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
      overflow: hidden;
      transition: transform 0.44s cubic-bezier(0.34, 1.35, 0.64, 1),
                  opacity 0.35s ease,
                  box-shadow 0.44s ease;
    }

    /* 底层卡片全域点击捕获帘幕（避免内部 iframe 吃事件，确保点击底层任意角落立即洗牌） */
    .card-curtain {
      display: none;
      position: absolute;
      inset: 0;
      z-index: 40;
      cursor: pointer;
      background: transparent;
    }
    .specimen-window.is-back .card-curtain {
      display: block;
    }

    /* macOS 风格顶部标题栏 */
    .window-bar {
      display: flex;
      align-items: center;
      gap: 0.55rem;
      height: 34px;
      padding: 0 0.85rem;
      border-bottom: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
      font-family: var(--font-mono);
      font-size: 0.65rem;
      letter-spacing: 0.08em;
    }
    .window-dots {
      display: inline-flex;
      align-items: center;
      gap: 5px;
    }
    .window-dots i {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      display: inline-block;
    }
    .dot-red { background: #ff5f56; }
    .dot-yellow { background: #ffbd2e; }
    .dot-green { background: #27c93f; }

    .window-title {
      font-weight: 600;
      color: var(--ink);
      letter-spacing: 0.12em;
    }
    .window-badge {
      font-size: 0.55rem;
      padding: 2px 5px;
      border-radius: 4px;
      background: color-mix(in srgb, var(--blue-500) 15%, transparent);
      color: var(--blue-600);
      font-weight: 600;
      letter-spacing: 0.1em;
      margin-left: auto;
    }
    html[data-theme="dark"] .window-badge { color: var(--blue-500); }

    /* 2026-10-27：原来标题栏右侧还有一个「Swap Deck ↗」胶囊按钮，实际永远看不到——
       底卡只从右边露出 ~27px（宽度不到胶囊的一半），露出来的永远是**被裁掉一半的蓝块**，
       看着像渲染出错（dark 下尤其明显）。换牌的可视入口统一收到页脚那一行
       .footer-action 上：整条露在底卡外面，文字完整、还是原生 <button> 可键盘操作。 */

    /* 视窗主体（放大长方形视窗） */
    .window-body {
      position: relative;
      width: 100%;
      height: 330px;
      background: #ece9e3;
      overflow: hidden;
      border-bottom: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
    }
    .window-canvas {
      display: block;
      width: 100%;
      height: 100%;
      border: 0;
    }

    /* 视窗底部标牌 */
    .window-footer {
      display: flex;
      align-items: center;
      justify-content: space-between;
      height: 32px;
      padding: 0 0.85rem;
      font-family: var(--font-mono);
      font-size: 0.62rem;
      color: var(--ink-muted);
      letter-spacing: 0.04em;
    }
    .footer-tip { color: var(--ink-faint); }
    .footer-deck-tag {
      display: none;
      font-size: 0.58rem;
      letter-spacing: 0.1em;
      color: var(--ink-muted);
      font-weight: 600;
    }
    .footer-action {
      display: none;
      font: inherit;
      appearance: none; -webkit-appearance: none;
      border: 0; background: transparent; color: inherit;
      cursor: pointer;
    }
    /* 键盘可见焦点：底卡的换牌按钮是这张卡片上唯一可 Tab 到的控件 */
    .footer-action:focus-visible {
      outline: 2px solid var(--blue-500);
      outline-offset: 2px;
    }
    .swap-icon {
      font-style: normal;
      font-size: 0.76rem;
      line-height: 1;
      display: inline-block;
      transition: transform 0.35s cubic-bezier(0.34, 1.35, 0.64, 1);
    }

    /* ── 前台卡片状态（is-front） ── */
    .specimen-window.is-front {
      z-index: 5;
      transform: translate(0, 0) scale(1);
      opacity: 1;
      pointer-events: auto;
      box-shadow:
        0 16px 36px -10px rgba(20, 24, 33, 0.16),
        0 6px 14px -4px rgba(20, 24, 33, 0.10);
    }

    /* ── 后台卡片状态（is-back，往右下偏移露出，克制细腻的自然物理阴影） ── */
    .specimen-window.is-back {
      z-index: 2;
      /* 位移量走变量：手机 / 平板几档会改这两个值，而 @keyframes 里的引逗动画
         也必须跟着同一个值走 —— 否则动画一开播就把卡片弹回桌面尺寸的 32/38px
         （2026-10-27 实测：390 视口下 dx 从 17 直接跳到 47，播完又弹回来）。 */
      --deck-x: 32px;
      --deck-y: 38px;
      transform: translate(var(--deck-x), var(--deck-y)) scale(0.98);
      opacity: 0.95;
      cursor: pointer;
      pointer-events: auto;
      box-shadow:
        0 24px 52px -12px rgba(20, 24, 33, 0.22),
        0 12px 28px -6px rgba(20, 24, 33, 0.16),
        0 4px 10px -2px rgba(20, 24, 33, 0.10);
    }
    /* 鼠标悬停底卡时：如从牌堆中抽牌般弹性滑出 14px，极具直觉可玩性（Affordance） */
    .specimen-window.is-back:hover {
      transform: translate(calc(var(--deck-x) + 14px), calc(var(--deck-y) + 10px)) scale(0.995);
      opacity: 1;
      box-shadow:
        0 32px 64px -10px rgba(20, 24, 33, 0.28),
        0 16px 32px -4px rgba(20, 24, 33, 0.18),
        0 6px 14px -2px rgba(20, 24, 33, 0.12);
    }
    .specimen-window.is-back:hover .swap-icon {
      transform: rotate(180deg);
    }
    .specimen-window.is-back .window-body,
    .specimen-window.is-back iframe,
    .specimen-window.is-back .window-canvas {
      pointer-events: none !important;
    }
    .specimen-window.is-back .footer-tip { display: none; }
    /* 底卡只从右侧露出 ~27px，而 .window-badge 是 margin-left:auto 右对齐的 ——
       不藏起来，露出来的就是一截被裁断的「U」（WEBGPU 的尾巴）。前台的徽章照常显示。 */
    .specimen-window.is-back .window-badge { display: none; }
    .specimen-window.is-back .footer-deck-tag {
      display: inline-block;
      color: var(--blue-600);
      opacity: 0.85;
    }
    .specimen-window.is-back .footer-action {
      display: inline-flex;
      align-items: center;
      gap: 0.35rem;
      color: var(--blue-600);
      font-weight: 600;
      padding: 2px 7px;
      border-radius: 4px;
      background: color-mix(in srgb, var(--blue-500) 10%, transparent);
      transition: background 0.2s ease, transform 0.2s ease;
    }
    .specimen-window.is-back:hover .footer-action {
      background: color-mix(in srgb, var(--blue-500) 20%, transparent);
      transform: translateX(1px);
    }

    /* 首次进入页面时的微引逗（Idle Nudge）：仅播一次，暗示底卡可抽拉对调 */
    @keyframes deck-hint-nudge {
      0%   { transform: translate(var(--deck-x), var(--deck-y)) scale(0.98); }
      40%  { transform: translate(calc(var(--deck-x) + 14px), calc(var(--deck-y) + 10px)) scale(0.995); }
      70%  { transform: translate(calc(var(--deck-x) - 2px), calc(var(--deck-y) - 2px)) scale(0.98); }
      100% { transform: translate(var(--deck-x), var(--deck-y)) scale(0.98); }
    }
    .specimen-window.is-back.hint-nudge {
      animation: deck-hint-nudge 0.78s cubic-bezier(0.34, 1.35, 0.64, 1) forwards;
    }

    /* ── 暗色模式适配 ── */
    html[data-theme="dark"] .specimen-window.is-front {
      background: linear-gradient(175deg, rgba(39, 45, 58, 0.95), rgba(26, 31, 41, 0.92));
      border-color: rgba(255, 255, 255, 0.14);
      box-shadow:
        0 18px 40px -12px rgba(0, 0, 0, 0.65),
        0 6px 16px -4px rgba(0, 0, 0, 0.45);
    }
    html[data-theme="dark"] .specimen-window.is-back {
      background: linear-gradient(175deg, rgba(32, 36, 46, 0.95), rgba(20, 24, 32, 0.92));
      border-color: rgba(255, 255, 255, 0.14);
      box-shadow:
        0 26px 56px -10px rgba(0, 0, 0, 0.72),
        0 12px 28px -6px rgba(0, 0, 0, 0.55),
        0 4px 10px -2px rgba(0, 0, 0, 0.40);
    }
    html[data-theme="dark"] .specimen-window.is-back:hover {
      box-shadow:
        0 32px 68px -8px rgba(0, 0, 0, 0.78),
        0 16px 34px -4px rgba(0, 0, 0, 0.62),
        0 6px 14px -2px rgba(0, 0, 0, 0.45);
    }
    html[data-theme="dark"] .specimen-window.is-back .footer-deck-tag,
    html[data-theme="dark"] .specimen-window.is-back .footer-action {
      color: var(--blue-400);
    }
    html[data-theme="dark"] .window-body { background: #141517; }
    html[data-theme="dark"] .window-bar,
    html[data-theme="dark"] .window-body,
    html[data-theme="dark"] .window-footer {
      border-color: rgba(255, 255, 255, 0.08);
    }
    html[data-theme="dark"] .window-title { color: var(--ink); }
    html[data-theme="dark"] .footer-tip { color: var(--ink-muted); }

    /* ── 区块通用 ── */
    .section { padding: 7rem 0; scroll-margin-top: 5.5rem; }

    /* 编辑式头部：标签 + 标题纵排，留出大片呼吸感 */
    .page-section .container {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
    }
    .page-section .container > .section-label { grid-column: 1 / -1; }
    .page-section .container > .section-header { grid-column: 1; align-self: end; }
    .page-section .container > .portfolio-grid,
    .page-section .container > .travel-list,
    .page-section .container > .inspo-list { grid-column: 1 / -1; margin-top: 3.25rem; }

    /* 三个版块「标题 → 第一眼看到的内容」对齐（2026-09-24 用户反馈「旅途记录低一点」）：
       作品卡与拍立得的内容都直接从列表顶开始，只有旅途项还带着 1.9rem 的
       「项与项之间的间距」—— 那层间距只为分隔相邻项而存在，第一项根本不需要，
       却让旅途页的内容起点比另外两个版块低一截
       （实测 1440：照片顶 338.8 vs 作品图 300 / 拍立得 308.4）。
       删掉它，三者的内容就落在同一水平线上（照片顶 308.4，与拍立得逐像素齐平）。
       ⚠️ 选择器权重 50 高于窄屏媒体查询里的 `.travel-item`（10），
       所以窄屏那条 `padding: 1.45rem 0.9rem 1.45rem 1.4rem` 的上内边距
       也会被这一条一并接管 —— 这正是我们要的（窄屏同样存在这个偏差）。 */
    .page-section .container > .travel-list > .travel-item:first-child { padding-top: 0; }

    /* 分区标签：胶囊徽章 + 状态圆点 + 编号 */
    .section-label {
      justify-self: start;
      display: inline-flex; align-items: center; gap: 0.5rem;
      padding: 0.4rem 0.42rem 0.4rem 0.76rem;
      border-radius: 999px;
      background: var(--surface);
      border: 1px solid var(--line-strong);
      box-shadow: var(--shadow-xs);
      font-size: 0.68rem; font-weight: 620;
      letter-spacing: 0.145em; text-transform: uppercase;
      color: var(--ink-muted);
      margin-bottom: 1.5rem;
    }
    .section-label::before {
      content: ''; width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto;
      background: linear-gradient(135deg, var(--blue-600), var(--blue-400));
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue-500) 15%, transparent);
    }
    .section-label::after {
      content: ''; margin-left: 0.16rem;
      padding: 0.08rem 0.44rem; border-radius: 999px;
      font-family: var(--font-mono); font-size: 0.62rem; font-weight: 600;
      letter-spacing: 0.05em; color: var(--ink-light);
      background: var(--surface-3);
      box-shadow: inset 0 0 0 1px var(--line);
    }
    #work .section-label::after { content: '01'; }
    #travel .section-label::after { content: '02'; }
    #inspo .section-label::after { content: '03'; }
    #devlog .section-label::after { content: '04'; }

    .section-header { display: flex; align-items: center; gap: 1rem; }
    .section-title {
      font-family: var(--font-display);
      font-size: clamp(2.05rem, 4.1vw, 2.95rem);
      font-weight: 600; line-height: 1.14; letter-spacing: -0.028em; color: var(--ink);
      text-wrap: balance;
    }

    /* ── 添加按钮 ── */
    .btn-add {
      display: inline-flex; align-items: center; justify-content: center;
      width: 36px; height: 36px; border-radius: 50%;
      border: 1.5px dashed color-mix(in srgb, var(--blue-500) 38%, transparent);
      background: transparent; color: var(--blue-400);
      font-size: 1.2rem; cursor: pointer;
      transition: color var(--t-fast), border-color var(--t-fast), background-color var(--t-fast), transform var(--t-fast);
      flex-shrink: 0; position: relative; top: 2px;
    }
    [hidden], .btn-add[hidden] { display: none !important; }
    .btn-add:hover {
      border-color: var(--blue-500); color: var(--blue-700);
      background: var(--blue-50); border-style: solid;
      transform: rotate(90deg);
    }
    .btn-add:focus-visible { box-shadow: var(--ring-blue); }

