/* ============================================================
   connect.css — 连接页
   这一版按 ATK HUB 的连接页重做，版式叫「空场 + 底注」：

     ┌───────────────────────────────┐
     │                               │   ▓ 产品悬在中上部，
     │          ▓▓▓▓▓▓▓              │     四周大留白。
     │         ▓▓▓▓▓▓▓▓▓             │
     │          ▓▓▓▓▓▓▓              │
     │                               │
     │                               │
     │          [＋ 连接设备]         │   ▓ 全部文字压在底部中央，
     │          欢迎使用              │     居中对齐。
     │          PDS-W                │
     │   蓝牙模式下浏览器读不到设备…   │
     └───────────────────────────────┘

   和被推翻的上一版（左文右物 Editorial Split）的差别不是「换了套皮肤」，
   是换了一种排版主张：
     · 上一版是编辑式的 —— 左边一大段文案、右边一个物件，信息密度高，
       读起来像产品官网的 hero。
     · 这一版是产品式的 —— 画面里几乎什么都没有，只有一件产品悬在空白里，
       文字全部退到底部做「注脚」。留白本身承担了「高端」这件事。
       ATK 的连接页就是这个结构，实测截图在 work/atk-capture/atk-2500.png。
     · 所以这里刻意【删掉】了上一版的同心环、刻度、遥测四角读数、扫描线、
       光尘 —— 那些是「仪器面板」的语汇，会把空场填满，正好抵消掉留白。
       现在只保留三样：环境辉光、产品本身、地面光斑。

   ── 主视觉为什么是 canvas 粒子 ──────────────────────────────
   用户要的是「ATK 那种炫酷的、高科技的动画」。ATK 的真实做法（读他们的
   加载脚本得到的结论，不是猜的）是【把 GIF 画到 canvas 上】：hub.atk.pro
   加载了 gif.js / gifToCanvas.js，画面里那些「产品自己动起来」的段落
   本质是逐帧位图，不是矢量动效。所以照抄「物体动」这条路，我们缺素材。

   改用【粒子聚合 / 崩解】：把产品图的 alpha 通道采样成几千个点，
   点从四散状态聚合成产品的形状，成型后与那张清晰的 PNG 交叉淡入，
   停一会儿，再崩解散开，循环。理由是它同时满足三件事：
     1) 形态还是那件产品（读者认得出），但呈现方式只有网页能做；
     2) 全部由 canvas 的坐标与透明度驱动，没有位图逐帧的内存开销；
     3) 只有一张静态 PNG 也能做出来，不需要任何视频素材。
   实现全部在 hero-particles.js，时序表写在那个文件头上。

   ── 三条改这个文件前必须知道的规矩 ──────────────────────────
   1) 【隐藏态只准写在 keyframe 的 from 里】。这是上一版踩掉整个大标题换来的：
      入场动画用 fill-mode:backwards 时，动画一结束就释放回基础规则，
      如果基础规则写的是「隐藏」，动画放完元素就永久消失，而且在
      getAnimations() 里查不到（已经播完被移除了），极难定位。
      规矩：基础规则 = 可见的自然状态；隐藏态放 from；fill-mode 一律 both。
   2) 【一个元素只准有一条 animation 声明】。两条分开写时，后写的简写会把
      前一条整体重置（当年 .r3 因此丢了入场，只剩旋转）。
      需要叠两条就写成一条逗号分隔的 animation，错开 delay。
   3) 【产品上不准有 transform】。粒子的目标坐标是按 .hero-still 的
      getBoundingClientRect() 算的，一旦产品带 transform（浮动、缩放、
      视差），矩形每帧都在变，粒子的形状就会和淡入的清晰图对不齐。
      产品要「活」起来靠的是辉光呼吸、掠光、和残留的余烬粒子，不是位移。
   ============================================================ */

:root{
  /* 产品高度。写成 clamp 而不是固定值：连接页的可用高度 = 视口高 - 72px
     顶栏 - 底部文字区，小屏笔记本（768px 高）上固定 400px 会把文字挤出画面。 */
  --hero-h:clamp(200px,40vh,400px);
}

.device-gate{background:transparent}

/* 连接页的骨架：上下两行。
   不用 absolute 定位文字块 —— 文字高度随字号变化（中文换行、小屏放大），
   absolute 的 bottom 值没法跟着变，必然在某个断点上压到产品。
   1fr 那一行吃掉全部剩余高度，产品在行内居中，文字块永远在它下面。 */
.gate-stage{
  position:absolute;inset:0;
  display:grid;grid-template-rows:minmax(0,1fr) auto;
  padding:0 6vw 52px;
}

