/* ==========================================================================
   WMPlay 介绍页 —— 全部样式。

   ------------------------------------------------------------------
   这一页**没有一张图片**。它的图形是 main.js 里那一片点阵，画的十个形状。
   所以这里没有 `.shot`、没有圆角卡片、没有投影 —— 只有**版面**：
   每一幕在自己的版面上留一个 `.stage` 空盒子（点阵照着它画），
   剩下的地方全部交给字。

   ------------------------------------------------------------------
   四条别破坏的：
     ① **全站没有一处实线分隔**。没有卡片、没有分隔线、没有描边按钮。
        要分隔只用**点列**（这一页的图形语言本来就是点）—— 见 `.specs .ldr`
        和 `.pts::before`。唯一的**实**线是行内链接的下划线。
        ⚠️ 原文是"没有一处用来分隔的边框"，第 9 轮加了两处点列之后改成现在这句：
           点列是点，不是边框，但它确实在"分隔"，所以规矩要说清楚是**哪种**线，
           而不是靠"点列不算边框"这种字面漏洞混过去。

     ①′ **点阵符号可以挂到版面之外的空白里，但绝不许挤动正文**。
        `h2::before/::after` 那枚 3×3 是绝对定位到负方向的（见「字」那节），
        正文的行数与盒宽**一个字节都不许变** —— 一行就是 ~50px，
        这一页和用户有约：一次滚轮就是一幕，一幕必须是一屏。
     ② 主色 `--a1 / --a2` 是从产品 `ui.html` 的 `--accent / --accent2` 抄的，
        不许另调一个蓝 —— 页面和软件得是同一个东西。
     ③ **不加载任何外部资源**（没有字体 CDN、没有图标库、没有统计脚本）。
        字体只用系统里有的：等线 + Bahnschrift，都是 Windows 自带。
     ④ 字号字体改了之后，**开场那行字是用点阵描出来的**（main.js 的
        `buildMark` 会去离屏画一遍这行字、采它的落墨像素）—— 所以改 `h1`
        的字号不用改 JS，两边是同一份参数。
   ========================================================================== */

:root{
  /* —— 颜色。白纸 + 一种蓝，除此之外没有第二种颜色。 —— */
  --paper:#ffffff;
  --ink:#12141a;          /* 标题、正文里的强调 */
  --body:#464c5c;         /* 正文 */
  --ink-3:#8d94a6;        /* 小字、次要说明 */
  --a1:#6c8cff;           /* 抄产品 ui.html 的 --accent */
  --a2:#a86bff;           /* 抄产品的 --accent2 */
  --haze:#f5f6fa;         /* 键帽、代码片的底 */

  /* —— 字。一行里 Latin 走 Bahnschrift（数字好看、有仪表感），
        汉字落到等线（比微软雅黑轻、更像排版而不是"系统提示框"）。 —— */
  --latin:"Bahnschrift","Segoe UI Variable Display","Segoe UI",sans-serif;
  --ui:"DengXian","等线","Microsoft YaHei","微软雅黑",sans-serif;

  --e:cubic-bezier(.22,.61,.36,1);

  /* 小节标题左侧那枚 3×3 符号的**中心点**颜色。九幕各自下面覆盖
     （见「字」那节 `h2::before/::after` 旁边那九条 `--orn`）。
     这里必须有个兜底值：没 JS、`prefers-reduced-motion`、或者将来加了一幕
     忘了写 `--orn` 的时候，符号仍然是这一页的主色，而不是一个透明窟窿。 */
  --orn:var(--a1);
}

*,*::before,*::after{box-sizing:border-box}

/* ⚠️⚠️ 全站文字**不可选**。这一页是"一次滚轮 = 一幕"的，鼠标在页面上任何地方
   一拖就会拉出一片蓝色选区（`html.jh` 那段接管只拦滚轮，拦不住拖选），
   而拖到一半松手还会顺手触发一次点击。整页没有任何一处需要选中文字
   （64 位校验码旁边那颗「复制」按钮就是为它准备的）。
   `::selection` 那条是**兜底**：万一哪个浏览器不认 `user-select`
   （老版 Edge 要 `-ms-` 前缀），选区至少是这道主色蓝，而不是系统那个高亮色。 */
html{-webkit-user-select:none;user-select:none}
::selection{background:rgba(108,140,255,.28);color:var(--ink)}

html{
  scroll-behavior:smooth;
  scroll-snap-type:y proximity;   /* 只在窄屏/禁动画时生效，见文件末尾 */
  -webkit-text-size-adjust:100%;
}
/* ⚠️ 接管滚轮（`.jh`）的时候必须把上面那两条关掉：浏览器的平滑滚动和
   吸附会和 main.js 里那条 620ms 的 rAF 补间**同时**去写 scrollTop，
   两个动画互相抢，表现是"拨一格走两格、偶尔卡住"。 */
html.jh{scroll-behavior:auto;scroll-snap-type:none}
/* 第 16 轮：手机上接管的是**触摸手势**（`main.js` 的 `TOUCH` 分支），
   挂的是 `.th` —— 和上面那条 `.jh` 干的是同一件事、理由也一模一样：
   不关掉的话，浏览器会和那条 620ms 的 rAF 补间**同时**写 scrollTop。
   ⚠️ 两个类**故意分开**：`.jh` 还会在别处被当成"这个站点是桌面玩法"的记号，
   合并成一个会让"手机上到底走哪条路"从 DOM 上读不出来。 */
html.th{scroll-behavior:auto;scroll-snap-type:none}

body{
  margin:0;background:var(--paper);color:var(--body);
  font-family:var(--latin),var(--ui);
  font-size:16px;line-height:1.9;
  letter-spacing:.01em;
  overflow-x:hidden;
}

/* 画布：铺满视口、固定、在后面。
   ⚠️ `width/height:100%` 这两条**不能省**。只给 `inset:0` 的话，画布这个
      **替换元素**会用它的固有宽度（＝HTML 里那个 `width` 属性，也就是
      main.js 每帧按 DPR 写进去的值），于是 `cv.width = W*DPR` 每帧把自己
      再放大一轮 —— 几秒后画布涨到几十万像素宽，页面看着只是"变卡"。 */
#field{
  position:fixed;left:0;top:0;width:100%;height:100%;
  z-index:0;pointer-events:none;
}
main,footer{position:relative;z-index:1}

/* ───────────────────────────────────────────────────────── 版面骨架 ── */
.wrap{width:min(1240px,90vw);margin:0 auto}

/* 一幕一屏。`justify-content:center` 让内容在剩下的空间里居中。
   ⚠️ 内边距是 `clamp(…, N vh, …)` 而不是固定值：视口一矮（笔记本 700px）
      它得跟着缩，不然六步/问答那两幕会溢出一屏 —— 而这一页和用户有约：
      **一次滚轮就是一幕**，一幕必须是一屏。 */
.act{
  position:relative;
  min-height:100vh;min-height:100svh;
  display:flex;flex-direction:column;justify-content:center;
  padding:clamp(72px,11vh,140px) 0 clamp(48px,8vh,110px);
  /* 触摸屏上（没有滚轮接管）靠吸附凑出"一格一幕"。桌面上 `html.jh`
     会把 `scroll-snap-type` 关掉，这条就不生效了。 */
  scroll-snap-align:start;
}

/* 两栏：左边字，右边舞台。舞台的宽度比字这边略大一点。 */
.wrap.split{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1.04fr);
  gap:clamp(28px,4.5vw,84px);
  align-items:center;
}
/* `.rev` 把舞台换到**左边**。这一笔不花什么代价，却是整页最划算的动效：
   3→4、5→6 之间，点阵会因为版面对调而**横穿整页**。
   ⚠️⚠️ 这里必须是 `>`（直接子元素）。原来写的是后代选择器，于是
   `.act.rev .wrap.split .stage` **把 `<figure>` 里那个舞台也选中了** ——
   而 `.fig` 自己是 flex 容器，舞台被塞了 `order:1`、题注是默认的 0，
   于是**镜像的那两幕题注跑到图上边去了**（其余几幕在下面）。
   屏幕上只是"题注位置不太一致"，一晃就过去了，查起来要一条条量盒子。 */
.act.rev .wrap.split > .col{order:2}
.act.rev .wrap.split > .fig,
.act.rev .wrap.split > .stage{order:1}

.col{max-width:33em}

/* 图 + 题注。题注挂**在图下面**，不在标题上面 ——
   每个标题顶着一行小字（"功能"/"教程"这种）是生成式页面最重的记号之一。
   这里那行小字是**给这张图起的名字**，所以它归图管。 */
.fig{
  margin:0;min-width:0;
  display:flex;flex-direction:column;gap:clamp(9px,1.5vh,16px);
}

/* 舞台：**空盒子**。它自己什么都不画，只负责告诉点阵"这一段画在哪儿"。
   点阵在它里面生成（归一化 0..1），每帧映到它的屏幕上位置。 */
.stage{position:relative;pointer-events:none;width:100%}
.s-wide{height:clamp(210px,45vh,410px)}
.s-tall{height:clamp(250px,52vh,470px)}
.s-rest{height:clamp(230px,48vh,430px)}
/* 密文字的那两幕，点阵退成一条横带。 */
.s-band{height:clamp(64px,12vh,122px);margin-bottom:clamp(16px,2.6vh,34px)}

/* 问答那一幕字最多，舞台缩成右边缘一条窄带（`drift`：一片很淡的浮尘）。 */
.s-rail{height:clamp(240px,50vh,440px)}
#faq .wrap.split{grid-template-columns:minmax(0,1fr) clamp(96px,13vw,180px)}
#faq .col{max-width:38em}

/* 下载那一幕：舞台在左、下载区在右，而且**文字那边给得更宽**（那里有
   四行参数和一串 64 位的 SHA-256）。 */
#download .wrap.split{grid-template-columns:minmax(0,.92fr) minmax(0,1.08fr)}
#download .col{max-width:32em}

/* ───────────────────────────────────────────────────────── 顶栏 ── */
/* ⚠️ 固定、不占文档流。上一版是"先排在文档流里、滚过 8px 再转 fixed"，
   于是第 1 幕的上沿在文档里是 64 而不是 0 —— 停点表里就多出一格
   "拨一下只动 64px"的废格。固定之后就干净了。 */
#bar{
  position:fixed;left:0;right:0;top:0;z-index:20;height:64px;
  display:flex;align-items:center;
  background:rgba(255,255,255,.80);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
}
#bar .wrap{display:flex;align-items:center;gap:clamp(20px,3vw,52px)}
.brand{
  display:inline-flex;align-items:center;gap:9px;
  color:var(--ink);text-decoration:none;font-size:17px;letter-spacing:-.01em;
}
.brand b{font-weight:700}
.brand .mark{width:18px;height:18px;fill:var(--ink);flex:0 0 auto}
.brand .mark .hi{fill:var(--a1)}
.nav{display:flex;gap:clamp(14px,2vw,30px);margin-left:auto}
.nav a{
  color:var(--ink-3);text-decoration:none;font-size:14.5px;
  transition:color .22s var(--e);
}
.nav a:hover{color:var(--ink)}

