/* AlistairBo · Portfolio — base: design tokens, reset, ambient background
       AlistairBo · Portfolio — Visual System v2
       主色：蓝（单色相体系）
       ══════════════════════════════════════════════════════════════ */

    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    button:focus, button:focus-visible,
    input:focus, input:focus-visible { outline: none; }

    /* ── 设计令牌 ── */
    :root {
      /* 蓝 · 主色 */
      --blue-50:  #eef4fe;
      --blue-100: #dbe8fc;
      --blue-300: #92b3f0;
      --blue-400: #6089e7;
      --blue-500: #3563d6;
      --blue-600: #274cae;
      --blue-700: #1d3b8a;
      --blue-900: #142c62;

      /* ㉚ 「实心蓝」统一渐变 —— Explore Works 主按钮 / 表单提交 / 启发条状态胶囊（Daily）
         共用同一条。浅色下是「深 → 亮」：blue-700 → 600 → 500，
         两端明度 ΔL* ≈ 18，铺在 50px 的胶囊上是一道清楚的过渡。
         ⚠️ 暗色侧**必须单独重写**（见 html[data-theme="dark"] 里的同名令牌）：
           暗色蓝阶是反相的（400 最沉、600 最亮），直接沿用它方向就反了。 */
      --grad-solid: linear-gradient(118deg, var(--blue-700) 0%, var(--blue-600) 55%, var(--blue-500) 100%);

      /* ㉛ 首屏中文标题的两级配色（2026-09-21）—— 基色 / 重音。
         起因（用户对照英文版）：中文三行**全都铺同一条蓝渐变**，等于处处是重点；
         「有温度」那三字虽是手写，却和两边的「的日常」同色域，手写感被同色吃掉。
         用户原话：「第一行、第二行、第三行全部是纯蓝字，有温度依然没有跳出来，
         缺乏英文版那种『大面积克制中忽然出现一处手写提亮』的惊艳感」。
         故把标题拆成两级：
           基色（前两行 + 末行的「的日常」）→ 沉稳的底色；
           重音（只有「有温度」三字）      → 实色 + 手写 + 波浪线。
         暗色若取 --ink（近白），重音的白就与基色同色、焦点会再次消失，
         故暗色改用 --ink-muted = #b3bccb（压低的冷灰），把亮度差让给纯白的手写。
         ⚠️ 基色的两种形态（实色 / 渐变）**必须整组覆盖**：渐变的生效条件之一是
         `-webkit-text-fill-color: transparent`，只动 background 而不动 text-fill，
         三行文字会整体隐形（见下方那条同名提醒）。
         （㉟ 起基色**又改回渐变了** —— 是本轮新的一条窄跨度渐变，不是 ㉛ 之前那条。）

         ㉝ 浅色基色第一轮软化（2026-09-21）——
         初版取 --ink = #11141c（近黑，L*≈7），用户反馈「这种黑色很扎眼」：
         近黑铺在 60px 巨字号上、对比 17.3:1，像一块实心黑板。先软化成中性冷墨 #232a36。
         但用户紧接着说「不能是蓝色的其他色调么……我真不喜欢黑色」——
         中性墨这条路到此为止：**基色必须留在蓝色体系内**。见 ㉞。

         ㉞ 基色改「靛蓝底 + 亮蓝重音」——同色相的双轴配色（2026-09-21 第三轮，现行）
         基色回到蓝之后，「基色 vs 重音」的分离轴从**色相**换成了**明度 + 彩度**：
           基色 --title-base = #2c3f70（靛蓝，L*28 / C*32 / 对底色 9.6:1）
           重音（浅色）      = --blue-500 #3563d6（L*45 / C*68 → ΔL* +17 / ΔC* +36）
         **重音从 --blue-600 提到 --blue-500 是必须的，不是顺手调**：
         实测若重音仍取 blue-600（L*36 / C*60），与靛蓝基色的 ΔL* 只有 **7.6** ——
         同色相又几乎同明度，重音直接糊掉（见对照图「靛蓝底 + 原重音」那格）。
         提到 blue-500 后 ΔL* 17 / ΔC* 36，两轴同时拉开；且它与波浪线的描边色
         `#3563d6` **正好同色**，「有温度」和它的波浪线合成一个完整的蓝色记号。
         注意只改中文这条（`html:not([lang="en"])`），英文 warmth 仍是 blue-600 不动。

         实测阶梯（playwright 逐档注入 → 元素截图 → 最暗 5% 像素均值 = 笔画核心色）：

           基色                L*   C*   对底色    ΔL*   ΔC*   结论
           #11141c（近黑）      7    6   17.3:1    28    54    用户判「扎眼」+「不喜欢黑色」
           #232a36（中性冷墨）  18    9   13.5:1    18    52    软化了，但仍被读作黑
           #3b4664（蓝灰）     30   19    8.8:1    15    48    可用；基色偏中性，蓝味不足
           #2c3f70（靛蓝）     28   32    9.6:1    17    36    ← 采用
           #45527a（中蓝灰）   36   25    7.2:1    -0    35    ✗ 与重音同明度，重音糊
           #5f6f96（雾蓝）     47   23    4.7:1   -12    37    标题发飘，对比不足

         **配色判据（可复用）**：基色与重音的分离靠 **ΔL* ≥ ~15 或 ΔC* ≥ ~30，至少满足其一**；
         同色相时必须两轴都看 —— 本案例最终 ΔL* 17 且 ΔC* 36，双轴同时满足。
         暗色侧不动（基色是灰、重音是纯白，ΔL*≈25）。

         ㉟ 基色改回「蓝色渐变」—— 同色相窄跨度版（2026-09-21 第四轮，现行）
         用户原话：「你给我改成蓝色渐变吧，和英文一样，过渡不要太大但能看出区分」。
         ⚠️ 这**不是**回到 ㉛ 之前那条渐变。那条是 blue-600→500→400（L*36→45→58），
         跨度 22、末端（L*58）**比重音还亮**，所以「有温度」掉进同一色域 ——
         那正是 ㉛ 要解决的问题。本轮的差别是「同色相、同轴向、整段压在重音之下」：

           起点 #20305a  L*20.7  C*28.5   ← 最沉（对底色 12.3:1）
           中段 #2c3f70  L*27.5  C*31.9   ← 沿用 ㉞ 的靛蓝，作为锚点（即 --title-base）
           末端 #33457d  L*30.5  C*35.8   ← 最亮（对底色 8.8:1），仍比重音低 14.5 档

         跨度 ΔL* 9.8 / ΔC* 7.3（英文浅色是 22，满足「过渡不要太大」）；
         与重音仍是 **ΔL* 14.5 / ΔC* 40** —— ㉞ 立的两条判据（≥15 或 ≥30）继续双轴达标。
         **浅色为什么做不到和英文一样宽**：重音 blue-500 就在 L*45，渐变末端一旦越过
         L*32 就会撞上重音的明度（㉞ 那张阶梯表里 #45527a L*36 那档就是反例）。
         英文之所以能拉到 L*58，是因为它的重音 warmth 取的是渐变**起点**色 blue-600，
         靠斜体 + 波浪线区分、不靠色差 —— 中文用不了这招：「有温度」是手写细笔
         （只有正文 0.64x 的墨量），没有色差就会彻底糊进基色里。
         暗色侧没有这个约束（重音是纯白 L*100），故直接沿用英文同参数，见 dark 块。

         ㊱ 跨度再放宽 + 格式对齐英文（2026-09-21 第五轮；档位已被 ㊷ 改写为英文同阶）
         用户原话：「改成和英文一样的，以英文格式为主，渐变的再明显一点」。
         ⚠️ **不能直接照抄英文那条**（blue-600 → 500 → 400）：实测重音「有温度」
         与紧邻的「的日常」ΔL* 只剩 **+1.9**，等于消失。成因是几何而非审美 ——
         三行都是 660px 满宽的渐变盒，「有温度」占最左 194px（0~29%），
         而英文那条在 29% 处的值已经是 L*41.5，正好落在重音 blue-500（L*45）上。
         英文的 warmth 之所以能用这条参数，是因为它**取的就是渐变起点色**、
         靠斜体 + 波浪线区分；中文重音是手写细笔（只有正文 0.64x 墨量），
         没有色差就会糊回基色。所以中文只能**整体下沉一档**：

           英文   blue-600 → blue-500 → blue-400   L*35.3 → 45.0 → 58.2  跨度 22.9
           中文   blue-700 → blue-600 → blue-500   L*27.4 → 35.3 → 45.0  跨度 17.6

         **令牌体系、角度（100deg）、节点位置（0 / 46% / 100%）与英文完全同构**，
         只是档位低一档 —— 这就是「以英文格式为主」的落地方式，且跨度从
         ㉟ 的 9.8 放到 17.6（+80%）。文字只覆盖渐变盒的 0~70%（三行都是左对齐短行），
         故**可见区间**的跨度从 ≈5.6 提到 ≈12，肉眼翻倍。
         重音随之上提一档 blue-500 → blue-400（#6089e7，L*58.2 / 对底色 3.22:1，
         大字号 AA 阈值 3:1 达标），与紧邻的「的日常」实测 ΔL* ≈ +24。 */
      --title-base: var(--blue-600);
      /* 首屏中文标题的基色渐变 · **整块一道对角**（㊴，2026-09-21 第八轮）。
         315deg = 从末行末尾的「常」（右下）指向首行开头的「用」（左上）：
         0% 端色落在「常」上，100% 端色落在「用」上。
         ⚠️ ㊷（2026-09-21 第十一轮）**色序与档位都已按英文改写**，理由见下方 ㊷ 段。
         现行色序 = **「用」沉 → 「常」亮**：
           0%  端（常，右下）= blue-400 #6089e7  ← 最亮
           46% 端（中段）    = blue-500 #3563d6
           100% 端（用，左上）= blue-600 #274cae  ← 最沉
         与英文（`.hero-title > span` 那条 100deg）**同色相、同令牌、同节点位置
         （0/46/100%）、同明暗走向**，只是轴向由 100deg 转成 315deg。
         （英文是「左沉右亮」，本条是「上沉下亮」；把整块拉远看，两者的明暗梯度
         朝同一个方向走 —— 切换语言时标题的「受光方向」不再翻面。）
         ⚠️ 改这里等于同时改「重音的可用范围」：**要换过渡方向，就把这条的色序倒过来** ——
         规则本体在下方 ㊴（父元素 .hero-title 上），不必动。
         ⚠️ 暗色那条（文件下方 dark 块）**不动**：它的色序 blue-400→600→700 在暗色
         令牌下本来就是「用亮 → 常沉」，与英文暗色一致（英文暗色 blue-600→500→400
         换算成暗色令牌恰好是「左亮右沉」）。 */
      --title-grad: linear-gradient(315deg, var(--blue-400) 0%, var(--blue-500) 46%, var(--blue-600) 100%);


      /* 中性色 · 冷调微蓝（消除原来的泛黄）。基底 #F8FAFC，灰阶向蓝通道偏移 */
      --ink:        #11141c;
      --ink-muted:  #3c4452;
      --ink-light:  #69717f;
      --ink-faint:  #98a0ae;
      --surface:    #ffffff;      /* 卡片 / 拍立得：纯白，在冷调底上自然浮凸 */
      --surface-3:  #eff4fa;
      --bg:         #f8fafc;      /* 极浅中性冷白（基底） */
      --bg-top:     #eff5fb;      /* 方案B：顶部泛微蓝的天空浅底 */
      --bg-bottom:  #fbfcfe;      /* 方案B：底部趋近纯净冷白 */
      --bg-subtle:  #eef3f9;
      --danger:     #d94a4a;

      /* 描边用带蓝相的浅灰（而非中性黑半透明），叠在冷白底上才不显脏 */
      --line:        rgba(30,60,100,0.10);
      --line-strong: rgba(30,60,100,0.17);
      --line-bold:   rgba(30,60,100,0.27);
      --nav-bg:      rgba(250,252,255,0.94);
      --control-bg:  rgba(255,255,255,0.72);
      --control-hover: var(--blue-50);
      --overlay-bg:  rgba(248,250,252,0.72);
      --modal-overlay-bg: rgba(20,32,54,0.38);
      --image-bg:    #e7eef7;
      --image-gradient-end: #e2ecfb;
      /* 主题切换波纹：切到暗色时用暗色底（读值发生在切换之前） */
      --wipe-next:   #0a0c11;

      /* 手账网格：32px 细格 + 160px 主格，1px 细线 */
      --grid-line:       rgba(30,60,100,0.045);
      --grid-line-major: rgba(180,200,220,0.15);
      /* 环境光：顶部冰川蓝柔光 + 底部极淡冷蓝余韵 */
      --glow-top:        rgba(224,237,255,0.55);
      --glow-top-2:      rgba(240,246,255,0.24);
      --glow-bottom:     rgba(53, 99, 214, 0.05);

      /* 阴影分层：收纳 → 悬浮 → 抬起。色温混入蓝青 rgba(20,40,70) 代替死黑，
         在微蓝底上更干净轻薄，不需要靠加重阴影来撑立体感 */
      --shadow-xs:   0 1px 2px rgba(20,40,70,0.05);
      --shadow-sm:   0 2px 6px -2px rgba(20,40,70,0.07), 0 1px 2px rgba(20,40,70,0.04);
      --shadow-card: 0 2px 4px rgba(20,40,70,0.04), 0 14px 28px -8px rgba(20,40,70,0.16), 0 4px 10px -2px rgba(20,40,70,0.06);
      --shadow-md:   0 20px 48px -14px rgba(20,40,70,0.22), 0 6px 16px -4px rgba(20,40,70,0.08);
      --shadow-lift: 0 36px 72px -24px rgba(20,40,70,0.26), 0 12px 24px -8px rgba(20,40,70,0.12);
      /* 缩略图悬停的「落地阴影」：紧贴下缘的接触影 + 外扩的环境影，
         合成出来是照片被轻轻抬起、下方压出一小片阴影（旅途列表悬停用） */
      --shadow-photo: 0 2px 5px -2px rgba(20,40,70,0.13), 0 13px 24px -12px rgba(20,40,70,0.30);
      --shadow-lg:   0 44px 90px -34px rgba(20,40,70,0.30);

      --radius-sm: 0.6rem;
      --radius-md: 0.95rem;
      --radius-lg: 1.35rem;
      --radius-xl: 1.75rem;
      --radius-2xl: 2.25rem;

      /* 展示衬线（拉丁 Playfair / 中文宋体），正文 Satoshi，标签与数字用等宽 */
      --font-display: 'Playfair Display', 'Noto Serif SC', 'Songti SC', 'STSong', 'Source Han Serif SC', 'Source Han Serif CN', ui-serif, Georgia, 'Times New Roman', serif;
      --font-body:    'Satoshi', 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
      --font-mono:    ui-monospace, 'SF Mono', SFMono-Regular, Menlo, 'Roboto Mono', monospace;

      --ease-spring: cubic-bezier(0.22, 0.61, 0.36, 1);
      --ease-out:    cubic-bezier(0.16, 1, 0.3, 1);
      --ease-inout:  cubic-bezier(0.65, 0, 0.35, 1);
      /* 旅途照片堆叠专用（2026-09-23）：起步果断、尾巴收得干净，
         末尾带一点点回弹，用来做「抽走一张相片」的手感。 */
      --ease-deck:   cubic-bezier(0.26, 0.94, 0.3, 1);
      --t-fast: 0.18s var(--ease-out);
      --t-mid:  0.2s var(--ease-out);

      /* 焦点环 */
      --ring: 0 0 0 3px color-mix(in srgb, var(--blue-500) 28%, transparent);
      --ring-blue: 0 0 0 3px color-mix(in srgb, var(--blue-500) 32%, transparent);

      /* ㊹ 详情弹层（旅途 / 启发）版式令牌 —— 2026-10-01。
         ─────────────────────────────────────────────────────────────────────
         起因（用户两张截图）：「这两个页面给我左对齐，退出按钮移到右上角」。
         实测（detail_geom.mjs @1440×900，本地 3999）揪出三处**互不相干的**病因，
         都不是「哪个元素写错了对齐」，而是三条各自独立的规则在互相抵消：

         ① 左右两条内边距**数值一样、含意不同**：
              弹窗：padding 2.25rem（= 36px）—— 内边距；
              .modal-header：padding 29.6px 36px 18.4px —— 也是内边距。
             两者都成立，但 .modal-header 里那个 ✕ 是 `flex: 0 0 32px` 的定宽块，
             于是「内容右缘」= 1150 − 36 − 32 = **1082px**，
             而正文右缘 = 1150 − 36 = **1114px**。
             ⇒ 右对齐坏在「✕ 占位」上，不是内边距。实测偏差 **32px**。
             左缘反而是齐的（两处都是 36px 内边距 ⇒ 都是 327/363px），
             所以「左对齐」的真实症状是**标题被挤窄后提前折行**：
               .detail-title 的可用宽 = 弹窗内容宽 − ✕ 宽 − header 的 gap(1rem)
                 = 788 − 32 − 16 = **740px**，实测标题盒恰好 **682px**（= 786 − 72 − 32）。
             标题一折行 → 看起来「整块往右缩了」，用户读作「没左对齐」。
             🔴 所以**不能只把 ✕ 挪走**：`space-between` 在单子项时把它推去左边，
                标题会**继续被挤窄**、折行照旧。必须同时把标题的可用宽还回去。

         ② 尺寸全写死，宽窄屏互不知情：`width: min(92vw, 860px)` 配上一票
            「720p 正合适」的定值（gallery 主图 `max-height: min(52vh, 540px)`、
            modal `max-height: min(86vh, 820px)`）。两处都只认视口、不认弹窗实际高度。
            窄屏必然溢出（列在 .detail-* 的窄屏段里，不在这里复述）。

         ③ **照片不在「左对齐」范围内**（这一条我判断错过两次，用户最后亲口纠正：
            「照片盒居中别左对齐我没说清楚」）。左对齐指的是文字轴，
            `.detail-main-photo` 维持基线写法不动 —— `width: 100%` 整幅盒 +
            `object-fit: contain` + `object-position: center center`，
            竖版照片左右对称留白（中性灰底 `--bg-subtle` 就是给 contain 留白用的）。
            🔴 别再顺手改它的 `object-position` 或盒宽，那会把照片挪出中轴。

         ⇒ 除主图外，其余全部收进下面这组令牌，`.detail-*` 本体只读令牌、不再出现定值：
            改版式 = 改令牌，窄屏覆盖 = 重定义令牌（见各 @media 段）。 */
      --detail-pad-x:        2.25rem;   /* 弹窗左右内边距（内容左缘基准） */
      --detail-pad-top:      2.25rem;
      --detail-pad-bottom:   2rem;
      --detail-title-size:   clamp(1.8rem, 4vw, 2.6rem);
      /* ✕ 的纵向补偿量：详情弹层的头部 pad-top 恒为 0（内缩由弹窗 pad-top 提供），
         与普通弹窗的结构差一层，靠这个值把「✕ 上距」拉平（推导见 .modal-close 段）。
         🔴 它**随断点变**：宽屏普通弹窗的 ✕ 是 flex 居中、窄屏是 flex-start，
            两者基准不同 ⇒ 窄屏必须归零（见 @media 段），否则会差 3.2px。 */
      --detail-close-nudge:  -0.2rem;
      /* 主图：高度按**弹窗自身高度**取（dvh），不再只认浏览器窗口 ——
         vh 只认窗口，弹窗还有 overlay 的垂直内边距与自身 86vh 上限，
         两者不同步就会溢出。 */
      --detail-photo-h:      clamp(220px, 46dvh, 520px);
      --detail-photo-fit:    contain;
      /* 照片在盒内居中 —— **不要改**：用户明确要求照片盒保持居中
         （「照片盒居中别左对齐我没说清楚」）。左对齐只作用于文字轴。 */
      --detail-photo-align:  center center;
      --detail-title:        var(--ink);
    }

    /* ㊺ 弹窗头部 + ✕ 的统一口径（**两类弹窗共用**，2026-10-01）
       ─────────────────────────────────────────────────────────────────────
       用户对照图把差异点出来：详情弹层（旅途/启发）与普通弹窗（添加/编辑）的 ✕
       是两套位置。实测（close_cmp.mjs @1440×900）：

         普通弹窗  右距 37.0  上距 33.8  与标题中心 Y 差 0      ← 定宽块，flex 居中
         详情弹层  右距 18.6  上距 18.6  与标题中心 Y 差 −85.7  ← 绝对定位贴角

       统一取**普通弹窗那套**：它出现频率更高，且「✕ 与标题同高」正好说明
       「关掉这个弹窗」。落到详情弹层上的做法不是照抄数值，而是给它保留
       同一套结构语义 —— 头部内边距与弹窗内边距对齐、✕ 与标题块垂直居中。

       ⚠️ 两处 token 是**同一份声明**（同一块 CSS 内、同一层叠上下文），
          `.modal:not(.detail-modal)` 与 `.modal.detail-modal` 权重相同，
          **靠源码顺序决胜** —— 详情版必须写在后面，否则会被前者盖掉。 */
    .modal:not(.detail-modal) {
      --modal-head-pad-top:    1.85rem;
      --modal-head-pad-x:      2.25rem;
      --modal-head-pad-bottom: 1.15rem;
      --modal-close-size:      32px;
      --modal-head-gap:        1rem;
    }
    .modal.detail-modal {
      /* 横向内缩交给 .detail-modal 自己的 --detail-pad-x，头部左右归零（免二次内缩）；
         纵向则交给头部自己（见下），这样 ✕ 到弹窗上/右边缘的距离
         与普通弹窗**逐像素相等**：
             普通弹窗： modal pad 0      + header pad 1.85rem / 2.25rem
             详情弹层： modal pad 1.85rem + header pad 0 / 2.25rem(由 modal 的 pad-x 提供)
         两条路径算出来的「✕ 右距 36px / 上距 29.6px」完全一致。 */
      --modal-head-pad-top:    0;
      --modal-head-pad-x:      0;
      --modal-head-pad-bottom: 1rem;
      --modal-close-size:      32px;
      --modal-head-gap:        1rem;
    }

    @font-face {
      font-family: 'Satoshi';
      src: local('Satoshi Light'), local('Satoshi-Light'),
           url('../fonts/satoshi-300.woff2') format('woff2');
      font-weight: 300;
      font-display: swap;
      font-style: normal;
    }
    @font-face {
      font-family: 'Satoshi';
      src: local('Satoshi'), local('Satoshi Regular'), local('Satoshi-Regular'),
           url('../fonts/satoshi-400.woff2') format('woff2');
      font-weight: 400;
      font-display: swap;
      font-style: normal;
    }
    @font-face {
      font-family: 'Satoshi';
      src: local('Satoshi Medium'), local('Satoshi-Medium'),
           url('../fonts/satoshi-500.woff2') format('woff2');
      font-weight: 500 600;
      font-display: swap;
      font-style: normal;
    }
    @font-face {
      font-family: 'Satoshi';
      src: local('Satoshi Bold'), local('Satoshi-Bold'),
           url('../fonts/satoshi-700.woff2') format('woff2');
      font-weight: 700 900;
      font-display: swap;
      font-style: normal;
    }
    /* 首屏中文标题专用衬线：只含「用代码与工具构建有温度的日常」15 字，3.6KB。
       为什么必须自托管这几个字 —— 实测（2026-09-18）站点从 fonts.loli.net 拉的
       Noto Serif SC 在 wght 400 与 500 下墨量完全相同（46,210 vs 46,210，同一真实字面），
       600/700 只是浏览器「合成加粗」；CDP CSS.getPlatformFontsForNode 报出的实际字面是
       Noto Serif SC ExtraLight。合成粗体只做整体等效横向加粗，宋体赖以立身的
       「横细竖粗」对比会被抹平 —— 这正是大字号下发虚、没有印刷感的真凶。
       改用真实静态 700 字面后墨量 60,801 → 65,338，笔画更实且横竖对比正常。
       子集外的字形会自动回落到后面的 Noto Serif SC / Songti SC，不会出现豆腐块。 */
    @font-face {
      font-family: 'HeroSerif';
      src: url('../fonts/noto-serif-sc-700-hero.woff2') format('woff2');
      font-weight: 700; font-style: normal; font-display: swap;
    }
    /* 手写体（关键词「有温度」专用，3 字 / 2.3KB）。
       注意 `InkBrush` 是**角色名**不是字体名 —— 换字体只改 src，CSS/JS 一律不动。
       当前指向：「山海众侠夜无雨W」的**最细档**（从字形轮廓腐蚀派生的 e=22，见下）。
       重量关系：正文是宋体真 700（基准 1.00x），这支细笔只有 **0.64x** ——
       用户要的效果是「同一道蓝里，突然换成一支细笔」的重量级差，
       而不是再去别处找一款字体（实测所有备选毛笔体都在 0.46–0.76x 之间，
       换谁都不如「从同一支笔里派生」可控）。
       派生脚本与全部档位：abobb-fontlab/derive-weight.py
       （e=10 → 0.88x、e=16 → 0.76x、e=22 → 0.64x，改一个数字即可换档）。
       授权与备选（当前未被引用，浏览器不会下载，换 src 即可）：
         assets/fonts/ma-shan-zheng-brush-hl.woff2   马善政毛笔楷膨胀档 e=+14 → 1.00x
           （SIL OFL 可商用；想换成「与正文等重的毛笔楷」时用它，OFL 全文见 assets/fonts/OFL-MaShanZheng.txt）
         assets/fonts/xingqi-ba-hand-hero.woff2       星期八de電子日記（NIPPER，个人非商业，0.76x）
       「山海众侠夜无雨W」版权 Shanghai Shisui Technology Co.,Ltd，来源字体天下，
       授权为「个人非商业免费」（本站为个人非商业站点）。 */
    @font-face {
      font-family: 'InkBrush';
      src: url('../fonts/shanhai-thin-hl.woff2') format('woff2');
      font-weight: 400; font-style: normal; font-display: swap;
    }
    /* 「AI」两个大写字母专用的西文衬线（2026-09-21 第三轮定稿：Source Serif 4）。
       三轮演进，每轮都是「实测 → 换 → 用户看 → 再实测」：
         ① Playfair Display 700（初版）→ 用户：「衬线脚非常大、字形方笨」
         ② Cormorant Garamond 600     → 用户：「AI 这两字母很突兀」
         ③ Source Serif 4 SemiBold    ← 当前
       ⚠️ ② 是个值得记下的教训。当时选型用的是「I 竖干宽 / 大写高」这个光学重量比，
         量出 Cormorant 600 = 0.152、中文「工」基准 ≈ 0.16，只差 4%，
         于是判定「重量对上了就能融进去」—— 结果恰恰相反，它是最突兀的一版。
         根因：Cormorant 属 **Didone 式高对比字体**，竖干细到像没上墨（可见的只有
         一条线），全靠又宽又高的字面把**总面积**撑回 0.152。可挨着一行粗壮宋体时，
         眼睛读的是**竖干的绝对宽度**，不是面积比值 —— 指标选错，拟合得再准也白搭。
         打字层面真正可用的判据只有一条：**笔画对比度必须与中文同档**。
       ③ 选 Source Serif 4 的理由：它就是思源宋体（Source Han Serif）的**拉丁母体**，
         同属 Adobe 的 Source 字系 —— x-height、笔画对比、衬线长度与中文同出一系；
         与 Playfair（现代式、方头衬线）、Cormorant（古典加拉蒙、尖头细衬线）都不同族。
         子集仍只含 A、I 两个字形，**1.6KB**，保留 opsz 轴（8–60，光学尺寸自适应）。
       同字号（240px）渲染后按「墨迹面积 / 外接框」算的墨量比，六个候选排队：
         Cormorant 600  0.255  ← ② 太轻
         Cormorant 700  0.291
         **Source Serif 4 600  0.306  ← ③ 采用**（正好落在「太轻」与「太笨」的中间档）
         Noto Serif SC 700     0.338
         Playfair 600          0.347  ← ① 太笨
         Noto Serif 600        0.390
       授权 SIL OFL 1.1，全文见 assets/fonts/OFL-SourceSerif4.txt
       ⚠️ 2026-09-21 收尾：②的 cormorant-garamond-ai.woff2 与它的 OFL 授权文件、
       以及早期自托管过的 playfair-display-latin(.woff2 / -italic.woff2)
       四个文件都已在站点与部署目录里删除（全站已无任何 url() 引用它们；
       Playfair 现在只从 fonts.loli.net 的 CDN 加载）。需要回滚见 /tmp 备份。） */
    @font-face {
      font-family: 'AISerif';
      src: url('../fonts/source-serif-4-ai.woff2') format('woff2');
      font-weight: 600; font-style: normal; font-display: swap;
    }
    /* 备用字体（当前未被任何规则引用，浏览器不会下载；换风格时改 src 即可）：
         assets/fonts/xingqi-ba-hand-hero.woff2  星期八de電子日記（NIPPER，个人非商业）
       2026-09-18 第四轮撤下的两款「山海众侠夜无雨W」字库（原字重 30KB + 派生细字重 41KB）
       已移出站点 → 项目根 abobb-fontlab/backup-shanhai/，站上不再引用。
       为什么撤：用户判定「主标题整段圆钝手写 = 低龄感 / 小红书手账感，工程师气质没了」。
       保留的教训（实测墨量，基准 = 正文宋体 700 的「的日常」= 0.3193）：
         山海自身 1.08x ｜ 星期八 0.76x ｜ 马善政原重 0.72x ｜ 志莽行书 0.70x ｜ 龙藏体 0.46x
       —— 「关键词换一款手写体」这条路上，除山海外全是偏轻的，谁上都像被弱化。
       结论：中文侧的正确结构是**大面积正统印刷体 + 极小面积手写笔触**（对齐英文版），
       手写体只承担「有温度」三字，并且必须先把墨量补到与正文等重。 */

    html[data-theme="dark"] {
      --blue-50:  #121a2b;
      --blue-100: #16223a;
      --blue-300: #32507f;
      --blue-400: #6d9be8;
      --blue-500: #83b0f4;
      --blue-600: #a9c8f9;
      --blue-700: #cbddfc;
      --blue-900: #e6eefd;

      /* 实心蓝渐变 · 暗色版。原先是 blue-500 → blue-400（#83b0f4 → #6d9be8），
         两端 ΔL* 只有 **7**，而且方向是「亮 → 沉」—— 铺在 50px 的胶囊上就是一块纯色，
         用户反馈「浅色模式是自然过渡，暗色模式不像」。
         改成 400 → 500 → 600：ΔL* ≈ 17，与浅色侧的 ≈18 对齐；
         顺序仍是「最沉 → 最亮」（暗色阶里 400 最沉、600 最亮），所以和浅色同构。 */
      --grad-solid: linear-gradient(118deg, var(--blue-400) 0%, var(--blue-500) 55%, var(--blue-600) 100%);

      /* 首屏中文标题的基色渐变 · 暗色版（㊴ 起同为整块对角，方向同浅色：右下沉 → 左上亮）。
         暗色蓝阶是反相的（400 最沉 / 700 最亮），所以**色值不能照抄浅色那组**：
         浅色 0% 端取 blue-700（最沉）、100% 端 blue-500（最亮）；
         暗色 0% 端取 blue-400（最沉）、100% 端 blue-700（最亮）。
         跨度仍是 L* 24：实测重音（纯白 L*100）与紧邻「的日常」ΔL* ≈ +18，
         在 ㉞ 立的 ≥15 判据之上。 */
      --title-base: var(--blue-600);
      --title-grad: linear-gradient(315deg, var(--blue-400) 0%, var(--title-base) 46%, var(--blue-700) 100%);


      --ink:        #f1f4f9;
      --ink-muted:  #b3bccb;
      --ink-light:  #8a93a4;
      --ink-faint:  #616a7b;
      --surface:    #151922;
      --surface-3:  #1c2230;
      --bg:         #0a0c11;
      --bg-top:     #0d1018;
      --bg-bottom:  #08090e;
      --bg-subtle:  #0f1218;
      --danger:     #f07575;

      --line:        rgba(255,255,255,0.08);
      --line-strong: rgba(255,255,255,0.15);
      --line-bold:   rgba(255,255,255,0.26);
      --nav-bg:      rgba(13,15,21,0.90);
      --control-bg:  rgba(255,255,255,0.055);
      --control-hover: rgba(131,176,244,0.14);
      --overlay-bg:  rgba(9,11,15,0.76);
      --modal-overlay-bg: rgba(3,5,9,0.66);
      --image-bg:    #191e29;
      --image-gradient-end: #1e2838;
      /* 主题切换波纹：切回浅色时用浅色底 */
      --wipe-next:   #f8fafc;

      --grid-line:       rgba(140,175,225,0.05);
      --grid-line-major: rgba(140,175,225,0.09);
      --glow-top:        rgba(109,155,232,0.16);
      --glow-top-2:      rgba(109,155,232,0.06);
      --glow-bottom:     rgba(131, 176, 244, 0.055);

      --shadow-xs:   0 1px 2px rgba(0,0,0,0.34);
      --shadow-sm:   0 2px 8px -2px rgba(0,0,0,0.5), 0 1px 2px rgba(0,0,0,0.3);
      --shadow-card: 0 2px 5px rgba(0,0,0,0.4), 0 16px 32px -10px rgba(0,0,0,0.78), 0 4px 12px -2px rgba(0,0,0,0.45);
      --shadow-md:   0 22px 50px -16px rgba(0,0,0,0.86), 0 6px 16px -4px rgba(0,0,0,0.5);
      --shadow-lift: 0 40px 80px -24px rgba(0,0,0,0.94), 0 12px 26px -8px rgba(0,0,0,0.6);
      --shadow-photo: 0 2px 6px -2px rgba(0,0,0,0.55), 0 15px 28px -14px rgba(0,0,0,0.85);
      --shadow-lg:   0 48px 96px -36px rgba(0,0,0,0.94);
    }

    /* ── 基础 ── */
    html {
      scroll-behavior: smooth;
      /* 方案B：纵向极轻渐层 —— 顶部泛微蓝的天空浅底 → 底部趋近纯净冷白。
         根元素背景会铺满整张画布（= 整页），所以渐层是全页而非仅视口 */
      background-color: var(--bg);
      background-image: linear-gradient(180deg, var(--bg-top) 0%, var(--bg) 44%, var(--bg-bottom) 100%);
      background-repeat: no-repeat;
      transition: background-color 0.24s var(--ease-out);
      -webkit-text-size-adjust: 100%;
    }
    html[data-theme="dark"] { color-scheme: dark; }
    html[data-theme="light"] { color-scheme: light; }

    body {
      font-family: var(--font-body);
      font-weight: 400;
      color: var(--ink);
      /* 必须透明：不透明的块级背景会绘制在 z-index:-1 的环境层之上，
         把 .ambient-bg 的网格与柔光整个盖掉（底色交给 html 画布） */
      background: transparent;
      line-height: 1.6;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      overflow-x: hidden;
      min-height: 100dvh;
      display: flex;
      flex-direction: column;
      font-feature-settings: 'kern' 1, 'liga' 1;
    }

    /* 路由 / 语言 / 主题切换态 —— 前两条只在 JS 存在时生效，见 head 里的 html.js。
       一律写成 `:where(html.js)`：门闸本身不吃权重，后面每条规则的权重才和加闸门前
       完全一致（理由见下方 .reveal 那段——这里踩过一次，把作品卡 hover 倾角压没了）。 */
    :where(html.js) body.route-loading .hero,
    :where(html.js) body.route-loading .page-section,
    :where(html.js) body.route-loading .footer { visibility: hidden; }
    :where(html.js) body:not(.language-ready) .nav-actions { visibility: hidden; }
    html.boot-fail .reveal { opacity: 1 !important; transform: none !important; }
    body.theme-transition,
    body.theme-transition .nav,
    body.theme-transition .hero,
    body.theme-transition .section,
    body.theme-transition .footer,
    body.theme-transition .modal-overlay,
    body.theme-transition .search-overlay,
    body.theme-transition .portfolio-card,
    body.theme-transition .travel-item,
    body.theme-transition .inspo-item,
    body.theme-transition .ambient-bg,
    body.theme-transition .toast {
      transition-duration: 0.2s;
      transition-property: background-color, background, border-color, color, box-shadow;
      transition-timing-function: var(--ease-out);
    }
    /* <body> 的初始 class 写死 home-page。没有 JS 时它不会被改写成 content-page，
       分区若在这里被 display:none，导航的 #work / #travel 锚点就全是死链 ——
       所以这条也归到 html.js 作用域：无 JS 退化成一张分区依次排开的长页。 */
    :where(html.js) body.home-page .page-section { display: none; }
    body.home-page .hero {
      min-height: calc(100svh - 96px);
      /* 2026-10-27：与 layout.css 的 .hero 同步改成按视口收紧 —— 这里特异性更高
         (0,2,0 对 0,1,0)，固定 6.5rem 会把那边的 clamp 整个压掉（实测 padding 仍是 104px）。
         四行标题下 1280×800 需要省 ~48px 才能让 CTA 留在首屏。 */
      padding-top: clamp(5rem, 11vh, 6.5rem);
      padding-bottom: clamp(3rem, 6vh, 4.5rem);
    }
    body.home-page .footer { padding: 1.5rem 0 1.6rem; }
    body.home-page .footer-links { margin-bottom: 0.7rem; }
    body.content-page .hero { display: none; }
    body.content-page .page-section { display: none; min-height: calc(100vh - 190px); padding-top: 8.5rem; }
    body.content-page .page-section.active { display: block; animation: sectionFadeIn 0.34s var(--ease-out); }
    /* @keyframes sectionFadeIn 的唯一定义在文件下方 ⑥（带 blur 的版本）。
       这里原来还有一份同名 from/to（translateY 6px、无 blur），被后面那份整体覆盖，
       2026-09-21 收尾时删除 —— 同名 keyframes 只保留最后一份。 */

    /* 选中文本：蓝绿渐变 */
    ::selection { background: color-mix(in srgb, var(--blue-300) 45%, transparent); color: var(--ink); }
    html[data-theme="dark"] ::selection { background: color-mix(in srgb, var(--blue-400) 34%, transparent); }

    /* 按钮与交互控件：彻底消除划选/拖拽时在圆角按钮背后露出的浅蓝矩形底框 */
    button,
    .btn-solid,
    .btn-line,
    .btn-cancel,
    .btn-submit,
    .btn-add,
    .hero-actions,
    .hero-actions *,
    .modal-actions,
    .modal-actions *,
    .modal-header,
    .modal-title,
    .modal-close,
    .nav-logo,
    .filter-tag {
      -webkit-user-select: none !important;
      user-select: none !important;
      -webkit-touch-callout: none;
    }

    /* 选区背景彻底清空：只针对 ::selection 伪元素，不影响按钮自身背景 */
    button::selection,
    button *::selection,
    .btn-solid::selection,
    .btn-solid *::selection,
    .btn-line::selection,
    .btn-line *::selection,
    .btn-cancel::selection,
    .btn-cancel *::selection,
    .btn-submit::selection,
    .btn-submit *::selection,
    .btn-add::selection,
    .btn-add *::selection,
    .hero-actions::selection,
    .hero-actions *::selection,
    .modal-actions::selection,
    .modal-actions *::selection,
    .modal-header::selection,
    .modal-header *::selection,
    .modal-close::selection,
    .modal-title::selection {
      background: transparent !important;
    }

    /* 保证表单输入框内文字仍可正常高亮选择 */
    .modal input,
    .modal textarea {
      -webkit-user-select: text !important;
      user-select: text !important;
    }
    .modal input::selection,
    .modal textarea::selection {
      background: color-mix(in srgb, var(--blue-300) 45%, transparent) !important;
      color: var(--ink) !important;
    }
    html[data-theme="dark"] .modal input::selection,
    html[data-theme="dark"] .modal textarea::selection {
      background: color-mix(in srgb, var(--blue-400) 34%, transparent) !important;
    }