/* ---------- 视觉区 ---------- */
.gate-visual{
  position:relative;min-height:0;
  display:grid;place-items:center;
  isolation:isolate;          /* 让混合模式只在这个子树里生效 */
}

/* 环境辉光：琥珀色的一大团柔光垫在产品后面。
   深色主题下它的作用是让近黑背景「有光源」；浅色主题下它同时兼了
   「产品影子」的活 —— 白底上的物体没有依托，一团暖光垫着才立得住。 */
.visual-halo{
  position:absolute;z-index:0;pointer-events:none;
  width:min(72vh,660px);height:min(72vh,660px);border-radius:50%;
  background:radial-gradient(circle,
    rgba(var(--accent-rgb),.16) 0%,
    rgba(var(--accent-rgb),.07) 34%,
    rgba(var(--accent-rgb),.02) 58%,
    transparent 74%);
}

/* 粒子画布盖满整个视觉区。粒子坐标由 JS 按 .hero-still 的矩形映射，
   所以画布尺寸和产品尺寸解耦 —— 产品换尺寸（窗口 resize）时重算一次即可。 */
.hero-particles{
  position:absolute;z-index:2;inset:0;
  width:100%;height:100%;display:block;pointer-events:none;
}

/* 产品。opacity 由 JS 驱动（聚合完成后淡入、崩解前淡出），
   这里的基础值必须是 1 —— 万一 JS 没跑起来（禁用 / 报错），
   静态图要照常显示，不能留下一片空白。 */
.visual-product{
  position:relative;z-index:3;
  display:grid;place-items:center;
}
.hero-still{
  display:block;height:var(--hero-h);width:auto;
  max-width:none;max-height:none;object-fit:fill;
  /* 投影让产品从背景里「浮」出来。浅色主题下不能用纯黑 —— 白底上的黑投影
     会发脏，换成带色相的深蓝灰，浓度也减半。 */
  filter:drop-shadow(0 22px 26px rgba(0,0,0,.5));
}
/* 浅色主题的产品投影要写成两层，不能只把深色那层调浅。
   原因：这是个银白配色的鼠标，落在 #f3f4f7 上本体和背景几乎同亮度，
   只有影子能把轮廓「切」出来。深色主题靠的是物体比背景亮，浅色主题反过来，
   所以影子必须紧贴轮廓 —— 原来那层 drop-shadow(0 20px 24px) 模糊半径 24px，
   糊在物体底下一大片，而最需要说清事的轮廓边缘恰恰是最糊的地方。
   补一层模糊半径 7px 的接触影，形状边界立刻立住。 */
html[data-theme="light"] .hero-still{
  filter:drop-shadow(0 3px 7px rgba(20,22,28,.16))
         drop-shadow(0 22px 26px rgba(20,22,28,.18));
}

/* 掠光：一道高光斜着扫过产品，给「持形」那几秒一点生命。
   用 overlay 混合，只改明暗不改色相。

   这里必须带 mask —— 是踩出来的：不带 mask 时这个伪元素是一个
   279×403 的矩形（inset:-6% -18%），overlay 混合又不会让「全透明」变成
   无操作，结果深色主题的崩解帧里，鼠标左边会凭空浮出一块硬边的浅灰方块。
   用产品图自己的 alpha 当 mask，绘制范围就被锁死在鼠标轮廓里，
   混合再怎么算都出不了这个形状。
   注意 inset 必须是 0 而不是负数：mask 的坐标系是元素盒子，
   盒子一旦比图片大，mask 会被拉伸，高光就会和产品错位。 */
.visual-product::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  mix-blend-mode:overlay;opacity:0;
  background:linear-gradient(104deg,transparent 38%,rgba(255,255,255,.55) 50%,transparent 62%);
  background-size:280% 100%;background-position:180% 0;background-repeat:no-repeat;
  -webkit-mask-image:url("models/hero-mouse-ml.png");
          mask-image:url("models/hero-mouse-ml.png");
  -webkit-mask-size:100% 100%;
          mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat;
          mask-repeat:no-repeat;
}

/* 地面光斑：产品脚下的一小片。
   深色主题下它是「地面被照亮」（暗底上看不见阴影，只看得见光）；
   浅色主题下反过来，是「地面被压暗」（白底上光斑反而显不出来）。 */