.cta{
  display:inline-block;flex:0 0 auto;
  background:var(--ink);color:#fff;text-decoration:none;
  border-radius:4px;padding:11px 22px;font-size:14.5px;line-height:1;
  transition:background .22s var(--e),transform .22s var(--e);
}
.cta:hover{background:#2b3040}
.cta:active{transform:translateY(1px)}
.cta-lg{padding:16px 30px;font-size:16px}

.tlink,.ul{
  color:var(--ink);text-decoration:none;
  border-bottom:1px solid rgba(18,20,26,.28);padding-bottom:2px;
  transition:border-color .22s var(--e),color .22s var(--e);
}
.tlink:hover,.ul:hover{color:var(--a1);border-color:var(--a1)}

/* ───────────────────────────────────────────────────────── 字 ── */

/* ⚠️⚠️ 这一行**同时是文案和画布的目标**（`data-field="mark"`）。它必须是
     **真实文字**，而且**不能从 HTML 里删掉**：点阵不是"在某处画个 logo"，
     而是去 offscreen 画一遍这段文字、把落墨的像素采成格点（`main.js` 的
     `buildMark`）。没 JS 时它就是一行普通的大标题。
     ⚠️ 有 JS 时它**自始至终是透明的**（不出现，连"先露一下再渐隐"都不要，
        见下面那一段），但那三个"点阵不画"的档位下必须把它还回来 ——
        否则首屏就是一整片白，开天窗，而且一个字都不报错。 */
h1.mark{
  margin:0;
  font-family:var(--latin);
  font-weight:400;
  /* ⚠️ 字号别贪大：它要能**装进 `.wrap`**。超出去的部分会被离屏画布裁掉
        （`buildMark` 只开 `盒子宽 + 余量` 那么大），点阵就会缺一块，
        而页面上看不出任何异常——只是字少了一截。 */
  /* ⚠️ 中间那个系数是**窄屏上唯一管用的一档**：226px 的上限要到 1100px 宽
        以上才生效，手机全靠 vw。原来 16.5vw 在 390 宽上只有 64px，
        字标缩成一小条、首屏空一大片。20.5vw 在 390 上是 80px（字标约 273px
        宽，`.wrap` 有 350px），窄到 320px 的机器也放得下。 */
  font-size:clamp(54px,20.5vw,226px);
  line-height:1;
  letter-spacing:-.022em;
  color:var(--ink);
  white-space:nowrap;
}
/* ⚠️⚠️ 开场那行字**根本不出现** —— 用户的原话是「开场灰字『露一下又突然消失』
   就不能把那个字直接删掉吗」。这一句换掉了之前三档的"铅笔稿"时间线
   （`html.js` 墨色 → `html.fx` 极淡灰 → `html.inked` 0.5s 渐隐）：
   只要点阵**起来了**，那行字就自始至终是透明的，没有"露"、也没有"消失"。

   ⚠️⚠️ **`<h1 class="mark" data-field="mark">` 这个元素不能从 HTML 里删掉** ——
      它**同时是文案和点阵的目标**：`main.js` 的 `buildMark()` 是量这个盒子的
      字号/字距、把**真正落墨的像素**采成格点的。整屏的"WMPlay"就是用点阵
      重描它。删了它，第 1 幕就没东西可描。
      所以"删掉那行字"只能做到"**它永远不出现**"，做不到"DOM 里没有它"。

   四档状态（改任何一个都要连着看 `main.js` 里 `.nofx` 那一行）：
     · 没 JS（`html:not(.js)`）  `#field` 不画 ⇒ 它就是首屏唯一的字，必须是墨色
     · JS 起来了、点阵在画         ⇒ **透明**（默认那档，下面这条规则）
     · 点了"减少动态效果"（REDUCED）`#field` 不画 ⇒ 必须还回来，靠下面那个
                                  `!important`（`!important` 压得住这条）
     · JS 起来了、但点阵**没起来**（`F` 为 null：没有 canvas 2d 上下文之类）
                                  ⇒ `main.js` 挂 `.nofx`，把字还回来。
                                  ⚠️ 没有这一档的话首屏就是**一片空白**，
                                     而且一个字都不报错。 */
html.js:not(.nofx) h1.mark{color:transparent}

h2{
  margin:0;
  font-size:clamp(25px,2.9vw,38px);
  font-weight:400;line-height:1.34;letter-spacing:.005em;
  color:var(--ink);
  max-width:20em;
}

/* ═══════════════════ 第 9 轮①：小节标题左侧那枚 3×3 点阵符号 ═══════════════
   把顶栏品牌标记那套形状语言（`index.html` 里的 `.brand .mark`）搬到九个小节
   标题上：**八颗墨点 + 正中一颗取这一幕的色锚**。中心那颗不是装饰 ——
   它让这枚符号变成**图例**：这一页每一幕的色相不同（`main.js` 的 `HUES`），
   符号告诉你"这一幕是哪个颜色"，同时把画布上的点和排版上的点接成一套。

   ⚠️⚠️ **它必须绝对定位到 h2 盒子的左侧负方向，绝不能用 `padding-left`。**
      `.col{max-width:33em}` 在 1440 宽下只用掉 528px（网格格有 576px），
      再拿 padding 挤掉 38px 很可能**多折出一行** —— 而一行就是 ~50px，
      直接威胁「一次滚轮 = 一幕、一幕就是一屏」那条约定。
      绝对定位则**盒宽、行数、幕高一个字节都不变**，符号挂在版面之外的空白里。
      （这正是 `h1.mark` 那条注释担心的同一件事，只是那里是画布裁字，这里是折行。）

   ⚠️ 两层的做法是刻意的：`::before` 画满**九**颗墨点，`::after` 再在正中
      **盖一颗**色锚点。比"用 mask 挖掉正中间那颗"省事得多，而且没有
      "mask 在某些 Edge 版本上算不算数"这种要赌的东西 —— 上层那颗是不透明的，
      底下那颗永远看不见。
   ⚠️ 27px 的盒子 = 3 格 × 9px，边界正好落在铺砖的格线上 ⇒ 不多不少九颗。
      改 `background-size` 就要同时改盒子和 `::after` 的 `background-position`。 */
h2{position:relative}
h2::before,h2::after{
  content:"";position:absolute;left:-38px;top:.30em;
  width:27px;height:27px;pointer-events:none;
  /* ⚠️ `radial-gradient` 的 `0 1.7px` 是"从圆心到 1.7px 都是实心"，
     后面那个 `transparent 1.8px` 才是收边 —— 写成 `var(--ink) 1.7px,
     transparent 1.7px` 也能画出圆，但边缘会有锯齿。 */
  background-image:radial-gradient(circle at 4.5px 4.5px,
                   var(--ink) 0 1.7px,transparent 1.8px);
  background-size:9px 9px;
}
/* 正中那一颗：一整块 27px 的砖上只有一个点，然后把它挪到第三格的位置。 */
h2::after{
  background-image:radial-gradient(circle at 4.5px 4.5px,
                   var(--orn) 0 1.7px,transparent 1.8px);
  background-size:27px 27px;
  background-position:9px 9px;
}
/* 幕 2~11 各自的色锚。⚠️ 下标是 `HUES[幕号-1]`，而这十条对应 `HUES[1]`~`HUES[10]`
   —— 第 1 幕（`#top`）用的是 `<h1>` 不是 `h2`，所以 `HUES[0]` 那档在这枚符号上
   用不到，只有画布用得上。
   ⚠️⚠️ 这十个值是**从 `main.js` 的 `HUES` 抄过来的**，`HUES` 一改这里就漂，
   而漂了**屏幕上什么都不报错**，只是颜色对不上。所以 `tools\_jh_check.py`
   里配了一条硬断言（⑬）：逐幕读 h2 的 computed `--orn`，必须逐个等于
   `dev().hues[幕号-1]`。**改了 `HUES` 就必须回来改这十条**，否则 ⑬ 会红。
   ⚠️ 第 16 轮插进来一幕（`#changelog` 排在 `#faq` 与 `#download` **之间**）⇒
      这一表的**顺序必须跟着 `HUES` 一起动**：`#download` 从第 9 档挪到第 10 档，
      它的值不变、位置变了。漏改的话屏幕上只是"两三幕的颜色有点像"。 */
#features {--orn:#2846be}
#tour     {--orn:#1862b6}
#tour-2   {--orn:#1278a8}
#tour-3   {--orn:#1a808e}
#tour-4   {--orn:#28707c}
#guide    {--orn:#3c6096}
#keys     {--orn:#2e46b2}
#faq      {--orn:#4e34b8}
#changelog{--orn:#5a30b4}
#download {--orn:#642cb0}
p{margin:0}
/* ⚠️ 段落间距写成 `p + p` / `h2 + p`（**两个元素**选择器，0,0,2），
   而不是 `.col > p`（一个类 + 一个元素，0,1,1）。后者会反过来把
   `.fine` / `.lead` / `.sha` 这些类上的 margin 压掉 —— 它们各自的
   margin 本来就该管自己的事。 */
p + p, h2 + p{margin-top:clamp(13px,1.9vh,22px)}

.claim{
  margin-top:clamp(18px,3vh,36px);
  font-size:clamp(19px,2.1vw,29px);font-weight:300;line-height:1.55;
  color:var(--ink);letter-spacing:.01em;
}
/* ⚠️ 「**只在某一个词上落色**」是通用模板最典型的记号之一 —— 一整句话里
   挑一个词染成主色，看着像做过了设计，其实在任何一页上都成立。
   这句话本身就够具体了，整句用墨色，让**主色第一次出现是第二幕那片点阵**。 */
.claim b{font-weight:400}

.lead{font-size:clamp(16px,1.3vw,18.5px);color:var(--body)}

/* 图的题注：给这张画起的名字（`小窗 420 × 646` 这种）。 */
.cap{
  font-family:var(--latin);font-size:12.5px;letter-spacing:.05em;
  color:var(--ink-3);margin:0;
}

.fine{font-size:13.5px;line-height:1.8;color:var(--ink-3);margin-top:clamp(12px,1.8vh,20px)}
.facts{font-size:13.5px;color:var(--ink-3);margin-top:10px}

code{
  font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
  font-size:.9em;background:var(--haze);border-radius:3px;padding:.12em .38em;
  color:var(--ink);
}

/* ───────────────────────────────────────────────────────── 开场 ── */
.hero-foot{
  display:flex;gap:clamp(24px,4vw,64px);align-items:flex-end;
  flex-wrap:wrap;margin-top:clamp(20px,3.4vh,44px);
}
.hero-say{max-width:34em;flex:1 1 22em}
.hero-say p:first-child{margin:0}
.acts{display:flex;gap:22px;align-items:center;flex-wrap:wrap;flex:0 0 auto}

/* ─────────────────────────────────────────────────────── 它是什么 ── */
/* ⚠️ 三条并列的要点**不是卡片**。上一版每一条都套一个圆角盒 + 同一个柔和
   投影 —— 那是"AI 生成感"里最标准的一套（同一套圆角、同一个
   `rgba(0,0,0,.1)`、每块内容都装进一样的盒子）。这里改成悬挂的一个点
   + 一句粗体领头语。那个点是这一页的点阵语言，不是装饰。 */
.pts{list-style:none;margin:clamp(22px,3.6vh,40px) 0 0;padding:0;display:grid;gap:clamp(14px,2.2vh,26px)}
.pts li{position:relative;padding-left:22px;font-size:15.5px;line-height:1.85}
.pts li::before{
  content:"";position:absolute;left:0;top:.74em;
  width:7px;height:7px;border-radius:50%;background:var(--a1);
}
.pts b{display:block;color:var(--ink);font-weight:600;font-size:16.5px;line-height:1.5}

/* ═════════════════ 第 9 轮③：三条要点用一条点列串起来 ═════════════════
   三条悬挂的点（`.pts li::before`）本来就是同一套语言，这里给它们补一条
   **极淡**的竖线，让"这是并列的三条"这件事不靠留白去猜。

   ⚠️⚠️ `.pts` 是 `display:grid`。伪元素默认会**变成一个网格子项**，
      也就是**凭空多出一行**（三条变四条，整幕往下长一截）。
      必须 `position:absolute` 把它摘出网格流 —— 绝对定位的子元素不算网格子项。
      `position:relative` 是给它的包含块（用 `.pts` 的 padding box）。

   ⚠️ `left:2.5px` 是配着上面那颗 7px 的点算的：点占 `left:0..7`，圆心在 3.5px；
      这条 2px 宽的带子覆盖 2.5..4.5px，圆心 3.5px —— 正好和点同心。
   ⚠️ 只在 `.pts` 上用，别推广到 `.steps`（那六步有编号、有自己的秩序，
      再加一条竖线会变成"时间线"，是另一种味道）。 */
.pts{position:relative}
.pts::before{
  content:"";position:absolute;left:2.5px;top:.95em;bottom:.4em;width:2px;
  pointer-events:none;opacity:.4;
  background-image:radial-gradient(circle at 1px 1px,
                   var(--ink-3) 0 .8px,transparent .95px);
  background-size:2px 8px;background-repeat:repeat-y;
}

/* ⚠️⚠️ **一列**，不是两列 —— 这是第 9 轮②改的，理由只有一条：
   点线导引要**有跑道**才成样子。
   两列时每个格子只有 ~242px，而 `窗口` 那一行的值
   （`420 × 646 / 1280 × 800`）一个就占 154.8px ⇒ **那一行根本没有位置放点**，
   实测它的点线只有 1.5px（两颗都不到），跟下面那一行 128.9px 的点线并排看，
   像是坏了。两列时每行的点线长度还只能是"格子减掉值"⇒ 四行四个长度
   （1.5 / 83.8 / 128.9 / 86.5），参差不齐。
   改成一列之后跑道有 ~430px，点线就能**定长**（看下面 `.ldr` 的 `flex-basis`），
   四行的点一样长、四个值也落在同一个 x 上 —— 那是点线导引本来的样子。
   ⚠️ 代价是这张表高了两行（~68px）。量过：`#features` 那一栏 560px、
      `#download` 426px，而一幕的内容高度有 810px 可用（1000 − 上下内边距
      110+80）⇒ 都还余着 250px 以上，幕高**一个像素都没变**（`_jh_check` ① 会盯）。 */
.specs{
  display:grid;grid-template-columns:minmax(0,1fr);
  gap:0 clamp(20px,3vw,44px);
  margin:clamp(20px,3.2vh,38px) 0 0;
  font-size:14px;line-height:1.7;
}
.specs > div{display:flex;gap:12px;align-items:baseline;padding:5px 0}
.specs dt{flex:0 0 5.6em;color:var(--ink-3);font-size:13.5px;margin:0}
.specs dd{margin:0;color:var(--body)}
.specs dd b{font-family:var(--latin);font-weight:600;color:var(--ink)}
/* ⚠️ `.dl2` 原来是把下载那张表**再拆一次**成四格（`.specs` 本就两列）。
   第 9 轮②把 `.specs` 统一改成一列之后，这一条会**反向覆盖**回去、
   把点线的跑道又抽掉。所以它不再写列数了 —— 列数归 `.specs` 管。
   类名在 HTML 里留着：它标的是"这是下载那一张表"，将来要单独调它还有地方挂。 */

/* ═════════════════ 第 9 轮②：规格表 `dt` 与 `dd` 之间的点线导引 ═════════════════
       窗口 ······ 420 × 646

   元素是**写在 HTML 里的**（`index.html` 那两个 `.specs` 里各一个
   `<i class="ldr" aria-hidden="true"></i>`），不是 `::after`。
   ⚠️ 为什么不用 `::after`：`.specs > div` 是 flex 容器，`::after` 只会变成
      **最后一个** flex 子项，也就是挂在 `dd` **后面** —— 而这里要的是
      "夹在 `dt` 和 `dd` 中间"，位置对不上。

   ⚠️⚠️ **绝不能用"给 `dd` 铺一层不透明底色把多余的点盖掉"这种做法。**
      这一页的底是 `#field` 那张 `position:fixed` 的**点阵画布**，它在内容的
      **背后**；给 `dd` 铺底色等于在点阵上糊一块白板，那一幕的形状就被啃掉一口。
      所以点线必须**天然只画在空隙里** ⇒ `flex:1 1 auto` 吃掉 `dt` 之后的
      全部余量，而 `dd` 自带自己的宽度，点线永远不会伸到字底下。

   ⚠️ `align-self:baseline` ＋ 5px 高：空盒子没有文字，它的基线就是**下外边距边**，
      于是这 5px 正好落在基线上方 —— 和省略号 `.` 坐的位置一样。
      高度 5px 远小于行高（14px × 1.7 = 23.8px）⇒ 这一行**不会因为它变高**，
      幕高也就不会变。 */
.specs .ldr{
  /* ⚠️ **定长**（`flex:0 1 5em`），不是 `flex:1 1 auto`。
     让它"吃掉剩余空间"看着更省事，但那样每一行的点线长度 = 格子 − 值，
     四行四个长度（两列时实测 1.5 / 83.8 / 128.9 / 86.5），而且**值的左边缘
     也参差不齐** —— 点线导引看着就像坏了。定长之后四行的点一样长、
     四个值落在同一个 x 上，这才是点线导引本来的用法。
     `1` 那个收缩因子留着：万一将来出现一个特别长的值，点线让路（缩到 0），
     值本身有 `white-space:nowrap` 保着不折行。 */
  flex:0 1 5em;min-width:0;align-self:baseline;height:5px;
  /* ⚠️⚠️ `margin:0 -6px` **不是随手写的负边距**，它是这一节唯一一处
     和"幕高不许变"直接挂钩的数字。父级 `.specs > div{gap:12px}` 会给
     这个元素**左右各留 12px**，也就是凭空多出 24px 的占用；而
     `#download` 那张 `.dl2` 的格子只有 234px 宽，`Windows 10 / 11，64 位`
     这一个值就要 144.6px —— **原来只剩 2.2px 余量**。多出来的 24px 一挤，
     那个 `dd` 当场**折成两行**（实测 h 23.8 → 47.6），下载那一幕整栏
     因此高了 12px、上下全挪位。把负边距抵掉一半，净占用只多 12px；
     剩下的靠下面 `dd{white-space:nowrap}` 兜住 —— 它**宁可让这一行稍微
     溢出格子，也绝不折行**，而格子右边有 43px 的栏间距接着，看不出来。 */
  margin:0 -6px;opacity:.55;
  background-image:radial-gradient(circle at 2.5px 2.5px,
                   var(--ink-3) 0 1px,transparent 1.12px);
  background-size:7px 5px;background-repeat:repeat-x;
}
/* ⚠️ 加这个是为了"值永远不折行"。`.specs dd` 默认 `flex:0 1 auto`：
   一旦点线把空间占满，**先被牺牲的是 `dd`**（basis 大 ⇒ 收缩权重全落在它头上）
   ⇒ 值折成两行 ⇒ 幕高变 ⇒ 威胁「一幕一屏」。
   `white-space:nowrap` 让 `dd` 的 min-content 就等于 max-content，
   它再也缩不动 ⇒ 空间不够时**改由点线收缩到 0**，值一个像素都不动。
   ⚠️ 这是"标尺要能分开两种收场"的一个实例：同样是把点线挤没，
      一种收场是"值不动"（要的），另一种是"值折行"（不要的），
      而两者在截图上看都是"点线短了"。 */
.specs dd{white-space:nowrap}

/* ─────────────────────────────────────────────────────── 六步 ── */
/* ⚠️ 宽屏是**三列两行**，不是一竖列。一竖列在 700~800 高的窗口里装不下
   （上一版实测溢出 150~190px），而这一页的规矩是一幕一屏。
   ⚠️ 编号在这里是**对的**：六步本来就是一个有先后的序列，
      不是"给并列的三块内容各贴一个 01/02/03"。 */
.steps{
  list-style:none;margin:clamp(18px,3vh,40px) 0 0;padding:0;
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(16px,2.8vh,32px) clamp(20px,2.6vw,42px);
}
.steps li{position:relative}
.steps .n{
  display:block;font-family:var(--latin);font-size:12px;font-weight:600;
  letter-spacing:.1em;color:var(--a1);margin-bottom:5px;
}
.steps h3{margin:0 0 5px;font-size:16.5px;font-weight:600;color:var(--ink);line-height:1.5}
.steps p{font-size:14.5px;line-height:1.8}
/* ⚠️ 第十五轮：`.steps` 里的码片**不许从中间断开**。第 3 步那颗
   「注册关联（.mp3 / .flac）」本来被折成「注册」/「关联（…）」两截，
   看着像两个词。这几颗码片最长的是 13 个全角字符（约 190px），而三列
   布局里最窄的格子也有 ~360px ⇒ `nowrap` 撑不破格子，只会整颗挪到下一行。
   ⚠️ 只挂在 `.steps` 底下，别推广到全局 `code` —— `.sha code` 那种长哈希
   本来就得靠 `word-break` 断。 */
.steps code{white-space:nowrap}

/* ─────────────────────────────────────────────────── 快捷键 ── */
.keygrid{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(22px,3.6vw,60px);margin-top:clamp(18px,3vh,38px);
}
.keybox h3{
  margin:0 0 14px;font-size:15px;font-weight:600;color:var(--ink);line-height:1.5;
}
.keybox h3 span{font-weight:400;font-size:13px;color:var(--ink-3);margin-left:8px}
.keybox ul{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.keybox li{display:flex;align-items:center;gap:14px;font-size:14.5px;line-height:1.6}
.keys{display:inline-flex;align-items:center;gap:5px;flex:0 0 auto;min-width:186px}
/* 键帽是**实物**，所以它有底、有圆角 —— 这不是"卡片"，是键。 */
kbd{
  font-family:var(--latin);font-size:12.5px;font-weight:500;color:var(--ink);
  background:var(--haze);border-radius:5px;padding:5px 9px;line-height:1;
  letter-spacing:.02em;transition:background .18s var(--e),transform .18s var(--e);
}
.keys i{font-style:normal;color:var(--ink-3);font-size:12px}
.keybox li:active kbd{background:#e7eaf4;transform:translateY(1px)}

/* ─────────────────────────────────────────────────── 常见问题 ── */
/* ⚠️ 九条**一列**排。两列的话每条只剩 ~300px 宽，
   问题行会被折成两行、答案一行只放得下十几个字，读起来很累。
   一列在这儿装得下：实测 1440×900 与 1280×700 都还留着一截余量
   （九条收着的时候约 370px 高）。 */
.faq{margin-top:clamp(14px,2.4vh,30px)}
.faq details{padding:clamp(6px,1.1vh,11px) 0}
.faq summary{
  display:flex;gap:12px;cursor:pointer;list-style:none;
  font-size:15px;font-weight:500;color:var(--ink);line-height:1.45;
  transition:color .22s var(--e);
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::before{
  content:"";flex:0 0 auto;width:5px;height:5px;border-radius:50%;
  background:var(--ink-3);margin-top:.66em;
  transition:background .25s var(--e),transform .25s var(--e);
}
.faq summary:hover{color:var(--a1)}
.faq details[open] summary::before{background:var(--a1);transform:scale(1.55)}
/* ⚠️⚠️ 那 8px 的间隔为什么写成 `flow-root` ＋ `<p>` 的 `margin-top`，
   —— 这一条是"问答收起/展开最后一刻顿一下"的根因，两条路都踩过，别改回去。

   〔第一版·坏〕`.ans` 没 padding、`<p>` 有 8px 上外边距 ⇒ 自然状态下这 8px
   **塌陷到 `.ans` 外面**（盒子顶边和字的顶边重合，间隔在盒子之上）；
   而动画期间 main.js 给 `.ans` 加 `overflow:hidden` —— 那会把它变成 BFC，
   外边距不再塌陷出去 ⇒ 同一个 8px 跑到了盒子**里面**。于是收尾摘掉 `overflow`
   那一拍，高度当场**少/多 8px**，下面的内容整体跳一下 = 用户说的"顿"。

   〔第二版·只治了一半〕把 8px 换成 `.ans` 的 `padding-top` ⇒ 两种状态下布局
   一样了，**展开**那条路确实好了。但 `*{box-sizing:border-box}` 意味着
   **`height:0px` 的元素的**边框盒**仍然是 8px**（padding 不吃 height）——
   于是收起动画收到底停在 **8.0px**，`details` 的高度在最后 ~130ms **停住不动、
   然后突然掉 8px**（下面的问答整体往上蹦一下）。展开是反方向、且发生在起步那
   一拍，被运动本身盖住了，所以只有收起看得见。
   ⚠️ 量出来的读数（`tools\_faq.py`）：收起动画末帧 `.ans` 高 **8.0px**、
   `details` 51.8px，交棒那一拍掉到 43.8px —— **8.0px 的跳**。

   〔现在〕`.ans{display:flow-root}` —— 它**永远**是个 BFC（不靠动画期间的
   `overflow:hidden`），那 8px 是 `<p>` 的外边距、被包在盒子里面 ⇒
   自然态和动画态的布局**从头到尾一致**，而且 `.ans` **没有 padding**，
   `height:0px` 就是真的 0。几何位置和"padding-top:8px"那版**完全一样**
   （盒顶之下 8px 才是字），只是末帧能真的收到底。
   ⚠️ 另一条路是动画期间用 `overflow:clip`（它不建立 BFC）—— 但那要赌浏览器
      对"clip 算不算 BFC"的实现细节，不如 `flow-root`（它的定义**就是**
      "一个 BFC，别的什么都不做"）来得没有第二种解释。 */
.ans{display:flow-root}
.ans p{
  margin:8px 0 0 17px;font-size:14px;line-height:1.85;color:var(--body);
}
.ans b{color:var(--ink)}

/* ─────────────────────────────────────────────── 更新日志 ── */
/* 这一幕是**一条一条的版本记录**，宽屏走两列（`columns:2`）。
   ⚠️⚠️ 两列不是为了好看，是为了**一幕一屏**：一列 25 条在 1440×1000 上实测
      ~900px，加上标题和横带直接顶出屏幕 —— 而这一页和用户有约。
      两列把高度砍掉一半，留出一截余量给更矮的笔记本窗口。
   ⚠️ 用 `columns` 而不是 `grid`：条目长短不一，grid 的每一行会被最高的那条
      撑开、白白浪费高度；`columns` 按高度均分。`break-inside:avoid`
      保证一条不会被拆到两列之间（拆开看着像两条记录）。
   ⚠️ 版本号和日期那两格是**定宽**的（都 `flex:0 0 auto` ＋ 一个 `em` 宽）——
      让它们跟着内容走的话，每条的文字起点都会差几个像素，整列看着是歪的。 */
.log{
  list-style:none;margin:clamp(14px,2.2vh,26px) 0 0;padding:0;
  columns:2;column-gap:clamp(24px,3.4vw,60px);
  font-size:13.5px;line-height:1.62;
}
.log li{
  break-inside:avoid;display:flex;align-items:baseline;gap:9px;
  margin:0 0 clamp(5px,.85vh,10px);
}
.log b{
  flex:0 0 auto;width:3.1em;
  font-family:var(--latin);font-size:12.5px;font-weight:600;
  letter-spacing:.03em;color:var(--orn);
}
.log i{
  flex:0 0 auto;width:3.5em;font-style:normal;font-family:var(--latin);
  font-size:11.5px;color:var(--ink-3);letter-spacing:.02em;
}
.log span{flex:1;min-width:0;color:var(--body)}

/* ───────────────────────────────────────────────────── 下载 ── */
.sha{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  margin-top:clamp(16px,2.6vh,30px);
}
.shalabel{font-family:var(--latin);font-size:11.5px;letter-spacing:.1em;color:var(--ink-3)}
.sha code{font-size:12px;word-break:break-all;color:var(--body);background:var(--haze)}
.copy{
  font:inherit;font-size:13px;color:var(--ink-3);background:none;border:0;
  border-bottom:1px solid rgba(18,20,26,.22);padding:2px 0;cursor:pointer;
  transition:color .22s var(--e),border-color .22s var(--e);
}
.copy:hover{color:var(--a1);border-color:var(--a1)}
/* 「复制成功」那一下是**回答用户的动作**，所以它值得有一点动静 ——
   而页面里其它任何地方都没有"自己动起来"的东西。 */
@keyframes copypop{
  0%{transform:scale(1)}
  40%{transform:scale(1.14)}
  100%{transform:scale(1)}
}
.copy.done{color:var(--a1);border-color:var(--a1);animation:copypop .34s var(--e)}

/* ⚠️⚠️ 第 16 轮：手机上那颗「仍要下载」和它上面那段提示，**样式只认 id**。
   它们**不带任何 class** —— `.cta{display:inline-block}` 会盖掉 `[hidden]`
   的 `display:none`（`[hidden]` 是 UA 表里的一条 `display:none`，作者表的
   类选择器一律赢），而这两样在造出来的那一拍必须能是"藏着的"。
   这里写的都是它们**已经显形之后**的样子。
   —— 这两样在桌面那一侧根本不存在（`main.js` 的 `TOUCH` 分支才造），
      所以不必进媒体查询；真要有，说明那台机器是触摸设备。 */
#dl-force{
  display:inline-block;margin-left:14px;
  font-size:14px;color:var(--ink-3);text-decoration:none;
  border-bottom:1px solid rgba(18,20,26,.22);padding:2px 0;
  transition:color .22s var(--e),border-color .22s var(--e);
}
#dl-force:hover{color:var(--a1);border-color:var(--a1)}
.dlnote{margin-top:clamp(12px,1.8vh,20px)}

/* ───────────────────────────────────────────── 幕标（右边缘那排点） ── */
/* ⚠️ 一列**编号**（01 / 02 / 03）是模板味最重的东西之一。这里用点：
      这一页的图形语言本来就是点，而且点数天然对应幕数。名字只在悬停时出现。 */
#rail{
  position:fixed;right:clamp(10px,1.2vw,22px);top:50%;transform:translateY(-50%);
  z-index:20;display:flex;flex-direction:column;gap:15px;align-items:flex-end;
}
/* ⚠️⚠️ 点必须**脱离文档流**（`position:absolute;right:0`），否则它对不齐。
   原来它是文字的 flex 兄弟、整条 `#rail` 又是 `align-items:flex-end`，
   于是每条锚点的右边缘齐了，**点却在靠外的第二位** —— 标签一长（"常见问题"
   比"下载"宽一倍）点就被顶到左边去。十条并排看，那条竖线是**歪的**。
   现在点钉死在自己那条的右边缘上，标签往左长，竖线一个像素都不动。
   `padding-right` 就是原来那个 10px 的间距 + 点自己的 6px。 */
#rail a{
  position:relative;display:flex;align-items:center;justify-content:flex-end;
  text-decoration:none;padding:2px 16px 2px 0;
}
#rail i{
  position:absolute;right:0;top:50%;transform:translateY(-50%);
  display:block;width:6px;height:6px;border-radius:3px;background:var(--ink-3);
  opacity:.45;transition:height .42s var(--e),background .42s var(--e),opacity .42s var(--e);
}
#rail a:hover i{opacity:.9}
#rail a.on i{height:20px;background:var(--a1);opacity:1}
#rail span{
  font-size:12.5px;color:var(--ink-3);white-space:nowrap;
  opacity:0;transform:translateX(5px);
  transition:opacity .3s var(--e),transform .3s var(--e);
}
#rail a:hover span{opacity:1;transform:none}

