/* ══════════════════════════════════════════════════════════════════════════
   Yarrow Landing — 版面

   版面 / 配色 / 动效由设计侧决定；文案与栏目属内容层，改动先过产品。

   方向：**老式单色液晶读数**，只有四级明度。见 tokens.css 顶部。

   骨架：12 列网格（不画线），全宽不居中。
   结构靠四样东西撑：**区块的深浅交替**、区块之间的满幅横线、
   元素自身的边框、**6px 点阵栅格**。
   · **不用浅浅交替** —— 原因是 g0/g1 对比只有 1.08 看不出来；
     四级之后 g0/g1/g2 已经**是同一个颜色**，连做的可能都没有。
     交替一律用深浅带（`.sec-dark` = 负显液晶），反差 10.43。
     刻意不用纯黑白（21.0）—— 真实液晶只有 6–8，我们落在中间偏液晶那侧。
   · 不画背景竖线 —— 121px 间距太大不成纹理，又会穿过段落和已有边框的面板
   · **静止态不用半透明、不用模糊**，过渡态可以。
     ⚠️ 这一条 2026-07-30 改过。原文是无条件的「不用半透明、不用模糊」，
        依据是四级给不了中间值。后来 hero 的雾化换条要用 blur + opacity，
        于是把这条规矩划了辖区而不是破例 —— 完整推导见 tokens.css 原则 5。
        **别看到页面上有模糊就以为是违规**，也别据此把无条件版本改回来：
        那会把雾化和步进器的擦除一起判死。
        目前静止态用到透明度的只有一处（.product-label span 的 0.6），
        它是设计侧明确要的，代价记在那条规则的注释里。
   ══════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════════
   探索开关 · data-explore
   ══════════════════════════════════════════════════════════════════════════
   写在 index.html 的 <html data-explore="..."> 上，空格分隔的词。
   删掉一个词就恢复对应的东西，加回来就再关掉。

     no-grid   隐藏 16px 纸纹（浅色带和深色带都关）。令牌仍在，只是置 none。
     no-viz    隐藏剩下的非 SVG 装饰件，并切换到纯文字版面
     no-reveal 关掉滚动入场的扫入，所有内容常显（2026-07-30 起默认开着）

     ── 2026-07-30 新增，两个都**默认关**，为的是 hero 视频上的文字可读性 ──
     hero-lift    提视频的黑阶：contrast(.6) brightness(1.315)。
                  把最暗处从 1.79:1 拉到 3.72:1，代价是画面对比只剩六成、高光近白。
     hero-plate   文字区垫一块实底玻璃板，文字对比拉满 10.43:1，
                  代价是只能在右 5 列和板子上下看到视频。
                  沿用已有规矩「自带边框的容器 = 平整纸面」，不算新发明。
     两个开关的实测数字见本文件 HERO 背景视频 一节。

   ⚠️ 2026-07-29：**生成式 SVG 图形已彻底删除，不再由 no-viz 管**。
      设计侧确认那些图形不会再用，元素与绘图代码一并去掉了
      （要找回：存档仓库 agentese-redesign 的 git 历史）。
      no-viz 现在只管 .scrollcue / .case-empty-mark / eyebrow 方块
      这几个非 SVG 的装饰件 —— 这些仍是隐藏，可恢复。

   为什么用属性而不是注释掉代码：注释掉会丢掉「为什么这么写」的上下文，
   而且恢复时容易漏。属性是一个字的开关，且 git diff 只有一行。
   ══════════════════════════════════════════════════════════════════════════ */

/* ── no-grid ─────────────────────────────────────────────────────────────
   --grid 被 background-image 引用，置 none 即可。
   .sec-dark 内部重新定义了自己的 --grid（浅色墨迹版），所以要单独再关一次。 */
[data-explore~="no-grid"]{--grid:none;}
[data-explore~="no-grid"] .sec-dark{--grid:none;}

/* ── no-reveal ───────────────────────────────────────────────────────────
   2026-07-30 起默认开着。设计侧的理由：页面内容都不是最终版，
   每次刷新都要滚一遍才能看到内容，改稿时纯属妨碍。

   ⚠️ **JS 侧一行没动，这是刻意的。** observe() 照常跑、照常加 .in，
      只是这两条规则把视觉效果压掉了。所以要恢复只需从
      index.html 的 data-explore 里删掉 no-reveal 这个词，不用碰脚本。
      如果当初图省事把 observe() 注释掉，恢复时就得同时改两个文件，
      而且很容易忘掉其中一个。

   落地效果与 prefers-reduced-motion 那条完全一致（见文件末尾），
   也就是说这个分支本来就一直在被一部分用户跑着，不是没验证过的新路径。

   恢复之前记得先调触发时机：原来 rootMargin 底部是 -6%，
   意思是元素要越过「视口底沿再往上 54px」才开始显形 ——
   于是必须先滚到它跟前，它才在屏幕最下沿冒出来。
   要留这个效果就把那个值改成正的（+15% 上下），让它提前开始。

   ⚠️ 选择器带上 html 是**必要的**，不是啰嗦。
      `[data-explore~="no-reveal"] .reveal.in` 的特异性是 (0,3,0)，
      和文件后面的 `.reveal[data-d="1"].in` 完全相同 —— 同级就靠先后决胜，
      而本节在文件最前面，会输。
      加个 html 变成 (0,3,1)，无论写在哪都稳赢。
      这个开关是「随时开随时关」的东西，不该对它在文件里的位置有要求。 */
html[data-explore~="no-reveal"] .reveal,
html[data-explore~="no-reveal"] .reveal.in{clip-path:none;animation:none;}

/* ── no-viz ──────────────────────────────────────────────────────────────
   两部分：先隐藏图形，再把因此变空的双列版面收成单列文字版面。
   ⚠️ nav 里的 logo 不算「图形化内容」，是品牌标识，保留。 */
/* 生成式 SVG 图形已彻底删除（2026-07-29），不再需要在这里隐藏 ——
   原来这份清单里的 .hero-viz / .story-illustration /
   .banner-side-viz / .viz-labels 以及一条 svg 兜底规则都已连同元素一起去掉。
   留在这里的是**非 SVG** 的装饰件，它们只是隐藏、随时可恢复。
   2026-08-03 新增的 How it works 控制台插图是正式内容，不受这个开关影响。 */
[data-explore~="no-viz"] .scrollcue,
[data-explore~="no-viz"] .case-empty-mark,
[data-explore~="no-viz"] .eyebrow i,
[data-explore~="no-viz"] .banner-eyebrow i{display:none;}

/* 图形一去，原来「文字左 / 图形右」的双列就只剩一根窄柱子。
   收成单列，并按文字自己的舒适行宽定宽度 —— 用 ch 不用栏位，
   因为现在决定版面的是行长（65–75 字符），不是网格。 */
[data-explore~="no-viz"] .banner-text{grid-column:1/9;}
[data-explore~="no-viz"] .banner-side{grid-column:9/13;}
[data-explore~="no-viz"] .banner-desc{max-width:min(56ch,100%);}
/* eyebrow 的方块没了，左边那道 gap 也要收掉，否则标签会莫名内缩 */
[data-explore~="no-viz"] .eyebrow,
[data-explore~="no-viz"] .banner-eyebrow{gap:0;}
/* How it works 已改为五行流程，不再由 no-viz 改写栏数。 */

*{box-sizing:border-box;margin:0;padding:0;}
/* 纸纹挂在 html 上而不是 body 上：
   body 的背景会被浏览器上提到画布，某些内嵌预览容器里这一步会失效。
   挂根元素最稳，且天然覆盖整个可滚动区域。 */
html{
  scroll-behavior:smooth;
  background-color:var(--paper);
  /* 点阵栅格是显示基底。--grid 现在是 radial-gradient 打点，
     必须同时给 background-size（节距），否则点会被拉成一整片。 */
  background-image:var(--grid);
  background-size:var(--grid-size);
  min-height:100%;
}
body{
  font-family:var(--font-body);
  color:var(--text);-webkit-font-smoothing:antialiased;
  font-size:16px;line-height:1.6;
  background:transparent;
}
/* 自带边框的容器 = 平整纸面，纸纹挡在外面。规则见 tokens.css */
.case-empty,.drop-menu{background-color:var(--paper);}
/* 小标签：与展示字同源，但字重高一档 —— 等宽字小尺寸下笔画容易糊 */
.mono{font-family:var(--font-mono);font-weight:var(--label-weight);
      font-variant-numeric:tabular-nums;}

:root{--pad:40px;--gut:24px;}   /* --navh 已随吸顶栏取消而移除 */
@media(max-width:900px){:root{--pad:20px;--gut:14px;}}

.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--gut);
      padding-left:var(--pad);padding-right:var(--pad);position:relative;}
/* .grid 的子元素若要 padding，必须带 var(--pad)，写 0 会覆盖左右边距 */

/* ── 区块 ───────────────────────────────────────────────────────────── */
/* 上下内边距提成变量：Products 那道撑满全高的竖线要靠它们算负边距。
   与 .mast / --mast-top 同一个理由 —— 两处必须是同一个值，
   写死两遍的话改了区块内边距，竖线会短一截或探出去。 */
.sec{position:relative;--sec-top:96px;--sec-bot:104px;
  padding-top:var(--sec-top);padding-bottom:var(--sec-bot);
  border-top:1px solid var(--edge);}
/* .sec-quiet（底色走 --surface = g1）已移除 —— 它正是「浅浅交替」，
   与纸的对比只有 1.08，等于什么也没做。About 区块改用 .sec-dark。 */

/* ── HERO 版块 ────────────────────────────────────────────────────────
   原来是空白占位（高度由 JS 镜像 Products），现在有内容了，
   高度改由内容决定，镜像已取消。

   轮播占左 7 列而不是通栏 —— 标题的三行硬换行是按 7 列算的
   （Azeret Mono 字宽 0.65em，−5% 字距后 0.60em，1280 视口每行 16.0 字符）。
   给它通栏，那些换行位置就不再对应实际行宽了。 */
.sec-hero .hero-rotator{grid-column:1/8;}
@media(max-width:900px){.sec-hero .hero-rotator{grid-column:1/-1;}}

/* ── HERO 背景视频 ────────────────────────────────────────────────────
   视频是**素材层**，不是界面层。四级量化管界面（字、线、填充块），
   连续调的素材从窗口里透过来，不受那条规矩约束 —— 否则视频根本没法用。
   这是四级体系的第二个明确例外，第一个是 .mast-tag（见 tokens.css 原则 4）。

   实测（测试素材：高调 3D 无限环，居中构图，主体占画面宽度 66 percent）：

     文字色 --on #2B2D31 压在视频上的对比度
     ┌────────────────┬──────────┬──────────┬────────────────────────┐
     │ 状态           │ 最暗0.2% │  2% 分位 │ 判定                    │
     ├────────────────┼──────────┼──────────┼────────────────────────┤
     │ 原片           │  1.79:1  │  2.19:1  │ 82px 大字都不过（需3.0）│
     │ hero-lift      │  3.72:1  │  4.25:1  │ 大字过，16px 正文不过    │
     │ hero-plate     │ 10.43:1  │ 10.43:1  │ 全过                    │
     └────────────────┴──────────┴──────────┴────────────────────────┘

   为什么原片过不了：主体居中且占画面宽度 66 percent，而文案在左 7 列 ——
   横向怎么挪都躲不开（试过放大后右移，主体左缘仍落在第 3 列）。
   这不是参数问题，是构图问题：hero 背景视频需要**主体偏一侧、另一侧留空**的素材。

   降级：这一节全是 CSS + HTML，无脚本参与。视频拿不到就露出 --off 玻璃底。 */
.sec-hero{overflow:hidden;background-color:var(--off);
  /* 上边框**宽度归零**，不是只改成透明。
     与导航栏的交界线由 .mast 的 border-bottom 单独负责，
     两边都画会叠成 2px —— 原来「上边比下边粗」就是这么来的。

     ⚠️ 为什么不能只写 border-top-color:transparent（第一版就是这么写的，错了）：
        绝对定位元素的 inset:0 是相对**内边距盒**解析的，不含边框。
        所以 .hero-video 和 .sec-hero::after（点阵）都从边框**以内**开始铺，
        而那 1px 透明边框露出的是 .sec-hero 自己的 background-color（玻璃 #DEE0E3）。
        玻璃比视频底色（约 206）亮，于是分隔线下面多出一条 1px 的亮缝。
        肉眼能看出来 —— 设计侧在截图里指出来的。
        「用 transparent 保住几何」在别处是对的，在这里保住的恰好是那条缝。 */
  border-top-width:0;
  /* 高度 +100px（2026-07-30）。加在上下内边距上，不设固定高度 ——
     轮播三条的行数一样，内容高度本来就稳定，给死高度只会多一个会打架的约束。

     96 → 144（+48）  上
     104 → 156（+52） 下   合计正好 +100

     为什么不是各加 50：**内边距要取 6px 的倍数**（点阵节距，tokens.css 有这条）。
     各加 50 得到 146 / 154，两个都不是 6 的倍数；48 / 52 得到 144（24 格）
     和 156（26 格），既凑够 100 又都落在格线上。
     顺带把原来的 104 修正了 —— 它本来就不是 6 的倍数（17.33 格）。

     多出来的高度上下大致对分，所以文案在版面里的相对位置没变，只是四周更空。
     想让 100px 全加在下面（文案更靠上、露出更多视频）：把上面改回 96px。

     ⚠️ .sec-hero 和 .sec 特异性相同（都是单个类），靠**在文件里更靠后**取胜。
        别把这条规则挪到 .sec 之前。构建期核对会检查这个顺序。 */
  --sec-top:144px;--sec-bot:156px;}
/* 三层，自下而上：视频 0 → 点阵 1 → 内容 2。
   .grid 本身已有 position:relative，这里只补 z-index */
.sec-hero > .grid{z-index:2;}