.visual-floor{
  position:absolute;z-index:-1;bottom:-6%;left:50%;transform:translateX(-50%);
  width:calc(var(--hero-h) * 0.92);height:calc(var(--hero-h) * 0.14);
  border-radius:50%;pointer-events:none;filter:blur(14px);
  background:radial-gradient(ellipse at center,rgba(var(--accent-rgb),.30),transparent 70%);
}
html[data-theme="light"] .visual-floor{
  background:radial-gradient(ellipse at center,rgba(20,22,28,.22),transparent 70%);
}

/* ---------- 角标读数 ---------- */
.gate-meta{
  position:absolute;z-index:6;top:24px;right:6vw;
  display:flex;align-items:center;gap:9px;
  font:500 9px var(--mono);letter-spacing:.16em;color:var(--muted);
}
.gate-meta i{width:1px;height:9px;background:var(--line-hi)}

/* ---------- 底部文案区 ---------- */
.gate-bottom{
  position:relative;z-index:6;
  display:flex;flex-direction:column;align-items:center;text-align:center;
}

/* 芯片按钮。ATK 把它放在大标题【上面】，尺寸很小、深色、药丸形 ——
   因为它是「一个动作」，不是「一个号召」，做大反而会和标题抢。
   注意它是【反色】的：浅色主题下深底浅字，深色主题下浅底深字。
   用 --text / --bg 这一对而不是硬编码，两种主题自动成立。 */
.gate-chip{
  display:inline-flex;align-items:center;gap:9px;
  height:36px;padding:0 15px 0 6px;
  border-radius:var(--r-pill);
  background:var(--text);color:var(--bg);
  font-size:12.5px;font-weight:600;letter-spacing:.01em;
  box-shadow:0 1px 2px rgba(0,0,0,.18),0 10px 24px -12px rgba(0,0,0,.5);
  transition:transform .3s var(--ease-out),box-shadow .3s var(--ease-out),background .3s var(--ease-out);
  cursor:url("data:image/x-icon;base64,AAACAAEAEBACAAgABQCwAAAAFgAAACgAAAAQAAAAIAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AAPwAAAD8AAAB/AAAA/4AAAf+AAAH/wAAD/8AAB3/AAAZ/4AAAf2AAANtgAADbIAABmwAAAZsAAAAYAAAAAAAAD4BwAA+AcAAPAHAADgAwAAwAMAAMABAACAAQAAAAEAAAAAAACQAAAA4AAAAOAAAADABQAAwAcAAOQPAAD+fwAA") 8 4,pointer;
}
.gate-chip b{
  display:grid;place-items:center;
  width:24px;height:24px;border-radius:50%;
  background:var(--accent);color:var(--accent-ink);
  font-size:15px;font-weight:400;line-height:1;
  transition:transform .34s var(--ease-out),background .34s var(--ease-out);
}
.gate-chip:hover{transform:translateY(-1px);box-shadow:0 2px 4px rgba(0,0,0,.2),0 16px 30px -12px rgba(0,0,0,.55)}
.gate-chip:hover b,.gate-chip:focus-visible b{transform:translate(2px,-2px) scale(1.08)}
.gate-chip:active{transform:translateY(1px) scale(.985)}
.gate-chip:disabled{opacity:.55;pointer-events:none}

/* 大标题。两行，各自从一条裁切带里升上来（mask reveal）——
   比整体淡入重，也比逐字弹跳安静，是这类页面最稳的选择。
   .tl 是裁切带（overflow:hidden），i 是被推上来的那一行。
   由于裁切带会切掉字形溢出，两行都必须给足行高（1.12）并且不带降部。 */
.gate-title{
  margin:22px 0 0;
  font-size:clamp(30px,3.6vw,50px);
  line-height:1.12;letter-spacing:-.045em;font-weight:700;
  color:var(--text);text-wrap:balance;
}
.gate-title .tl{display:block;overflow:hidden;padding-bottom:.04em}
.gate-title .tl i{display:block;font-style:normal}
.gate-title em{font-style:normal;letter-spacing:-.02em}

/* ============================================================
   入场编舞
   全部挂在 body:not(.booting) 下面 —— 动画的起跑线。
   CSS 动画在样式表生效时就开始了，而那时可能还没首次绘制；
   如果不等一帧就开跑，用户看到的是「已经播了一半」的画面。
   index.html 末尾那段内联 requestAnimationFrame 负责摘掉 booting。
   （必须内联：它决定「摘 class」和「首次绘制」谁先发生，
     交给外链脚本会受网络时序影响，不保证。）
   ============================================================ */