/* ───────────────────────────────────────────────────────── 页脚 ── */
footer{background:#f7f8fc;padding:clamp(40px,6vh,72px) 0 26px;margin-top:0}
/* ⚠️ `:not(.fbot)` —— 版权行也挂着 `.wrap`（它要对齐版面），
   不加这个它会被当成第四条 fcol 摊成一排。 */
footer .wrap:not(.fbot){display:flex;gap:clamp(28px,5vw,80px);flex-wrap:wrap}
.fcol{display:flex;flex-direction:column;gap:9px;min-width:150px}
.fcol:first-child{flex:1 1 20em}
.fcol h4{margin:0 0 4px;font-size:13px;font-weight:600;color:var(--ink);letter-spacing:.04em}
.fcol a{color:var(--ink-3);text-decoration:none;font-size:14px;transition:color .22s var(--e)}
.fcol a:hover{color:var(--a1)}
.fcol .brand{color:var(--ink);font-size:16px}
.fbot{
  margin:26px auto 0;padding-top:18px;font-size:12.5px;color:var(--ink-3);
  border-top:1px solid rgba(18,20,26,.07);
}

/* ───────────────────────────────────────── 窄屏 / 触摸 ── */
/* 顶栏那五条在 1080 以下就挤了 —— 品牌 + 五条 + 那颗下载键差不多 640px，
   窗口一窄它们会先贴着边、再折行。到这时候幕标（右边缘那排点）也一起收起。 */
@media (max-width:1080px){
  #rail{display:none}
  .nav{display:none}
}
/* ⚠️ 这枚符号是**挂在版面外面**的（`left:-38px`），而版面到视口边缘只有
   `5vw`（`.wrap{width:min(1240px,90vw)}`）⇒ 视口一窄它就先贴边、再跑出屏幕。
   1000px 是量出来的界线：那里 5vw = 50px，符号左缘离视口还有 12px；
   `.rev` 那三幕符号挂在两栏之间，那里间隙是 4.5vw = 45px，还有 7px。
   再窄就直接收起来 —— **宁可没有它，也不能压到字或跑出屏幕**。 */
@media (max-width:1000px){
  h2::before,h2::after{display:none}
}

@media (max-width:860px){
  /* 一栏：图在字**下面**，而且不许太高（手机上一屏本来就窄）。 */
  .wrap.split{grid-template-columns:minmax(0,1fr);gap:clamp(20px,4vh,34px)}
  .act.rev .wrap.split .col,
  .act.rev .wrap.split .fig,
  .act.rev .wrap.split .stage{order:0}
  #faq .wrap.split{grid-template-columns:minmax(0,1fr)}
  /* ⚠️⚠️ 第 16 轮：`#download` 在窄屏上**一直没跟着变一栏** —— 这是个**老毛病**，
     不是这一轮带进来的（对照版 r16pre 同样）。根因是**特异性**：
     `styles.css:163` 那条 `#download .wrap.split{minmax(0,.92fr) minmax(0,1.08fr)}`
     带 id（1-2-0），把上面那条 `.wrap.split`（0-2-0）压住了；`#faq` 当初在
     **这个媒体查询里**补过一条专门的来配平，`#download` 漏了。
     实测（390 宽、触摸档）：`.col` 只剩 **147.1px**（`.wrap` 有 351），
     `<dl>` 的**值**从 x=107 一路排到 **x=251.7**，而点阵舞台 `.stage` 的左缘在
     **x=197.8** ⇒ 值压在点阵上 **53.9px**（对照版 56.9px）。用户报的
     「一些部分有重叠」就是这个：一行只放得下四五个字，日期和"Windows 10 / 11"
     是叠在粒子上的。⚠️ 桌面（>860px）**一个字都不动**。 */
  #download .wrap.split{grid-template-columns:minmax(0,1fr)}
  .col,#faq .col,#download .col{max-width:none}
  .s-wide,.s-tall,.s-rest,.s-rail{height:clamp(150px,26vh,250px)}
  .s-band{height:clamp(54px,9vh,96px)}
  /* ⚠️ 第 16 轮：更新日志那一列在手机上**必须是一列**。两列的话每格只剩
     ~160px，一条记录会折成四五截、读不成句。这一幕在手机上比一屏高，
     由 `main.js` 的 `stopList()` 给"比一屏高的幕"补中间停点来兜住
     —— 「一次滑动 = 一格」这条契约仍然成立。 */
  .log{columns:1}
  .steps{grid-template-columns:repeat(2,minmax(0,1fr))}
  .keygrid{grid-template-columns:minmax(0,1fr)}
  .keys{min-width:150px}
  .hero-foot{align-items:flex-start}
  /* 这一档才是窄屏真正生效的那一条（上面的 226px 上限在 1100px 宽以下都
     够不着）。20.5vw 在 390 上是 80px，`WMPlay` 约 273px 宽，`.wrap` 有
     350px —— 留够了余量，再窄的机器也不会把字挤出画布。 */
  h1.mark{font-size:clamp(44px,20.5vw,150px)}
}
@media (max-width:520px){
  .steps{grid-template-columns:minmax(0,1fr)}
  .specs{grid-template-columns:minmax(0,1fr)}
}

/* ───────────────────────────────────── 禁动画 / 没有 JS ── */
/* ⚠️ 一刀切。点阵整块不画（它的一切都是动画），`<h1>` 也就必须是**真字** ——
   否则这一页的首屏会是一片空白。 */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto;scroll-snap-type:none}
  *,*::before,*::after{
    animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important;transition-delay:0ms!important;
  }
  #field{display:none}
  h1.mark{color:var(--ink)!important}
  #rail i{transition:none}
}
/* 没有 JS 时同理：点阵根本不会起来。 */
html:not(.js) #field{display:none}
html:not(.js) h1.mark{color:var(--ink)}

