
      /* ══════════════════════════════════════════════════════════════════
         首页场景令牌（本页命名空间）
         ------------------------------------------------------------------
         深色幕的色值定义在这里，不在 :root —— 只有首页用得到，
         写进全局令牌会污染其他页面。命名空间前缀 `--hm-`（home）。
         ⚠️ 必须走 var() 引用而不是裸写色值：色彩审计会判「未收口」。
         ══════════════════════════════════════════════════════════════════ */
      #main {
        --hm-ink:#05090F;
        --hm-ink-3:#0E1A2B;
        --hm-accent:#5AA2FF;
        /* 滚动驱动的动态量：下面是**无 JS / 降级**时的兜底值，shared.js 用
           setProperty 写在具体元素上覆盖继承值。
           🔴 必须在此**显式定义** —— 只被 var() 引用而从未定义的变量会被
              tools/audit_color.py 判为「幽灵变量」（craft 页的 --p / --hp 同族）。 */
        --hm-rd-2:2px;   /* 2px 高的短横线（分页点 / 帧指示条）两端圆润用；阶梯 radius 无此档 */
        /* ── 舞台安全区（S1–S4 四个滚轴场景共用）─────────────────────────────
           根因：导航是 position:fixed / 高 4.5rem，而 .sc-pin 是 top:0 + height:100svh
           + justify-content:center ⇒ 内容按**整屏**居中，重心必然比「导航之下的可见区」
           高 header/2；画幅越高越明显。1440 宽实测「导航底 → 画面顶」：
             视口 900 ⇒ **+0.4px** · 768 ⇒ **−65.6px** · 680 ⇒ **−109.6px**
           （负值 = 画面顶被导航压住，页面上看不出报错，只是内容被吃掉。）
           S0 开屏的 .landing-stage 从 2026-09-30 起就写了 padding-top:var(--header-height)，
           四个新场景当时没跟 —— 本轮补齐，并把口径统一收敛到下面这组令牌。 */
        --hm-st-pad:clamp(var(--space-4),3svh,var(--space-8));   /* 内容区上下呼吸 */
        --hm-st-top:calc(var(--header-height) + var(--hm-st-pad));          /* 内容上界 */
        /* 2026-10-18 删 `--hm-st-bottom` / `--hm-st-dots`：只有 S4 的 pin 用它算下界，
           S4 改裂屏后 pin 不留 padding，这两个令牌没有引用者。 */
        /* 2026-10-18：`--hm-st-box` / `--hm-fact-cap` 随 S4 裂屏改版一起退役
           （原来用来「由可用高反算工厂图尺寸」，现在图是 112% 满高，不再反算）。 */
        /* 各场景「非画面」占位 —— 从可用高里扣掉，剩下的才给画面。
           🔴 画面尺寸一律由**可用高反算**（长宽比与素材一致 ⇒ 零裁切），
              不再用写死的 1040px 上限：上限写死会在宽屏把画面压得比视口小得多
              （1920 下只占 54% 宽，整屏空荡荡）。 */
        /* ⚠️ 值都是**实测**的，不是估的：估小了内容会溢出安全区，把「导航下的余量」
           再吃掉（实测估小 24px ⇒ 顶部只剩 22px，等于把旧问题搬到引言上）。 */
        /* 2026-10-17 删 `--hm-rail-cap`：长廊改成满高竖裁后，题注不再由「可用高反算
           画面宽」，这个令牌没有引用者（留着会被 audit_color 判「0 引用 token」）。 */
        /* 2026-10-18 删 `--hm-fact-cap`：工厂区题注随旧横移方案一起退役。 */
        /* （原 `--hm-morph-meta` 随 S2 摘下删除 —— 留一个没人引用的令牌会被
           色彩审计 ③「0 引用 token」当场判红。） */
        /* 元素墙（S3）的按列显形进度：兜底 1 = **无 JS 时直接满显**（终态）。
           每档的**时间窗**用 `--t0 / --t1` 两个量表示（JS 只写 `--dg-t`，一档一窗）。
           以 6 列（≥1024）为例：
             · 内档（第 3、4 列）  --t0:0     --t1:.08   ⇒ 约 0.4 屏内**立稳**
             · 中档（第 2、5 列）  --t0:.08   --t1:.165
             · 外档（第 1、6 列）  --t0:.165  --t1:.25   ⇒ 铺满完成（≈1.4 屏）
           🔴 为什么内档要**最先立稳**：参照站里跑马灯压的是一条**已经立稳的**网格。
              内档若到中段还在半空，跑马灯阶段就只剩「几块图挂在屏幕下半截」——
              客户报的「跑马灯阶段图片明显要小了很多」正是这个观感
              （**不是单格尺寸问题**：后来按尺寸改成 4 列，客户立刻否掉「4 列反而丑了」）。
           🔴 为什么不写死档位/常数：档位与列数绑定（`nth-child` 按断点给），
              写死必然在某个断点出现死窗口或提前到位。 */
        --dg-t:1;
        --t0:0;
        --t1:1;
        /* 跑马灯位移（vw；JS 每帧写）。兜底 0 ⇒ 无 JS 时静止在左缘、文字可读。 */
        --dm-x:0;
        /* （原 `--scan / --scanA / --lineA / --hintA` 四个擦除驱动量随 S2 摘下删除。） */
      }

      /* ══════════════════════════════════════════════════════════════════
         S0 开屏（2026-09-30 落地，本轮**结构不动**）
         ══════════════════════════════════════════════════════════════════ */
      .home-hero-content { text-align:center; max-width:1100px; }
      /* 2026-09-22：副标从 h1 内挪出，成为紧跟其后的兄弟 p（H1 只留 brand slogan）。
         保留 display:block —— 万一有人挪回 span，行内/块级的观感不因此变化。
         ⚠️ 改这里要同步 probe_hero_contrast.js 的 SEL/HIDE_TEXT，否则该行文字
            不进对比度判据、且会残留在底板图里污染读数。
         ⚠️ 注释里别写带尖括号的标签名 —— 冒烟用正则全文捞 H1（非贪婪），
            会先命中注释里那个，把 H1 文本读成注释尾部（本轮就踩了，红了一条判据）。 */
      .home-hero-slogan-sub { display:block; line-height:1.7; margin:var(--space-6) 0 var(--space-8); font-size:clamp(1rem,2.4vw,1.25rem); color:rgba(255,255,255,.88); letter-spacing:.06em; }

      /* ============ 序幕 hero · 全屏百叶「落地仪式」三段版（2026-09-30 v2）============
         机制参考 tympanus SVG 蒙版滚动转场，但不照抄：叙事是富喆的「落地」三步——
         ① 生活场景（顶层百叶 A，前半程错峰收起）→ ② 落地实景（中层百叶 B，后半程
         收起、锚点方向与 A 相反）→ ③ 设计手稿（底层静态图，终态定格）。
         副标三段换手，进度细线呼应，CTA 在终段浮出。
         合规：三段均为 AI 意向演绎（同一虚拟客厅，机位已量化对齐）。
         ⚠️ 2026-10-06 用户指令：图片上的角标胶囊已全站移除 ⇒ 意向留痕下沉到
         图片 alt 文本与页脚说明句；介质说明只在 alt 与页脚留痕，不进可见正文。
         回退（铁律）：无 JS / prefers-reduced-motion ⇒ shared.js 不挂 .landing-armed
         ⇒ 两层百叶隐藏、--p 停在默认 1 ⇒ 直接呈现终态（设计手稿 + 全部文案 + CTA）。
         ⚠️ 改这里文字结构要三处联动：probe_hero_contrast.js 的 SEL/HIDE_TEXT + smoke。 */
      .hero-landing { display:block; min-height:0; height:320vh; padding-top:0; overflow:visible; }
      .landing-stage { --p:1; position:sticky; top:0; height:100svh; overflow:hidden; display:flex; align-items:center; justify-content:center; padding-top:var(--header-height); }

/* 🔴 2026-10-28（V-03 复测后客户拍板）：首屏「局部压高光」。
   实测复测结论：不是整幅过曝 —— ≥95% 白的像素**全在左侧**（横向第 2/3 带 11.1% / 18.3%），
   右侧三带只有 0.01–0.12%；过曝区是「左上白墙 + 楼梯白漆栏杆 + 窗光直射的地板光斑」。
   这条只削最亮那一档，让地板光斑回出木纹，不整体压暗（均亮度 70.5→67.7）。
   效果：≥95% 白 7.89% → **2.04%**（−74%），标题对比度 8.72 → 8.22（几乎不动）。

   ⚠️ 必须写在 `.landing-stage > svg` 上，两个位置都不行：
      · `.hero-bg`（及其内部的 img / svg / image）上 filter **完全无效** ——
        实景根本不在 `.hero-bg` 里，而在 `.landing-stage` 的**同级** `<svg><image>`；
        且 `.hero-bg` 那层被 SVG mask 绕过，加了也不渲染。
      · `.landing-stage` 上会把**同级的标题/副标/按钮一起压暗**（文字也在这层里）。
   ⚠️ 曾试过 `brightness(.94) contrast(1.04)`：≥95% 白只降到 5.47%，不够；
      `.92 + 1.06` 才到 2.04%，且肉眼几乎看不出整体变暗。 */