/* ── 点阵压在视频之上 ────────────────────────────────────────────────
   意义不只是叠个纹理：点阵continue到视频上，视频就变成「显示在这块屏上」，
   而不是「从一个洞里透出来」。这是液晶那条思路的核心动作。

   ⚠️ 为什么不用 var(--grid)：
      1. no-grid 开关把 --grid 置为 none，会把这层一起关掉 ——
         但页面基底该不该显示、和 hero 视频上该不该有点阵，是两件事。
      2. tokens.css 那条硬规矩：gradient 里不套 var()，
         嵌套自定义属性在部分内嵌预览 / 老 WebKit 会整条失效。
      所以这里和 tokens.css 一样写字面值，各自独立。

   点的颜色 #A2A7AE 是**推出来的，不是挑出来的**：
   页面基底是「淡 #B3B7BD」压在「玻璃 #DEE0E3」上，强度 1.52。
   视频的平坦底色实测约 206（比玻璃暗），要保持同样的 1.52 就得把点压深两阶 ——
   H215 S7% L66 = #A2A7AE，对视频底 1.54、对玻璃底 1.83。
   （直接沿用淡 #B3B7BD 的话对视频底只有 1.28，基本看不见 ——
     和深色带那次一样，跨底色时要对齐的是**对比度**，不是色值。）

   ⚠️ 固定色的点阵盖在会动的画面上，强度不可能恒定：
      在平坦底色上是 1.54，压到无限环的暗面（约 97）时会反过来变成亮点。
      这是真液晶的物理行为（点格固定、图像在变），不是 bug。
      要恒定强度只能让点阵跟着画面取值 —— 那是 canvas 的活，不是 CSS。

   想让它更明显：把 #A2A7AE 换成「中 #6D747E」（对视频底 3.00）。
   想改密度：6px 两处一起改，radial 的 .6px/.7px 是点半径，不用动。 */
.sec-hero::after{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background-image:radial-gradient(circle at center, var(--dot-media) .6px, transparent .7px);
  background-size:6px 6px;
}
/* 三段素材叠在同一个位置，靠 opacity 交叉淡化换段（2026-08-01）。
   ⚠️ 这里用 opacity 是合法的，不违反四级：视频是**素材层**不是界面层
      （见 tokens.css 原则 4 的素材层边界），而且交叉淡化是**过渡态**
      （原则 5）—— 两条都指向同一个结论。
      收敛性也满足：淡化结束后一段是 1、其余是 0，没有停在中间态的。

   ⚠️ 不用 visibility 做延迟隐藏（文字那边是那么做的）。
      视频要**两段同时在播**才能交叉淡化，隐藏任何一段都会露出底色。
      所以三段始终 display 在场，只有 opacity 在动。 */
.hero-video{
  position:absolute;inset:0;z-index:0;
  width:100%;height:100%;
  object-fit:cover;      /* 铺满区块，不留边、不变形 */
  pointer-events:none;   /* 不吃鼠标事件，文字照常可选中 */
  opacity:0;
  transition:opacity var(--vid-t) ease-in-out;
}
.hero-video[data-on]{opacity:1;}
/* 开关一：提黑阶。数值是解方程解出来的，把 100 映到 146、213 映到 235 */
[data-explore~="hero-lift"] .hero-video{filter:contrast(.6) brightness(1.315);}
/* 开关二：文字区垫实底玻璃板。
   padding 会把文字往内推 32px —— 这是开关状态下的取舍，不是定稿版面 */
[data-explore~="hero-plate"] .sec-hero .hero-rotator{
  background-color:var(--off);border:1px solid var(--hair);padding:32px;
}
/* 自动播放的动图对前庭敏感的人是实际负担，且这是纯装饰。
   ⚠️ 别把这个 media 块删了 —— 之前 .reveal 的 reduced-motion 块被正则吃掉过，
      表面看不出来，但等于把无障碍承诺悄悄撤了。 */
@media(prefers-reduced-motion:reduce){
  .hero-video{display:none;}
}

/* 深色带的收边：.sec-dark 本身的规则在 tokens.css 里。
   这里只处理它和相邻元素的交界 —— footer 不是 .sec，选择器覆盖不到。 */
.sec-dark + .foot{border-top-color:transparent;}

/* ── hero 上下的横线 ──────────────────────────────────────────────────
   最终状态：**顶部留一条、底部不留。**
     顶部（与导航栏交界）：1px「淡」档，与右栏竖线同规格 —— 写在 .mast 规则上
     底部（与 products 交界）：不画，视频的明暗自己分区

   下面这段记的是「原来为什么是两条粗细颜色都不一样的线」，
   因为撤掉再加回来的过程里，这三条各自的来源必须清楚，否则又会撞出 2px。

   ⚠️ 涉及**三条**边框，不是肉眼看到的两条。
      「上下两条粗细颜色不一样」就是这么来的：

        hero 顶部  .mast 的 border-bottom  = --on   #2B2D31 深
                 + .sec 的 border-top     = --edge #2B2D31 深   ← 两条叠在一起
                 ────────────────────────────────────────────
                 实际 2px 深线

        hero 底部  #products 的 border-top = --edge
                 但 #products 是 .sec-dark，那个作用域里 --edge 解析成 #DEE0E3
                 ────────────────────────────────────────────
                 实际 1px 浅线

      同一个令牌 --edge 在两个作用域里是两个颜色 —— 这是 .sec-dark 的设计，
      不是 bug，但两条线放在一起看就露馅了。

   用 transparent 而不是 none：保住那 1px 的几何位置，
   区块高度和栅格对齐都不动，跟上面 .sec-dark + .foot 那条同一手法。
   透明边框露出的是元素自己的背景，所以看不见。

   ⚠️ .sec-hero 自己的 border-top 必须保持透明。
      顶部那条线由 .mast 的 border-bottom 负责，两边都画就又回到 2px 了 ——
      这正是原来那条「上边比下边粗」的成因。

   降级：顶部的线画在 .mast 上，与视频无关，所以视频拿不到时它照样在，
   导航栏和 hero 仍然分得开。底部则完全依赖视频的明暗，视频不在就没有分界。

   ⚠️ 这两条的**声明位置**都不在本节，本节只解释原因：
        .mast 的 border-bottom       → 写在 .mast 规则里
        .sec-hero 的 border-top      → 写在 .sec-hero 规则里（HERO 背景视频 一节）
      为什么不在这里就地覆盖 —— 踩过两次，是两种不同的坑：

        1. 级联坑：`.mast{border-bottom-color:transparent}` 写在本节，
           特异性与 .mast 自己那条相同，而后者在文件里更靠后，
           同特异性后者胜 → 覆盖**静默失效**。
        2. 可读性坑：同一个选择器写成两条规则散在两节，
           查「.sec-hero 到底是什么样」时只看到第一条就会得出错的结论。
           这两个坑都是构建期核对抓出来的，不是看出来的。

      规矩：**一个选择器只写一条规则**，跨节的说明用注释互相指。 */
.sec-hero + .sec{border-top-color:transparent;}
.foot.sec-dark{border-top-color:transparent;}

.eyebrow{display:flex;align-items:center;gap:10px;grid-column:1/-1;
  font-size:10.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--text-faint);margin-bottom:44px;}
.eyebrow i{width:6px;height:6px;background:var(--ac-base);display:block;flex:none;}

/* ── 排版 ───────────────────────────────────────────────────────────── */
/* 展示字全部走 Azeret Mono。
   字号比无衬线方案下调约 25% —— 等宽字每个字符同宽，同字号下横向要宽得多，
   不调的话 "The future," 会冲出栏宽。字距一律收负，否则大字号会散架。 */
/* ⚠️ 裸标签的字号阶梯（h1 / h2 / h3 / p）2026-08-05 搬到 assets/type-scale.css。
   原因：案例详情页要复用本文件里的 masthead，于是它也引了 landing.css，
   而那四条裸标签规则会把详情页里 10 个裸 h3、13 个裸 p 重新排版。
   首页与申请页必须在本文件**之前**引入 type-scale.css，否则标题会掉回浏览器默认。
   详情页**不引**它 —— 它有自己的字号体系（case-detail.css）。 */


/* ── MASTHEAD · 导航条 ────────────────────────────────────────────────
   参照 craft.wild.as 的顶部条带 .hhead：大字左、小件右，高 209px 不吸顶。
   （原来的半透明 + 毛玻璃吸顶栏已拆：液晶上不存在半透明也不存在模糊，
     半透明会产生第五个颜色，模糊需要连续明度过渡，四级两样都给不了。）

   它是**导航条**，不是一屏 hero：高度由内容定，内容顶部对齐。
   背景色是关键 —— 点阵栅格铺在 html 上，这里盖一层不透明玻璃色，
   条带内部就是平整玻璃、没有点阵，与页面其余部分自然分开。
   （同 tokens.css 里「有边框的容器盖玻璃色」那条规则，此处是同一个机制。）
   ⚠️ 照参照**不吸顶**。要吸顶加 position:sticky;top:0 —— 一行，
      但它有 200px 上下，吸顶会吃掉不少视口。 */
/* 上下内边距提成变量 —— 竖线要靠它们算负外边距，两处必须是同一个值。
   写死两遍的话，以后改了导航栏高度，竖线会悄悄短一截或探出去。 */
.mast{position:relative;background-color:var(--off);
  --mast-top:36px;--mast-bot:30px;
  /* 导航栏与 hero 的交界线。2026-07-30 定：**规格和右栏那道竖线完全一致** ——
     1px、「淡」档。所以这里直接写 var(--dim)，和 .mast-side 的 border-left
     用的是同一个令牌，不走 --hair 那个别名 ——
     两条线要「一样」，就该看得出用的是同一个值，而不是两个恰好相等的名字。
     （hero 的下边界仍然不画线，见「hero 上下的横线」那一节。） */
  border-bottom:1px solid var(--dim);
  padding-top:var(--mast-top);padding-bottom:var(--mast-bot);}
/* stretch 而不是 start —— 右栏要撑满高度，.mast-foot 的 margin-top:auto 才有东西可推 */
.mast-in{align-items:stretch;}
.mast-copy{grid-column:1/8;}
/* ── 导航栏那句 The future, simulated. ────────────────────────────────
   2026-07-30 改小并减淡，为的是不和 hero 的 82px 抢主角。

   ⚠️ 必须写成 .mast-copy h1，不能改全局 h1 ——
      hero 轮播那三条也是 h1，改全局会把主角一起缩掉。

   字号 30px（1440 视口）。行高取 36px = 6 格，踩点阵节距，不用 1.17 的比例值。
   字距从 --disp-tracking(-5%) 换成 --disp-tracking-md(-3.5%)：
   -5% 是给 60px 以上的字定的，等宽字在 30px 上收那么紧笔画会挤。

   ── 颜色：走「中」档 ──────────────────────────────────────────────

     全 #2B2D31  对玻璃 10.43   原状态
     中 #6D747E  对玻璃  3.57   ← 现在这句用这档（.mast-tag 也是这档）
     淡 #B3B7BD  对玻璃  1.52   试过，太虚，像没加载完

   **这一档在这里其实不欠账，和 .mast-tag 的情况不一样。**
   AA 的门槛是按字号分的：24px 以上算「大字」，要求 3.0；以下算正文，要求 4.5。
   这句 30px 属于大字，3.57 > 3.0 —— 通过。
   .mast-tag 之所以欠 0.93，是因为它 12px，走的是 4.5 那条线。

   ⚠️ 所以字号下限被抬到了 24px：原来是 clamp(20px, ...)，
      窄屏收到 20px 就掉进「正文」档，同一个颜色立刻从通过变成不达标。
      **门槛是台阶不是斜坡，24px 这个位置不能跨过去。**
      桌面端（1140px 以上）取值仍是 30px，看不出区别，改的只是窄屏行为。 */
.mast-copy h1{
  font-size:clamp(24px,2.1vw,30px);
  line-height:36px;
  letter-spacing:var(--disp-tracking-md);
  color:var(--fill);
}
/* 光标跟着字号缩：原来 2x5 格（12x30px）是配 82px 大字的，
   压在 30px 的字上会比字还高。改 1x3 格（6x18px），仍是整格数。
   颜色也跟着这句话走 —— 光标比字还深会变成版面上最抢眼的东西。 */
.mast-copy .caret{
  width:var(--cell);height:calc(var(--cell) * 3);
  background:var(--fill);
}
/* 右栏：一道「淡」档竖线分隔（照参照图那条），所以要给内缩留位。

   ── 竖线撑满导航栏高度 ──────────────────────────────────────────────
   原来只到内容高度，上下各短 36 / 30px。原因不是 stretch 没生效 ——
   stretch 撑满的是**栅格行**，而栅格行在 .mast 的内边距**以内**，
   所以竖线天然到不了导航栏的上下边缘。

   做法：上下各拉一个负外边距顶到 .mast 的边缘，再用同样的值把内边距补回来。
   边框跟着盒子长满全高，而**内容的位置一点没动**（负外边距和内边距抵消）。

   为什么不用 .mast 上的绝对定位伪元素：那样得用 calc 把
   「第 8 条栅格线在哪」重算一遍（--pad + 7 列 + 7 沟槽），
   等于把栅格数学抄第二份，将来栅格一改就悄悄错位。
   负外边距的写法里，线始终长在拥有它的那个元素上，没有第二份真相。 */
.mast-side{grid-column:8/13;display:flex;flex-direction:column;
  gap:30px;border-left:1px solid var(--dim);padding-left:24px;
  margin-top:calc(var(--mast-top) * -1);
  margin-bottom:calc(var(--mast-bot) * -1);
  padding-top:var(--mast-top);
  padding-bottom:var(--mast-bot);}
/* 沉到右栏底部：logo 靠左、小标语靠右，同一行、底边对齐。
   （.mast-id 那层上下堆叠的包裹已移除。） */
.mast-foot{margin-top:auto;display:flex;align-items:flex-end;
  justify-content:space-between;gap:20px;}