/* ══════════════════ 第 10 轮②③：会飞进来的产品窗口（`#vignette`） ══════════════════
   四幕导览，每一幕把**产品真的那一屏**画出来、倾斜着从画面外滑进来，落定、
   站一拍，再交给点阵（交棒那一下见 `main.js` 的 `gate`）。

   ⚠️⚠️ 它是 `position:fixed` 的，**不进文档流**。这一页和用户有约：「一次滚轮 =
      一幕、一幕必须是一屏」，画在文档流里就会改幕高、十幕得重新量一遍。
      fixed 则**页高一个字节都不变** —— `tools\_boxes.py` 的 `w`/`h` 两列
      就是守这一条的。
   ⚠️⚠️ 第 12 轮：它**依然是 `fixed`**，但**跟着背景走**（用户原话：
      「UI卡片应该跟着背景而不是屏幕」）。做法不是改成 `absolute` ——
      那个"从画面外飞进来"的起步位置（好几百像素）会变成文档的横向溢出，
      凭空加一条横向滚动条、还把窗口算进文档尺寸。**改的是 `main.js`**：
      出生时记一个 `sy0`，之后每帧 `translateY` 里恒带着 `(sy0 − 当前滚动量)`。
      所以这一块 CSS **一个字都不用改**，改的是它被摆在哪一帧。
   ⚠️ 窗口是**深色**的，而整页是白纸。这一页别处一个深色块都没有 —— 这不是
      花活：产品本来就是深色，这是全站**唯一出现产品真色**的地方。
   ⚠️ 里面**没有一张图片**：点阵是 `radial-gradient` 铺砖铺的，字形是内联 SVG
      （`currentColor`，颜色归 CSS 管）。和文件头上那条「不加载任何外部资源」
      是同一条规矩 —— 尤其**不许**用 `assets\shots\*.webp`（用户原话：
      「最好不要截图」「截图有种廉价感」）。
   ⚠️⚠️ **尺寸一律写 `em`**，由 JS 给 `#vignette` 写一个 `font-size`
      （＝实际宽 ÷ 设计宽）⇒ 下面每个数字都是"设计像素"，一处缩放到底。
      不用容器查询（`cqw` 得赌 Edge 版本），也不会出现"框缩了字没缩"。

   ⚠️⚠️ **窗口底色是一块平的 `#0c0f18`，内部不铺任何渐变**。理由不是审美：
      小窗那圈频谱是先画一整块辐条、再拿一个圆形把它中间盖掉做出来的
      （见下面 `.vg-cover`），那块圆形盖子的颜色**必须**和底色逐像素相同，
      底下但凡有渐变就会露一圈缝。
   ========================================================================== */