.landing-stage > svg { filter: brightness(.92) contrast(1.06); }
      .hero-landing .hero-bg { z-index:0; }
      .hero-landing .hero-bg img { animation:none; transform:none; }
      /* pointer-events:none：两层百叶是纯装饰的视觉层， 本就不该吃鼠标事件。此前它会拦截首屏 hover —— 而 给 <image> 加了 <title>，SVG title 会触发浏览器原生 tooltip，整屏 hover 弹字是明显干扰。 none 之后事件穿透到下层 .hero-content（CTA 更可点），tooltip 也不再出现。 */
      .landing-blinds { display:none; position:absolute; inset:0; width:100%; height:100%; pointer-events:none; }
      .blinds-b { z-index:1; }
      .blinds-a { z-index:2; }
      .landing-armed .landing-blinds { display:block; }
      /* 移动端专用叠层（home.py 里两个 .m-film）：桌面一律不显示，走 SVG 百叶。
         ≤767 由下面的媒体查询翻成 block + opacity 叠化。 */
      .m-film { display:none; }
      /* 入场仪式（一次性）：顶层百叶错峰展开，生活场景从实景掠影上揭幕。
         只挂 .landing-armed 之下 ⇒ 无 JS / reduced-motion 回退不受波及；
         滚动驱动写的是 height/y 属性，与此 transform 动画不冲突 */
      .landing-armed .blinds-a mask rect { transform-box:fill-box; animation:blinds-in 1.05s var(--ease-out-quart) backwards; }
      .landing-armed .blinds-a mask rect:nth-child(odd) { transform-origin:50% 0; }
      .landing-armed .blinds-a mask rect:nth-child(even) { transform-origin:50% 100%; }
      .landing-armed .blinds-a mask rect:nth-child(2) { animation-delay:.05s; }
      .landing-armed .blinds-a mask rect:nth-child(3) { animation-delay:.10s; }
      .landing-armed .blinds-a mask rect:nth-child(4) { animation-delay:.15s; }
      .landing-armed .blinds-a mask rect:nth-child(5) { animation-delay:.20s; }
      .landing-armed .blinds-a mask rect:nth-child(6) { animation-delay:.25s; }
      .landing-armed .blinds-a mask rect:nth-child(7) { animation-delay:.30s; }
      .landing-armed .blinds-a mask rect:nth-child(8) { animation-delay:.35s; }
      .landing-armed .blinds-a mask rect:nth-child(9) { animation-delay:.40s; }
      .landing-armed .blinds-a mask rect:nth-child(10) { animation-delay:.45s; }
      @keyframes blinds-in { from { transform:scaleY(0); } }
      /* 2026-10-28（V-04）：手机上也要两行。原先这里 display:none 把 <br class="br-l">
         关掉，桌面靠 :196 的 display:block 才两行 ⇒ 手机被压成一行贴边。
         删掉这条即可（<br> 默认就断行），桌面规则保持不变。 */
      .hero-landing .hero-overlay { z-index:3;
        /* 三段图均是明亮系 ⇒ 沿用「亮底深字」。移动端=居中径向纱（下方 media 覆盖）；
           桌面端文字全部左置（tympanus 式电影感）⇒ 左侧奶油白纵向纱，右侧画面全透。 */
        background:
          linear-gradient(90deg, rgba(250,248,244,.80) 0%, rgba(250,248,244,.55) 26%, rgba(250,248,244,.14) 48%, rgba(250,248,244,0) 64%),
          linear-gradient(180deg, rgba(250,248,244,.50) 0%, rgba(250,248,244,0) 22%, rgba(250,248,244,0) 74%, rgba(250,248,244,.28) 100%); }
      .hero-landing .hero-content { position:relative; z-index:4; color:var(--navy-900); text-shadow:0 1px 24px rgba(250,248,244,.55); }
      /* 首页开屏为亮图 ⇒ 透明头（未 scrolled）同步翻深色，滚过揭幕轨道才回 .scrolled 白底 */
      /* 首页页头是**深字**（下方 nav-link 翻成 rgba(11,31,58,.86)），
         所以共享层那条暗 scrim 在这里要反过来 —— 用奶油白，否则深字压暗底更看不清。
         本文件只在首页加载，是最干净的区分方式（不必给 body 加类或改模板）。 */
      .site-header:not(.scrolled)::before {
        background: linear-gradient(180deg,
          rgba(250, 248, 244, .82) 0%,
          rgba(250, 248, 244, .42) 46%,
          rgba(250, 248, 244, 0) 100%);
      }
      .site-header:not(.scrolled) .nav-link { color:rgba(11,31,58,.86); }
      .site-header:not(.scrolled) .brand-logo-white { opacity:0; }
      .site-header:not(.scrolled) .brand-logo-color { opacity:1; }
      .site-header:not(.scrolled) .mobile-menu-btn { color:var(--navy-900); }
      /* 副标三段换手（倒叙）：落地之后，才是生活 ⇒ 效果图什么样，家就什么样 ⇒ 一切，从一张手稿开始 */
      .sub-swap { position:relative; display:block; height:1.7em; font-family:var(--font-display); font-weight:500; font-size:clamp(1.375rem,3vw,2rem); color:var(--navy-900); }
      .sub-swap > span { position:absolute; inset:0; }
      .sub-a { opacity:calc(1 - min(max((var(--p) - .10) * 6, 0), 1)); }
      .sub-b { opacity:min(min(max((var(--p) - .30) * 5, 0), 1), calc(1 - min(max((var(--p) - .58) * 6, 0), 1))); }
      .sub-c { opacity:min(max((var(--p) - .76) * 5, 0), 1); }
      /* 🔴 2026-10-21：副标压在首图亮部，实测最暗 5% 只有 2.77:1（需 4.5）。
   改法不改版式：色值从 80% 透明改实色，另加白色柔光把深色背景抬起来。 */
      /* 2026-10-28（V-04，客户拍板）：删除「苏州富喆科技旗下全屋整装品牌」这一行。
         它是全屏仅此一处的白胶囊（13px 深字 + 暖白底 .78），压在主标与副标之间、
         把两级标题的节奏切断。主体信息由页脚 + JSON-LD 的 legalName 承担。 */
      .hero-landing .home-hero-slogan-sub { color:rgba(11,31,58,.82); }
      /* 2026-10-08：CTA 独立门控。
         ⚠️ 先关掉 shared.css 的 `.hero-actions` fade-up —— 它以 **Animation 来源**压过
         下面两条声明（forwards 终值 opacity:1 / transform:none），等于这两条自诞生起
         就是「写了不生效」（实测：强制 --p 从 0 到 1，computed opacity 恒为 1）。
         `animation:none` 之后，淡入才真的按 --p 走。
         阈值 .86→.18、乘数 7→**6**：p≥0.18 起淡入、p≥0.347 满。
         ⚠️ 乘数用 6 而非报告给的 4：×4 时「p≥0.43 才满」，实测 1440 下 y=600 只有
         op 0.49、390 下 y=356 只有 0.33 —— 半透明的按钮压在亮照片上像**禁用态**。
         ×6 把「虚的那一段」从 0.25 个 p（495px@1440）压到 0.167 个 p（330px）。
         注：p 恰在 0.18 的那一两帧 opacity≈0 但仍可点 —— 与改动前 p=0.96 的行为同族，不另加特判。 */
      .hero-landing .hero-actions { animation:none; opacity:min(max((var(--p) - .18) * 6, 0), 1); transform:translateY(calc((1 - min(max((var(--p) - .18) * 6, 0), 1)) * 16px)); }
      /* 🔴 2026-10-23（无障碍）：门控从 visibility 换成 opacity。
         visibility:hidden 会把节点**移出焦点序与无障碍树** —— 1440 首屏 Tab 过去根本到不了
         主 CTA，只剩页头那个小 CTA 兜底；AX 里它是 notVisible。
         现在：透明 + 禁指针，但**留在 Tab 序**；键盘一进来就 `:focus-within` 立刻显形，
         不会出现"焦点落在看不见的按钮上"，鼠标也不会误点到隐形按钮。 */
      .landing-armed .hero-actions { opacity:0; pointer-events:none; }
      .landing-armed .hero-actions:focus-within { opacity:1; pointer-events:auto; }
      /* ⚠️ .p-end 仍按 p>0.96 走 —— 它还管百叶终态，阈值不许动。CTA 另挂 .p-cta。 */
      .landing-armed.p-cta .hero-actions { opacity:1; pointer-events:auto; }
      .landing-armed.p-end .hero-actions { opacity:1; pointer-events:auto; }
      /* 🔴 2026-10-21 体检：移动端首屏**一个 CTA 都看不到** ——
         页头 CTA 在该断点 0×0、悬浮 CTA 要上滑才出现、首屏主 CTA 又要滚到 p≥.18 才解锁
         ⇒ 390 下首屏可见 CTA = 0。手机上没有「悬停/滚动预热」这回事，直接常显。
         桌面保留原动态（首屏另有页头 + 悬浮两处兜底）。 */
      /* 🔴 2026-10-26 上面这条断点定在 767，漏掉了 **768–1023 平板档**。
         该档 .site-nav 被折叠（见 shared.css 的 1024 断点）、页头 CTA 宽 0、汉堡在里面，
         而首屏 CTA 又被 --p 门控 ⇒ 实测 768/900 首屏**可见转化入口 = 0**（1440/1024 尚有页头 CTA）。
         修法：把「常显」放宽到 1023，与 shared.css 的导航断点对齐；桌面（≥1024）保留开场动画。
         ⚠️ 只改 opacity / pointer-events，不动 --p 驱动与 sub-swap、百叶、进度线。 */
      @media (max-width: 1023px) {
        .landing-armed .hero-actions { opacity:1; pointer-events:auto; }
        .hero-landing .hero-actions { opacity:1; transform:none; }
      }
      .hero-landing .btn-outline { border-color:rgba(11,31,58,.45); color:var(--navy-900); background:rgba(255,255,255,.42); }
      @media (hover:hover) { .hero-landing .btn-outline:hover { background:rgba(255,255,255,.6); border-color:rgba(11,31,58,.7); } }
      /* 滚动引导（仅开场可见）+ 进度细线 */
      /* 2026-10-08：`left:50%` 使 shrink-to-fit 的可用宽只剩**半屏**，
         而内容固有宽 206.3px ⇒ 移动端（360/375/390）全折行，390 下末行只剩一个「生」。
         `width:max-content` 让宽度按内容取（居中由 translateX(-50%) 保证），
         `max-width` 兜住「文案变长 / 屏极窄」时仍能正常折行（不是硬 nowrap）。 */
      .landing-hint { position:absolute; left:50%; bottom:24px; transform:translateX(-50%); z-index:5; margin:0; width:max-content; max-width:calc(100% - 2rem); display:flex; flex-direction:column; align-items:center; gap:var(--space-2); font-size:var(--fs-small); letter-spacing:.18em; color:rgba(11,31,58,.85); opacity:calc(1 - min(max(var(--p) * 8, 0), 1)); }
      .landing-hint::after { content:""; width:1px; height:26px; background:linear-gradient(rgba(11,31,58,.75), transparent); animation:hint-drop 1.6s var(--ease-out-quart) infinite; }
      @keyframes hint-drop { 0% { transform:scaleY(0); transform-origin:top; } 45% { transform:scaleY(1); transform-origin:top; } 55% { transform:scaleY(1); transform-origin:bottom; } 100% { transform:scaleY(0); transform-origin:bottom; } }
      /* 🔴 2026-10-30：width 改 transform。
         原写法 width:calc(var(--p)*100%) 每帧改的是**布局属性** ⇒ 滚动驱动下每帧一次 layout；
         scaleX 走合成层，零布局零重绘。视觉完全一致（都是左端锚定向右长）。 */
      .landing-progress { position:absolute; left:0; bottom:0; z-index:6; height:2px; width:100%; background:var(--blue-400); transform:scaleX(var(--p)); transform-origin:0 50%; }
      @media (min-width:768px){
        .landing-stage { justify-content:flex-start; }
        /* 🔴 2026-10-30：补 width:100%。只有 max-width:660 时，`.hero-content` 作为
           flex item 会**收缩到内容宽**（实测 1000px 档只有 342px，由 h1「你负责喜欢」那行决定），
           副标「效果图什么样，家就什么样」30px×13 字 ≈ 390px 放不下 ⇒ 折成两行，
           而 .sub-swap 高度锁死 1.7em（一行）⇒ 第二行溢出、压到 CTA 按钮上。
           给个 width 后它撑到 max-width 660，副标一行放得下。
           ≥1024 视觉不变（容器本就更宽，仍被 660 截断）。 */
        .hero-landing .hero-content { text-align:left; width:100%; max-width:660px; margin-left:clamp(var(--space-4),6.5vw,7.5rem); }
        .hero-landing .home-hero-content .hero-title { font-size:var(--fs-d1); white-space:normal; line-height:1.12; }
        .br-l { display:block; }
        .hero-landing .hero-actions { justify-content:flex-start; }
        .hero-landing .sub-swap { text-align:left; }
      }
      @media (max-width:767px){
        .hero-landing { height:260vh; }
        /* 🔴 2026-10-30（移动端掉帧，客户反馈「手机上卡、电脑没问题」）：
           initLandingBlinds 每帧对 2 组 × 10 条 <rect> 写 height + y（共 40 次属性写入），
           每次都会让两个全屏 SVG <mask> 重新栅格化：390×844 @DPR3 ≈ 296 万像素/层 × 2
           = 每帧 590 万像素 alpha 重算。桌面栅格化在合成线程 + 独立显存扛得住；
           iOS / 中低端安卓走软件光栅化 ⇒ 掉帧。

           修法**不是**把两层都关掉（那样首屏会从「实景照」退化成「线稿」，第一印象垮掉）：
             ① `mask-image:none` 压掉 SVG 的 mask 表现属性（CSS 属性优先于表现属性）
                ⇒ blinds-a 整层恒显示，**首屏仍是实景照**；JS 仍在改 rect，但没有 mask
                依赖它，不再触发任何栅格化（属性写入本身是微秒级）。
             ② 揭幕改由 `opacity` 承担（合成层属性，零重绘）：p>0.30 起淡出，
                到 p≈0.70 完全露出底层线稿 —— 桌面「实景 → 线稿」的叙事在手机上保住了。
             ③ blinds-b（中间态「落地实景」）直接不显示：手机上三段逐帧揭幕本来就看不见，
                留两层只会多一份全屏合成。
           ⚠️ `-webkit-mask-image` 必须同时写（Safari 不认无前缀的那条）。 */
        /* 🔴 2026-10-30 第二轮（客户实测：摘掉 mask 之后**仍然卡**）：
           真瓶颈不是 mask，是「改 rect ⇒ 整棵 SVG 脏 ⇒ 全屏 filter 每帧重算」这条链。
           所以 ≤767 让两棵 SVG **整个退出渲染树**（display:none 的元素不重绘、不跑 filter），
           改由 home.py 里那两个 .m-film（纯 <img>）做 opacity 叠化：
             ① p .08→.46  生活场景(landing-life)淡出 ⇒ 露出落地实景(landing-real)
             ② p .52→.90  落地实景淡出 ⇒ 露出底层手稿(hero-bg)
           两段区间不重叠：opacity 是 source-over，两层同时半透明时总覆盖率
           = 1−(1−x)(1−y) < 1，底层线稿会透上来发灰。
           `will-change:opacity` 让它提前进合成层，滚动时只走合成、不重绘。
           ⚠️ 桌面（≥768）不进这个媒体查询，SVG 百叶与那条 filter 照旧。 */
        .landing-armed .landing-blinds { display: none; }
        .m-film { display: block; position: absolute; inset: 0; width: 100%; height: 100%;
          object-fit: cover; z-index: 1; will-change: opacity; }
        .m-film-life { z-index: 2; opacity: calc(1 - min(max((var(--p) - .08) * 2.6, 0), 1)); }
        .m-film-real { z-index: 1; opacity: calc(1 - min(max((var(--p) - .52) * 2.6, 0), 1)); }
        .hero-landing .hero-overlay {
          background:
            radial-gradient(ellipse 96% 44% at 50% 50%, rgba(250,248,244,.62) 0%, rgba(250,248,244,.30) 58%, rgba(250,248,244,0) 80%),
            linear-gradient(180deg, rgba(250,248,244,.52) 0%, rgba(250,248,244,0) 24%, rgba(250,248,244,0) 72%, rgba(250,248,244,.30) 100%); }
        .landing-hint { bottom:56px; }
        /* 2026-10-28（V-04）：主标提到 36px 后，副标 22px 与它只差 14px ⇒ 层级仍糊。
           降到 17px，并把这行的上间距收紧（原 --space-6=24px 是给白胶囊留的位置）。 */
        .sub-swap { font-size:var(--fs-body-lg); }
        .home-hero-slogan-sub { margin-top:var(--space-3); }
      }

      /* ── 场景引语（S1 总起句 / S4 开场句，两处共用）──────────────────────────
         🔴 2026-10-16 **从 .sc-pin 内移出到 section 层**。原因：引语原先在 sticky
            舞台里占位，而舞台只有 100svh —— 引言淡出后仍占一条 104px 的空带，
            画面被压小、且整块重心下移（实测 1440×900：画面从 1014 掉到 886 宽、
            可见内容比安全区中心低 62px）。移到 section 顶层后它随页面滚走，
            舞台把全部可用高让给画面。semantically 也更对：这句话引入的是**整节**，
            不是某一帧的画面。
         ⚠️ 之后 shared.js 的 measure() 必须按 pin.offsetTop 扣掉这一段，
            否则 p 映射会整体偏移（轨道起点错位 = 用户说的「停留位置不正」）。
         ⚠️ 用 padding-top 不用 margin-top：首子元素的 margin 会与 section
            折叠（section 没有 border/padding、不是 BFC）⇒ 整节下移，而
            pin.offsetTop 又读不到它。 */
      .rail-head, .f-head {
        padding:var(--hm-st-pad) var(--space-6) 0;
        margin:0 auto var(--space-8);
        width:max-content; max-width:calc(100% - var(--space-12));
        text-align:center; color:#fff; }
      .rail-head, .f-head { font-family:var(--font-display); font-weight:600;
        font-size:clamp(1.25rem,2.4vw,1.875rem); line-height:1.34; letter-spacing:.01em; }

      /* ══════════════════════════════════════════════════════════════════
         S1 · 六个风格长廊（2026-10-20：两图一帧 → 一图满幅 + 软边擦除）
         ══════════════════════════════════════════════════════════════════
         机制：外层是滚动轨（height 决定行程），内层 sticky pin 钉住一屏，
         滚动进度 p 驱动**一条边界从左推过去**（图不动）。
         🔴 为什么改 —— 不是审美，是账：
            六张 hero 都是 1536×950（1.6 横幅），而旧版 `.rail-item{flex:0 0 50vw}`
            × 100svh 的显示盒是 **0.8 竖幅** ⇒ object-fit:cover 只剩中间一条：
            1440 下可见图宽 **49%**、1024 下 **35%**、768 下 **24%**。
            「两图一帧」当年是**为位移距离做的妥协** —— 一图一帧要 600vw 轨道、
            500vw 位移，按 1vw/svh 的速度得 **600svh** 才走得完。
            擦除**不需要位移**（图不动、只有边界推过去）⇒ 满幅只要 260svh，
            比旧版 300svh 还少 40svh。要同时拿到「大图」与「快速下拉」，只能走这条路。
         🔴 用 mask 不用 clip-path：硬边在 50/50 那一瞬读作「两张画拼错了」；
            3% 羽化（约 43px@1440）读作「一次光扫过去」。
         ⚠️ 只给「正在被推开的那一张」写 mask，其余帧 visibility:hidden ——
            六张全屏图叠着合成会白烧 GPU。判定在 shared.js 的 edge / top。
         ⚠️ `.rail-track` 旧的 `align-self:flex-start` 补丁随 flex 一起退役（不再是横向轨道）。 */
      /* 2026-10-20 二次调：260svh 时每个风格只摊到 **32svh ≈ 288px** 滚动量，
         一次触控板滑动（800–1500px）直接把六张全扫过去 —— 客户实测「下拉一下就全滑过」。
         ⇒ 行程 160svh → 440svh（节高 540svh），每段 88svh ≈ 792px ≈ 0.88 屏，一格一次滑动。 */
      .sc-gallery { position:relative; height:calc(100svh + 440svh); background:var(--hm-ink); }
      .sc-gallery .sc-pin { position:sticky; top:0; height:100svh; overflow:hidden;
        display:block; padding:0; }
      .rail-track { position:relative; height:100%; }
      /* 六张同框：z-index 由 data-k 决定（上层盖下层），默认全隐，由 JS 逐帧点名 */
      .rail-item { position:absolute; inset:0; height:100%; overflow:hidden; margin:0;
        visibility:hidden; }
        /* ⚠️ 不写 will-change:mask-image：六张全屏图各挂一条 = 六个常驻合成层
           （4–5 张还是 visibility:hidden），内存白占；mask 本身是绘制期作用，
           提层不会让遮罩更快。 */
      /* 无 JS 兜底：至少留第一张（不写这条会整屏空 —— 六张全 visibility:hidden） */
      .rail-item[data-k="0"] { visibility:visible; }
      .rail-item img { display:block; width:100%; height:100%; object-fit:cover;
        object-position:center; background:var(--hm-ink-3); }
      /* 手机竖屏满屏 + 横构源图 ⇒ 每张只露中间 26~29% 宽（见 2026-10-26 的裁切对照）。
         逐张挑窗口的结论（客户 2026-10-26 圈定，其余保持 center）：
           中古 75%、轻法式 25%（壁炉一侧）；原木先定 75%，二次看过实拍后回到 50%
           （75% 上半是大片空木墙，主体压在下 1/3，偏空）。
         ⚠️ 必须挂 `data-s`（风格 slug）而不是 `data-k`：data-k 是**槽位序号**，
            风格重排一次就全错位，而且错得很隐蔽（图上还是好看的，只是换了一张）。
         ✅ 桌面端实测几乎不受影响：卡片在 ≥1024 是**满幅**（1440×900，与源图 1.617 近乎同比例）
            ⇒ 可见约 99%、裁切 1%，`object-position` 在那边看不出差别。
            （注意 `sizes` 写的是 `60vw`，与实际满幅不一致；但源图 1536 已是最大档，
             顶多让桌面少挑一档，不产生实际损失 —— 2026-10-26 记录在案，不改。） */
      .rail-item[data-s="yuanmu"] img { object-position:50% 50%; }
      .rail-item[data-s="zhonggu"] img { object-position:75% 50%; }
      .rail-item[data-s="qingfashi"] img { object-position:25% 50%; }
      /* 文字压图：渐变只压下半（上半保持原图画质），caption 贴左下 */
      .rail-item::after { content:''; position:absolute; inset:0; pointer-events:none;
        background:linear-gradient(to top, rgba(5,9,15,.92) 0%, rgba(5,9,15,.40) 24%,
                   rgba(5,9,15,0) 54%); }
      .rail-item figcaption { position:absolute; left:var(--space-6); right:var(--space-6);
        bottom:var(--space-10); z-index:1; margin:0; color:#fff; }
      .rail-no { display:block; font-size:var(--fs-nano); letter-spacing:.22em;
        color:var(--hm-accent); margin-bottom:var(--space-2); }
      .rail-item b { display:block; font-family:var(--font-display); font-weight:700;
        font-size:clamp(1.75rem,3vw,2.75rem); line-height:1.1; letter-spacing:-0.015em; }
      .rail-item span { display:block; margin-top:var(--space-1-5);
        font-size:var(--fs-small); color:rgba(255,255,255,.66); }
      /* S1 长廊与 S4 五图带共用一套翻页点（同一形制，不新造一套 48px/2px 的裸字面） */
      /* 🔴 z-index 必须高于 .rail-item（JS 逐格写 0..5）：原来只有 2，
         第 4 格起圆点被当前图整排盖住（移动端审视实测 elementFromPoint 命中的是 IMG）。 */
      .rail-dots, .f-dots { position:absolute; left:50%; bottom:var(--space-5); z-index:10;
        transform:translateX(-50%); display:flex; gap:var(--space-2-5); margin:0; padding:0; list-style:none; }
      .rail-dots i, .f-dots i { display:block; width:48px; height:2px; border-radius:var(--hm-rd-2);
        background:rgba(255,255,255,.22); transition:background var(--duration-normal) var(--ease-out-quart); }
      .rail-dots i.on, .f-dots i.on { background:var(--hm-accent); }
      /* 手机端：常驻浮标压底部 69px（实测浮标顶 775 / 视口 844）⇒ 圆点与题注整体上移
         一个浮标高，否则「当前第几格」和整段题注都被浮标盖住（且圆点不可点）。
         用令牌相加而不是写 56px：56 不在间距阶梯上，会触发裸字面判据。 */
      @media (max-width:767px) {
        .rail-dots, .f-dots { bottom:calc(var(--space-5) + var(--space-16)); }
      }
      /* 窄屏翻页点收窄：合并时与文件末尾那组 @media (max-width:360px) 重复（两侧各加了一次），
         保留末尾带完整推导的那组，此处不再重复声明。 */
      /* 总起句：满屏后必须**绝对定位压在图上**（pin 是 sticky 且 DOM 在后，会盖住它 ⇒ 提 z-index）；
         只在前 12% 行程里显现，之后交给图自己说。 */
      .rail-head { position:relative; z-index:3; margin:0 auto;
        padding:var(--space-12) var(--space-6) var(--space-6);
        /* 🔴 必须显式建栈：伪元素是 `z-index:-1`，没有 stacking context 时会掉到
           section 背景（--hm-ink）下面 —— 表现是「暗场时有时无」，取决于当前 opacity。
           `opacity` 只在 <1 时才建栈，等于把可读性交给「滚到哪一格」。 */
        isolation:isolate;
        /* 满屏图后总起句压在画面上：原木/法式这类高亮图白字会糊，
           三道阴影（近实+远柔）才能在任意底图上站住 */
        text-shadow:0 1px 2px rgba(5,9,15,.45), 0 6px 24px rgba(5,9,15,.55);
        /* 🔴 2026-10-20：一图满幅后标题左上角正好落在原木风的浅色天花上，
           实测白字对底 **1.99:1**（大字须 ≥3）。阴影救不了大面积亮底 ⇒
           加一层**只包住文字块**的径向暗场，随 --headA 一起退场。
           负 margin 抵消 padding，文字位置与加之前逐像素一致。
           ⚠️ 不能做成 pin 层的整条暗带：站头是深色字，压暗顶栏会让导航更难读。 */
        }
      /* 暗场**不能**画在 .rail-head 自己的背景上：它的盒子只有文字那么大，
         渐变贴着字边走 ⇒ 读作一块深色方块。挂在伪元素上、外扩一圈才像压角。 */
      /* 2026-10-21 客户：标题背后的暗场是「一块深色方块」，拿掉；
         标题已移到图上方（--hm-ink 深底），白字本身达标。 */

      @media (max-width:767px) {
        /* 触摸端：同样一屏一张、同样擦除（竖向滚动推边界，不必横滑）。
           高度保持 360svh 不动 —— 手机上字号小、读得慢，不给它减行程。 */
        .sc-gallery { height:360svh; }
        .rail-item figcaption { left:var(--space-5); right:var(--space-5);
          /* 常驻浮标占底部 69px（实测浮标顶 775 / 视口 844）⇒ 题注整体上移一个浮标高，
             否则「包豪斯」这类两行题注的下半截正好被浮标切掉。
             🔴 这条是触摸端的**最终规则**（比上面的通用规则更靠后）；改题注位置只改这里。
             🔴 2026-10-28（UX-17 复测）：原来只有 24+64=88px，题注底边落在 756，
                与浮标顶 775 只余 **19px**，不足 24px 安全区。补一个 space-2(8px) ⇒ 96px，
                实测间距 27px。
             ⚠️ 这 88 里已经含浮标的 70px（底距 14 + 高 56），浮标高度若变，这里要跟着变
                —— 两处都硬编码，没有共享变量。
             ⚠️ 本文件是**源**（src/index-page.css）；产物叫 public/assets/css/page-index.css，
                文件名是倒过来的。改产物会被 generate_site.py 覆盖。 */
          bottom:calc(var(--space-6) + var(--space-16) + var(--space-2)); }

      }

      /* ══════════════════════════════════════════════════════════════════
         S2 · 两条竖排（参照站 section① · 动效 A）—— 2026-10-17 新增
         ══════════════════════════════════════════════════════════════════
         🔴 术语澄清（客户 2026-10-17 09:49）：「跑马灯」= **两条竖排往上跑的
            图片**（参照站动效 A），不是横向滚动的文字（动效 B）。此前 5 轮全改在 B 上。
         参照站实测（真源码）：`--grid-columns:2` · `--grid-max-width:**300px**`
            （每列仅 134px）· 44 格（2 列 × 22 行）· 单格 134×112（ratio 1.2）·
            gap **32px** · `.grid__item { perspective:800px }` ·
            **每格独立 ScrollTrigger**（`top bottom+=10%` → `bottom top-=25%`，
            单格区间实测 1327px）。
         我们：**36 格 = 2 列 × 18 行**（`_TILES` 复用，正好整除，不用重复图）。
         🔴 **这一段不是 sticky**：参照站就是让网格自然滚过视口，每格按
            **自己在视口里的位置**决定进度 ⇒ 不需要 pin、不需要 `--dg-t`。
         🔴 零依赖手写：每帧只给**处于视口带内**的几格（实测约 6 格）写一个 `--p`，
            其余格的 transform/filter 全由 CSS 从 `--p` 三点插值派生。 */
      .sc-strips { position:relative; background:var(--hm-ink);
        padding:20svh 0 12svh; }
      /* 条宽 = 参照站 `--grid-max-width:300px`；窄条是这一屏的**签名**，
         别为了「填满画面」加宽 —— 两列窄条 + 大留白正是它的观感。 */
      .st-track { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
        gap:var(--space-8); width:min(300px,74vw); margin:0 auto; }
      .st-item { position:relative; margin:0; aspect-ratio:1.2;
        /* 🔴 2026-10-23（UI 评审 §五.2）：**必须高于跑马灯**。
           原状是跑马灯 `z-index:2` 压在图上面 —— 48px 白字横穿两列 134px 的小照片，
           字压在照片细节上两边都读不清，整屏于是读作「调试页」（评审原话）。
           现在图在字**前面**，字从两列窄条**背后**穿过去：照片完整、字也完整，
           只在窄条那 300px 里被干净地遮掉 —— 这才是参照站那种「type behind objects」
           的层次，而不是叠印。
           ⚠️ 这里不写数字更大的 z-index 也能生效，但**必须显式写**：`.st-item` 是
           `position:relative` 且 z-index:auto 时落在「定位但 auto」那一层，永远在
           z-index ≥1 的下面 —— 只改 `.st-mark` 的 z-index 是改不动的。 */
        z-index:2;
        border-radius:var(--radius-sm); perspective:800px;
        /* 🔴 三点插值：`--p` 0=刚进视口底 · .5=视口中央 · 1=已出视口顶
           （区间口径照参照站：起点 `bottom+=10%`、终点 `top-=25%`，见 JS）。
           派生量全是 (--pi, --po) 的**线性**组合 —— 不要写成乘积，
           CSS calc 里 `var(--pi) * var(--po)` 这种两数相乘是不合法的。 */
        --p:1;                                  /* 兜底终态：无 JS 时格子在「自然态」可见 */
        --pi:clamp(0, calc(var(--p) * 2), 1);    /* 进场：0→1 */
        --po:clamp(0, calc((var(--p) - .5) * 2), 1);  /* 出场：0→1 */
        /* 亮度专用的**平台段**（2026-10-17 客户「图是不是有点暗」）：
           原来 brightness = --pi - --po ⇒ 只有 p=0.5 那一行是 100%，两侧线性掉到 0。
           实测 1440×900：可见 7 行里只有 1 行 0.95，其余 0.83 / 0.74 / 0.61 / 0.52 /
           0.38 / 0.17 —— 整屏读作「黑底上几个小窗」。参照站同位置有 6 行清晰可见
           （它的亮度曲线带缓动、不是线性）。
           ⇒ ① 平台 [0.36, 0.64] 内保持满亮 ② 两端不压到 0、留 30% 底
              （压到全黑是「圆柱感」的代价，但客户要的是「看得见」）。
           ⚠️ 仍必须用 (--pbi, --pbo) 的**线性组合**：CSS calc 里两个数不能相乘。 */
        /* ⚠️ 两段坡度**故意不等宽**：进场 0.02→0.42（缓，从下方淡入）、
           出场 0.64→0.84（陡，出画要快）—— 等宽会把"中段才亮"的落差压平
           （实测 1440 下只剩 1.5 倍，护栏 `明暗落差 ≥2` 判红）。 */
        --pbi:clamp(0, calc((var(--p) - .02) / .4), 1);
        --pbo:clamp(0, calc((var(--p) - .64) / .2), 1);
        --sgn:1; }
      /* 左右两列**镜像**（参照站 isLeftSide：左列 xPercent -40 / skewX -20 / rotateZ +5） */
      .st-item:nth-child(2n) { --sgn:-1; }
      .st-card { position:absolute; inset:0; overflow:hidden;
        border-radius:inherit; background:var(--hm-ink);
        backface-visibility:hidden; will-change:transform, filter;
        transform:
          translate3d(calc(var(--sgn) * (-40% + 40% * var(--pi) - 20% * var(--po))),
                      calc(100% - 100% * var(--pi)), 0)
          rotateX(calc(70deg - 70deg * var(--pi) - 50deg * var(--po)))
          rotateZ(calc(var(--sgn) * (5deg - 5deg * var(--pi) - 1deg * var(--po))))
          skewX(calc(var(--sgn) * (-20deg + 20deg * var(--pi) + 10deg * var(--po))));
        /* 🔴 提亮策略（2026-10-17 两轮，**以第二轮为准**）：
           ① 第一轮：把 `brightness` 从「线性三角」改成「平台 + 底」——
              原来只有 p=0.5 那行是 100%、两侧掉到 0，整屏发黑（客户报「有点暗」）。
           ② 第二轮：平台给到 126% 后，**亮图爆了**（客户：「有些图太亮，亮到曝光」）。
              实测 36 张：1.26 下有 **12 张过曝 >2%**、最多 **12.19%** 的像素被压成纯白。
           ⇒ 正解不是「降回 1.0」，而是 **先用 contrast<1 把高光压回、再用 brightness 抬整体**：
              平台段 = `contrast 86%` + `brightness 109%`，
              映射 = ((x − 0.5) × 0.86 + 0.5) × 1.09。
              实测同一批图：**平均亮度 85 → 83（几乎不变）· 过曝 12.19% → 1.78%
              · 反差 std 1.22 → 0.94（原来是被乘法撑硬了，不是真的通透）**。
           🔴 **顺序不能反**：contrast 必须在 brightness **之前**。
              反了就是先乘后压，中间值已越过 1.0 被裁掉，白的地方照样死白。
           `calc(400% - 314% * --pbi + 400% * --pbo)` → 平台 86%
           `calc(34%  + 75% * --pbi - 75% * --pbo)` → 平台 109%（p=0/1 时 34%）
           ⚠️ 两端不压到 0（34% 底）：圆柱感抵不过「看不清」。
           ⚠️ blur 7px → 5px：134px 宽的图上 7px 已经糊成一片色块。
           ⚠️ 这两个平台值是**像素模拟**定的，不是手调：改它们之前先跑
              `tools/check_strip_exposure.py`（会算出过曝面积与平均亮度）。 */
        filter: blur(calc(5px - 5px * var(--pi) + 3px * var(--po)))
                contrast(calc(400% - 314% * var(--pbi) + 400% * var(--pbo)))
                brightness(calc(34% + 75% * var(--pbi) - 75% * var(--pbo))); }
      .st-card > img { width:100%; height:100%; object-fit:cover; display:block; }
      /* 跑马灯（文字）：从 S3 搬到这里 —— 参照站 `.mark` 是 `position:fixed`，
         区间 = **本节**从「顶到视口底」到「底到视口顶」。
         放在这一节的额外好处：背景是黑底 + 一条 300px 窄带 ⇒ 白字天然可读
         （压在 S3 那面亮墙上是糊的，这是 10-17 遗留的最后一个观感隐患）。
         🔴 2026-10-23（UI 评审 §五.2）：`z-index` 从 2 降到 1 —— 让字**退到图后面**
            （配 `.st-item{z-index:2}`）。评审报的「风格名互压」实际是「字压在照片上」：
            48px 白字横穿 134px 的小图，字和图互相吃掉细节。
         🔴 两端**渐隐**（mask）：原来 `overflow:hidden` 是硬切，词被视口边缘一刀切断
            （评审：「美式中古左切、轻法被切」）。marquee 的词本来就该进出画面，
            硬切看起来像渲染出错、淡出看起来像有意为之 —— 只改「怎么退出」，不改行程。
         ⚠️ mask 不算底色：`probe_home_stages.js` 的 `cmpMarkClear` 判的是
            `backgroundColor` alpha 与 `backgroundImage`，加 mask **不会**把那条
            「字离场后留一条黑带」的护栏弄红（那正是客户 2026-10-17 报过的现象）。
         ⚠️ 渐隐宽度 7%：1440 下约 100px，比一个字窄，看起来是「淡出」而不是「掐掉半个词」。 */
      .st-mark { position:fixed; left:0; top:50%; width:100vw; z-index:1;
        transform:translateY(-50%); margin:0; overflow:hidden;
        -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 7%,#000 93%,transparent 100%);
        mask-image:linear-gradient(90deg,transparent 0,#000 7%,#000 93%,transparent 100%);
        pointer-events:none; }
      .st-mark-in { display:flex; gap:var(--space-12); width:max-content;
        font-family:var(--font-display); font-weight:400; color:#fff;
        /* 48px = 正文 3 倍。🔴 不写 `3rem`（B1 判「阶梯外裸字面」）；
           也不新建 `--fs-marquee:3rem`（A1 会被 page-thermo 的 48px 序号撞红）。 */
        font-size:calc(var(--fs-body) * 3); line-height:1; letter-spacing:normal;
        will-change:transform;
        transform:translateX(calc(var(--dm-x,0) * 1vw)); }
      .st-mark-in > span { white-space:nowrap; }

      /* ══════════════════════════════════════════════════════════════════
         叙事：整屋在 S1 看过了，这一屏**放大看构件**。而「多」本身就是论据 ——
         六个风格各 6 张构件特写、36 张一次铺满，比任何一句「我们很专业」都硬。

         **进场机制（权威口径 = codrops「Staggered3DGridAnimations」的 animateGridFull，
         交接单 2026-10-16）**：卡片尺寸全程不变，变的只是「已经显形了哪些格子」，
         顺序**由中列向两侧**（`delay = |列−中列| × 0.2`，列内所有格子同时动）。
         轮廓因此是「中间一整竖列先立起来、两侧各矮一截」的**阶梯**。
         ⚠️ 我们**不做 3D**（客户明确）：原 demo 那条窄网格用
            `rotateX 70°→−50° + blur + brightness(0%)` 做反转与明暗，本屏不采用。

         🔴 性能三条（客户明确要求「基于性能优化」）：
           ① **JS 每帧只写一个 `--dg-t`**；36 个格子的 opacity/位移全由 CSS 从它 +
              本档时间窗（`--t0/--t1`，按 `nth-child` 给）算出来。
              **逐帧写 36 个格子的 transform 是这一屏最容易踩的坑。**
           ② 容器**不带任何 transform/filter** ⇒ 只有合成、没有重排，也没有大合成层。
           ③ 瓦片是**专门派生的小图**（560×420 q74，与格子同比例 ⇒ 零裁切）：
              源图 1960KB → 36 张合计 411KB、平均 11.4KB；并保留 lazy
              （实测首屏瓦片请求 0 个）。
         🔴 满屏是**刻意的**：本屏不再是「安全区内的舞台」而是满屏画布 ——
            固定导航压在它上面（导航在白底态自带背景）。
            ⇒ 护栏 check_home_stages 对本屏的判据由「导航底→主体顶 ≥20px」改为
              「铺满视口」（改判据必须记去向，README 表行已同步）。
         🔴 兜底 `var(--dg-t,1)` ⇒ **无 JS / reduce 直接呈现终态**（满显、不位移）。
         🔴 瓦片放在 `assets/styles/tiles/`（**不是** assets/tiles/）：stamp_assets 的
            正则是 `assets/styles/**` —— 换个目录就是「三不管资产」，裸引用永远没人盖版本号。
         ⚠️ **本注释里不要写「不存在的令牌」的引用写法**：产物注释卫生与「悬空令牌」
            两条判据都**扫产品文本、不剥注释** —— 注释里引一个已删掉的令牌照样判红。
            （本批实测踩过一次：注释还在描述两版前的机制，引了个已经不存在的名字。） */
      /* 2026-10-16 客户指令：本屏背景换**黑**（原为深蓝 #0A1420）。
         换完 S1/S3/S4 三个深色幕统一用同一个 `--hm-ink`（#05090F）——
         参照站也是纯黑底，缝隙与页边透出的就是它。 */
      /* 节高 200svh：行程 = **1.00 屏**（≈ 参照站 section③ 的 930px = 1.03 屏）。
         🔴 2026-10-17 第二次修正（客户：「进入下一板块要拉三下」）：
            原来写的是 600svh（5 屏行程），但那个量是**为跑马灯定的** ——
            而跑马灯（横向文字）后来搬去了 S2。搬走之后这 5 屏就没有任何东西要吃：
            档位时间窗把铺满压在 `--dg-t 0→.25`（dp≈0.28 ≈ 1.4 屏），
            余下 **3.5 屏整墙静止**，观感就是「手指要划三下，画面一动不动」。
            ⇒ 节高收到 200svh（行程 1 屏），档位改为**铺满整段**（`--t1` 末档 = 1），
              铺满完成点落在 dp≈0.92 —— 墙一直在升，最后一点点刚好交给下一节。
         ⚠️ 别再把节高按「跑马灯时长」定：跑马灯现在在 `.sc-strips`，两节互不相干。 */
      .sc-detail { position:relative; height:200svh; background:var(--hm-ink); }
      .sc-detail .sc-pin { position:sticky; top:0; height:100svh; overflow:hidden; }
      /* 满屏瓦片：列数固定 6（`_TILES` 是**行优先**的「同一位次 × 六个风格」，
         改列数会把这层设计破坏 ⇒ 列数不可动，只能动行高）。
         🔴 **缝隙必须留**（客户原话「图和图之间要有间隔，全部贴在一起视觉太乱了」）：
            gap 16px ≈ 格宽的 7%，再配 radius-sm 圆角 —— 读起来才是「一墙卡片」，
            而不是「一张被切碎的图」。实测参照站 16px，同值。 */
      /* 🔴 历史坑（2026-10-16，别再用老解法）：`inset:0`（四边全钉）给出确定高度后，
         `aspect-ratio: 4 / 3` 撑出来的行在 min-content 计算里塌成 0 ⇒ 6 行被压扁到刚好
         塞进 100svh，每格 172.5px 塞进 140px 的行里 ⇒ **行与行重叠 20.5px**
         （横向间隙却正常，所以看起来「只有上下连在一起」）。
         ⇒ 当时改成「高度留 auto + 行按 aspect-ratio 取满 + 溢出裁掉」。
         🔴 2026-10-17 又换成确定高度，**但这次不会塌**：行的尺寸由
            `grid-template-rows: repeat(6, minmax(0,1fr))` 给（不是 aspect-ratio 撑），
            格子 `aspect-ratio` 已删、由 grid 的 stretch 填满行 —— 两者的失效条件不同。
            ⚠️ 谁要把格子改回 `aspect-ratio`，必须同时把行改回 auto，否则就是上面那个重叠。 */
      .d-grid { position:absolute; inset:var(--hm-st-top) 0 0 0; display:grid;
        grid-template-columns:repeat(6,minmax(0,1fr));
        /* 🔴 2026-10-17 客户拍板「一屏看全」：**行数 6、行高 1fr**，格子由网格
           行高决定（不再各自 `aspect-ratio: 4 / 3`）。这样 36 格刚好填满
           「导航底 → 视口底」这块可用高，末行不会被切。
           实测 1440×900：可用高 801，行高 (801−5×16)/6 = **120**，格 227×120。
           ⚠️ `inset` 四值里 top 用 `--hm-st-top`（= 导航高 + 呼吸），
              不能写 0 —— 写 0 会让第一行整行藏到 fixed 导航后面（只露 65px 的断行）。
           ⚠️ 只有 ≥1024 走「一屏看全」：≤1023 是 4 列 × 9 行，一屏塞 36 格会把
              格压成 3.9:1 的细条 ⇒ 那些断点保持原来的「溢出流」形态（见媒体查询）。 */
        grid-template-rows:repeat(6,minmax(0,1fr)); gap:var(--space-4);
        /* 左右渐隐：参照站 `main.shadow::after` 的等价物（那边是叠一层
           `linear-gradient(to right,#000,transparent,#000)` 的黑色遮罩）。
           这里做成 mask ⇒ 不做额外 DOM，且淡出目标就是节自身的底色。 */
        -webkit-mask-image:linear-gradient(90deg,transparent 0,var(--hm-ink) 9%,
          var(--hm-ink) 91%,transparent 100%);
        mask-image:linear-gradient(90deg,transparent 0,var(--hm-ink) 9%,
          var(--hm-ink) 91%,transparent 100%); }
      /* 按**列**显形：同列的格子共用一对时间窗 `--t0/--t1`。
         🔴 为什么是「按列」而不是「按格」：交接单（codrops 原实现 animateGridFull）
            是 `columns[index % numColumns]` 分组 + `delay = |列−中列| × 0.2`，
            **列内所有格子同时动** ⇒ 中间一整竖列先立起来、两侧各矮一截，
            轮廓是**十字/阶梯**。按格排出来的是**菱形**，形状不对。
         🔴 按列位移还有个附带好处：列内各格位移量相同 ⇒ 行间距严格保持，
            不可能出现「半路的格子压住邻格」（按格位移就会）。
         行程取 **320%**（≈ 参照站的 `yPercent: 450` 量级）：卡片从下方很远的地方升上来。
         按列位移 ⇒ 行程再大也不会越界，也不必像按格位移那样受「缝隙宽度」限制。
         🔴 进度全在 CSS 里算，JS 每帧只写**一个** `--dg-t` ⇒ 36 格零 JS 写入。
         🔴 尺寸沿革（客户 2026-10-17 两次，**以第二次为准**）：
            ① 先报「跑马灯阶段图片明显要小了很多」⇒ 我改了**两处**：
               列数 6→4（单格 227→348）**以及**内档时序（见下）；
            ② 随即改口「入场后的列数不要改了，4 列反而丑了；跑马灯的图片尺寸要改小」
               ⇒ **列数回 6**：单格仍是 227×170，占视口宽 15.7%。
            👉 也就是说：①真正要修的是「跑马灯阶段图挂着半截」，而不是单格变粗；
               那条由**内档时序**修的，留着；列数这条撤回。别再动列数。 */
      .d-cell { position:relative; margin:0; overflow:hidden;
        background:var(--hm-ink); border-radius:var(--radius-sm);
        /* 显形 = 「当前进度 `--dg-t` 落在本档时间窗 [--t0,--t1] 里的位置」。
           窗口由档位给（下面的 nth-child 规则），JS 只写一个 `--dg-t`。
           🔴 不变量：**末档的 --t1 必须 = 1** —— 它决定「铺满完成点」，
              也保证最后一段行程里仍有列在动（否则就是「墙停着、手指继续划」）。
              2026-10-17 前一版末档写 .25（铺满压在前 1/4）⇒ 后 3.5 屏空转，
              客户报「进入下一板块要拉三下」。现在三档均分满整段。
           ⚠️ 时间窗**必须重叠**（0→.5 / .1→.6 / .25→1），不能接力：
              接力时「同一时刻只有两列在动」，其余整片纯黑 —— 观感就是
              「图变少了/变小了」（客户 2026-10-17 两次报过这个感觉）。
              重叠后同一时刻 ≥4 列在飞，才像参照站 `delay = |列−中列|×0.2` 的连续错峰。
              ⚠️ 但仍要保证**任何时候都有格子在动**（末档 --t1 = 1）。 */
        --c:clamp(0,calc((var(--dg-t,1) - var(--t0)) / (var(--t1) - var(--t0))),1);
        opacity:var(--c);
        transform:translateY(calc((1 - var(--c)) * 320%)); }
      /* 列档位：`nth-child` 跟着 DOM 序走 ⇒ 列数随断点变也不用改生成器。
         ⚠️ 三个断点块的顺序即优先级（同特异性、后到者胜），小屏块必须在最后。 */
      .d-cell:nth-child(6n+3), .d-cell:nth-child(6n+4) { --t0:0;    --t1:.5; }
      .d-cell:nth-child(6n+2), .d-cell:nth-child(6n+5) { --t0:.1;   --t1:.6; }
      .d-cell:nth-child(6n+1), .d-cell:nth-child(6n+6) { --t0:.25;  --t1:1; }
      .d-cell img { width:100%; height:100%; object-fit:cover; display:block; }
      /* 🔴 跑马灯（横向文字）**已搬到 S2**（`.st-mark` / `.st-mark-in`）。
         依据：参照站里它压的是 section① 的**两条竖排**，不是这面满屏墙；
         而且它 `.mark` 是 `position:fixed`（浮整页），在这里只能 `absolute`。
         搬走的连带影响（都已处理，别漏）：
           · 5 条跑马灯判据随文字一起搬到 S2（`probe_home_stages.js` 的 strip 分支）；
           · `--dm-x` 改由 S2 自己写（`.sc-strips` 上），S3 不再有；
           · reduce 降级里的 `.d-mark` 覆盖改成 `.st-mark`。
         ⚠️ 别把旧 CSS 留在这里当「备用」：没有对应 DOM 的类会被 smoke 的
            「类名撞名 / 组件缺失」类判据判红。 */
      /* 2026-10-16 客户指令：**去掉悬浮题注**（原来 hover 会在图底压一条黑条写风格名）
         ⇒ 一格只留图。可访问性靠 alt（「原木风 · 地面（设计意向）」），不靠常显文字。 */
      @media (min-width:768px) and (max-width:1023px) {
        /* 🔴 2026-10-30（客户：平板「图片变形了」）—— 10-24 那版解法必须撤回：
           「四边钉 + 行高 1fr」把 36 格压进 100svh，1080 宽下是 4 列 × 9 行，
           实测单格约 240×46（5:1），`object-fit:cover` 只保留中间一条横切片，
           读起来就是「图被压扁/变形」—— 格子小到那个程度，比例已经没有意义。
           约束本身不可调和：一屏 36 格 × 可读比例（4:3）≈ 4×9×180 ≈ 1600px 高，
           而平板可用高只有 500–600px。**要么扁、要么看不全**，二者必选其一。
           ⇒ 这一档放弃「一屏看全」，改成自然流网格：格子回 4:3、整墙随页面正常滚动。
           动效代价：没有 pin ⇒ `stage_range` 的 span = max(1, …) = 1，JS 写的
           `--dg-t` 恒为 1 ⇒ 逐列升起不再发生（格子直接全显）。这是有意的取舍。
           ⚠️ **只覆盖 768–1023**：≤767 的单格约 97×76（ratio 1.28）本来就看着正常，
              而且一屏 + 逐列升起是手机端的设计，不跟着改（见下一个断点块）。
           ⚠️ ≥1024 的一屏 6×6 也不受影响，别照抄到那个断点。 */
        .sc-detail { height: auto; }
        .sc-detail .sc-pin { position: static; height: auto; overflow: visible;
          padding: var(--section-pad-block) 0; }
        .d-grid { position: static; inset: auto; height: auto;
          grid-template-columns: repeat(4, minmax(0, 1fr));
          grid-template-rows: none;
          gap: var(--space-3);
          padding: 0 var(--space-5);
          -webkit-mask-image: none; mask-image: none; }
        .d-cell { aspect-ratio: 4 / 3; opacity: 1; transform: none; }
        /* 4 列两档：**重叠**（内档 0→.55、外档 .2→1），不是接力 —— 接力会让
           「同一时刻只有两列在动」，其余纯黑，看着像图变小了（2026-10-17 实测观感）。 */
        .d-cell:nth-child(4n+2), .d-cell:nth-child(4n+3) { --t0:0;   --t1:.55; }
        .d-cell:nth-child(4n+1), .d-cell:nth-child(4n+4) { --t0:.2;  --t1:1; }
      }
      @media (max-width:767px) {
        .sc-detail { height:200svh; }
        /* 🔴 2026-10-24：≤767 原来沿用「3 列 × 12 行 + aspect-ratio 4/3 + 只钉上边」
           ⇒ 整面墙高 **1210px** 而 pin 只有 844 ⇒ 27–35 号 **9/36 格永远看不到**，
           且首行顶到 0 被 72px 固定站头整排盖住（移动端审视实测）。
           改成与 ≥1024 同一条思路：**四边钉 + 行高 1fr**，把 36 格压进「站头底 → 视口底」
           这块可用高。列数取 4（不是 6）：390/6 = 65px 一格太小，4 列 × 9 行
           ⇒ 单格约 97×76，既装得下又还认得出是「一墙卡片」。
           ⚠️ 行高必须走 grid-template-rows:1fr、格子改回 aspect-ratio:auto ——
              两者同时留着就是 10-16 那个「行互相压 20.5px」的复现条件（见上方历史坑）。
           ⚠️ 列数变了 ⇒ 逐列显形档位也必须换成 4n（下面三条 3n 规则已删，
              沿用 768–1023 的 4n 档位）。
           🔴 2026-10-30：平板那档（768–1023）已改成自然流，这一档**不跟**——
              手机单格 97×76（ratio 1.28）本来就正常，一屏 + 逐列升起是手机端的设计。
              所以这里要把平板的摊平覆盖回来：pin 回 sticky、网格回绝对定位、格子回 auto。 */
        .sc-detail .sc-pin { position: sticky; top: 0; height: 100svh; overflow: hidden; padding: 0; }
        .d-grid { position: absolute;
          inset:var(--hm-st-top) 0 calc(var(--space-16) + var(--space-4) + env(safe-area-inset-bottom, 0px)) 0;
          height: auto; padding: 0;
          grid-template-columns:repeat(4,minmax(0,1fr));
          grid-template-rows:repeat(9,minmax(0,1fr)); gap:var(--space-2);
          -webkit-mask-image:none; mask-image:none; }
        .d-cell { aspect-ratio:auto;
          opacity: var(--c);
          transform: translateY(calc((1 - var(--c)) * 320%)); }
        /* 🔴 4n 档位必须跟着这一档一起写：10-30 把「768–1023」从 `max-width:1023`
           收窄成 `min-width:768 and max-width:1023` 之后，≤767 不再命中那一段，
           于是退回基态的 **6n** 分组 —— 4 列布局下同一列里格子分属不同档位，
           dp=0.5 时外档还在下沉 118%，行间隙实测 **−76.5**（探针当场红）。
           这正是原注释里「列数变了 ⇒ 档位必须换成 4n」那条依赖。 */
        .d-cell:nth-child(4n+2), .d-cell:nth-child(4n+3) { --t0:0;   --t1:.55; }
        .d-cell:nth-child(4n+1), .d-cell:nth-child(4n+4) { --t0:.2;  --t1:1; }
      }

      /* S4 · 落地 · 五图带（2026-10-20）
         一屏一帧、横向平移：图是主角，一个词 + 一行细节压在左下。
         🔴 `--lineA` 由 JS 写（主张句只在前 12% 行程在场）⇒ 这里必须给**静态默认值**，
            否则 audit_color 判「幽灵变量」（与 `--fl-n`、旧的 `--cut` 同一类坑）。 */
      .sc-factory { position:relative; height:calc(100svh + 312svh); background:var(--hm-ink);
        }
      .sc-factory .sc-pin { position:sticky; top:0; height:100svh; overflow:hidden; padding:0; }

      .f-track { display:flex; height:100%; width:500%; will-change:transform; }
      /* ⚠️ figure 的 UA 默认 margin 是 1em 40px —— 必须归零。五帧各多 40px，
            四步累计能差出 320px，末帧会露出上一帧一条（预览件实测踩过）。 */
      .f-frame { position:relative; flex:0 0 20%; height:100%; margin:0; overflow:hidden;
        background:var(--hm-ink-3); }
      .f-frame img { width:100%; height:100%; object-fit:cover; display:block;
        /* 2026-10-20 客户「图有点灰蒙蒙」：原来是 saturate(.78) + brightness(.92)，
           实测渲染后平均亮度 **94.8**，而原图文件是 **147.6** —— 一层 CSS 就吃掉 36%。
           现场照本身明度正常，不需要整幅压暗；去饱和靠 .92 一档就够压住蓝/红地膜。 */
        filter:saturate(.92) contrast(1.03) brightness(1); }
      .f-frame::after { content:''; position:absolute; inset:0; pointer-events:none;
        /* 收窄到 46%：原来 34% 高的渐变从 94% 起跳，等于把画面下三分之一整个压黑 ——
           「灰」的一半来自这里。题注的可读性改由 .f-cap 自己的椭圆暗场保证。 */
        background:linear-gradient(to top, rgba(5,9,15,.94) 0%, rgba(5,9,15,.62) 11%,
                   rgba(5,9,15,.26) 26%, rgba(5,9,15,0) 44%); }
      /* 宽度用 right 卡（不用 max-width：新裸字面会顶破 design_tokens 的阶梯外基线） */
      .f-cap { position:absolute; left:clamp(var(--space-5),5vw,var(--space-10));
        right:45%; bottom:clamp(var(--space-10),9svh,var(--space-16)); z-index:3;
        margin:0; color:#fff;
        /* 建栈：伪元素是 z-index:-1，没有 stacking context 会掉到渐变下面 */
        isolation:isolate; }
      .f-cap::before { content:''; position:absolute; z-index:-1; pointer-events:none;
        left:-110px; bottom:-96px; width:min(780px,76vw); height:340px;
        background:radial-gradient(50% 50% at 28% 72%,
          rgba(5,9,15,.90) 0%, rgba(5,9,15,.66) 40%, rgba(5,9,15,.28) 66%,
          rgba(5,9,15,0) 86%); }
      .f-cap b { display:block; font-family:var(--font-display); font-weight:700;
        font-size:clamp(1.9rem,4.2vw,3.2rem); line-height:1.08; letter-spacing:-0.02em; }
      .f-cap i { display:block; font-style:normal; margin-top:var(--space-3);
        font-size:clamp(.8125rem,1.1vw,.9375rem); line-height:1.75; color:rgba(255,255,255,.84); }
      /* 主张句头顶那层压暗：只在前 12% 行程存在（与句子共用 `--lineA`，一起淡出）。
         第一帧左上角是大面积亮墙，白字裸压读不出；给整块加个盒子又太重。
         ⚠️ 它盖在**所有帧**上，所以必须跟着句子一起退场 —— 否则后四帧顶部永远是脏的。 */

      /* 2026-10-21 客户：S4「暗场 + 主张句」整体拿掉 —— 暗场读作深色方块，
         且这句话与这一屏的实际内容（放线→保护→安装→工厂→展厅）已对不上。 */

      @media (max-width:767px) {
        /* 触摸端不做横移：五帧纵向自然流，图 4:3、题注在图下 —— 题注不出图就不用赌图底亮度。
           主张句用 flex `order:-1` 提到最前：盖在图上会被 fixed 站头吃掉（同「舞台让开导航」）。 */
        .sc-factory { height:auto; }
        .sc-factory .sc-pin { position:relative; height:auto; padding:0;
          display:flex; flex-direction:column; }
        .f-track { display:block; width:auto; transform:none !important; }
        .f-frame { flex:none; height:auto; margin:0 0 var(--space-6); }
        .f-frame img { height:auto; aspect-ratio: 4 / 3; }
        .f-frame::after { display:none; }
        .f-cap { position:static; right:auto; padding:var(--space-4) var(--space-5) 0; }
        .f-cap b { font-size:clamp(1.5rem,6.6vw,2rem); }


        .f-dots { display:none; }
      }

      /* ══════════════════════════════════════════════════════════════════
         S5 · 精选案例（案例卡沿用 A 案；对外不提介质，选品见 _recent_cases）
         案例卡样式全部来自 shared.css，本页不加一条 —— 避免「同一个组件
         在两处长得不一样」这种最难查的漂移。
         ══════════════════════════════════════════════════════════════════ */
      .sc-cases { padding:var(--section-pad-block) 0; background:var(--white); }

      /* ── S5 胶片带（2026-10-20 方案①）───────────────────────────────
         通栏出血：第三张被**视口边缘**切开，而不是被容器切一半
         （切一半会出现「半张图 + 完整标题」，看着像没做完）。
         🔴 首卡左沿必须与容器内容左沿对齐：padding = 容器左内距 + 居中留白。 */
      .cx-rail { width:100vw; margin-left:calc(50% - 50vw); }
      .cx-track {
        /* 🔴 内距必须与 .container 的**同一套令牌**逐级同步（24 / 32 / 48），
           否则首卡会比标题左沿多缩或少缩一截 —— 实测用错一级就差 24px。
           公式 = 居中留白 + 容器内距；窄屏退化成容器内距本身。 */
        --cx-gutter: var(--space-6);
        --cx-pad: max(var(--cx-gutter), calc(50vw - var(--container-max) / 2 + var(--cx-gutter)));
        display:flex; gap:var(--space-5);
        overflow-x:auto;
  /* 2026-10-23：`overflow-x:auto` 会让 `overflow-y:visible` 计算成 `auto` ⇒
     这条带子在卡片 reveal 动画未触发时（卡片 translateY(32px)）多出 24px 纵向可滚区，
     鼠标停在这里滚轮会**先滚这条带子**，页面卡住不动。y 轴必须显式关掉。 */
  overflow-y:hidden;
  overscroll-behavior-x:contain; scroll-snap-type:x mandatory;
        padding:0 var(--cx-pad) var(--space-2);
        /* 🔴 只给 padding 是**不够**的：mandatory 吸附会把首卡吸到滚动容器左沿
           （无视内距）⇒ 开屏自己 scrollLeft=144、第一张被切掉一半。
           必须同时写 scroll-padding-left。判据：开屏 scrollLeft === 0 且首卡贴内容左沿。 */
        scroll-padding-left:var(--cx-pad);
        scrollbar-width:none;
      }
      .cx-track::-webkit-scrollbar { display:none; }
      /* 🔴 2026-10-22：卡宽 520 → 420（客户定）。一屏可见 2.67 → 3.27 张、8 格走完 6 步。
         再窄就要动题注字号（420 是「两行题注 + 一行元信息」还能整行放下的下界，实测 380 起元信息换行）。 */
      .cx-track > .case-card, .cx-track > .cx-all { flex:0 0 420px; scroll-snap-align:start; }
      @media (max-width:1023px) { .cx-track > .case-card, .cx-track > .cx-all { flex:0 0 80vw; } }

      /* 末尾的「查看全部案例」引导卡（2026-10-22）——
         不是案例卡：体块只复用 .case-shot / .case-label 的**几何**，不挂 .case-card
         （那条是「单点案例卡模板」的判据，混进来会被当成第二套模板）。
         为什么需要它：全实拍只有 7 个小区，第 8 格只能给出口。 */
      .cx-all { display:flex; flex-direction:column; text-decoration:none; }
      .cx-all-shot {
        position:relative; display:grid; place-items:center;
        aspect-ratio: 4 / 3; border-radius:var(--radius-lg);
        background:var(--navy-900); color:var(--white);
        transition:background var(--duration-normal) var(--ease-out-quart);
      }
      .cx-all:hover .cx-all-shot { background:var(--navy-800); }
      .cx-all-in { display:grid; gap:var(--space-2); text-align:center; padding:0 var(--space-5); }
      .cx-all-in b { font-size:var(--fs-h4); font-weight:600; letter-spacing:.04em; }
      .cx-all-in i { font-style:normal; font-size:var(--fs-small); letter-spacing:.04em; color:rgba(255,255,255,.62); }
      .cx-all-arrow {
        position:absolute; right:var(--space-5); bottom:var(--space-5);
        font-size:var(--fs-body-lg); line-height:1;
        transition:transform var(--duration-normal) var(--ease-out-quart);
      }
      .cx-all:hover .cx-all-arrow { transform:translateX(4px); }
      .cx-all:focus-visible { outline:2px solid var(--blue-500); outline-offset:4px; border-radius:var(--radius-lg); }
      /* 与 .container 的断点逐级对齐（640 / 1024），别自己拍数字 */
      @media (min-width:640px) { .cx-track { --cx-gutter: var(--space-8); } }
      @media (min-width:1024px) { .cx-track { --cx-gutter: var(--space-12); } }
      .cx-nav { display:flex; align-items:center; gap:var(--space-3); margin-top:var(--space-5); }
      /* 六段刻度而不是一根会跑的细线：细线读起来像装饰下划线，分段才看得出「共几套」 */
      /* 刻度**限宽**：铺满 1184px 时每段 170px，读起来像一条分隔线，不像「六格进度」。
         限到 360（每段约 54px）才看得出「共六套、走到第几套」。 */
      .cx-bar { flex:0 1 360px; display:flex; gap:var(--space-1-5); align-items:center;
        margin-right:auto; }
      .cx-bar i { flex:1; height:2px; background:var(--gray-200); transition:background var(--duration-normal); }
      .cx-bar i.on { background:var(--navy-900); }
      .cx-btn { width:40px; height:40px; border-radius:50%; flex:none;
        border:1px solid var(--gray-200); background:var(--white); color:var(--navy-900);
        cursor:pointer; font-size:var(--fs-small); line-height:1; }
      .cx-btn:disabled { opacity:.28; cursor:default; }
      /* 刻度行与 CTA 之间要留口气：实测两者间距为 0，按钮像贴着刻度长出来的 */
      .sc-cases .section-footer { margin-top: var(--space-8); }

      /* ══════════════════════════════════════════════════════════════════
         S6 · 收束 · 规格与承诺
         ══════════════════════════════════════════════════════════════════
         旧版是「3 个大数字 + 4 张白卡」= 两组同类信任证据各排一遍。
         改成 Apple 规格表式：**四个事实压成一行**（数字小、密度高、一眼扫完），
         下面四条承诺用竖线分隔，不再用卡片壳。
         🔴 次链接刻意**不用按钮壳**：`.btn-outline` 是白底半透明描边，配深色幕
            用的；放到 cream-200 这类浅底上本色与背景亮度太接近 ⇒ 等于没有。 */
      .sc-close { padding:var(--section-pad-block) 0; background:var(--cream-200); }
      .sc-close-h { max-width:820px; margin:0 0 var(--space-10); }
      .sc-close-h h2 { margin:0 0 var(--space-4); font-size:clamp(1.6rem,3.4vw,2.6rem);
        font-weight:700; line-height:1.2; letter-spacing:-0.015em; color:var(--navy-900); }
      .sc-close-h p { margin:0; font-size:var(--fs-body-sm); color:var(--gray-600); line-height:1.7; }
      .spec-row { display:grid; grid-template-columns:repeat(4,minmax(0,1fr));
        border-top:1px solid rgba(11,31,58,.14); border-bottom:1px solid rgba(11,31,58,.14); }
      .spec-row > div { padding:var(--space-5) var(--space-4) var(--space-5) 0;
        border-left:1px solid rgba(11,31,58,.10); padding-left:var(--space-4); }
      .spec-row > div:first-child { border-left:0; padding-left:0; }
      .spec-v { display:block; font-family:var(--font-display); font-weight:700;
        font-size:clamp(1.25rem,2.1vw,1.75rem); line-height:1.15; color:var(--navy-900);
        font-variant-numeric:tabular-nums; }
      /* ⚠️ 单位用 rem 不能用 em：小字号判据的换算基准是**根字号 16px**，
         `.62em` 会被算成 9.92px 判「<12px」（实际渲染是相对 20~28px 的父元素，
         视觉上够大但判据不认）。`.8rem` = 12.8px，两处都通过。 */
      /* 单位与数字的贴合：原来 margin-left:2px，加上「h / ㎡」这类窄字形自带的左右留白，
         视觉上像「48 h」断成两截（汉字单位「座 / 年 / 个区」因为字身方、四边有墨，看不出来）。
         收到 0 并把单位**微降 0.04em**，让窄单位的视觉重心贴住数字底部；汉字单位几乎无变化。 */
      .spec-v u { text-decoration:none; margin-left:0; font-size:var(--fs-small); font-weight:600;
        vertical-align:-0.04em; color:var(--blue-600); }
      .spec-l { display:block; margin-top:var(--space-2); font-size:var(--fs-small);
        font-weight:600; color:var(--navy-900); }
      .spec-n { display:block; margin-top:3px; font-size:var(--fs-small);
        line-height:1.5; color:var(--gray-600); text-wrap:pretty; }
      .pledge-row { display:grid; grid-template-columns:repeat(4,minmax(0,1fr));
        gap:var(--space-6); margin-top:var(--space-10); }
      .pledge h3 { margin:0 0 var(--space-2); font-size:var(--fs-body-lg);
        font-weight:600; color:var(--navy-900); }
      .pledge p { margin:0; font-size:var(--fs-small); line-height:1.75; color:var(--gray-600); }
      .sc-close-cta { margin-top:var(--space-12); display:flex; flex-wrap:wrap;
        gap:var(--space-3) var(--space-7); align-items:center; }
      .sc-close-link { font-size:var(--fs-body-sm); font-weight:500;
        color:var(--color-interactive); text-decoration:none;
        border-bottom:1px solid transparent;
        transition:color var(--duration-normal), border-color var(--duration-normal); }
      @media (hover:hover) {
        .sc-close-link:hover { color:var(--color-interactive-hover);
          border-bottom-color:currentColor; }
      }
      @media (max-width:1023px) {
        /* 🔴 2026-10-27：4 格排 3 列会落成「3 + 1」—— 第 4 格单独占第二行左侧，
           右边 2/3 全空（实测截图 _final-audit/preview/no-2021/index-spec-768.png）。
           改成 2 列 ⇒ 2×2，没有空行也没有孤格。边框按 2 列重推：
           奇数列清左边框，第 3、4 格补上边框把两行接起来。 */
        .spec-row { grid-template-columns:repeat(2,minmax(0,1fr)); }
        .spec-row > div { padding-top:var(--space-4); padding-bottom:var(--space-4); }
        .spec-row > div:nth-child(odd) { border-left:0; padding-left:0; }
        .spec-row > div:nth-child(3),
        .spec-row > div:nth-child(4) { border-top:1px solid rgba(11,31,58,.10); }
        .pledge-row { grid-template-columns:repeat(2,minmax(0,1fr)); }
      }
      @media (max-width:767px) {
        .spec-row { grid-template-columns:repeat(2,minmax(0,1fr)); }
        .spec-row > div:nth-child(odd) { border-left:0; padding-left:0; }
        .pledge-row { grid-template-columns:1fr; gap:var(--space-5); }
        /* 🔴 2026-10-28（V-05，客户拍板）：触摸端**撤掉 z-index:3**，改回与桌面同一套层次
           —— 图在前、字从两列窄条背后穿过去。
           ⚠️ 这条曾经是 3（2026-10-23 加），当时的判断是「条占 74%，字退到后面只剩两端
              残字，比压在图上也还能读更糟」。**客户看过四格实测对照后主动选了残字这一版**
              （预览：`site/_preview/v05-preview.html` 第 ② 格）：宁可让字被干净地切断，
              也不要 48px 白字压在照片细节上。
           ⇒ 不要再凭「残字不好看」把它改回 3。真要改，先看预览页再谈。
           基态 `.st-mark` 是 z-index:1、`.st-item` 是 2 ⇒ 这里**什么都不用写**；
           一旦在此处写了任何 z-index ≥2 的值，字就会重新压到图上。 */

      }

      /* ══════════════════════════════════════════════════════════════════
         S6 移动端重排（2026-10-24 · 方案 A+B）
         ------------------------------------------------------------------
         基线：390 下 S6 1225px（1.5 屏、0 图）+ 留资区 2281px（2.7 屏）
              + 页脚 839px ⇒ 结尾 5.2 屏纯文字。
         目标：数据 2×2 卡（第 5 格 CTA 横跨整行）→ 信任 2×2 卡 → 迷你留资条；
              留资区去掉与页脚重复的左卡。桌面端一行不动。
         🔴 .spec-cta / .mini-quote 默认 display:none，只在 ≤767 出现；
            这样 1440 的 DOM 结构与视觉完全不变（验收判据 4）。
         ══════════════════════════════════════════════════════════════════ */
      .spec-cta { display:none; }
      /* 「完整保障」入口分两套：
         · 桌面：放进既有 .sc-close-cta 行内（不新增一行，S6 高度不变）；
         · 手机（≤767）：改成整行卡片，正文隐藏，详见方案 C。 */
      .sc-close-more { display:none; }
      .sc-close-cta .sc-close-link--why { display:none; }
      @media (min-width:768px) {
        .sc-close-cta .sc-close-link--why { display:inline; }
      }
      .mini-quote { display:none; }
      /* 迷你留资条的**卡片样式放全局**（显隐仍由上面这条 + ≤767 的 display:block 控制）。
         原因：2026-10-24 合并时 `@media (max-height:520px)` 少了一个 `}`，
         其后所有规则被吞进那个条件里 —— 卡片样式若写在 media 内，一旦再次发生同类事故，
         表现就是「白卡消失、文字裸奔在米色底上」，而页面不报错、控制台全绿。 */
      .mini-quote-card { background:var(--white); border:1px solid rgba(11,31,58,.07);
        border-radius:10px; padding:var(--space-4); box-shadow:var(--shadow-sm); }
      .mini-quote-card h3 { font-family:var(--font-display); font-size:var(--fs-body-lg); font-weight:800; }
      .mini-quote-card p { margin-top:5px; font-size:var(--fs-small); line-height:1.55; color:var(--gray-500); }
      /* 🔴 2026-10-27 UX-07：这里原本是「伪输入框 + 小按钮」两段式 ——
         左半截写着 placeholder「请输入手机号」、带 1px 边框 46px 高，
         长得和真输入框一模一样，实际整个 <a> 只是跳 #quote-submit。
         业主戳进去以为能打字，结果弹出弹层 = 伪控件挫败。
         报告给的两条路里选了②（保留跳转、外观改回按钮），因为①要做第二套
         表单逻辑，与「按钮只跳 #quote-submit，不写第二套表单」的既定架构冲突。
         现在整条就是一个完整按钮；同意行里的《隐私政策》是真链接。 */
      .mini-quote-go { display:flex; align-items:center; justify-content:center;
        gap:var(--space-2); margin-top:var(--space-3); min-height:48px;
        padding:0 var(--space-4); border-radius:var(--radius-md);
        background:var(--navy-800); color:var(--white);
        font-size:var(--fs-body); font-weight:600; text-decoration:none;
        transition:background var(--duration-fast) var(--ease-out-quart); }
      .mini-quote-go:hover { background:var(--navy-900); }
      .mini-quote-go:focus-visible { outline:2px solid var(--blue-500); outline-offset:2px; }
      .mini-quote-arrow { font-size:1.05em; line-height:1; }
      .mini-quote-consent { display:block; margin-top:9px; font-size:var(--fs-small); color:var(--gray-500); }
      .mini-quote-consent a { color:inherit; text-decoration:underline; text-underline-offset:3px; }
      /* 图标只服务移动端卡片；桌面保持原「纯文字四条」排版 */
      .pledge-ic { display:none; }
      @media (max-width:767px) {
        /* 数据卡：.spec-row 用 display:contents 把 4 个数据提升为 .spec-grid 的格子，
           第 5 格 .spec-cta 才能与它们同网格 —— 不额外加一层 wrapper。 */
        .spec-grid { display:grid; grid-template-columns:1fr 1fr; gap:var(--space-2-5); }
        .spec-row { display:contents; }
        .spec-row > div { padding:var(--space-3-5) var(--space-3-5) var(--space-3); background:var(--white);
          border:1px solid rgba(11,31,58,.06); border-radius:var(--radius-md); min-height:104px;
          border-top:1px solid rgba(11,31,58,.06); }
        /* 🟠 必须逐字覆盖 1023/767 两条块里的 nth-child 补丁，否则首列会没内边距。
           🔴 2026-10-24 补第 4 格：1023 档是 3 列、第 4 格正好是行首 ⇒ 那里把
           `padding-left` 清了；到 2 列布局下第 4 格变成**右列**，那条清边距就成了
           「48h 整卡贴左、其余卡都有 14px」——用户实测就是这个。
           4 格后不再有第 5 格，原 `:nth-child(5)` 规则一并去掉。 */
        .spec-row > div:first-child,
        .spec-row > div:nth-child(odd),
        .spec-row > div:nth-child(4) { padding-left:var(--space-3-5); border-left:1px solid rgba(11,31,58,.06); }
        .spec-row > div:nth-child(4) { border-top:1px solid rgba(11,31,58,.06); }
        .spec-v { font-size:32px; }
        /* 🔴 390（iPhone 12/13）上「12000㎡」是**临界值**：macOS/Chromium 下刚好 136px 放得下，
           但 iOS Safari 的 Outfit 数字更宽 ⇒ 单位掉到第二行（用户实测两台手机都这样）。
           只降这一项、并禁止换行：宁可略小，也不能断成「12000 / ㎡」。 */
        .spec-v--wide { font-size:clamp(1.375rem, 7.2vw, 1.75rem); }
        .spec-v { white-space:nowrap; }
        .spec-l { margin-top:7px; font-size:var(--fs-nano); }
        .spec-n { margin-top:3px; font-size:var(--fs-small); line-height:1.45; color:var(--gray-500); }
        .spec-cta { display:flex; flex-direction:column; justify-content:center;
          grid-column:1/-1;
          padding:var(--space-3-5); background:var(--navy-800); border-radius:var(--radius-md); min-height:104px;
          text-decoration:none; }
        .spec-cta .spec-l { margin-top:0; font-size:var(--fs-small); color:var(--white); }
        .spec-cta .spec-n { color:rgba(255,255,255,.62); }
        .spec-go { margin-top:var(--space-2); font-size:var(--fs-nano); font-weight:600; color:var(--white); }
        /* 信任卡：4 张全留，2×2；文案一个字不改，只换容器 */
        .pledge-row { grid-template-columns:1fr 1fr; gap:var(--space-2-5); margin-top:var(--space-2-5); }
        .pledge { padding:var(--space-3-5); background:var(--white);
          border:1px solid rgba(11,31,58,.06); border-radius:var(--radius-md); min-height:104px; }
        .pledge-ic { display:block; width:22px; height:22px; color:var(--blue-600); }
        .pledge-ic svg { display:block; width:22px; height:22px; }
        .pledge h3 { margin:var(--space-2-5) 0 var(--space-1); font-size:var(--fs-small); line-height:1.3; }
        .pledge p { font-size:var(--fs-small); line-height:1.55; color:var(--gray-500); }
        /* CTA 已横跨整行占满最后一行 ⇒ 末尾那颗主按钮在手机上是重复的，只留供应链文字链 */
        .sc-close-cta { margin-top:var(--space-5); }
        .sc-close-cta .btn-primary { display:none; }
        /* 方案 C：细则与 FAQ 在 why-us.html，手机端只留卡片标题 + 一个入口。
           卡片正文隐藏（不删 DOM）→ 桌面端四条完整段落逐字不变。 */
        .pledge p { display:none; }
        .pledge { min-height:0; }
        .sc-close-more { display:flex; align-items:center; justify-content:space-between;
          margin-top:var(--space-2-5); padding:var(--space-3) var(--space-3-5); background:rgba(255,255,255,.6);
        border:1px dashed rgba(11,31,58,.18); border-radius:var(--radius-md);
        font-size:var(--fs-small); font-weight:600; color:var(--navy-800); text-decoration:none; }
        /* 🔴 2026-10-26 原来整块 display:none —— 而这 6 问正是「填手机号之前」
           要问的：多少钱 / 效果能否一致 / 工期 / 会不会加价 / 老房局改 / 材料是否自营。
           移动端是主流量，删掉答疑等于把异议留到提交之后。这里恢复前 3 条（价格、效果、工期），
           其余 3 条仍在 why-us 详情页。只恢复展示，不改文案、不动 details 交互。 */
        .quote-faq { display:block; margin-top:var(--space-6); }
        .quote-faq .faq-grid { grid-template-columns:1fr; }
        .quote-faq .faq-item:nth-child(n+4) { display:none; }
        .quote-faq .quote-faq-title { font-size:var(--fs-h4); }
        /* 迷你留资条：首个就近入口，按钮只跳 #quote-submit（不写第二套表单）。
           卡片本体样式已提到全局（见文件上方），这里只负责显隐与底色。 */
        /* 上留白 64、下留白交给下一节自己的 padding：两侧同色（米），
           若这里再加 64 ⇒ 卡片到「为什么敢把这个家」变成 128，与上方不对称。 */
        .mini-quote { display:block; padding:var(--section-pad-block) 0 0; background:var(--cream-200); }
        /* 留资区瘦身：左卡是「服务列表 + 热门链接」，与页脚重复（各 6/5 条），手机上让表单占满整行 */
        .quote-left-card { display:none; }
        .quote-grid { grid-template-columns:1fr; }
        /* FAQ 收紧行距：保留全部 6 条，不藏内容（与「不用 display:none 藏内容」一致） */
        .quote-faq .faq-grid { gap:var(--space-2); }
        .quote-faq .faq-item summary { padding-top:var(--space-2-5); padding-bottom:var(--space-2-5); font-size:var(--fs-small); }
      }

      /* ══════════════════════════════════════════════════════════════════
         降级：prefers-reduced-motion ⇒ 四个滚轴场景全部摊平成静态堆叠，内容零缺失
         ------------------------------------------------------------------
         🔴 必须**逐一覆盖 JS 写过的 inline style**。CSS 的 !important 能压 inline，
            但得先想到压哪几条 —— 实测漏过两条：`.rail-item` 的 opacity（JS 按
            「离中心距离」设 0.55~1）与 `.f-item` 的 translateY（视差错位），
            都会在 reduce 下**残留**。
         ══════════════════════════════════════════════════════════════════ */
      @media (prefers-reduced-motion: reduce) {
        .sc-gallery, .sc-detail, .sc-factory { height:auto; }
        .sc-gallery .sc-pin, .sc-detail .sc-pin,
        .sc-factory .sc-pin { position:static; height:auto;
          padding:var(--section-pad-block) 0; }
        .rail-track { display:block; }
        /* 🔴 摊平态必须把 absolute 与 mask 一起摘掉：只摘 mask 会六张叠在一格里，
           只摘 absolute 会剩一条被裁的边。visibility 也要压回来（基态是 hidden）。 */
        .rail-item { position:static; inset:auto; margin-bottom:var(--space-8);
          visibility:visible !important; opacity:1 !important;
          -webkit-mask-image:none !important; mask-image:none !important; }
        /* 2026-10-17：`--rail-w` 随满高竖裁一起退役（`.rail-item img` 已改成
           100%×100% + object-fit:cover）。摊平态下 `.rail-item` 是 block，
           `height:100%` 相对 auto 高的 track 失效 ⇒ 高度回到原图比例，不竖裁 —— 正好。 */
        /* 引语移出舞台后是 section 层正文 ⇒ **不隐藏**（旧版在舞台里，藏它是
           为了不重复；现在藏了等于丢文案）。分页点在摊平态无意义，仍隐藏。 */
        .rail-dots { display:none; }
        /* 元素墙摊平：摘掉绝对定位，按普通栅格往下排。
           ⚠️ 逐格显形是纯 calc（`--dg-t` 兜底 1 ⇒ 全显），**没有 animation/filter
              可关** —— 所以这里只需处理定位，别顺手写一堆 `none !important`。
           🔴 摊平后**必须把格子的 `aspect-ratio` 还回去**：基态的格子没有自己的
              高度（靠 `grid-template-rows: repeat(6,1fr)` 的确定高度撑），
              一摊平成 `position:static` + 自动高，行里的格子会**塌成 0 高**。
              这是「改一处忘配一处」的典型 —— 基态改成确定高度时就得同时想着这条。 */
        .d-grid { position:static; grid-template-rows:none; gap:var(--space-2);
          padding:0 var(--space-5);
          -webkit-mask-image:none; mask-image:none; }
        .d-cell { aspect-ratio: 4 / 3; }
        /* 跑马灯摊平成一行正文（reduce/降级下「内容零缺失」）：
           摘掉**固定定位**与位移（它是 `position:fixed`，摊平态必须落回文档流），
           让它按普通文字居中排、不遮任何东西。
           ⚠️ 重复份数是**装饰性复制**，不是内容 ⇒ 摊平态只留第一条
              （留 10 条同样的句子读屏会念 10 遍，那才是真的冗余）。
           ⚠️ 2026-10-17 起基态已无背景/无 padding ⇒ 这里不再重复声明 `background:none`。 */
        .st-mark { position:static; transform:none;
          margin:var(--space-8) 0 var(--space-2);
          /* 🔴 必须撤销基态那层两端渐隐：摊平之后它变成**文档流里的一行词云**，
             7% 的左右 mask 会把第一个词与最后一个词淡掉 —— 那是真内容缺失，
             不是装饰（`.st-mark` 平时 aria-hidden，摊平态它就是给 reduce 用户看的正文）。
             reduce 块在基态之后、同选择器 ⇒ 这里写 `none` 就能盖住。 */
          -webkit-mask-image:none; mask-image:none; }
        /* 横向文字摊平成词云：每个词都是一条独立信息（旧版是同一句复制 6 份，只能留一条） */
        .st-mark-in { display:flex; flex-wrap:wrap; justify-content:center;
          width:auto; gap:var(--space-3) var(--space-5); transform:none; text-align:center;
          font-size:clamp(1.125rem,2.6vw,1.75rem); }
        /* 两条竖排的逐格 3D 在 reduce 下必须停：**`--p:.5`** 才是「自然态」。
           🔴 2026-10-17 实测纠错：这里原来写 `--p:1`，注释也写「brightness 100%」——
              **那是错的**。`--p` 的口径是 0=刚进视口底 / .5=视口正中 / 1=已出视口顶，
              所以 1 是**出画端**：旧公式下 brightness = 1−1 = **0（全黑）**，
              格子还被 rotateX(-50°) 压成 73px 高（自然态 112px）。reduce 用户看到的是
              一列**又黑又扁**的图 —— 这条一直静默（截图不看 reduce 就发现不了）。
              `.5` 的取值：pi=1 / po=0 ⇒ brightness 126%、blur 0、contrast 100%、
              位移/旋转/skew 全为 0 —— 正好是自然态。
           ⚠️ 用 `!important` 压 inline —— JS 是往 style 上写 `--p` 的，
              普通声明压不住 inline（本项目已有多次同类教训）。 */
        .st-item { --p:.5 !important; }
        .st-card { will-change:auto; }
        /* 五图带摊平成竖排：`transform` 必须压掉（JS 往 track 上写内联），
           帧高回到图的比例；题注照旧在图下（与 ≤767 同一套排法）。 */
        .f-track { display:block; width:auto; height:auto; transform:none !important; }
        .f-frame { height:auto; margin:0 0 var(--space-6); }
        .f-frame img { height:auto; aspect-ratio: 4 / 3; }
        /* 主张句回到最前（与 ≤767 同一套排法）：留在 DOM 原位会掉到五张图之后 */
        .sc-factory .sc-pin { display:flex; flex-direction:column; }
        .f-dots { display:none; }
      }


/* ══════════════════════════════════════════════════════════════════════
   减少动效：开屏不再保留 260vh / 320vh 滚动轨（2026-10-24）
   ----------------------------------------------------------------------
   reduce 下 shared.js 不挂 .landing-armed（百叶与文字交换都不做），但
   .hero-landing 的高度仍是 260vh（手机）/ 320vh（桌面）⇒ 实测 390 下
   **2194px = 2.6 屏**，其中前 1200px 画面完全不变 —— 纯死滚动。
   这里摊平成「一张静态屏 + 常规流」：stage 取消 sticky，高度交给内容，
   用 min-height 保证仍然满屏。其余舞台（S1/S2/S3/S4/craft）早已同样摊平。
   ⚠️ 必须写在文件末尾：同优先级下后者胜，前面 @media (max-width:767px)
      里还有一条 .hero-landing{height:260vh}。
   ══════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .hero-landing { height: auto; }
  .landing-stage { position: static; height: auto; min-height: 100svh; }
}


/* ══════════════════════════════════════════════════════════════════════
   矮视口（横屏手机）：S3 元素墙改自然流（2026-10-24）
   ----------------------------------------------------------------------
   这一档与宽度断点是**正交**的，宽度分档管不到它：
     · 844×390（横屏）：落在 768–1023 的「溢出流」档 ⇒ 36 格整墙 1460px
       钉在 390px 的 pin 里，实测只有 **8/36** 完整可见；
     · 740×360（横屏）：落在 ≤767 的「一屏」档 ⇒ 4 列 × 9 行被压进 263px 可用高，
       每格只剩 **14px 高**（报告记的 20/36 是旧版 4 列，现在更糟）。
   两种都读不出来。这里不再要求「塞进一屏」，改成常规流 + 6 列：
   844 下每格约 127×95，整墙约 700px，正常滚动即可读全。
   🔴 必须显式让格子显示：摊平后滚动行程没了，`--dg-t` 会停在 0，
      而 `.d-cell` 的 opacity 是按 `--dg-t` 在各自时间窗里的进度算的 ⇒
      不写这两行就是「一整墙看不见的格子」（比原来更糟）。
   ⚠️ 必须写在文件末尾：前面 @media (max-width:767px) 与 (max-width:1023px)
      两组都会命中这个宽度，同优先级下后者胜。
   ══════════════════════════════════════════════════════════════════════ */
@media (max-height: 520px) and (orientation: landscape) {
  .sc-detail, .sc-factory { height: auto; }
  .sc-detail .sc-pin {
    position: static; height: auto; overflow: visible;
    padding: var(--section-pad-block) 0;
  }
  .d-grid {
    position: static; inset: auto; height: auto;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    grid-template-rows: none;
    gap: var(--space-2);
    padding: 0 var(--space-5);
    -webkit-mask-image: none; mask-image: none;
  }
  .d-cell { aspect-ratio: 4 / 3; opacity: 1 !important; transform: none !important; }
}

/* ── 320：翻页点整排落在屏内（2026-10-24 移动端审视）──────────────
   6 个点 × 48px + 5 × 10px = 338px > 320 ⇒ 首尾各被裁 9px（实测）。
   缩到 36px + 8px = 256px；只影响 ≤360，桌面/平板不变。 */
@media (max-width:360px) {
  .rail-dots i, .f-dots i { width:var(--space-9); }
  .rail-dots, .f-dots { gap:var(--space-2); }
  /* 320：32px 的「12000㎡」在 136px 宽的卡里放不下，单位会掉到第二行
     （实测 12000 断行 + ㎡ 单独一行）。降到 24px + 12px 才排得下。 */
  .spec-v { font-size:var(--fs-xl); }
  .spec-v--wide { font-size:var(--fs-xl); }
  .spec-v u { font-size:var(--fs-nano); }
}

    /* ---------- 首屏叙事进度（2026-11-02）----------
       只读：点灯与跳转都基于 initLandingBlinds 写出的 --p，不新增驱动器。
       无 JS / reduced-motion ⇒ 不挂 .landing-armed ⇒ 整块 opacity:0、不可点。
       ⚠️ 必须带底色：首屏是浅色实拍，纯白 58% 文字在亮区实测不可读（2026-11-02 截图）。
          沿用 .hero-chip 的「深底 + 模糊」，与右侧分页胶囊同一族。
       ⚠️ 门禁口径（首跑踩了四条）：字号 ≥ --fs-nano（12px）、圆角走 --radius-full、
          色值走 --navy-900、hover 必须包进 (hover: hover) 否则触摸端粘住。 */
    .landing-steps {
      position: absolute; left: clamp(1.25rem, 4vw, 3.25rem); bottom: var(--space-5); z-index: 7;
      display: flex; gap: var(--space-1); padding: var(--space-1); border-radius: var(--radius-full);
      background: rgba(7, 18, 35, .34);
      border: 1px solid rgba(255, 255, 255, .22);
      -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
      opacity: 0; pointer-events: none;
      transition: opacity var(--duration-normal) var(--ease-out-quart);
    }
    .landing-armed .landing-steps { opacity: 1; pointer-events: auto; }
    .ls-item {
      display: flex; align-items: baseline; gap: var(--space-1); padding: var(--space-1) var(--space-3) var(--space-1-5);
      background: none; border: 0; border-radius: var(--radius-full);
      font: inherit; font-size: var(--fs-nano); letter-spacing: .06em;
      color: rgba(255, 255, 255, .72); cursor: pointer;
      transition: color var(--duration-normal) var(--ease-out-quart),
                  background-color var(--duration-normal) var(--ease-out-quart);
    }
    .ls-item b { font-weight: 600; font-size: var(--fs-nano); opacity: .8; font-variant-numeric: tabular-nums; }
    @media (hover: hover) {
      .ls-item:hover { color: #fff; }
    }
    .ls-item[aria-current="step"] { color: var(--navy-900); background: rgba(255, 255, 255, .92); }
    .ls-item[aria-current="step"] b { opacity: .65; }
    .ls-item:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
    @media (max-width: 767px) {
      .landing-steps { left: 14px; bottom: 12px; }
      .ls-item { padding: var(--space-1-5) var(--space-2-5); }
      .ls-item span { display: none; }
    }