@media (prefers-reduced-motion:no-preference){

  /* 呼吸和掠光都挂在 11s 上 —— 和粒子循环同一个周期，整页只有一个节拍。
     之前给它们各写 7s，两个周期互质，看久了会显出一种说不出的乱。 */
  body:not(.booting) .visual-halo{animation:haloIn 1.5s var(--ease-out) both,haloBreath 9.6s var(--ease-io) infinite 2.4s}
  body:not(.booting) .visual-product::after{animation:sweep 9.6s var(--ease-io) infinite}

  body:not(.booting) .gate-chip{animation:chipIn .8s cubic-bezier(.3,.74,.32,1) both .12s}
  body:not(.booting) .gate-title .tl:nth-child(1) i{animation:lineUp 1s cubic-bezier(.22,.75,.25,1) both .26s}
  body:not(.booting) .gate-title .tl:nth-child(2) i{animation:lineUp 1s cubic-bezier(.22,.75,.25,1) both .37s}
  body:not(.booting) .gate-meta{animation:noteIn .9s var(--ease-out) both .62s}
}

@keyframes haloIn{
  from{opacity:0;transform:scale(.78)}
  to{opacity:1;transform:none}
}
@keyframes haloBreath{
  0%,100%{transform:scale(1);opacity:1}
  50%    {transform:scale(1.06);opacity:.82}
}
/* 掠光的百分比要对着 hero-particles.js 的时序表读。
   周期 9.6s，「持形」是 3.45s → 7.20s，也就是 36% → 75%。
   改粒子时序时这四个数要跟着改，否则高光会在粒子还没聚拢时就扫过去。 */
@keyframes sweep{
  0%,31%   {opacity:0;background-position:180% 0}
  36%      {opacity:1}
  71%      {opacity:1;background-position:-80% 0}
  75%,100% {opacity:0;background-position:-80% 0}
}
@keyframes chipIn{
  from{opacity:0;transform:translateY(16px) scale(.94)}
  to  {opacity:1;transform:none}
}
@keyframes lineUp{
  from{transform:translateY(112%)}
  to  {transform:none}
}
@keyframes noteIn{
  from{opacity:0;transform:translateY(10px)}
  to  {opacity:1;transform:none}
}

/* ---------- 响应式 ---------- */
/* 窄屏时产品不再是「悬在空场里」的主角，它退到文案后面当氛围 ——
   手机上的竖向空间要优先保证标题和说明读得舒服。 */
@media (max-width:860px){
  :root{--hero-h:clamp(150px,26vh,240px)}
  .gate-stage{padding:0 22px 40px}
  .gate-title{font-size:30px;margin-top:18px}
  .gate-meta{display:none}
  .visual-halo{width:min(88vw,420px);height:min(88vw,420px)}
}
/* 矮屏（横屏笔记本、小窗口）：产品再收一档，否则底部四行文字会被压出画面。
   只对高度设限，不动宽度 —— 宽度由上面的断点管。 */
@media (max-height:720px) and (min-width:861px){
  :root{--hero-h:clamp(160px,34vh,280px)}
  .gate-stage{padding-bottom:36px}
  .gate-title{font-size:clamp(26px,3vw,36px);margin-top:16px}
}

/* 已连接时连接页让位：顶栏那颗「连接设备」按钮和页面中央的大按钮是同一件事，
   同时出现会让人以为要点两次。 */
body:has(#deviceGate:not([hidden])) .connection{display:none}

/* 连接页显示时禁掉整页滚动。
   原因：.app-shell 是靠 visibility:hidden 藏起来的（不是 display:none），
   它照样占布局高度 —— 实测连接页状态下 documentElement.scrollHeight 比
   视口多 252px，于是右侧出现一条滚动条，而且能滚出一片空屏。
   用 :has() 挂在「连接页可见」这个条件上，而不是写 body:not(.device-connected)：
   后者在设备已连接、连接页正在做退场动画的那 0.5 秒里就提前解锁了滚动。 */
html:has(#deviceGate:not([hidden])){overflow:hidden}

/* 连接页的顶栏和下面的场连成一片。
   光去掉 ui-v2.css 里那条 border-bottom 还不够 —— 顶栏自己还有一层
   --glass 背景（浅色主题下是 rgba(246,247,250,.76)，比页面底 #f3f4f7 亮），
   去掉线之后会剩一条「色带」，还是有分割感。
   这里把连接页状态下的顶栏彻底做透明（连同 backdrop-filter 一起去掉，
   留着会平白多一层合成），整片从顶到底只有一种底色。
   配置页不受影响：那边顶栏是 sticky 的，滚动时内容要从它下面穿过，
   需要那层毛玻璃压住字。 */
body:has(#deviceGate:not([hidden])) .topbar{
  background:transparent;
  backdrop-filter:none;
  -webkit-backdrop-filter:none;
}