/* ⚠️⚠️ 整扇窗的尺寸全是 `em`，基准是 `#vignette` 的 font-size —— 也就是 JS
   每帧写进去的"一个设计像素"（大窗实测 1.02871px、小窗 1.01952px）。
   **但 `em` 只在"自己没设 font-size"的元素上才等于设计像素**：一个元素一旦
   自己写了 `font-size`，它（以及它的子元素）的 `em` 就变成**它自己那一档**。

   所以本块有两条规矩，改任何一个数字之前先看清楚属哪一条：

   ① **元素自己不设 `font-size`** ⇒ `em` 就是设计像素，照常写。
      （`.vg-ft` / `.vg-bar` / `.vg-ctl` / `.vg-mid` / `.vg-tb` / `.vg-card`
       / `.vg-row` / `.vg-spec` / `.vg-grid` 这些壳都是这一类。）
   ② **元素自己设了 `font-size`**（文字元素）⇒ 它的 `em` 是"自己那档"，
      间距必须写 `calc(设计像素数 / 自己那档)` 才回到设计像素。
      例：`.vg-sub{font-size:9.6em; margin-top:calc(3em / 9.6)}` ⇒ 3 设计像素。

   ⚠️ 漏掉 ② 的后果是**乘两次**，而且不容易看出来：`.vg-sub` 的
      `margin-top:3em` 会算成 3 × 9.79 ≈ **29.4px**（想要 3px）、
      `.vg-times` 的 `7em` 算成 **60px**（想要 7px）。实测就是这两个数 ——
      小窗那扇的"两行小字"被撑开、底下 `1:39 / 4:12` 和三个键**顶出窗外**，
      被 `.vg{overflow:hidden}` 裁掉，画面上只是"这扇窗底部有点空"。 */

#vignette{
  position:fixed;left:0;top:0;width:0;height:0;
  z-index:2;pointer-events:none;
  font-size:1px;              /* JS 每帧覆盖；这只是它没跑起来时的兜底 */
  transform-origin:50% 50%;
  visibility:hidden;will-change:transform,opacity;
}
#vignette.on{visibility:visible}

/* 一扇窗。四个变体共用这层壳，只有内容是各自的。 */
.vg{
  position:absolute;inset:0;display:none;overflow:hidden;
  background:#0c0f18;border-radius:11em;
  color:#fff;
  /* ⚠️ 影子要**大、偏、带蓝**，不要那种四周均匀的灰雾 —— 后者是"卡片"
     的记号。这个是"一块深色的东西浮在白纸上"，方向和距离都得是真的。 */
  box-shadow:0 28em 60em -14em rgba(10,16,42,.42),
             0 2em 6em -2em rgba(10,16,42,.20),
             inset 0 .1em 0 rgba(255,255,255,.07);
}
#vignette[data-v="ring"]  .vg-ring,
#vignette[data-v="max"]   .vg-max,
#vignette[data-v="queue"] .vg-queue,
#vignette[data-v="bars"]  .vg-bars{display:block}

/* 窗里那层很淡的底纹点阵：产品每一屏都有，铺上它四扇窗才像同一个软件。
   间距比封面那层**粗一倍**（8em vs 4.6em），免得两层点打架出摩尔纹。 */
.vg-grid{
  position:absolute;inset:0;pointer-events:none;
  background-image:radial-gradient(circle at 4em 4em,
                   rgba(255,255,255,.13) 0 .45em,transparent .52em);
  background-size:8em 8em;
}

/* 点阵铺砖的两种密度。⚠️ `0 1.7px` 那种写法（同色写到半径、再卡一个透明的
   同值）边缘会有锯齿，所以一律"实心到某个值 + 后面收边"。 */
.vg-dots{
  background-image:radial-gradient(circle at 2.3em 2.3em,
                   rgba(255,255,255,.42) 0 .5em,transparent .6em);
  background-size:4.6em 4.6em;
}

/* —— 标题栏（小窗那两扇共用）—— */
.vg-tb{
  position:absolute;left:0;right:0;top:0;height:30em;
  display:flex;align-items:center;gap:7em;padding:0 13em;
}
.vg-brand{
  display:flex;align-items:center;
  /* 规矩 ②：自己设了 font-size ⇒ 间距要除掉自己那档（想要 6 设计像素）。 */
  gap:calc(6em / 7.8);
  font-family:var(--latin);font-size:7.8em;font-weight:600;
  letter-spacing:.2em;color:rgba(255,255,255,.86);
}
.vg-brand i{
  /* ⚠️ 这颗点**继承** `.vg-brand` 的 7.8em。不把字号按回去的话，下面
     `width:5em` 会算成 5 × 7.8 ≈ 39px（想要 5px）—— 一颗快撑满标题栏的紫球。
     所以先把字号还原成设计像素（`calc(1em / 7.8)`），下面照规矩 ① 写。 */
  font-size:calc(1em / 7.8);
  width:5em;height:5em;border-radius:50%;display:block;
  background:linear-gradient(140deg,var(--a1),var(--a2));
  box-shadow:0 0 6em rgba(108,140,255,.75);
}
.vg-icons{
  margin-left:auto;display:flex;align-items:center;gap:7em;
  color:rgba(255,255,255,.40);
}
.vg-icons svg{display:block;width:9em;height:9em}

/* —— 小窗：封面圆 + 一圈辐条频谱 ——
   三层，从下往上：
     `.vg-spokes` 一整块**圆**（`border-radius:50%` 自己把背景裁成圆）上画
     一圈圈辐条 → `.vg-cover` 一块**和底色同色**的圆把它中间盖掉 ⇒ 剩下
     那一圈就是频谱环 → `.vg-art` 再叠上封面本体。
   ⚠️ 中间那块盖子的颜色**必须**是平的 `#0c0f18`（见文件头那条）。 */
/* ⚠️ 这里的 `126em` 和下面 `.vg-ft` 的 `height` **必须同进同退** ——
   它们界的是同一条边（封面区的下沿＝页脚的上沿）。 */
.vg-mid{position:absolute;left:0;right:0;top:30em;bottom:126em}
.vg-orbit{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:min(232em,80%);aspect-ratio:1;
}
/* ⚠️ 第 14 轮起 `.vg-spokes` 只是个**壳**（定位用），辐条本体在它那 16 个
   `<i>` 上 —— 每段一根，各带自己的角度扇区和自己的长度。
   原来那一整块（一个圆 + 一层辐条蒙版）拆成了 16 份，**画出来的东西一模一样**，
   只是现在每段能单独"往外长 / 往里收"。 */