/* 副标题：12px / 行高 18px（6 的倍数，踩点阵节距）。
   颜色用「中」档 —— **这是「液晶上没有灰字」那条原则的第一个例外**，
   设计侧明确要求往淡走。记清代价：

     全 #2B2D31  对玻璃 10.43  AA 通过（原值）
     中 #6D747E  对玻璃  3.57  ← 现在用这个，差 0.93 不达 AA
     淡 #B3B7BD  对玻璃  1.52  基本不可见，所以「往淡 2 阶」字面上做不到

   ⚠️ 四级的代价在这里现形：**3.57 与 10.43 之间是空的。**
      十级时还有 5.82 / 9.71 这些档可用，压成四级就没有中间地带了。
      要既淡又达标，只有三条路：把字号提到 24px 以上（当小标语不合适）、
      往四级里加第五档（破坏「只有四级」）、或者接受这 0.93 的欠账。
      现在选的是第三条 —— 它是一句次要标语，不是必读信息。

   2026-07-29 再调：字号 12 → 11px，并去掉 34ch 的行宽上限，
   改为在右栏里自动换行。行高保持 18px（6 的倍数，踩点阵节距）。

   ⚠️⚠️ 这条规则曾经**整条失效过两次**，两次都是注释写坏了，症状一模一样：
      CSS 里明明写着 11px 和「中」档，浏览器量出来是 15px 和「全」档 ——
      因为整条规则没被解析成规则，那句话实际走了通用的 p 规则。

      第一次：只写了注释的结束分隔符、漏了开始分隔符。
      第二次：**在注释正文里写了字面的分隔符**去解释第一次的错，
              那个结束分隔符当场把注释关掉，后面的中文成了游离的 CSS 垃圾，
              连着下面这条规则一起被当成选择器吞掉。

      规矩：**注释正文里绝不写字面的注释分隔符，一律用文字描述。**
      （HTML 注释同理，之前也是这么栽的。）
      build-preview.py 现在会检查这一条。 */
/* 12px / 行高 18px（6 的倍数，踩点阵节距）/ 字重 400 → 300（减一级）。
   ⚠️ 字重减轻会**叠加**在已有的对比度欠账上：这句话本来就是「中」档 3.57、
      比 AA 差 0.93，字画再细一档，实际可读性比数字看着更弱。
      300 这一档 tokens.css 的字重清单里已经有（散文首段在用），不算新增。 */
/* ⚠️ 字间距 2026-08-05 当天试过改成 0，同日又恢复 .05em ——
      设计侧的判断是「没间距是真难看」。**别再动它。**
      （字间距会影响 index.html 里那句标语的硬换行能不能撑住两行，
        换算见那边的注释。） */
.mast-tag{font-size:12px;line-height:18px;font-weight:300;
  letter-spacing:.05em;color:var(--fill);text-align:right;}
@media(max-width:900px){
  /* 改变量而不是直接写 padding —— 竖线的负外边距是按这两个变量算的，
     这里写死 padding 的话，两边就不是一个数了，线会探出去 8px。 */
  .mast{--mast-top:28px;--mast-bot:24px;}
  .mast-copy,.mast-side{grid-column:1/-1;}
  /* 窄屏是横线分隔、上下堆叠，不存在「撑满高度」的问题，
     所以把上面那套负外边距**显式还原**。
     只还原 margin-top 不还原 margin-bottom 的话，右栏会比容器多探出 30px。 */
  .mast-side{border-left:none;padding-left:0;border-top:1px solid var(--dim);
    margin-top:36px;margin-bottom:0;
    padding-top:28px;padding-bottom:0;gap:28px;}
}
.brand{display:flex;align-items:center;text-decoration:none;border:none;color:var(--text);}
/* Yarrow 组合锁定标识。源文件 brand/yarrow_logo_forlight.svg，内联在 index.html 里。
   ── 尺寸按 wordmark 的实际比例核算（2026-07-31 换 logo 后重算）──
   ⚠️ 下面这组数**替换了旧 logo 的那组**（旧的是「总高 180 单位、cap 84 单位
      = 46.7%」）。新 logo 的比例完全不同，照抄旧结论会得出错的高度。

   新 logo 实测（viewBox 0 0 169 40，用 svgpathtools 解析每条 path 的 bbox）：
     mark          y 0…40      占满全高
     大写 Y 的 cap  y 6…33.9    27.9 单位 = 总高的 69.8%
     小写 x-height             20.2 单位 = 总高的 50.5%
     mark 右缘 37 / 文字左缘 49，间距 12 单位在 logo 内部 → .brand 不用 gap

   height:24px 时：Y 的 cap 16.8px、小写 x-height 12.1px。
   **新 logo 的 wordmark 比旧的大得多**（69.8% vs 46.7%），
   所以同样的 24px 总高，现在字反而偏大。要往小调的换算表：
     24px → cap 16.8px      20px → cap 14.0px
     18px → cap 12.6px      16px → cap 11.2px（≈ 旧 logo 在 24px 时的观感）
   ⚠️ 组合锁定自带 wordmark，字体是第三个家族（几何无衬线），
      与 Azeret Mono + Lexend 并存这件事还没解决。
      要退回「mark + 等宽字排 Yarrow」的话，把 #yr-lockup 换成 #yr-mark 并恢复 .brand 的 gap。 */
.brand-lock{height:24px;width:auto;color:var(--text);display:block;}
/* 2026-08-01：从 flex 改成 grid —— 见下方 .nav-links 那段的完整推导。
   四个链接占第 1/3/5/7 列；语言按钮删除后第 9 列保持为空，
   让首页与详情页的四个链接继续落在完全相同的横坐标上。
   中间的 fr 列就是可伸缩的间距。
   ⚠️ 不能再写 gap：gap 是固定值，会叠加在 fr 之上，把比例弄乱。
      间距**全部**由 fr 列承担。 */
.mast-row{display:grid;align-items:start;
  grid-template-columns:auto 1fr auto 1fr auto 1fr auto 3fr auto;}

/* ⚠️⚠️ 每一项**必须显式指定列号**，否则整套失效。
   第一版漏了这一步，症状是四个链接完全紧贴（PRODUCTSHOW IT WORKSRESEARCHABOUT）。

   原因是我把 fr 当成了「间距」，而它是**列**：
   grid 的自动摆放会按顺序把项目塞进**下一个可用列**，不会跳过 fr 列。
   于是项目会落进连续列 —— 其中第 2、4 列本该是间距却被链接占了，
   而真正预备当间距的第 6、8 列反倒空着。链接之间自然一点空隙都没有。

   显式放到奇数列之后，偶数列（1fr / 1fr / 1fr / 3fr）才真的空出来当间距，
   余量按 6 份分配：三份给组内、三份给组间，比例锁 1:3。

   ⚠️ 代价：列号写死了，**加一个导航项就要同时改两处**
      —— grid-template-columns 加一对「auto 1fr」，这里加一条 nth-of-type。
      build-preview.py 已加一道门：链接数与这里的列号数不一致就报错。 */
.nav-links a:nth-of-type(1){grid-column:1;}
.nav-links a:nth-of-type(2){grid-column:3;}
.nav-links a:nth-of-type(3){grid-column:5;}
.nav-links a:nth-of-type(4){grid-column:7;}
/* .mast-row .nav-links 的 flex 规则已删 —— 现在 .nav-links 是 display:contents，
   它自己不再是布局项，flex/min-width 对它没有意义。 */
/* margin-left:auto 已撤 —— 组间距现在由 3fr 那一列给，比例才锁得住。
   auto 会把余量一把吃掉，fr 的比例就失效了。
   ⚠️ 列号同样必须显式写，理由见上面 .nav-links a 那段。 */
.mast-row .nav-right{grid-column:9;justify-self:end;}
@media(max-width:560px){
  /* 窄屏挤不下同一行，按钮换到下一行靠左 */
  .mast-row{flex-direction:column;gap:18px;}
  .mast-row .nav-right{justify-content:flex-start;}
}

/* 两端对齐撑满栏宽 —— 从参照搬来的手法。text-align 只管非末行，
   单行要靠 text-align-last 才会被撑开，两条都要写。 */
/* ── 2026-08-01：撤掉两端对齐，改 flex + 固定 gap ─────────────────────
   设计侧要的是「导航读成一组，语言切换是独立的一件」。
   **两端对齐做不到这件事** —— 它把链接撑满容器宽度，
   间距由容器有多宽决定，不由我定；容器越宽，链接反而越散。
   要让它们靠紧成一组，就必须先不再撑满。

   原来那条是照 craft.wild.as 那两行被撑开的小标签做的，
   CSS 里当时就标了「这是我的判断、不是你明说的，不想要就删这两行」——
   现在删了。

   gap 18px = 3 个点阵格：比两端对齐时窄得多，但 10.5px + .14em 字距下仍不挤。
   语言按钮那边用 margin-left:auto 顶到最右，两组之间的空白最大化。

   ⚠️ 一个实测结论**保留下来**，虽然对应的写法已经不需要了：
      Chrome 会拉伸 U+00A0（不换行空格）。当时的现象是 "HOW IT WORKS"
      词间距被撑到极宽，420px 容器里自然宽 94px 的词被撑满整行。
      CSS Text 规范说 NBSP 不是换行机会、不该参与两端对齐，**Chrome 不按这条做**。
      当时的解法是给每个链接 display:inline-block（原子行内盒，
      两端对齐算法不往它内部塞空间）。现在没有两端对齐了，这个workaround 撤掉，
      但那条「别照规范推、要在浏览器里量」的教训比 workaround 本身值钱。
      HTML 里的 &nbsp; 留着，作用只剩「保证 HOW IT WORKS 不被拆到两行」。 */
/* ══ 2026-08-01 第二版：两处间距都改成流体，比例锁 1 : 3 ══════════════
   设计侧要求：组内与组间都自适应，且组内 < 组间，比例由我定。

   ⚠️ **只有 Grid 的 fr 能做到这件事**，试过的另两条路都不行：
        flex 的 gap 是固定长度，不吃余量 —— 只能靠媒体查询一档档写死
        margin:auto 会把余量**均分**给所有 auto —— 得到 1:1，做不出比例

   做法：把 .mast-row 变成 grid，四个链接和语言按钮都是它的直接网格项，
   之间用 fr 列当「可伸缩的间距」：

     auto  1fr  auto  1fr  auto  1fr  auto  3fr  auto
     链接1  g   链接2  g   链接3  g   链接4  3g  语言

   fr 总量 6 份，所以组内 = 余量/6、组间 = 余量/2，**比例恒定 1:3**，
   而且两端仍然分别贴住右栏的左右边缘（这是 margin:auto 那版的优点，保住了）。

   ── 比例为什么取 1:3 ────────────────────────────────────────────────
   不是随手定的。1440 视口下余量 206.7px，算出组内 34.5 / 组间 103.3 ——
   几乎正好等于设计侧上一轮已经认可的那组 36 / 98。
   也就是说 1:3 是**对着已被接受的观感反推出来的**，不是凭感觉挑的数。
   1:2 是分组能被察觉的下限，1:3 让它没有歧义。

   ── 实际取值（用 fontTools 量的真实链接宽度：四项合计 273.7px，
        语言按钮 48.6px，固定占用 322.3px）────────────────────────────
     视口    右栏可用   余量    组内     组间
     1920      729    407    67.8px  203.4px
     1600      595    273    45.5px  136.4px
     1440      529    207    34.5px  103.3px
     1280      462    140    23.3px   69.9px
     1100      387     65    10.8px   32.4px

   ⚠️ fr 永远不会变负 —— 空间不够时它收缩到 0，**所以不可能溢出**。
      这比上一版的固定 gap + flex-wrap 稳：那版在 1100 以下会溢出，
      要靠换行兜底。现在是自然收窄。
      940px 以下 auto 列本身就装不下了，但 900px 那条媒体查询会改成上下堆叠，
      中间那 40px 的窗口可以不管。

   ⚠️ <nav> 上用了 display:contents，让它的孩子直接成为 .mast-row 的网格项。
      已显式补 role="navigation" + aria-label —— display:contents 历史上
      在部分浏览器里会连同元素的隐含语义一起去掉，显式 role 不受影响。 */
.nav-links{display:contents;
  /* 字体样式留在这里 —— display:contents 只去掉盒子，元素仍在树上、仍参与继承 */
  font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;line-height:22px;}

/* 上一版（固定 36px + 媒体查询降到 24px）已废，理由见上。留一行备忘：
   当时那套需要两条媒体查询才勉强守住比例，而现在一条都不需要。 */

/* ↓ 以下是上一版的注释，记的是「为什么 36px 有失效宽度」，仍然有参考价值 ↓
   gap 18 → 36px（2026-08-01，设计侧要求扩大一倍）。
   ⚠️ 36px 有一个失效宽度，不是随便加大就行的 —— 实测（用 fontTools 量了四个
      链接在 Azeret Mono 500 / 10.5px / 字距 .14em 下的真实宽度，合计 273.7px，
      加语言按钮约 48.6px）：

        视口    右栏可用   组间空白（margin-left:auto 吃掉的余量）  组内/组间比
        1920      729px            298px                          1 : 8.3
        1600      595px            165px                          1 : 4.6
        1440      529px             98px                          1 : 2.7
        1280      462px             32px                          1 : 0.9   ← 分组塌了
        1200      429px          **溢出 2px**

      **组间必须明显大于组内，邻近原则才成立**（比值 >1.5）。
      36px 只在 1334px 以上满足；再窄就得退回 24px（4 格），
      所以下面有一条 1340px 的媒体查询。这不是保守，是那个比值算出来的。

   flex-wrap 是兜底：宁可换行也不要溢出。
   ↑ 以上整套（固定 gap + 两条媒体查询 + flex-wrap）已被上面的 fr 方案取代 ↑ */
.nav-links a{font-size:inherit;color:var(--text);text-decoration:none;border:none;
  padding-bottom:2px;border-bottom:2px solid transparent;white-space:nowrap;}
.nav-links a:hover,.nav-links a.on{color:var(--text);border-bottom-color:var(--ac-base);}
.nav-right{display:flex;align-items:center;gap:14px;}

.nav-drop{position:relative;}
/* Login 与语言切换统一规格：都带方框，字号/字距/大写与导航链接一致，
   整行于是读成一条仪器面板的功能条，而不是「链接 + 按钮」两种东西。
   （原来只有语言切换带框、Login 是 13px 无框正文字，两者不成套。）
   方框不加圆角 —— 液晶段码是矩形的，--radius 在这里刻意不用。 */
.nav-drop-trigger{font-family:var(--font-mono);font-size:10.5px;
  font-weight:var(--label-weight);letter-spacing:.14em;text-transform:uppercase;
  color:var(--text);background:none;cursor:pointer;
  border:1px solid var(--rule);padding:5px 9px;
  display:flex;align-items:center;gap:6px;}
/* 边框转「全」档表示激活 —— 颜色缺席时，交互态只能靠这类形状/明度跳档 */
.nav-drop-trigger:hover,.nav-drop.open .nav-drop-trigger{border-color:var(--on);}
/* ── 箭头：CSS 画的，不是字符 ─────────────────────────────────────────
   ⚠️ 原来是文本 `⌄`（U+2304 DOWN ARROWHEAD）。**我们两个字体里都没有这个字形**
      （用 fontTools 查过 cmap，Azeret Mono 和 Lexend 都没有），
      于是它回退到系统字体，垂直度量与周围的字完全不同 ——
      所以「文字和箭头没垂直居中」这件事，靠 align-items 是修不好的：
      偏移量来自那个回退字形自己在 em 盒里的位置，
      而且 macOS / Windows / Linux 各不一样，改一个魔法数字只能对一个平台。

   改成用两条 1px 边框拼一个方角、旋转 45 度得到「v」。
   好处有三：与字体无关、尺寸由我们定、而且「线」正是这套系统的语汇。

   ⚠️ translateY(-1px) 是**光学补偿**，不是随手加的：
      这个形状的重心比它的盒中心略低（墨都集中在下半），
      盒对齐得到的视觉效果偏低。要微调就动这一个数。 */
.chev{
  width:var(--cell);height:var(--cell);
  border-right:1px solid currentColor;
  border-bottom:1px solid currentColor;
  transform:rotate(45deg) translateY(-1px);
  flex:none;
}
.nav-drop.open .chev{transform:rotate(225deg) translateY(-1px);}
/* ── 残影拖尾（液晶签名 2）─────────────────────────────────────────
   原来是 opacity .16s ease 的平滑淡入。两个问题：
   半透明会产生四级之外的中间色；平滑淡入是发光屏的语汇，不是液晶的。

   液晶的行为是：**点亮很快，熄灭很慢**（液晶分子回位需要时间），
   所以切换时旧画面会糊一下。这里就照着做：
     进入 —— clip-path 分 3 格扫开，无延迟，快
     离开 —— 先原样停留 90ms（那一下糊），再扫回去
   用 clip-path 而不是 opacity，一个像素的颜色都不会被插值。 */
.drop-menu{position:absolute;top:calc(100% + 10px);right:0;min-width:186px;
  border:1px solid var(--edge);z-index:70;
  clip-path:inset(0 0 100% 0);visibility:hidden;
  transition:clip-path .14s steps(3,end) .09s, visibility 0s linear .23s;}
.nav-drop.open .drop-menu{clip-path:inset(0 0 0 0);visibility:visible;
  transition:clip-path .12s steps(3,end), visibility 0s;}
.drop-menu a{display:flex;justify-content:space-between;align-items:baseline;gap:16px;
  padding:11px 14px;font-size:14px;color:var(--text);text-decoration:none;border:none;
  border-bottom:1px solid var(--hair);}
.drop-menu a:last-child{border-bottom:none;}
.drop-menu a:hover{background:var(--ac-wash);}
.drop-menu a .mono{font-size:9px;letter-spacing:.16em;color:var(--text-faint);}
@media(max-width:900px){.nav-links{display:none;}}

/* ── HERO 文案（现在住在 masthead 左栏里）─────────────────────────────
   .sec-hero / .hero-grid / .hero-copy 的栏位规则已随合并移除，
   位置改由 .mast-copy 决定。--navh 也不再有人用。 */
/* .hero-sub 已移除 —— 副标题搬到右栏 logo 下方，改名 .mast-tag */
.scrollcue{position:absolute;bottom:24px;right:var(--pad);display:flex;align-items:center;
  gap:9px;font-size:9.5px;letter-spacing:.2em;color:var(--text-faint);}
.scrollcue i{width:1px;height:26px;background:var(--g4);display:block;
  animation:cue 1.8s steps(1,end) infinite;transform-origin:top;}
/* ⚠️ 原来是 opacity .4→1 的连续渐变。四级系统里没有 .4 这一档 ——
   改成「点亮 / 熄灭」两态硬切（steps 缓动），这是液晶的动效签名之一：
   离散点亮，不是连续淡入。 */
@keyframes cue{0%,49%{transform:scaleY(.34)}50%,100%{transform:scaleY(1)}}

/* ── PRODUCTS ───────────────────────────────────────────────────────── */
/* ── Products：左右各半 ────────────────────────────────────────────────
   2026-07-30。原来是 tab 切换（.track-labels / .tab-panel，已删）。

   竖线画在右半边的 border-left 上，**不是**在容器中间放绝对定位伪元素。
   同 masthead 那道竖线的理由：伪元素得用 calc 把「第 7 条栅格线在哪」
   重算一遍（--pad + 6 列 + 6 沟槽），等于把栅格数学抄第二份，栅格一改就错位。
   线长在拥有它的元素上，只有一份真相。

   两半各 6 栏。右半边靠 padding-left 让出视觉间距；
   左半边**不给** padding-right —— 那样两半的文字块宽度就不等了，
   等分版面里最容易被看出来的就是这种不对称。

   ── 竖线撑满整片区域（2026-07-30）─────────────────────────────────
   第一版画在 `.product-half + .product-half` 的 border-left 上，
   那样它只有右半边内容那么高。要撑满整个 Products 带，两件事都得做到：

     1. **eyebrow 并进同一个栅格。** 否则竖线上方隔着一个高度不固定的兄弟元素
        （「PRODUCTS」那行），没有任何常量能让负边距跨过它。
        并进来之后，竖线用 grid-row:1/-1 就能盖住 eyebrow 那一行。
     2. **负边距吃掉区块自己的上下内边距**（--sec-top / --sec-bot），
        和 .mast-side 那次同一手法。这两个值现在是变量，只有一份真相。

   为什么用一个专门的栅格项而不是绝对定位的伪元素：
   **栅格项写 grid-column:7 就落在第 7 条列线上，不需要任何 calc。**
   绝对定位得自己算「--pad + 6 列 + 6 沟槽」，等于把栅格数学抄第二份，
   栅格一改就悄悄错位。这个理由在导航栏那道竖线上已经用过一次。

   ⚠️ 它和右半边占同一条列线（都从第 7 条线开始），是刻意的重叠 ——
      栅格允许重叠，两者的左缘因此严格对齐，不会差一个 gap。 */
/* ⚠️⚠️ 这一节里**每一项的行列都写死**，一个都不能靠自动摆放。原因有两层：

   第一层：`grid-row:1/-1` 里的负数是**从显式栅格的末端往回数**的。
   不写 grid-template-rows 时显式栅格一行都没有、只有 1 条线，
   于是 -1 解析成第 1 条线，起止相同，规范要求忽略终止线，退化成 span 1。

   第二层（第一次修没修好的地方）：**补上 grid-template-rows 也不够。**
   竖线是「行列都确定」的项，会被优先放好，占住第 1、2 行的第 7 列。
   而 eyebrow 是 `grid-column:1/-1` + 自动行 —— 自动摆放会**避开已占用的格子**，
   于是它挤不进第 1 行，被推到第 3 行；两个半边跟着被推到第 4 行。
   结果竖线跨的第 1、2 行是空的，高度接近 0，看起来还是很短。

   所以三者全部显式定位。显式定位的项**允许互相重叠**，
   避让只发生在自动摆放阶段 —— 这正是我们要的：竖线和右半边共用第 7 条列线。

   以后加第三行内容，这里的行号要一起改。 */
.product-split{row-gap:0;grid-template-rows:auto auto;}
.product-split > .eyebrow{grid-row:1;}
.product-half{display:flex;flex-direction:column;}
/* ⚠️ 2026-08-03 Pulse 删除后这里**必须是单条显式规则**，不能留 of-type 那一对。
   留着会静默出错，而且症状不像原因：
     Threshold 会同时满足 :first-of-type 与 :last-of-type，
     :last-of-type 在文件里更靠后 → 它赢 →
     Threshold 拿到 grid-column:7/13 + padding-left，**跳到右半栏去**；
     --name-optical 也顺带拿到 -0.036em（这个碰巧对，但是靠运气）。
   现在写死：唯一的产品占左 7 栏，光学修正值明确写成 T 的值。
   build-preview.py 有一道门禁止 .product-half:first/last-of-type 回来。

   原来右半栏的内缩用 --pad（40px）而不是 --gut（24px），道理是
   「左半边文字距页边 40px，右半边文字就该距中线 40px」——
   等右侧内容定下来重新分栏时，这条对称性要拿回来。 */
.product-split > .product-half{grid-column:1/7;grid-row:2;
  --name-optical:-0.036em;}   /* Threshold 的 T。换产品名要按 .product-name 那节重算 */   /* Threshold 的 T */
/* ⚠️ 竖线**关掉**（设计侧 2026-08-04：「不用分隔线，自然的嵌入是最好的效果」）。
   一天之内它开关了两次，两次理由不同，别当成一次反复：
     08-03 关 —— 右半栏是空的，「竖线 + 右边什么都没有」读成加载失败
     08-04 开 —— 右半栏放了地球，竖线重新有对象
     08-04 关 —— 设计判断：地球该自然嵌入，一条硬线会把它框成一幅插图
   规则本身完整保留，要恢复只需删掉 display:none 那一行。 */
.split-rule{display:none;}
.split-rule--params{grid-column:7;grid-row:1/-1;
  border-left:1px solid var(--hair);
  margin-top:calc(var(--sec-top) * -1);
  margin-bottom:calc(var(--sec-bot) * -1);}
/* CTA 沉底：两段介绍字数不同，不拉平的话两个「→」会一高一低 */
.product-half .product-cta{margin-top:auto;}
@media(max-width:900px){
  /* 窄屏改上下堆叠，竖线换成横线 —— 与 .mast-side 窄屏那次同一手法。
     ⚠️ padding-left 必须显式还原，只改 border 会留下一截莫名的左缩进。 */
  /* ⚠️ 选择器必须和桌面端**同样具体**。桌面端写的是
     `.product-split > .product-half:first-of-type`（特异性 0,3,0），
     这里若只写 `.product-half`（0,1,0）会输，堆叠根本不生效。
     行也要显式还原成 auto，否则两半仍被钉在第 2 行里，会左右并排。 */
  .product-split{grid-template-rows:none;}
  /* ⚠️ 原来这里有两条 last-of-type 规则，给第二个半栏加上边框做分隔。
     Pulse 删掉之后只剩一个半栏，那条边框会变成**唯一产品头上一道莫名的横线**。
     没有第二块要分隔了，所以整条撤掉，只保留通栏。 */
  .product-split > .product-half{grid-column:1/-1;grid-row:auto;padding-left:0;}
  /* ⚠️ 竖线必须藏掉。堆叠之后它仍在第 7 条列线上，
     会变成一条穿过两段文字正中的竖线 —— 只改 .product-half 不管它是不够的。 */
  .split-rule{display:none;}
}

/* ══════════ Products 右半栏：线稿地球（2026-08-04）══════════════════════
   行为与几何在 assets/globe.js（生成物）。这里只管版面与遮罩。 */
/* ⚠️⚠️ **不许把行撑高，而且右半栏的内容一点都不许参与行高的计算。**
   （设计侧 2026-08-04：不改变版块原有高度 + 地球露出的高度等于版块高度）

   ⚠️ 这里修掉一个**真 bug**：Products 区块每 3 秒抖一下。
      根因是文字块原来在正常文档流里 —— 各条简介长度不同，换行数在 1 行和
      2 行之间变，块高就变；而它参与决定网格行高，于是**整个区块跟着跳**。
      初次 setLabel() 把空文字填上时还有一次更大的跳。
      opacity 动画本身不影响布局，所以那不是原因 —— 是 textContent 换了。

   修法：框与文字块**都绝对定位**，于是 .product-globe 自身的内容高度是 0，
   永远不参与行高；行高纯由左半栏（Threshold 文字）决定。
   顺带把框从「行高 − 文字块」变成「整个行高」→ 球半径大 30–40%。

   ⚠️ 绝对定位的子元素以**定位祖先的 padding 盒**为包含块，
      所以 inset:0 会自动尊重 padding-left。这一条在本项目里踩过反面
      （hero 那次 border + inset:0 留了 1px 缝），这次正好借它省一层容器。 */
/* ⚠️ grid-row **1/-1**：横跨小标签行与内容行，再用负边距吃掉版块自己的
   上下内边距 —— 于是框高 = 整个版块高度（设计侧 2026-08-04：地球要充满版块高度）。
   原来只占第 2 行，白丢 --sec-top 96 + --sec-bot 104 + 小标签行 ≈ 220px。

   ⚠️ grid-row:1/-1 只在有 grid-template-rows 时才有意义 —— −1 是从**显式网格**
      末端数回来的。.product-split 已声明 grid-template-rows:auto auto，成立。
      本项目踩过一次这条（当年 .split-rule 撑满全高就是这个坑）。

   ⚠️ 负边距不会改变行高：本元素的内容是绝对定位的（内在高度 0），
      不参与行高计算，所以「充满版块」和「不改变版块高度」不冲突。 */
.product-globe{grid-column:7/13;grid-row:1/-1;padding-left:var(--pad);
  position:relative;min-width:0;
  margin-top:calc(var(--sec-top) * -1);
  margin-bottom:calc(var(--sec-bot) * -1);
  /* ⚠️⚠️ 2026-08-05：向右吃掉 .grid 的 padding-right。
     缝隙是量出来的 —— grid-column 7/13 的右缘落在第 13 条列线（1440 视口下
     x=1400），而 .grid 有 padding-right:--pad(40)，所以框的右缘离视口右边
     **正好差一个 --pad**，球在那儿被硬切、留一道 40px 的暗带。
     设计侧：「不要移动地球，把遮罩宽度向右加大到没有缝隙。」

     ⚠️ 这条负边距有一个必须同时处理的连带，否则球会跟着右移 15.6px
        （正是设计侧不要的）：W 从 668 变成 708，而 globe.js 里旧的
        CX = max(W*0.70, R+10) 会让比例项胜出。所以那个比例项已删除，
        CX 现在只由「左球缘留 10px」定。
        **改这条负边距时去看一眼 tools/build-globe.py 的 CX。** */
  margin-right:calc(var(--pad) * -1);}