.vg-spokes{position:absolute;inset:0}
.vg-spokes i{
  position:absolute;inset:0;border-radius:50%;
  background:conic-gradient(from -90deg,
    #6c8cff,#a86bff 26%,#6c8cff 54%,#a86bff 80%,#6c8cff);
  /* 外缘用一层柔和的白雾压一下，免得环的外边比里边"硬" */
  opacity:.92;
  /* 这一段的长度：一个圆，半径由 `--k`（main.js 每帧写）给。
     `50%` = `.vg-cover` 那圈（= 辐条的**可见起点**，径向渐变的 100% 是
     `closest-side` = 轨道的半径）；`46.5%` = 环带剩下的厚度（116−58 = 58，
     再留 4 给轨道自己那圈 `border-radius:50%` ⇒ 54/116）。 */
  --rp:calc(50% + var(--k,1) * 46.5%);
  /* 三层蒙版**取交集**：
       ① 辐条：和改之前那一层逐字相同（全环共用一份相位 ⇒ 16 段的辐条连得上）
       ② 这一段的角度扇区：`-90°` 起、每段 22.5°，填 84%（产品 `LED_N = 16`、
          `span = step * 0.84`）—— 16 段之间那 16% 就是产品里的"角度缝"
       ③ 这一段的长度：上面那个圆。末端收 2.2% 的软边（≈2.5em）：产品最外
          那一格的亮度本来就是半格（`a = f * 0.96`），硬切一刀反而不像它。
     ⚠️⚠️ **少了 `intersect`（或浏览器不认）时三层是相加的** ⇒ 整圈辐条铺满、
        和改之前**一模一样** —— 屏幕上看着完全正常，只是它一辈子不动。
        所以这一条只能靠"各段外半径不一样"去量，不能靠眼睛说"环还在"。 */
  -webkit-mask-image:
    repeating-conic-gradient(from -90deg,#000 0 1.15deg,transparent 1.15deg 3.5deg),
    conic-gradient(from calc(-90deg + var(--i,0) * 22.5deg + 1.8deg),
      #000 0 18.9deg,transparent 18.9deg),
    radial-gradient(circle closest-side at 50% 50%,
      #000 0 var(--rp),transparent calc(var(--rp) + 2.2%));
  -webkit-mask-composite:source-in;
  mask-image:
    repeating-conic-gradient(from -90deg,#000 0 1.15deg,transparent 1.15deg 3.5deg),
    conic-gradient(from calc(-90deg + var(--i,0) * 22.5deg + 1.8deg),
      #000 0 18.9deg,transparent 18.9deg),
    radial-gradient(circle closest-side at 50% 50%,
      #000 0 var(--rp),transparent calc(var(--rp) + 2.2%));
  mask-composite:intersect;
}
.vg-cover{
  position:absolute;inset:25%;border-radius:50%;background:#0c0f18;
}
.vg-art{
  position:absolute;inset:29%;border-radius:50%;
  background-image:
    radial-gradient(circle at 2.3em 2.3em,
      rgba(255,255,255,.60) 0 .52em,transparent .62em),
    linear-gradient(148deg,#22367f 0%,#5a3ea6 52%,#241c52 100%);
  background-size:4.6em 4.6em,100% 100%;
  -webkit-mask-image:radial-gradient(circle at 50% 50%,#000 0 84%,transparent 96%);
  mask-image:radial-gradient(circle at 50% 50%,#000 0 84%,transparent 96%);
}

/* —— 小窗底部：曲名 / 艺人 / 进度 / 1:39 4:12 / 三个键 ——
   ⚠️ `height` 是量出来的，不是拍的：这一摞的实际高度是
      名 28 + 3 + 艺人 19 + 13 + 进度 3 + 7 + 时间 16 + 5 + 三个键 31 = **125px**，
      原来的 `118em`（120px）装不下 —— 底下那排 `1:39 / 4:12` 和三个键被
      `.vg{overflow:hidden}` 裁在窗外（画面上只是"这扇窗底部有点空"）。
      126em = 128px，留 3px 余量。改这一条记得同步 `.vg-mid` 的 `bottom`。 */
.vg-ft{position:absolute;left:0;right:0;bottom:0;height:126em;padding:0 20em}
.vg-name{
  font-size:14.5em;font-weight:600;letter-spacing:.01em;
  color:rgba(255,255,255,.94);white-space:nowrap;overflow:hidden;
}
.vg-sub{
  margin-top:calc(3em / 9.6);      /* 规矩 ②：想要 3 设计像素 */
  font-size:9.6em;color:rgba(255,255,255,.40);white-space:nowrap;overflow:hidden;
}
/* 进度条：底轨 + 已播的渐变 + 末端一颗游标（游标是产品最认得出来的那一笔）。 */
.vg-bar{
  position:relative;height:2.6em;border-radius:2em;margin-top:13em;
  background:rgba(255,255,255,.13);
}
.vg-bar b{
  position:absolute;left:0;top:0;bottom:0;width:38%;border-radius:2em;
  background:linear-gradient(90deg,var(--a1),var(--a2));
}
.vg-bar u{
  position:absolute;left:38%;top:50%;width:3.4em;height:3.4em;
  margin:-1.7em 0 0 -1.7em;border-radius:50%;
  background:#c9b3ff;box-shadow:0 0 7em rgba(168,107,255,.9);
}
.vg-times{
  display:flex;justify-content:space-between;
  font-family:var(--latin);font-size:8.4em;color:rgba(255,255,255,.42);
  margin-top:calc(7em / 8.4);      /* 规矩 ②：想要 7 设计像素 */
}
.vg-ctl{display:flex;align-items:center;justify-content:center;gap:15em;margin-top:5em}
.vg-ctl button{
  display:flex;align-items:center;justify-content:center;
  padding:0;border:0;background:none;color:rgba(255,255,255,.72);
  /* ⚠️⚠️ 这一行**不能删**。表单控件（button/input/select/textarea）**不继承**
     父元素的 font —— 浏览器的 UA 样式表给它定死了 `font:400 13.3333px Arial`。
     而整扇窗每个尺寸都是 `em`，`em` 的基准正是 `#vignette` 的 font-size
     （JS 每帧写的"一个设计像素"，大窗实测 1.02871px）。
     少了这一行，`button` 内部所有 `em` 都按 **13.3333px** 算：
     `.vg-ctl svg{width:13em}` → 173px、`.vg-ctl .play{width:30em}` → 400px
     （实测逐字就是这个数），那排键把整行撑到 400px 高、再把 `.vg-who`
     挤成 0 宽 —— 画面上是"一颗 400px 的蓝紫圆盘 + 一枚巨大的三角"。
     ⚠️ 小窗那扇**症状一模一样，只是被 `.vg{overflow:hidden}` 裁在窗底外面**，
        所以只看小窗会以为"它本来是对的"，别只修大窗。 */
  font:inherit;
}
.vg-ctl svg{display:block;width:13em;height:13em}
/* 中间那颗播放键：实心圆 + 蓝紫渐变 + 光晕。整扇窗的视觉落点。 */
.vg-ctl .play{
  width:30em;height:30em;border-radius:50%;color:#fff;
  background:linear-gradient(140deg,var(--a1),var(--a2));
  box-shadow:0 0 16em rgba(120,120,255,.55),0 4em 12em rgba(10,16,42,.5);
}
.vg-ctl .play svg{width:14em;height:14em}

/* —— 大窗：满幅点阵封面 + 底部一整条控制栏 —— */
.vg-cover-area{
  position:absolute;left:0;right:0;top:0;bottom:132em;overflow:hidden;
  background-image:
    radial-gradient(circle at 2.6em 2.6em,
      rgba(255,255,255,.50) 0 .58em,transparent .68em),
    linear-gradient(200deg,#20306e 0%,#4a338f 42%,#141a35 78%,#0c0f18 100%);
  background-size:5.2em 5.2em,100% 100%;
  -webkit-mask-image:linear-gradient(to bottom,#000 0 76%,transparent 100%);
  mask-image:linear-gradient(to bottom,#000 0 76%,transparent 100%);
}
.vg-max .vg-ft{height:132em;padding:0 24em;display:flex;flex-direction:column;justify-content:center}
.vg-max .vg-bar{margin-top:0}
.vg-max .vg-row{display:flex;align-items:center;gap:16em;margin-top:13em}
.vg-max .vg-row .vg-times{flex-direction:column;gap:0;margin:0;font-size:9em}
.vg-max .vg-who{flex:1 1 auto;min-width:0}
.vg-max .vg-name{font-size:13em}
.vg-max .vg-sub{font-size:8.6em;margin-top:calc(2em / 8.6)}  /* 规矩 ② */
.vg-max .vg-ctl{margin:0;gap:13em}

/* —— 队列：一叠卡片，选中的那张抬起来 —— */
.vg-queue .vg-list{
  position:absolute;left:0;right:0;top:30em;bottom:0;padding:10em 15em 0;
  display:flex;flex-direction:column;gap:8em;
}
.vg-card{
  position:relative;              /* 第 14 轮②：那张"在呼吸"的卡要挂一层覆盖 */
  display:flex;align-items:center;gap:11em;
  padding:8em 10em;border-radius:7em;
  background:rgba(255,255,255,.045);
}
.vg-card .th{
  width:46em;height:46em;border-radius:5em;flex:0 0 auto;
  background-image:
    radial-gradient(circle at 2.3em 2.3em,
      rgba(255,255,255,.52) 0 .5em,transparent .6em),
    linear-gradient(148deg,#25398a,#523a9c 58%,#1d1746);
  background-size:4.6em 4.6em,100% 100%;
}
.vg-card .tx{min-width:0;flex:1 1 auto}
.vg-card .tx b{
  display:block;font-size:11em;font-weight:600;
  color:rgba(255,255,255,.88);white-space:nowrap;overflow:hidden;
}
.vg-card .tx s{
  display:block;text-decoration:none;
  font-size:8.2em;color:rgba(255,255,255,.36);white-space:nowrap;overflow:hidden;
  margin-top:calc(2.5em / 8.2);    /* 规矩 ②：想要 2.5 设计像素 */
}
/* ⚠️ 「正在播的那一首」只靠三处：底色、左边缘那道主色、标题更亮。
   不去加边框 —— 这一页全站没有一处实线分隔，产品里也只用一道色条。 */
.vg-card.on{background:rgba(108,140,255,.15);box-shadow:inset 2.5em 0 0 var(--a1)}
.vg-card.on .tx b{color:#fff}
/* ═══════════ 第 14 轮②：「正在唱的那张卡」轻轻呼吸 ═══════════
   用户点名要的第三件事（原话「队列窗里正在唱的那张卡轻轻呼吸」）。
   真实软件里正在播的那张卡是**整张铺满频谱**的（第七十八 / 八十七轮），
   "它在动"本来就是那张卡的常态；这一页的卡是画出来的、没有频谱，所以给
   它一个更轻的对应物：整张卡一呼一吸。

   ⚠️⚠️ 动的是**不透明度**，就这一条：
       · 不动 `box-shadow`（那条主色边）—— 阴影每帧要重画一遍，白花；
       · 不动 `transform` —— 卡里的字会被糊掉（这一页的字全是小字号）。
   ⚠️ 这层是**盖在**缩略图和字上面的（`absolute` + 文档序在最后）⇒ 呼吸时
      整张卡连字一起亮一下。只盖底色的话字一动不动，读出来是"底色在闪",
      不是"这一首在唱"。
   ⚠️ 周期 4.3s **故意和 `MUS = 6.4` 那句乐句错开**：跟着乐句走的话，整张卡
      会和窗外那片点阵一起起伏，看着像整页在呼吸。
   ⚠️ 不写 `display:none` / `.nofx` 的例外：`html:not(.js) #vignette` 和
      `prefers-reduced-motion` 那两条**整扇窗都不做**（见下面那个断点），
      这里跟着一起消失，不用再判一次。 */
.vg-card.on::after{
  content:"";position:absolute;inset:0;border-radius:7em;pointer-events:none;
  background:rgba(108,140,255,.20);
  animation:vgBreath 4.3s ease-in-out infinite;
}
@keyframes vgBreath{
  0%,100%{opacity:.20}
  50%    {opacity:.95}
}

/* —— 频谱：一排点柱 —— */
.vg-bars .vg-top{
  position:absolute;left:0;right:0;top:0;height:44em;
  display:flex;align-items:center;padding:0 22em;
  color:rgba(255,255,255,.38);
}
.vg-bars .vg-top svg{display:block;width:11em;height:11em}
.vg-bars .vg-top em{
  margin-left:auto;font-style:normal;font-family:var(--latin);font-size:9em;
  color:rgba(255,255,255,.45);
}
.vg-spec{
  position:absolute;left:22em;right:22em;bottom:34em;height:196em;
  display:flex;align-items:flex-end;gap:5.4em;
}
/* 每根柱子＝一列点（`repeating-linear-gradient` 竖直铺点），高度是各写的。
   ⚠️ 三档颜色靠 `nth-child`，不是每根挂一个行内样式 —— 谱形本身
   （哪根高哪根低）才需要逐根写。
   ⚠️ 第 14 轮：`--h`（index.html 里逐根写的那份**谱形**）没有动，乘上去的
      `--k` 才是每帧变的活值。**最高就是谱形本身**（`--k` 只往下走）——
      往上长会顶出 `.vg-spec` 的 196em 被裁掉，而且"画出来的谱形"
      就不再是"它能到的最高形状"了。 */
.vg-spec i{
  flex:1 1 0;display:block;height:calc(var(--h) * var(--k,1));border-radius:2em;
  background-image:repeating-linear-gradient(to top,
    currentColor 0 1.7em,transparent 1.7em 3.4em);
  color:#6c8cff;
}
.vg-spec i:nth-child(n+8){color:#8a7bff}
.vg-spec i:nth-child(n+15){color:#a86bff}
/* 柱底一条极淡的基线（产品里就有这么一道），不是分隔线。 */
.vg-spec::after{
  content:"";position:absolute;left:0;right:0;bottom:-6em;height:.8em;
  border-radius:1em;background:rgba(255,255,255,.07);
}

/* ⚠️ 窄屏（和 `.wrap.split` 变一栏、点阵缩成一条横带同一个断点）与禁动画：
   **窄屏桌面**不做这扇窗。它是给桌面那 576px 宽的舞台盒子配的；窄窗口里
   舞台只剩 `clamp(150px,26vh,250px)` 高，一扇窗塞进去等于一团糊。

   ⚠️⚠️ 第 16 轮：这道门从"窄屏一刀切"改成**和 `main.js` 的 `vigOK()` 同一条
      界线** —— 窄屏**桌面**（指针 fine）照旧不做，窄屏**触摸**做（用户点名
      要的：手机上"UI 卡片不见了"）。两边必须一起动：只放宽 JS 那一头就是
      "只放窗不挖洞"（点压在窗口上），只放宽 CSS 这一头就是"只挖洞不放窗"
      （手机上留一个永远填不上的空洞）。同一个理由，`build()` 那段写着。
   ⚠️ 判据用 `(pointer:fine)` 而不是 `(max-width:860px)` 单独一条：它就是
      `TOUCH` 里那道 `!pointer:fine` 的正面写法，两边读的是同一件事。 */
@media (max-width:860px) and (pointer:fine){
  #vignette{display:none!important}
}
/* ⚠️⚠️ 第 16 轮：**触摸设备**上给那扇产品窗留出位置。
   门必须和 `main.js` 的 `vigOK()` 是同一条（那边 `TOUCH ? 320 : 861`）——
   `(pointer:coarse)` 就是 `TOUCH` 里 `!pointer:fine` 的正面写法。
   ⚠️ 竖窗那两幕（`ring` / `queue`，设计尺寸 300×461）要**高**盒子；
      横窗那两幕（`max` / `bars`，560×350）要**宽**盒子。给一样高的话，
      横窗会被 `vigCore` 按比例缩到很小（它的设计宽 560 本来就超过手机屏宽），
      里面的字就糊了 —— 这一处只能按形状分档。
   ⚠️ 按**幕号**挑而不是按 `.s-tall` / `.s-wide` 挑：`.s-wide` 还被
      `#features` 用着（那一幕没有窗），一起改会白白把那一幕拉高。 */
@media (max-width:860px) and (pointer:coarse){
  #tour   .stage,
  #tour-3 .stage{height:clamp(300px,44svh,420px)}
  #tour-2 .stage,
  #tour-4 .stage{height:clamp(200px,32svh,300px)}
}

/* ⚠️⚠️ 第 16 轮：那两个 `position:fixed` 的层**不许跟着"布局视口"变宽**。
   手机上的 Chromium 会把**布局视口**撑大 —— 而 `position:fixed` 的包含块就是它。
   `#field` 本来写的是 `width:100%;height:100%`（`#bar` 是 `left:0;right:0`），
   于是它俩跟着变宽，可画布的**位图**还是按视口算的那个尺寸 ⇒ 整片点阵被横向
   拉长（实测 1.2333 倍），屏幕上就是**粒子超出了范围**、糊在幕外面。用户
   "把宽度变一下再变回去就正常了"，是因为那一下触发了真正的视口重算。
   `#bar` 更坏一层：它是 fixed，它变宽之后又把自己这个宽度算回根滚动区，
   **把那个膨胀锁住**，于是再滚也不会自愈。
   `vw`/`vh` 量的永远是**视口**（实测膨胀时 `100vw` 仍是 390）⇒ 钉住这两个，
   视口怎么被撑都不影响它们，也就不可能再锁住。

   ⚠️⚠️ **撑大它的到底是什么（这一轮抓到了，见 `_shots` 里那支逐帧探针）**：
      是**进场动画那一拍横向溢出的瞬态** —— 正文的飞入（`translateX(±110px)`，
      实测 `FIGCAPTION.cap` 的右缘在动画里到过 **480.5**）与那扇产品窗的飞行
      （`#vignette.on` 的 `transform` 把矩形推到过 **x=775.6**）。移动端的
      布局视口会按**到过的最大值**锚定，且**不回缩** ⇒ 480.5 取整就是
      **481**（探测里那个 `inner=[481,962]` 正是这么来的）。
      它是**老毛病**（对照版 r16pre 同样命中，用户报的"超出范围"就是它），
      不是这一轮带进来的；而**可见的那一半已经被下面这两行治好**。

   ⚠️ 这一条挂在 `(pointer:coarse)` 上、**不再挂在 `max-width:860px` 上**：
      平板也是触摸设备，同样会吃到这个膨胀，而"版面什么时候变一栏"和
      "视口会不会被撑大"是两件不相干的事。触摸设备上没有传统滚动条 ⇒
      `100vw` 就是**真视口**（没膨胀时和 `100%` 完全一样）⇒ 正常情况下它是
      **空操作**；桌面是 `pointer:fine`，这条一个字都碰不到。 */
@media (pointer:coarse){
  #field{width:100vw;height:100vh}
  #bar{width:100vw}
}
@media (prefers-reduced-motion:reduce){
  #vignette{display:none!important}
}
html:not(.js) #vignette{display:none}

/* ═════════════════ 第 10 轮③：顶栏下面那条**点阵电平表** ═════════════════
   一条 18 颗的小方块，跟着 `.wrap` 的左边缘（和 brand 对齐）。
   读的是"换幕"这一个事件：**每换一幕先冲到底、再慢慢落回** —— 快起慢落，
   就是电平表的弹道。于是"往下滚"看起来像"在播放这一页"。

   ⚠️ 它**必须是 fixed 的独立一层**，不能画在 `#field` 上：`#field` 是
      `z-index:0`，而正文 `main` 是 `z-index:1` —— 画在那儿会被正文盖住。
      这里给 `z-index:20`，和顶栏同层。
   ⚠️ 铺满宽度的一排点会**看起来像一条虚线分隔**，而这一页第 ① 条规矩是
      "全站没有一处实线分隔"（点列可以当分隔，但这一排**不是**分隔，它是仪表）。
      所以它只有 105px 宽、不横跨版面 —— 读起来才是"一个器件"，不是"一条线"。
   ⚠️ 画布的 CSS 宽高和 `main.js` 里的 `MET_M / MET_PITCH / MET_DOT` 是**一套数**：
      18 颗 × 6px 间距 - 3px = 105px。改一处就得改另一处（下面那句注释里也写了）。 */
#meter{position:fixed;left:0;right:0;top:64px;z-index:20;pointer-events:none}
#meter .wrap{height:0}
#meter-cv{display:block;width:105px;height:3px;margin-top:6px}
@media (max-width:860px){#meter{display:none!important}}
@media (prefers-reduced-motion:reduce){#meter{display:none!important}}
html:not(.js) #meter{display:none}

/* ═════════════════ 第 10 轮④：让**已经有的**点自己长出来 ═════════════════
   第 9 轮加了三处静态饰件（h2 左边那枚 3×3 符号、`.specs` 的点线导引、
   `.pts` 左边那条点列）。它们本来就是"这一页的点阵语言"，这一轮让它们
   **进场时自己画出来** —— 不新增形状、不动一个字、不动版面。

   ⚠️⚠️ 钩子是 `.act.cur`（`main.js` 的 `update()` 已经在加：全场只有一幕带着它）。
   ⚠️⚠️ **`transition` 只写在 `.act.cur` 那一支上**，初态那一支不写。CSS 过渡取的是
      "新状态"里的 `transition` 值 ⇒ 进场走 .5s 的慢过渡、**离场是瞬时的**。
      这正好是想要的：进场要看得见，离场那一幕已经在屏幕外了，瞬时就够了。
      （反过来写：初态带 `transition`，则往下滚时上一幕会**当着你的面把手脚收回去**。）
   ⚠️ 初态一律挂在 `html.js` 底下 —— 没有 JS 时永远不会有人加 `.cur`，
      饰件会**一辈子藏在那儿**。写在 `html.js` 里，没 JS 就退化成现在这一版。

   三处的**揭示方向**都跟着各自的形状走，不是随便定的：
     h2 那枚 3×3  —— 从左上角斜着推开（一颗一颗落，五拍）
     `.specs` 点线 —— 从左往右（它就是一段"导引"，指向右边的值）
     `.pts` 点列   —— 从上往下（竖着串起三条要点） */

/* ① 3×3 符号：从左上角斜着推开。
   `inset(0 100% 100% 0)` → `inset(0)`：右、下两条边一起收 ⇒ 露出来的是一块
   从左上角长大的矩形。九颗的出场顺序因此是 1→2→3→2→1（按"第几圈"分层），
   正是一颗一颗落下来的样子。
   ⚠️ `inset()` 的四条边一起插值，所以这里从"全遮"到"全开"只有四个数要动 —— 
      比 `polygon()` 稳（`polygon` 要求两边点数一致，改一个角就要数一遍）。 */
html.js .act h2::before,
html.js .act h2::after{clip-path:inset(0 100% 100% 0)}
html.js .act.cur h2::before{clip-path:inset(0);transition:clip-path .5s var(--e)}
/* 正中那颗（`--orn` 那一颗）**最后落**，所以它比另外八颗晚一档、并且带一点
   缩放 —— 它是这枚符号的"色锚"，值得单独收一下尾。 */
html.js .act h2::after{opacity:0;transform:scale(.55)}
html.js .act.cur h2::after{
  clip-path:inset(0);opacity:1;transform:none;
  transition:clip-path .3s var(--e) .34s,opacity .3s var(--e) .34s,
             transform .3s var(--e) .34s;
}

/* ② `.specs` 的点线导引：从左往右画出来。
   ⚠️ 用 `clip-path` 而不是 `transform:scaleX()` —— 缩放会把那排**圆点**
      横向压成椭圆，`clip-path` 只是把已经画好的点**揭出来**，点始终是圆的。 */
html.js .act .specs .ldr{clip-path:inset(0 100% 0 0)}
html.js .act.cur .specs .ldr{clip-path:inset(0);transition:clip-path .55s var(--e) .10s}

/* ③ `.pts` 左边那条点列：从上往下。
   ⚠️ 它是 `position:absolute` 的（见上面那节，为了不变成网格子项），
      所以直接裁就行，不碰 `.pts` 的网格。 */
html.js .act .pts::before{clip-path:inset(0 0 100% 0)}
html.js .act.cur .pts::before{clip-path:inset(0);transition:clip-path .5s var(--e) .18s}

@media (prefers-reduced-motion:reduce){
  html.js .act h2::before,
  html.js .act h2::after,
  html.js .act .specs .ldr,
  html.js .act .pts::before{clip-path:none!important;opacity:1!important;transform:none!important}
}

/* ═════════════════ 第 12 轮②：正文**飞进去**（从自己所在的那一侧） ═════════════════

   用户原话：「**文字的转场应该是飞进去而不是展开**」—— 第 11 轮那版是"原地
   分档揭开"（`clip-path` 一档一档往下走），他不要展开、要**飞进来**。
   方向问过了，他选的是「**从侧边飞进来（和产品窗同侧）**」。

   于是这里只剩两件事：**从侧面位移进来** ＋ **显形**（不显形会看见它从邻居
   身上横着穿过去）。整块是**纯 CSS**，`index.html` 一个字节都没改 ——
   钩子是现成的 `.act.cur`（`main.js` 的 `update()` 一直在加，全场只有一幕
   带着它）。要单独撤掉这一轮，把从这行到文件末尾整段删掉即可。

   ⚠️⚠️ **必须是 `animation`，不能是 `transition`。**
      `.cur` 是**会掉的**：新的一幕到 42% 视口时上一幕就掉类，而那一拍上一幕
      **下半截还在屏幕上**（约 -0.58 屏高）。用 `transition` 的话，掉类那一拍
      元素**当场弹回初始态**（被推到旁边 48px 之外再瞬间回到 0）—— 用户会看见
      正文闪一下。用 `animation` 没有这件事：类一掉动画就没了，元素回到
      **自己的常驻样子**，而**常驻样子就是动画的终态** ⇒ 掉类那一拍是
      **恒等的一拍**，看不见。
      （这正是第 6 轮那条「别拿'静止时的噪声底'给'在动的过场'定罪」的同族：
        **钩子的离开态必须等于常驻态**。）

   ⚠️ `transform` 比第 11 轮那套 `clip-path` **宽容**：`clip-path` 按**边框盒**
      切，所以挂在盒子**外面**的东西会被整条裁掉**而且结束后也回不来** ——
      最典型的就是 `h2::before` 那枚 3×3 符号（`left:-38px`，第 9 轮加的）。
      `transform` 不动裁剪，符号跟着标题一起飞 ⇒ 这一轮 **`h2` 不再需要特例**，
      和正文走同一条规则（第 11 轮那条 `vrFade + vrRise` 的 h2 专条已删）。

   ⚠️ **块 = 版面上的叶子块**，容器本身不飞、只飞容器里的东西。
      容器六种：`.wrap` `.col` `.fig` `.steps` `.keygrid` `.faq`。
      那一长串 `:not(...)` 里 **`.col` / `figure` 是"容器不当块"** ——
      它们自己也在 `.wrap > *` 里面，不排除的话就会出现"外层在飞、里层也在飞"
      的双重动作（父层把子层刚飞出来的那一段又推一次）。
      十幕合计 40 块、**最少的一幕也有 2 块**（`#top` 2 / `#features` 3 /
      四幕介绍各 3 / `#guide` 6 / `#keys` 3 / `#faq` 9 / `#download` 5）。

   ⚠️⚠️ **两个绝不许被 `transform` 碰的东西**（都排除掉了）：
      · `h1.mark`（`[data-field="mark"]`）—— `buildMark()` 与 `setStage()` 现读
        它的 `getBoundingClientRect()`，`transform` **会**改这个矩形 ⇒ 点阵那一幕
        整体偏；而且它在 `html.js` 下本来就是 `color:transparent` 的**隐形元素**，
        动它没有任何收益。
      · `div.stage` —— `vigSpawn` 和点阵的 `want` 都现读它的矩形，同一个道理。

   ⚠️ 横向飞行会把块推到版面外最多 110px（右边那一栏、还有镜像幕的左栏）
      ⇒ 靠 `body{overflow-x:hidden}` 收住。**没有它的话**，右栏飞进来的那一秒
      会闪出一条横向滚动条，而那一下正好落在"一次滚轮一幕"的过场里。 */

:root{
  --vr-d:.42s;     /* 一块自己飞完要多久 */
  --vr-fd:.24s;    /* 显形要多久（比飞行短：先看见它、再看它飞完剩下那一段） */
  --vr-x:110px;    /* 从侧面多远的地方飞进来（1.5 个字宽：要一眼看出"横着进来"，
                      小了就只剩"淡入时抖了一下"，而用户要的正是"飞进去"） */
  --vr-t0:.08s;    /* 第一块什么时候起跑 */
  --vr-st:55ms;    /* 块与块之间错开多少（用户点名的那个数，两轮没动过） */
}

/* 方向：`--vr-dir` 是一个**单位数**（-1 左 / +1 右），乘在 `--vr-x` 上 ⇒
   一条 `@keyframes` 管两个方向，不用把动画写两遍。
   ⚠️ 它必须**继承得到**（自定义属性默认继承）—— 用它的那条 `vrFly` 是写在
      `@keyframes` 里的，值按**元素自己**那一份算。
   ⚠️ 一句话记法：**从自己所在的那一侧进来**。
      两栏那几幕于是从两侧**合拢**；而舞台（还有那扇产品窗）都在 `.fig` 那一栏
      ⇒ 题注、窗口同一个方向进来，正是用户要的"一套语言"。 */
html.js:not(.nofx) .act.cur{--vr-dir:-1}                       /* 默认：从左边 */
html.js:not(.nofx) .act.cur .wrap.split > .col{--vr-dir:-1}    /* 左栏：从左边 */
html.js:not(.nofx) .act.cur .wrap.split > .fig{--vr-dir:1}     /* 右栏：从右边 */
html.js:not(.nofx) .act.cur.rev .wrap.split > .col{--vr-dir:1} /* 镜像那几幕：对调 */
html.js:not(.nofx) .act.cur.rev .wrap.split > .fig{--vr-dir:-1}

/* 索引：`nth-child` 就是文档顺序（从上到下）。挂在**所有**容器子元素上 ——
   `h1`/`h2`/`.stage` 也跟着占一个下标，这样"标题先到、正文晚 55ms"是自然结果，
   而它们自己不揭（下面的选择器会把它们挑出去）。
   ⚠️ 最长的容器是 `.faq`（九条 `details`）⇒ 要写到 9；
      `n+9` 那一条是保险：将来加一条问答也不会掉回 `--ai:0`。 */
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(1){--ai:0}
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(2){--ai:1}
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(3){--ai:2}
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(4){--ai:3}
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(5){--ai:4}
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(6){--ai:5}
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(7){--ai:6}
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(8){--ai:7}
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > :nth-child(n+9){--ai:8}

@keyframes vrFly{
  from{transform:translateX(calc(var(--vr-dir,0) * var(--vr-x)))}
  to{transform:translateX(0)}
}
@keyframes vrFade{from{opacity:0}to{opacity:1}}

/* 主规则：从侧面**飞进来** + 淡入。两条并列的 animation 各走各的时间函数 ——
   位移走 `cubic-bezier(.16,.84,.24,1)`（**重减速**：前一小段就吃掉大半行程，
   后面是"落位"而不是"滑到"），显形短一截 ⇒ 先看见它、再看它飞完剩下那段。
   ⚠️ 位移和显形**必须分成两条**：一条 `steps()` 套两个属性的话两个都会一跳一跳。

   ⚠️⚠️ **`> *` 和那一串 `:not()` 之间绝不许有空格/换行。** 写了换行，
      那个空白就变成**后代组合符**：选择器从"容器里的直接子元素"悄悄变成
      "直接子元素**里面**的某一个后代"，于是最上面那几块（`p.claim` 这种
      没有子元素的）一条动画都拿不到 —— 而 `--ai` 那一排是单行写的、
      照样生效，所以 `getComputedStyle().--ai` 有值、`animation-name` 却是
      `none`，症状极像"CSS 没加载"。**整条复合选择器写成一行。** */
html.js:not(.nofx) .act.cur :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > *:not(h1):not(.stage):not(.col):not(figure):not(.wrap):not(.steps):not(.keygrid):not(.faq){
  animation:vrFly var(--vr-d) cubic-bezier(.16,.84,.24,1) both,
            vrFade var(--vr-fd) cubic-bezier(.55,.06,.35,1) both;
  animation-delay:calc(var(--vr-t0) + var(--ai,0) * var(--vr-st));
}

/* ══ 第 13 轮①：还没轮到的那一幕，从**画面上**收起来 ══════════════════════
   用户报的是：「当我滚动到这幕时，这个文字会先在那出现，然后突然消失，进行
   飞入动画」。进场动画挂在 `.act.cur` 上，而 `.cur` 要等这一幕的顶边越过 42%
   视口的**那一拍**才轮到它 —— 在那之前它的正文一直是常驻态（＝动画的终态），
   已经占了屏幕下半截；`from` 一挂上，同一块字当场消失再飞回来。

   ⇒ 让"还没轮到"的那几幕**别画**。哪几幕收着由 `main.js` 的 `update()` 每帧
      写 `.off`（当前那一幕 + 我们正从哪一侧离开的那一幕，理由写在那儿）。

   ⚠️⚠️ 用 `visibility`，**不许**换成 `display:none` / `opacity:0`：
      · `display:none` 把盒子从版面里抽掉，`getBoundingClientRect()` 一律变 0
        —— 十幕的舞台盒子是整片点阵的**唯一参照**（`setStage` 读它），
        `tools\_boxes.py` / `_vignette.py` 也全靠它，抽掉等于把参照物砸了；
      · `opacity:0` 会新建层叠上下文、还会让"这一块在不在"变成像素问题。
      `visibility:hidden` 三者都没问题：**版面照旧，只是不画**。
   ⚠️ 挂在 `.act` 整个幕上（不是逐块）：`visibility` 会继承，里面的块、图注、
      舞台盒子一起收起来，一条规则管到底，也不会和下面那条复合选择器打照面。
   ⚠️ 门还是 `html.js:not(.nofx)` 那道老门：没有 JS、点阵没起来、
      `prefers-reduced-motion` 三种情况一条都不生效，正文自始至终在那儿。 */
html.js:not(.nofx) .act.off{visibility:hidden}

/* 兜底：没有 JS / `.nofx` / 减少动效 ⇒ 一条都不生效，正文自始至终在那儿。
   前两条靠上面 `html.js:not(.nofx)` 这个门（和全站其余动效同一道）。
   ⚠️⚠️ 第三条**必须自己写**：全局那条 reduced-motion 只把 `animation-duration`
      压到 .001ms，**没管 `animation-delay`** —— 而 `both` 填充在**延时期**
      停在 `from` 那一帧（`opacity:0` ＋ 整块停在侧面 110px 外），于是"减少动效"
      的用户会先看到最多 0.52s 的**空白**、再"啪"地全亮。这里整条撤掉，
      内容立刻、完整地在那儿。 */
@media (prefers-reduced-motion:reduce){
  html.js:not(.nofx) .act.cur
    :is(.wrap,.col,.fig,.steps,.keygrid,.faq) > *{animation:none!important}
}