.globe-frame{position:absolute;inset:0;overflow:hidden;}
.globe-frame canvas{display:block;width:100%;

  /* ⚠️⚠️ 底部渐隐用 mask-image，**不要**改成盖一层背景色渐变。
     区块本身有一层 6px 点阵基底（html 那条 radial-gradient）——
     盖不透明渐变会把点阵一起糊掉，渐隐区会出现一条「洗白」的带。
     mask-image 是真透明，点阵能透出来。

     --globe-fade 由 globe.js 写入（= 渐隐起点 / 框高，百分比），
     它锚在**球心**上（最低那个钉的投影），不是框高的固定比例。

     ⚠️ 两个前缀都要写：Safari 到现在仍只认 -webkit-mask-image。
        顺序无所谓，但少写一个就有一整个浏览器看到硬边。 */
  -webkit-mask-image:linear-gradient(to bottom, #000 0,
                     #000 var(--globe-fade,75%), transparent 100%);
          mask-image:linear-gradient(to bottom, #000 0,
                     #000 var(--globe-fade,75%), transparent 100%);}

/* 浮动坐标：跟着钉走。定位靠 transform（globe.js 每帧写），
   left/top 会每帧触发布局。 */
/* ⚠️ 时长与气泡的淡出对齐（.22s）。坐标的 opacity 是 globe.js **每帧**写的，
   所以这条 transition 主要当平滑滤波用；但 next() 里那一次「设成 0」走的
   也是它 —— 换文字要等的就是这个 .22s。见 .globe-label 的长注释。 */
.globe-coord{position:absolute;left:0;top:0;
  font-size:10px;letter-spacing:.10em;color:var(--text-mute);
  white-space:nowrap;pointer-events:none;opacity:0;
  will-change:transform,opacity;transition:opacity .22s ease-out;}

/* ── 信息板（设计侧 2026-08-05：直角 · 浅底深字 · 闪三下出现）─────────────
   这一轮把 08-04 那版的三件事全撤了，逐条记下为什么，免得来回摆：
     · 圆角 6px → **0**。系统「圆角半径一律 0」的**唯一破例已撤销**，
       现在这条规矩零例外。
     · 气泡尾巴 → 去掉。SVG 那条可变长度的尾巴一并删了，找回：
       git log -S 'globe-tail'。**代价要说清楚**：现在信息板与钉之间
       没有任何连线，关联只靠**距离**（45–63px，附近没有别的东西）。
     · 深底浅字 → **浅底深字**（负显里的一块正显读数，像仪表上贴的标签纸）。

   ── 浅底的透明度是算出来的，不是调出来的 ──
   翻成浅底之后，失效机制也翻面了：海岸线（#DEE0E3）比板子**更亮**，
   而深色文字盖在板子之上，所以「亮线穿过字形」不再发生 ——
   剩下的只有**板内的明度涟漪**（透出来的海岸线）。两个数一起管：
       文字(#2B2D31) 对 板子压在底色上   ≥4.5     实测 8.4
       板子压在海岸线上 : 压在底色上     ≤1.35    实测 1.25
   0.88 是同时满足这两条、又让「半透明」真的看得出来的位置。
   ⚠️ build-preview.py 那道门现在**按实际颜色算这两个数**，不再只比一个
      写死的 0.85 —— 因为改颜色（而不是改透明度）会让写死的门静默失效：
      正则找不到旧色号，`_low` 为空，门就默认通过了。

   ── 出现方式：淡入淡出，**与坐标同一个驱动** ──
   设计侧原话（当天先要闪三下、改成闪五下，最后回到淡入淡出）：
   「淡入的时机和坐标保持一致。」
   所以这不是「时机相近」而是**同一个数**：globe.js 每帧把同一个 markerA
   （由剩余角距算出）写进坐标和信息板的 inline opacity，CSS 两边同一条 .22s。
   ⚠️ 因此**不要给 .globe-label 写 opacity 的 .on 类或 animation** ——
      inline 样式优先级更高，写了也不生效，只会让人以为它在管事。
      闪五下那版找回：git log -S 'globe-strike'。

   ── 「有点慢」慢在哪 ──
   原来 leadT 按 SETTLE=420ms 从 0 斜坡到 1，`.on` **只在斜坡到头才加**，
   再走 .34s 淡入 → 球停下后约 **760ms** 信息才完全出现。
   现在停下的第一帧就点亮，闪现动画自己占 340ms。SETTLE 降到 200ms，
   它现在只负责「让弹簧彻底停住」，不再管信息什么时候出现。

   ── 位置：解出来的，不是选出来的 ──
     安全矩形 = 上/左/右 留 MARGIN（= 本栏的 --pad），
                **下边界取 min(框高−MARGIN, 渐隐起点)**
     ↑ 下边界锚在渐隐起点上是关键：原来只夹到框底，最低那一站落在
       y=654…766（框高 810）—— 既贴着版块下缘，又整块泡在球已经淡光的
       空白里。那就是设计侧说的「太靠近边缘」的实际成因。
   候选按固定顺序（左下 / 左上 / 右下 / 右上）试，第一个整块落在安全矩形内
   且不压住钉的胜出。**顺序固定是为了不闪** —— 按分数挑会在相邻两站之间
   来回换边。行为在 globe.js，构建期有一道门算过全部站点的三种框尺寸。

   ── max-width 216px 是解出来的 ──
   气泡越宽，钉左侧越放不下（钉停在 x≈300…383）。实测：
     262px → 多数站点被夹到安全矩形边界上（余量归零）
     240px → 窄框下仍有四站被夹
     216px → 三种框尺寸、全部站点**都有余量**
   下限由最长的 .globe-tag 决定；再窄会增加全大写标签折行的风险。
   所以 216 是兼顾落点余量与标签可读性的窗口。
   ⚠️ 不能用 ch —— ch 按 .globe-label **自己**的字号算，而它继承的是正文
      字号、不是简介的 12px；写 38ch 实际得到 262px（就是被夹的那个数）。

   ⚠️ 仍然是 position:absolute —— 一旦回到文档流，Products 会随简介长度跳动
      （各条简介换行数不同）。那个 bug 修过一次，构建期有门锁着。 */
.globe-label{position:absolute;left:0;top:0;width:max-content;
  max-width:min(216px,calc(100% - 2 * var(--pad)));
  padding:14px 16px 15px;
  /* = --on(#DEE0E3) 加 0.88 不透明度。写字面量不写 color-mix：
     本项目的令牌层一律字面量（--hair 当年从 color-mix 改回来就是这条）。 */
  background:rgba(222,224,227,.88);
  /* 无边框 —— 浅块压在深底上，边缘对比本来就拉满，再描一条线是多的。
     而且 --hair 是给深底用的发丝线，放在浅块上是跨底搬色号。 */
  /* opacity:0 只是**无脚本时的初值**；有脚本时由 globe.js 写 inline opacity。 */
  opacity:0;pointer-events:none;
  /* ⚠️⚠️ .22s 与坐标同值，也是 globe.js 里 FADE_OUT_MS(230) 的依据 ——
     换文字要等淡出结束（修 2026-08-05 那个「旧文字瞬间变成新文字」的 BUG）。
     改这个数必须同步改 tools/build-globe.py 的 FADE_OUT_MS；构建期与
     build-preview.py 各有一道门盯着这一对。 */
  transition:opacity .22s ease-out;
  will-change:transform,opacity;}
/* 三行文字同色（#2B2D31 = 深色带里的 --text-invert，「反白」= 反成深色），
   层级靠字号与字重，不靠再多一个色号 —— 深色带里 --text/--text-2/--text-mute
   本来就全部解析成同一个 #DEE0E3，这里只是把同一套做法翻了面。 */
.globe-name{display:block;font-size:11.5px;letter-spacing:.14em;color:var(--text-invert);}
/* 能力标签与 .product-list 那四个同一套语汇（9.5px 全大写 + 宽字距） */
.globe-tag{display:block;margin-top:11px;font-size:9.5px;letter-spacing:.18em;
  color:var(--text-invert);}
/* ⚠️ 这里原来写 max-width:min(38ch,100%)，现在只留 100%：
   行宽已经由 .globe-label 的 216px 封住（内容宽 184px ≈ 30 字/行），
   再写一个更宽的 38ch 是**看起来在管事、实际上永远不生效**的死约束。 */
.globe-brief{display:block;margin-top:8px;max-width:100%;
  font-size:12px;line-height:1.6;font-weight:300;color:var(--text-invert);}

/* 降级保险：脚本挂掉 / 无 JS 时全部内容仍可读。**别删。** */
.globe-sr{position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;margin:0;padding:0;}

@media(max-width:900px){
  /* 窄屏两半堆叠 —— 与 .product-half 同一手法，padding 必须显式还原 */
  /* 窄屏堆叠后没有「左半栏定高度」这回事了 —— 容器自己给出高度，
     框和文字块仍然绝对定位在它上面。 */
  .product-globe{grid-column:1/-1;grid-row:auto;padding-left:0;
    margin-top:44px;margin-bottom:0;height:360px;}

}
@media(prefers-reduced-motion:reduce){
  /* globe.js 在此模式下只画一帧不启动循环，那一帧的 markerA = 1，
     所以信息板与坐标的 inline opacity 直接就是 1 —— 这里只需要关掉过渡。
     ⚠️ 别在这里写 opacity：inline 样式优先级更高，写了也不生效。
        （2026-08-05 之前这里写的是 opacity:1 + animation:none，那时透明度
         走的是 .on 类，现在改成了 inline，旧写法已经失效。） */
  .globe-label{transition:none;}
  .globe-coord{transition:none;}
}

/* .track-labels / .track-label / .tab-panel 已随 Products 改版删除（2026-07-30）——
   两个产品改成左右各半，不再有 tab。规则见上面「Products：左右各半」一节。
   要找回：存档仓库 agentese-redesign 的 git 历史 */

/* 2026-07-30：去外框、字重降一档、透明度 0.6。

   ⚠️ padding 也必须一起去掉，不是漏了。
      原来是 `border:1px + padding:4px 10px`，框一撤，那 10px 左内边距还在，
      标签就会比它下面的产品名**右移 10px** —— 版面上最显眼的那种错位。
      间距改由容器的 gap 负责（8px → 18px，补上撤掉的 20px 横向内边距）。

   ⚠️ 字重从 --label-weight(500) 降一档到 400。
      `.mono` 把 500 加在 <p> 上，span 继承，所以要在 span 上覆盖。

   ⚠️ opacity 0.6 又是一处静止态的第五色。实测（深色带底 #2B2D31）：
        --text-mute 在 .sec-dark 里是 #DEE0E3，对底 10.43
        叠 0.6 之后实际呈现 #96989C，对底 4.77
      9.5px 属于正文档，AA 门槛 4.5 —— **4.77 通过，但余量只有 0.27**。
      再降透明度或再减字号就会掉下去，改之前先算。

   ⚠️⚠️ 这里 opacity 用得安全（span 里只有文字），但它暴露了一个检查盲区：
      checkBands() 读的是 getComputedStyle(el).color，**不含 opacity**，
      所以它会报 10.43 而不是真实的 4.77 —— 我依赖了一路的那道门在这个
      写法上是瞎的。已在 landing.js 里把祖先链上的 opacity 也算进去。 */
/* 2026-07-30：标签从产品名**上方**移到**下方**。

   间距跟着重新分配，不是照搬原值 —— 原来上下是 22 / 20px，几乎相等，
   标签夹在名字和介绍中间会读成「谁都不属于」。
   标签是产品名的从属信息，所以**上紧下松**：
     产品名 → 6px → 标签 → 30px → 介绍
   6 和 30 都是点阵格的倍数（1 格 / 5 格）。 */
.product-label{display:flex;gap:18px;margin-bottom:30px;}
.product-label span{font-size:9.5px;letter-spacing:.18em;text-transform:uppercase;
  font-weight:var(--label-weight-soft);opacity:.6;color:var(--text-mute);}
.product-name{font-family:var(--font-display);font-size:clamp(30px,3.4vw,46px);
  font-weight:250;letter-spacing:var(--disp-tracking);line-height:1;
  /* 22 → 6px：下面紧跟的是它自己的标签，不是介绍段。见 .product-label */
  margin-bottom:6px;
  /* ── 光学左对齐 ──────────────────────────────────────────────────
     不是「字体的问题」，是**等宽字的必然结果**：等宽字把每个字形塞进同一个
     固定宽度的盒子里居中，所以左侧边距（left side bearing）天生比比例字大得多。
     46px 下就是好几个像素，肉眼看得见。

     实测（从 woff2 里读 hmtx，不是估的）：

       元素                      首字   左侧边距
       产品名 Pulse      46px     P     4.23px
       产品名 Threshold  46px     T     2.35px
       标签   PERSONAL  9.5px     P     0.93px
       介绍   Know…      16px     K     1.65px
       介绍   Threshold… 16px     T     0.48px

     ⚠️ 有两件事没法「修好」，只能取舍：
        1. 两个产品的首字不同（P / T），左侧边距差 1.9px ——
           **一个统一的修正值不可能同时对两个都准**，所以按产品各给一个。
        2. 标签和介绍自己都不齐（0.93 vs 1.65px）。下方本来就没有一条
           唯一的「左边缘」，所以目标是让大字的墨边**接近**小字的墨边，
           不是精确相等。小字那 0.7px 的差在 9.5/16px 上看不出来。

     修正值写成 em 而不是 px —— 字号是 clamp(30px,3.4vw,46px)，
     窄视口会变小，px 会跟着错。em 按自身字号缩放，自动跟上。

     ⚠️ **这两个值是绑在首字母上的。** 产品改名就得重算，方法：
        python3 -c "from fontTools.ttLib import TTFont; f=TTFont('<azeret-mono-var.ttf>'); \
        print(f['hmtx'][f.getBestCmap()[ord('X')]][1]/f['head'].unitsPerEm)"
        （把 X 换成新的首字母，得到的 em 值减去约 0.03em 就是修正量）*/
  margin-left:var(--name-optical, 0);}
/* 字重 300（2026-07-30）。Lexend 的 300 是既有档位，不是新增的
   （--font-body 的字重清单见 tokens.css；index.html 的字体轴是 300..500，覆盖得到）。

   ⚠️ 这条规则同时作用于 Pulse 和 Threshold 两段介绍 —— 它们是并列的同类元素，
      只改一段会让两个 tab 切换时字重跳变。要只改 Pulse 就得拆成 #pulse 限定。 */
.product-question{font-size:16px;line-height:1.6;font-weight:300;
  color:var(--text-2);max-width:min(40ch,100%);}
/* .product-divider 已删（2026-07-30，设计侧）—— 两个产品内部不再有横向分隔线。
   要找回：存档仓库 agentese-redesign 的 git 历史 */
/* margin-top 36px（6 个点阵格）是补上 .product-divider 撤掉后丢的那段间距 ——
   原来是「介绍 → 30px → 分隔线 → 24px → 标签」，线一去，标签就贴到介绍上了。
   没有线分隔，所以取 36 而不是原来的 55，比原来紧一点更合。 */
.product-list{list-style:none;display:flex;flex-wrap:wrap;gap:7px;
  margin-top:36px;margin-bottom:32px;}
.product-list li{font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;
  border:1px solid var(--rule);padding:5px 10px;color:var(--text-mute);}
/* 常态不画下划线（2026-07-30，设计侧要求去掉）。

   ⚠️ align-self:flex-start 是**必须**加的，不是顺手优化。
      .product-half 是纵向 flex 容器，flex 项默认在交叉轴上拉伸 ——
      所以这个 inline-flex 的链接实际被拉成了整栏宽，
      它的 border-bottom 于是横贯半个版面，看起来像一条区块分隔线，
      而不是一条下划线。这就是截图里「按钮下面那条横线」的真身：
      它一直是 .product-cta 自己的边框，不是遗留的 .product-divider。
      只去掉边框、不改拉伸的话，hover 时那条线还会以整栏宽度冒出来。

   hover 时才出现下划线 —— 交互反馈还得留着，
   而且这套系统里「液晶上没有灰字」，状态不能只靠字色变化。 */
.product-cta{align-self:flex-start;
  display:inline-flex;align-items:center;gap:9px;font-size:15px;color:var(--text);
  text-decoration:none;border-bottom:1px solid transparent;padding-bottom:3px;}
.product-cta:hover{color:var(--ac-text);border-bottom-color:var(--ac-base);}
.product-cta .a{font-family:var(--font-mono);font-size:13px;}

/* ── PRODUCTS / FORECAST AUDITS ────────────────────────────────────────
   案例不是第二段产品说明，而是紧接产品的证据层。卡片默认只给封面与简介；
   桌面 hover 把上半部切换成审计拓扑。详情内容刻意不塞进首页，等独立页接入。

   拓扑的线路动画默认暂停，只有正在 hover 的一张卡运行。这样页面静置时不会
   让 SVG 持续占用 CPU；减少动效模式下则完全停用。 */
.case-section{background-color:var(--paper);}
.case-showcase-head{align-items:end;}
.case-heading{grid-column:1/8;}
.case-heading .eyebrow{margin-bottom:24px;}
.case-heading h2{font-family:var(--font-display);font-size:clamp(34px,4.4vw,60px);
  font-weight:250;letter-spacing:var(--disp-tracking);line-height:1.08;}
.case-grid{margin-top:54px;row-gap:var(--gut);}
.case-card{grid-column:span 6;min-width:0;border:1px solid var(--rule);
  background-color:var(--paper);display:flex;flex-direction:column;overflow:hidden;}
.case-card-top{height:42px;padding:0 18px;border-bottom:1px solid var(--hair);
  display:flex;align-items:center;justify-content:space-between;
  font-size:9px;letter-spacing:.18em;text-transform:uppercase;}
.case-visual{position:relative;height:264px;flex:none;border-bottom:1px solid var(--hair);
  overflow:hidden;background-color:var(--paper);}
.case-cover,.case-topology{position:absolute;inset:0;}
.case-cover{background-color:var(--paper);
  transition:opacity .22s steps(4,end),visibility 0s linear .22s;}
.case-cover-img{display:block;width:100%;height:100%;object-fit:cover;}
.case-topology{opacity:0;visibility:hidden;padding:36px 18px 36px;
  transition:opacity .22s steps(4,end),visibility 0s linear .22s;
  /* 图片不退出，只在其上叠一层浅色磨砂玻璃。字面 rgba 是旧浏览器兜底；
     color-mix 让以后换纸色时遮罩仍跟随令牌。 */
  background-color:rgba(222,224,227,.3);
  background-color:color-mix(in srgb,var(--paper) 30%,transparent);
  -webkit-backdrop-filter:blur(9px) saturate(.72);
  backdrop-filter:blur(9px) saturate(.72);}
/* 点阵作为拓扑的坐标基底，只存在于磨砂层，不改变底图本身。 */
.case-topology::before{content:"";position:absolute;inset:0;background-image:var(--grid);
  background-size:var(--grid-size);opacity:.52;pointer-events:none;}
.case-topology::after{content:"INFERENCE TRACE  /  06 INDEPENDENT ESTIMATES";
  position:absolute;z-index:2;left:18px;top:13px;font-family:var(--font-mono);
  font-size:8px;font-weight:var(--label-weight);line-height:1;letter-spacing:.14em;}
.case-topology svg{position:relative;z-index:1;display:block;width:100%;height:100%;
  color:var(--text);overflow:visible;}
.case-links path,.case-flow-lines path{fill:none;vector-effect:non-scaling-stroke;}
.case-links path{stroke:var(--fill);stroke-width:1;stroke-dasharray:1 4;}
.case-flow-lines path{stroke:var(--text);stroke-width:3;stroke-linecap:round;
  stroke-dasharray:1 21;animation:case-flow .72s linear infinite;
  animation-play-state:paused;}
.case-flow-lines path:nth-child(2n){animation-delay:-.24s;}
.case-flow-lines path:nth-child(3n){animation-delay:-.47s;}
@keyframes case-flow{to{stroke-dashoffset:-22;}}
.case-nodes rect{fill:color-mix(in srgb,var(--paper) 88%,transparent);
  stroke:var(--text);stroke-width:1;rx:3px;
  vector-effect:non-scaling-stroke;}
.case-nodes text{fill:var(--text);font-size:11px;font-weight:var(--label-weight);
  letter-spacing:.08em;text-anchor:middle;}
.case-nodes use{color:var(--text);}
.case-nodes > g:nth-child(n+4):nth-child(-n+6) rect{stroke-dasharray:3 2;}
.case-nodes > g:last-child rect{fill:var(--text);stroke:var(--text);}
.case-nodes > g:last-child text{fill:var(--paper);}
.case-nodes > g:last-child use{color:var(--paper);}
.case-topology-key{position:absolute;left:18px;right:18px;bottom:12px;
  z-index:2;display:grid;grid-template-columns:repeat(4,1fr);gap:6px;
  font-size:8px;line-height:1;letter-spacing:.12em;text-transform:uppercase;}
.case-topology-key span{display:flex;align-items:center;gap:6px;}
.case-topology-key span::before{content:"";width:4px;height:4px;flex:none;
  border:1px solid var(--text);}
.case-topology-key span:nth-child(2),.case-topology-key span:nth-child(3){justify-content:center;}
.case-topology-key span:last-child{justify-content:flex-end;}
.case-topology-key span:last-child::before{background:var(--text);}
/* 只在标题与简介所在的底部文字面板显示 Research 同款点阵；
   section、状态栏、封面与拓扑区域仍保持平整。 */
.case-card-copy{padding:24px 24px 30px;flex:1;background-color:var(--paper);
  background-image:var(--grid);background-size:var(--grid-size);}
.case-meta{font-size:9px;letter-spacing:.16em;text-transform:uppercase;margin-bottom:16px;}
.case-card-copy h3{font-family:var(--font-display);font-size:clamp(19px,1.8vw,24px);
  font-weight:300;letter-spacing:var(--disp-tracking-md);line-height:1.3;max-width:28ch;}
/* ⚠️⚠️ `:not(.case-meta)` 是**必须的**，不是保险起见。
      2026-08-05 设计侧要求删掉卡片大标题下的说明文字。删完之后
      `.case-card-copy` 里**最后一个 <p> 就变成了顶部那行 .case-meta**，
      于是这条 14px 正文规则会套到那个 9px 的小标签上 ——
      而它的权重（一个类 + 一个类型 + 一个伪类）压得过 .case-meta（一个类），
      小标签的字号会被顶成 14px。加上 :not() 之后这条规则再也碰不到它。

      规则本身**留着不删**：说明文字以后要是加回来（在 h3 后面放一个无 class 的
      <p>）它立刻还能用，不用回来重写。现在它匹配 0 个元素，是刻意的空转。 */
.case-card-copy > p:last-of-type:not(.case-meta){font-size:14px;line-height:1.65;font-weight:300;
  max-width:48ch;margin-top:15px;color:var(--text-2);}
.case-card-cta{width:max-content;margin-top:22px;display:inline-flex;align-items:center;gap:12px;
  font-size:9px;letter-spacing:.13em;text-transform:uppercase;}
.case-card-cta span{font-size:14px;line-height:1;transition:transform .14s steps(2,end);}

/* ── 整卡可点 · 只对「有详情页」的卡片生效 ──────────────────────────────────
   2026-08-05 设计侧的三条要求：没有链接的卡片停掉 hover 效果；有 hover 的
   鼠标要变手型；点整张卡（而不是只点底部按钮）就能进详情页。

   做法是**拉伸链接**（stretched link），不是 JS：给底部那个 <a> 加一个铺满
   整卡的 ::after。于是整张卡在命中测试上就是这个链接的一部分。
   一次拿到四样东西：
     · 整卡可点  —— 覆盖层属于 <a>，点哪儿都是点它
     · 手型      —— a[href] 的浏览器默认光标就是 pointer，不用自己写 cursor
     · 按钮 hover 态跟着亮 —— 悬停覆盖层就等于悬停 <a>，:hover 自然成立
     · 零脚本    —— 符合「内容的可见性绝不依赖脚本成功」
   为什么不用 JS 给卡片绑 click：脚本挂掉整卡就点不动了，而且键盘用户拿不到
   焦点。为什么不把整卡包进 <a>：卡里有 h3 和图，块级内容塞进链接语义更差，
   而且以后卡里要再放第二个链接就废了。

   ⚠️ 门控用 :has(.case-card-cta) —— **不需要在 HTML 上加标记**，
      哪张卡有 CTA 就自动有效果，不用回来改 CSS。
      :has() 不被支持时整条规则失效 → 三种效果都没有，但**链接照样能点**，
      退化方向是安全的（丢的是增强，不是内容）。

   ⚠️ z-index:4 是算过的：.case-topology 的 ::after 用了 z-index:2、svg 用了 1，
      而 .case-visual 只是 position:relative（z-index:auto，不生成层叠上下文），
      所以那些值和覆盖层在同一个层叠上下文里比。写 1 或 2 会被拓扑图盖住，
      表现是「卡片上半部分点不动」。

   ⚠️ .case-card 必须 position:relative，覆盖层才按卡片定位。
      这不会影响卡内已有的绝对定位元素 —— 它们都在 .case-visual 里，
      而 .case-visual 自己就是 relative，仍然锚在它上面。

   代价说清楚：覆盖层会挡住卡内文字的选中。这是拉伸链接的固有取舍，
   卡里是短文案、不是要摘抄的内容，可以接受。 */
.case-card:has(.case-card-cta){position:relative;}
.case-card:has(.case-card-cta) .case-card-cta::after{
  content:"";position:absolute;inset:0;z-index:4;}

@media(hover:hover) and (pointer:fine){
  /* 桌面悬浮反馈：整卡轻抬，封面在自己的裁切框内缓慢放大。
     两段动效速度刻意错开，卡片先响应、图片随后跟上，避免像一次硬缩放。 */
  .case-card:has(.case-card-cta){
    transition:transform .24s cubic-bezier(.2,.72,.2,1),
      box-shadow .24s cubic-bezier(.2,.72,.2,1);
  }
  .case-cover-img{transition:transform .46s cubic-bezier(.2,.72,.2,1);}
  .case-card:has(.case-card-cta):hover{
    z-index:2;transform:translateY(-7px);
    box-shadow:0 14px 30px color-mix(in srgb,var(--text) 13%,transparent);
  }
  .case-card:has(.case-card-cta):hover .case-cover-img{transform:scale(1.045);}
  /* 直接悬停按钮本身时的箭头位移。整卡悬停时也会走这一条 ——
     因为覆盖层属于这个 <a>，悬停卡片任意位置都算悬停它。 */
  .case-card-cta:hover span{transform:translateX(4px);}
  /* ⚠️ 这两条原来是 .case-card:hover，无条件对所有卡片生效 ——
     于是没有详情页的卡片也会亮起拓扑图、鼠标却不是手型，
     等于给了一个点不动的邀请。现在只给有链接的那张。 */
  .case-card:has(.case-card-cta):hover .case-topology{opacity:1;visibility:visible;transition-delay:0s;}
  .case-card:has(.case-card-cta):hover .case-flow-lines path{animation-play-state:running;}
}

/* 键盘用户拿到同样的提示。拉伸链接的焦点框只有一行小字那么高，很难看见，
   所以把焦点画在整张卡上，并且顺带把拓扑图也点亮，与鼠标 hover 对称。
   ⚠️ 用 :focus-visible 而不是 :focus —— 鼠标点击也会产生 focus，
      那样点完一张卡它会一直亮着焦点框。 */
.case-card:has(.case-card-cta):focus-within .case-topology{
  opacity:1;visibility:visible;transition-delay:0s;}
.case-card-cta:focus-visible{outline:none;}
.case-card:has(.case-card-cta:focus-visible){
  outline:2px solid var(--text);outline-offset:2px;}
@media(prefers-reduced-motion:reduce){
  .case-cover,.case-topology{transition:none;}
  .case-flow-lines path{animation:none;}
  .case-card:has(.case-card-cta),.case-cover-img{transition:none;}
  .case-card:has(.case-card-cta):hover{transform:none;box-shadow:none;}
  .case-card:has(.case-card-cta):hover .case-cover-img{transform:none;}
}
@media(max-width:900px){
  .case-heading{grid-column:1/-1;}
  .case-grid{margin-top:42px;}
  .case-card{grid-column:1/-1;}
  /* 触屏只保留封面；避免 tap 后浏览器留下一个黏住的伪 hover 状态。 */
  .case-topology{display:none;}
  .case-cover{opacity:1!important;visibility:visible!important;}
}
@media(max-width:560px){
  .case-heading h2{font-size:32px;}
  .case-visual{height:216px;}
  .case-card-copy{padding:20px 18px 24px;}
}


/* .story-* 与 .essay 两组规则已随 Philosophy 区块一并移除（2026-07-29）——
   内容浓缩成三条主张进了 hero 轮播，那套散文排版没有实例了。
   其中一条结论被 hero 继承：**长段落不用等宽字**，解释句走 Lexend。 */

/* ── HOW IT WORKS ───────────────────────────────────────────────────── */
/* 精简后的五个步骤收进一块单屏“流程控制台”。桌面且视口足够高时，
   标题与控制台整体 sticky，原生页面滚动位置直接映射到五步内容。
   短视口、移动端与减少动效模式退回点击切换。 */
/* ⚠️⚠️ 2026-08-05：**钉版效果整块撤销**（设计侧「how it works 的钉版效果去掉，
   版块的尺寸恢复到正常」）。撤掉的是三条规则：
     .how-scroll.how-scroll-active{height:...}                 撑出约 2.5 屏的滚动余量
     .how-scroll.how-scroll-active .how-stage{position:sticky;top:0;min-height:100svh;…}
     .how-scroll.how-scroll-active .how-stage > *{flex:none;}
   找回：git log -S 'how-scroll-active'

   ⚠️ 这是钉版的**第二次撤回**（第一次是 2026-07-29）。两次撤回的都是**做法**，
      不是当初那个观察 —— 五步文案确实是递进的（02 的 before、04 的 learning loop、
      05 的回路），只是「让它可扫读」这个取舍赢了。别再用「五步是并列关系」当论据。

   ⚠️ 撤销之后这个版块**只能点击切换**（tab 或进度线），没有自动轮播、没有滚动驱动。
      这本来就是短视口 / 移动端 / prefers-reduced-motion 三种情况下一直在走的那条路，
      现在成了唯一的路 —— 所以 .how-console:not(.how-console-on) 那组降级规则
      更要留着（脚本挂掉时五段全部平铺可读）。

   ⚠️ 留下来的两个包裹层（.how-scroll / .how-stage）现在是**普通 div**，
      没有视觉作用。刻意保留：--how-console-h 挂在 .how-scroll 上，
      而且哪天再试钉版，结构不用重搭。 */
.how-scroll{--how-console-h:clamp(520px,calc(100svh - 96px),700px);}
.how-console{grid-template-columns:minmax(280px,3fr) minmax(0,9fr);gap:0;align-items:stretch;}
.how-tabs{grid-column:1;display:grid;grid-template-rows:repeat(5,minmax(0,1fr));
  border:1px solid var(--rule);border-right:0;background:var(--paper);}
.how-tab{position:relative;min-width:0;min-height:0;padding:17px 18px 20px;
  appearance:none;border:0;border-bottom:1px solid var(--hair);border-radius:0;
  display:flex;flex-direction:column;align-items:flex-start;gap:11px;
  background:var(--paper);color:var(--text);font:inherit;text-align:left;cursor:pointer;
  transition:background-color .12s steps(2,end);}
.how-tab:last-child{border-bottom:0;}
.how-tab::after{content:"";position:absolute;right:0;top:0;bottom:0;width:3px;background:transparent;}
.how-tab[data-on]{background:rgba(222,224,227,.075);}
.how-tab[data-on]::after{background:var(--text);}
@media(hover:hover){
  .how-tab:hover{background:rgba(222,224,227,.035);}
  .how-tab[data-on]:hover{background:rgba(222,224,227,.075);}
}
.how-tab-num{display:flex;align-items:center;justify-content:center;width:30px;height:22px;
  font-size:10px;letter-spacing:.12em;color:var(--text-faint);}
.how-tab[data-on] .how-tab-num{background:var(--text);color:var(--paper);}
.how-tab-title{font-family:var(--font-display);font-size:15px;font-weight:350;
  letter-spacing:-.02em;line-height:1.28;}

.how-console-body{grid-column:2;height:var(--how-console-h);min-width:0;
  display:grid;grid-template-columns:minmax(300px,4fr) minmax(0,6fr);
  border:1px solid var(--rule);background:var(--paper);}
.how-console-media{grid-column:2;grid-row:1;position:relative;min-width:0;overflow:hidden;background-color:var(--paper);
  background-image:var(--grid);background-size:var(--grid-size);}
.how-console-img{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;
  transform:scale(.93);mix-blend-mode:screen;opacity:0;visibility:hidden;
  transition:opacity .52s cubic-bezier(.22,.61,.36,1),
    transform .72s cubic-bezier(.22,.61,.36,1),visibility 0s linear .72s;}
.how-console-img[data-on]{z-index:1;opacity:1;visibility:visible;transform:scale(.9);
  transition-delay:0s;}
.how-console-flow{position:absolute;inset:0;z-index:2;display:block;width:100%;height:100%;
  transform:scale(.9);mix-blend-mode:screen;pointer-events:none;opacity:0;visibility:hidden;}
.how-console-flow[data-on]{opacity:1;visibility:visible;}
.how-flow-route{fill:none;stroke:none;}
.how-flow-signal-tail{stroke:var(--text);stroke-width:2.25;stroke-linecap:round;opacity:.72;}
.how-flow-signal-halo{fill:var(--text);opacity:.18;}
.how-flow-signal-ring{fill:none;stroke:var(--text);stroke-width:1;opacity:.72;}
.how-flow-signal-core{fill:var(--text);}
.how-console-copy{grid-column:1;grid-row:1;min-width:0;padding:50px 36px 30px;
  display:grid;grid-template-rows:1fr auto;
  border-right:1px solid var(--hair);background:var(--paper);}
.how-panels{align-self:start;min-width:0;}
.how-panel{padding:0;border:0;}
.how-console-on .how-panel[hidden]{display:none;}
.how-console-on .how-panel[data-on]{animation:how-copy-in .38s cubic-bezier(.22,.61,.36,1) both;}
@keyframes how-copy-in{
  from{opacity:0;transform:translateY(8px)}
  to{opacity:1;transform:translateY(0)}
}
.how-panel h3{font-family:var(--font-display);font-size:clamp(28px,3vw,42px);
  font-weight:250;letter-spacing:var(--disp-tracking-md);line-height:1.12;}
.how-panel p{max-width:36ch;margin-top:24px;font-size:17px;line-height:1.6;font-weight:300;}
.how-console-track{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;align-items:end;}
.how-console-track button{position:relative;height:18px;padding:0;appearance:none;border:0;
  background:transparent;cursor:pointer;}
.how-console-track button::before{content:"";position:absolute;left:0;right:0;top:50%;height:3px;
  transform:translateY(-50%);background:var(--hair);transition:background-color .2s ease;}
.how-console-track button[data-on]::before{background:var(--text);}
@media(hover:hover){
  .how-console-track button:hover::before{background:var(--fill);}
}

/* JS 失败时五段文案必须仍然可读；增强成功后才收成单一活动面板。 */
.how-console:not(.how-console-on) .how-console-body{height:auto;min-height:600px;}
.how-console:not(.how-console-on) .how-console-copy{display:block;}
.how-console:not(.how-console-on) .how-panel{margin-top:24px;}
@media(prefers-reduced-motion:reduce){
  .how-console-img{transition:none;}
  .how-console-flow{display:none;}
  .how-console-on .how-panel[data-on]{animation:none;}
  .how-tab{transition:none;}
  .how-console-track button::before{transition:none;}
}

@media(max-width:900px){
  .how-console{display:block;}
  .how-tabs{display:flex;height:auto;overflow-x:auto;overscroll-behavior-inline:contain;
    border-right:1px solid var(--rule);}
  .how-tab{flex:0 0 160px;min-height:88px;padding:15px 14px 18px;
    border-right:1px solid var(--hair);border-bottom:0;}
  .how-tab:last-child{border-right:0;}
  .how-tab::after{left:0;right:0;top:auto;bottom:0;width:auto;height:3px;}
  .how-console-body,.how-console:not(.how-console-on) .how-console-body{
    height:auto;min-height:0;display:flex;flex-direction:column;border-top:0;}
  .how-console-media{height:min(92vw,520px);flex:none;}
  .how-console-copy{min-height:330px;padding:30px 20px 28px;
    border-right:0;border-top:1px solid var(--hair);}
  .how-panel h3{font-size:clamp(26px,8vw,36px);}
  .how-panel p{font-size:16px;}
}

/* ── RESEARCH ───────────────────────────────────────────────────────── */
.case-empty{grid-column:1/13;border:1px dashed var(--g4);min-height:240px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  text-align:center;padding:44px 20px;}
.case-empty-mark{width:10px;height:10px;background:var(--ac-base);margin-bottom:22px;}
.case-empty-title{font-family:var(--font-display);font-size:19px;font-weight:300;letter-spacing:-.035em;
  color:var(--text);margin-bottom:10px;}
.case-empty p{max-width:min(40ch,100%);font-size:14px;}

/* ── ABOUT ───────────────────────────────────────────────────────────── */
#about{--sec-top:60px;--sec-bot:66px;overflow:hidden;isolation:isolate;}
.banner-box{align-items:stretch;min-height:360px;z-index:1;}
.banner-text{grid-column:1/7;display:flex;flex-direction:column;justify-content:flex-start;
  grid-row:1;position:relative;z-index:2;padding:48px 0 8px;}
.banner-eyebrow{display:flex;align-items:center;gap:10px;font-size:10.5px;
  letter-spacing:.2em;text-transform:uppercase;color:var(--text-faint);margin-bottom:24px;}
.banner-eyebrow i{width:6px;height:6px;background:var(--ac-base);display:block;flex:none;}
.about-statement{font-family:var(--font-display);font-size:clamp(16px,1.35vw,20px);
  line-height:1.55;font-weight:250;letter-spacing:var(--disp-tracking-md);
  color:var(--text);max-width:min(42ch,100%);margin:0;}
/* 词标嵌进句子替代文字 Yarrow；在上一版两倍尺寸的基础上回收 20%。 */
.about-wordmark{display:inline-block;width:5.68em;height:1.344em;vertical-align:-.3em;
  margin-right:.12em;color:var(--text);overflow:visible;}
.about-flow-field{position:absolute;z-index:0;inset:0;width:100%;height:100%;
  display:block;pointer-events:none;opacity:.72;
  /* 版块级背景：上半部图形从版块顶部展开，About 自身边界负责最终裁切。 */
  transform:translate(25%,46%) scale(1.92);transform-origin:50% 50%;
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 8%,#000 92%,transparent 100%);
          mask-image:linear-gradient(to bottom,transparent 0,#000 8%,#000 92%,transparent 100%);}
.about-side-actions{position:relative;z-index:2;display:flex;align-items:center;
  gap:16px;flex-wrap:wrap;margin-top:auto;margin-bottom:0;}
.about-social{display:inline-flex;align-items:center;gap:10px;min-height:38px;
  padding:0 14px;color:var(--text);background:var(--paper);border:1px solid var(--edge);
  font-size:11px;letter-spacing:.08em;text-decoration:none;}
.about-social:hover{color:var(--text);border-color:var(--text);}
.about-social svg{width:14px;height:14px;display:block;flex:none;}
/* ⚠️ padding-bottom:0 不是可省的。tokens.css 里的全局 a 规则给所有链接留了
      border-bottom:1px + padding-bottom:1px（系统里链接靠下划线区分，没有彩色）。
      .btn 覆盖了 border 和 text-decoration，**但漏了那 1px 的 padding** ——
      于是按钮比它的两个内部块高 1px（38.5 对 37.5），底部露出一条按钮自己的底色。
      压在深色文案块下面看不见，压在浅色箭头块下面就成了一条贯穿整个按钮的细线，
      看起来像刻意加的横线装饰。2026-08-05 设计侧指出后才查出来源。
      注意这条要写在 .btn 上而不是改 tokens.css —— 全局那条对正文链接是对的，
      按钮才是例外。 */
.btn{display:inline-flex;align-items:stretch;font-size:13.5px;text-decoration:none;
  border:none;padding-bottom:0;cursor:pointer;line-height:1;}
.btn .t{padding:12px 18px;}
.btn .a{padding:12px 12px;font-family:var(--font-mono);font-size:11px;}
.btn-pri{background:var(--text);color:var(--paper);}
/* 箭头块原本是黄。彩色缺席时不能用 --ac-base —— 按钮底是 --text（g9），
     g8 压在 g9 上几乎看不见。

   2026-08-05：填充由 --paper 改为 --dim。
     原来的写法只在**深色带里**成立：那里 --paper 解析成深色，于是「浅按钮上的深块」。
     About 这一天改成浅色带之后方向就反了 —— --paper 与页面底色**是同一个值**，
     箭头块整块融进背景，看起来像按钮右端缺了一角、箭头飘在外面。

     ⚠️ 关键约束：这套四级色阶里 --paper / --surface / --surface-2 / --off
        **是同一个颜色**。所以「稍微亮一点／暗一点」这种做法在这里不存在，
        中间调只有 --dim 一个。原来的 hover 用 --surface-2，等于没有 hover。

     --dim 的实测（两个作用域完全对称，浅色 #B3B7BD / 深色 #45494F）：
        箭头块压在按钮 --text 上   对比 6.85   清晰
        箭头块对页面底 --paper     对比 1.52   不会融进背景
        hover 时箭头字 --paper 压在 --text 上  对比 10.43

     hover 的语义改成「箭头块并入按钮、字反色」：块从 --dim 变成 --text 与按钮同色，
     字从 --text 变成 --paper。深浅两个作用域都自洽，因为用的全是会翻转的语义令牌。

     ⚠️ 真主题色进来后这里应改回主题色填充。 */
.btn-pri .a{background:var(--dim);color:var(--text);}
.btn-pri:hover .a{background:var(--text);color:var(--paper);}
/* ⚠️ 这一条不是可省的。有一条**全局** a:hover 规则把字色改成 --ac-text，
      它的权重（一个类型 + 一个伪类）高过 .btn-pri（一个类），所以按钮的
      字色在 hover 时被它接管。而主题色至今未定，--ac-text 现在回落成墨色 ——
      实测两个作用域里它都**恰好等于 --text，也就是按钮自己的底色**：
        浅色作用域  按钮底 rgb(43,45,49)    hover 字色 rgb(43,45,49)
        深色作用域  按钮底 rgb(222,224,227) hover 字色 rgb(222,224,227)
      结果是鼠标一放上去整行文案就消失。这个 bug 与深浅无关，
      2026-08-05 之前 About 还是深色带时同样存在，只是没人往按钮上放过鼠标。
      .btn-pri:hover 是两个类级选择器，权重压得住全局那条。
      ⚠️ 真主题色进来后要回来复核：如果 --ac-text 变成一个真的强调色，
         这条可以放开，但要先确认它压在 --text 底上的对比度。 */
.btn-pri:hover{color:var(--paper);}
@media(max-width:900px){
  .banner-box{min-height:0;}
  .banner-text{grid-column:1/-1;}
  .banner-text{grid-row:1;padding:0;}
  .about-statement{max-width:min(42ch,100%);}
  .about-side-actions{margin-top:32px;}
  .about-flow-field{transform:translateY(12.5%) scale(1.25);}
}
@media(max-width:560px){
  .about-side-actions{gap:10px;}
  .about-social{padding:0 12px;}
}

/* ── FOOTER ─────────────────────────────────────────────────────────── */
.foot{border-top:1px solid var(--edge);padding:38px 0 40px;}
/* ⚠️ 页脚的共享部分（.foot-row / .foot p / .foot a / .foot-slogan）
   2026-08-05 搬到了 assets/foot.css —— cases/ 下的详情页不引 landing.css，
   原来那些规则在 case-detail.css 里还有一份，两处会漂移。
   **这里只留 .foot 外壳**（上面那条 border-top），它两页合理地不同，别合并。
   页面必须 link foot.css，tools/check-footer.py 会检查这一点。 */

/* ══════════════════════════════════════════════════════════════════════
   HERO 轮播 · 三条并列主张
   ══════════════════════════════════════════════════════════════════════
   界线是 .rot-on 这个 class（由 JS 添加）：

   · 没有 .rot-on  → 三条**纵向依次全部显示**、都可读，步进器隐藏。
     这是默认态，脚本失败 / prefers-reduced-motion 都落在这里。
   · 有 .rot-on    → 三条叠在同一个网格单元里，只显示当前那条。

   把增强写进 class、而不是把降级写进 class —— 同 .pin-on 当初那条理由：
   万一 JS 挂了，浏览器什么都不用做就已经是可读状态。
   ══════════════════════════════════════════════════════════════════════ */

/* ── 默认（未接管）：纵向排开 ─────────────────────────────────────── */
.hero-slide + .hero-slide{margin-top:48px;}
.hero-steps{display:none;}

/* 解释句：Lexend，行高 24px（4 个点阵格）。
   行宽锁在 52 字符 —— 三条分别 79/82/77 字符，都会落成 2 行，
   轮播时不跳高（不锁的话字数差会导致 1 行 / 2 行来回跳）。 */
/* 字距 5%（2026-07-30）。Lexend 只能加正字距，不能收负的 ——
   它是按阅读效率设计的，字怀本来就开、默认字距本来就松。

   ⚠️ 加字距会让句子变宽，有从 2 行挤成 3 行的风险，那样轮播会跳高。
      实测过（Lexend 300 / 16px，容器 52ch = 509.4px）：
        三句加 5% 后最宽 489.4px，**仍然都是 2 行**，安全。
      1ch 是「0」的前进宽度，不随 letter-spacing 变，所以容器宽度没跟着涨 ——
      这就是为什么必须实测而不能想「反正容器也是按字宽算的」。
      以后改字号、字重、字距或这三句的文案，这个余量（20px）要重算。 */
.hero-say{font-size:16px;line-height:24px;color:var(--text);
  letter-spacing:.05em;
  max-width:min(52ch,100%);margin-top:26px;}

/* ── 轮播态 ──────────────────────────────────────────────────────── */
/* 三条同占一个网格单元 → 容器高度取最高的那条，切换时版面绝不跳。
   不用 position:absolute —— 那样容器会塌，高度得手写死。 */
.rot-on .hero-stage{display:grid;}

/* ── 雾化换条 ────────────────────────────────────────────────────────
   2026-07-30：原来是 clip-path 分 6 级台阶扫入（lcd-swap，已删）。
   设计侧改为**模糊淡出/淡入**：旧的一句糊开散掉，新的一句从糊到清聚拢。

   ⚠️ 这个效果全程会产生上百级中间灰，和「只有四级」正面冲突。
      处理方式不是破例而是划界，见 tokens.css 原则 5：
      **四级管静止态，不管过渡态。** 硬要求是过渡必须收敛 ——
      blur 必须归零、opacity 必须归 1 或 0，不许停在半糊的中间态。

   visibility 那条 transition 是关键，不是多余的：
     退场态 visibility 0s **延迟 --fog-t** → 旧的一句在整个淡出期间仍然可见
     入场态 visibility 0s 无延迟          → 新的一句立刻可参与渲染
   少了这个延迟，旧的一句会在动画开始的瞬间就消失，就没有交叠，
   看到的是「空一下再出现」而不是雾化。

   进出用不同的缓动：出场 ease-in（越散越快，像被吹开），
   入场 ease-out（先快速聚焦再稳住）。两条规则各写各的，不共用。 */
.rot-on .hero-slide{grid-area:1/1;margin-top:0;
  opacity:0;filter:blur(var(--fog));visibility:hidden;
  transition:opacity var(--fog-t) ease-in,
             filter  var(--fog-t) ease-in,
             visibility 0s linear var(--fog-t);}
.rot-on .hero-slide[data-on]{
  opacity:1;filter:blur(0);visibility:visible;
  transition:opacity var(--fog-t) ease-out,
             filter  var(--fog-t) ease-out,
             visibility 0s;}

/* ── 解释句的打字机 ──────────────────────────────────────────────────
   由 landing.js 把整句拆成「已打出」+「未打出」两个 span，
   未打出的那半用 visibility:hidden —— **它仍然占位**，
   所以断行是按整句算的，打字过程中版面一格都不动。

   为什么不用 min-height 预留高度：那只能保住块的总高，
   保不住断行位置；字数增长到换行的那一刻仍会跳。占位才是真的不跳。

   ⚠️ 降级：脚本没跑 / 拆分出错 / 打到一半抛异常时，整句都必须在。
      JS 侧有 try/catch 与一个到点强制补全的兜底，见 makeTyper()。

   雾化切进来的时候整句是**空的**（全文都在未打出那半里），
   所以小字不参与模糊，等雾散净了才开始打 —— 设计侧的要求。 */
.say-rest{visibility:hidden;}

/* ── 打字头 ──────────────────────────────────────────────────────────
   一小段下划线，坐在当前插入点。**只在打字过程中存在**，打完就撤。

   为什么这不算破「光标只放一处」那条规矩：
   导航栏那个方块光标是**静止态**的元素，一直在那儿闪；
   这个打字头只存在于过渡态，页面停下来的任何一刻都只有一个光标。
   和 tokens.css 原则 5 是同一条边界，只是这次管的是元素而不是颜色。

   ⚠️ 宽度必须是 0，视觉标记画在 ::after 里绝对定位。
      给它真实宽度的话，它插在「已打出」和「未打出」之间会把整句撑宽，
      断行位置就会随打字进度左右跳 —— 前面费劲保住的版面稳定性会毁在这里。

   打字时不闪：移动中的插入点再闪烁就成了两个动作在打架。
   形状用下划线而不是方块，与导航栏那个刻意区分开。 */
.say-caret{display:inline-block;width:0;position:relative;}
.say-caret::after{content:"";position:absolute;left:1px;bottom:-1px;
  width:var(--cell);height:2px;background:var(--on);}

.rot-on .hero-steps{display:flex;gap:14px;margin-top:44px;}
.hero-steps button{font:inherit;font-family:var(--font-mono);
  font-weight:var(--label-weight);font-size:10px;letter-spacing:.16em;
  color:var(--text);background:none;border:none;cursor:pointer;
  display:flex;align-items:center;gap:7px;padding:0;}
/* 当前项靠**格子填充**区分，不靠字色 —— 液晶上没有灰字。
   熄灭的格子只描边（「淡」档），点亮的填实（「全」档）。

   ── 平滑切换（2026-07-30）─────────────────────────────────────────
   原来是瞬间换色，所以显得生硬。改成**自下而上把格子填满**：
   填充色画成一张 linear-gradient，只动 background-size 的高度。

   为什么用擦除而不是直接给颜色加 transition：
   颜色渐变的中间帧是「一个偏灰的方块」，而擦除的每一帧都只有
   「填了的部分是全档 + 没填的部分是空」两种像素 —— **过渡期间也不出四级**。
   原则 5 允许过渡态越界，但这里不需要用到那个额度，就别用。
   顺带它更像仪器：格子是被点亮的，不是被调暗的。

   数字本身不做任何淡化 —— 「液晶上没有灰字」那条对静止态仍然有效。 */
.hero-steps button::before{content:"";width:6px;height:6px;flex:none;
  border:1px solid var(--dim);
  background-image:linear-gradient(var(--on),var(--on));
  background-repeat:no-repeat;
  background-position:bottom;
  background-size:100% 0%;
  transition:background-size .42s cubic-bezier(.4,0,.2,1),
             border-color .42s ease;}
.hero-steps button[aria-selected="true"]::before{
  border-color:var(--on);background-size:100% 100%;}

@media(max-width:900px){
  .hero-slide + .hero-slide{margin-top:36px;}
  .hero-say{font-size:15px;margin-top:20px;}
}

/* ══════════════════════════════════════════════════════════════════════
   动效 · 液晶的三个签名
   ══════════════════════════════════════════════════════════════════════
   原来这里是 opacity 0→1 + translateY 的 .7s cubic-bezier 平滑渐变。
   **那是不对的** —— 一个液晶单元只有几个状态，拿上百级 alpha 去淡入，
   本质上是在假装它有连续明度。三条签名替换它：

   1. **离散扫入**（不是淡入）
      用 clip-path 的 steps() 让内容一「行格子」一行地出现，像屏幕刷新。
      ⚠️ 刻意不用 opacity —— 半透明会产生四级之外的中间色，
         而 clip-path 只是遮挡，一个像素的颜色都不会被插值。
      这是「四级」这条原则对动效的直接约束。

   2. **残影拖尾**（不是干净切换）
      老液晶响应慢，状态切换会糊一下。做法是让**离开**比**进入**慢，
      而且中途经过「中」档 —— 就是那一下糊。

   3. **跳格**（不是平滑缓动）
      读数是一格一格跳的。所有缓动一律 steps()，不用 ease / cubic-bezier。

   还有一个最识别性的元素：**闪烁的光标**（见 .caret）。
   ══════════════════════════════════════════════════════════════════════ */

/* 1 · 离散扫入 —— 7 级台阶，约 60ms 一跳 */
.reveal{clip-path:inset(0 0 100% 0);}
.reveal.in{animation:lcd-scan .42s steps(7,end) forwards;}
.reveal[data-d="1"].in{animation-delay:.12s;}
@keyframes lcd-scan{to{clip-path:inset(0 0 0 0);}}

/* 3 · 跳格：下拉箭头分 3 格转，不平滑 */
.chev{transition:transform .15s steps(3,end);}

/* 光标：一个格子大小的实心块，硬闪不淡入淡出。
   steps(1,end) 让它只有「亮 / 灭」两态 —— 真实液晶光标就是这样。
   ⚠️ 这是页面上唯一一处「持续动」的东西，刻意只给 hero 一个，
      多了就变成装饰灯串，液晶的克制感就没了。 */
/* 尺寸从 em 改成**按点阵格子定**：2 格宽 × 5 格高（12×30px）。
   这样更贴液晶的物理事实 —— 光标是屏幕上的硬件单元，不是字体里的字形，
   所以它不该随字号缩放，而该跟栅格对齐。
   （原来是 .42em × .86em，在 82px 标题下是 34×70px，太大了。） */
.caret{display:inline-block;
  width:calc(var(--cell) * 2);height:calc(var(--cell) * 5);
  background:var(--on);vertical-align:baseline;margin-left:.14em;
  animation:lcd-blink 1.06s steps(1,end) infinite;}
@keyframes lcd-blink{0%,49%{opacity:1}50%,100%{opacity:0}}

@media(prefers-reduced-motion:reduce){
  /* 关掉一切动效，内容直接是最终态。光标停在「亮」，不闪。 */
  .reveal,.reveal.in{clip-path:none;animation:none;}
  .caret{animation:none;}
  .chev{transition:none;}
  .scrollcue i{animation:none;}
  /* 雾化：去掉模糊与过渡，但**保留 opacity 的 0/1 与 visibility** ——
     三条是叠在同一个网格单元里的，全都设成可见会直接叠字糊成一团。
     所以这里关的是「怎么切」，不是「显示哪一条」。 */
  .rot-on .hero-slide,
  .rot-on .hero-slide[data-on]{transition:none;filter:none;}
  /* 步进器的擦除也停掉，直接是空或满 */
  .hero-steps button::before{transition:none;}
  /* 打字机由 JS 侧检测 prefers-reduced-motion 后直接补全整句，
     不在这里处理 —— 它改的是 DOM 内容，CSS 关不掉。 */
}
