/* ==========================================================================
   登录页场景（「下一站启程」）
   --------------------------------------------------------------------------
   参考 https://mxdcact.web.sdo.com/project/20260831preview/ 的首页：
   最底是天空背景，上面两层云向左循环飘动，再上面是那艘飞船。

   坐标系刻意选了「1920x1080 的设计舞台 + 整体等比缩放（cover）」：
   参考站用 rem（视口高 / 1080）定尺寸，效果等价；用固定舞台后每个元素的
   坐标都能直接写在设计稿上，超管拖动时也直接存这套坐标，不必在
   「视口高」和「视口宽」之间来回换算。

   cover 而不是 contain：舞台始终铺满视口、绝不露边，超出部分由
   #login-view 的 overflow: hidden 裁掉。否则窗口比例和 16:9 不一致时
   会露出底色（黑边/白边），风景背景上非常明显。

   摆位数据（船的大小、纸娃娃的位置）由 login-scene.js 从
   /api/login-layout 拉下来写成内联样式，这里只负责静态外观与动效。
   ========================================================================== */

/* 枫叶飘落是主站（苹果风木框）那套皮肤的装饰。登录页换了皮肤后它会飘在
   场景之上抢视线，所以在这个页面里关掉。 */
.leaves { display: none !important; }

#login-view {
  position: relative;
  display: block;
  min-height: 100vh;
  padding: 0;
  overflow: hidden;
  /* 兜底色，与 bg.jpg 的天空色接近 —— 图片没加载出来时也不会是刺眼的白 */
  background: #6fb4e6;
}

/* ---------- 舞台 ---------- */
.login-stage {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1920px;
  height: 1080px;
  margin: -540px 0 0 -960px;
  transform: scale(var(--login-scale, 1));
  transform-origin: center center;
  pointer-events: none;
}

/* ---------- 天空背景 ---------- */
/* 参考站的背景是一张 1920x1080 的渐变（bg.jpg），铺满舞台即可 */
.login-sky {
  position: absolute;
  inset: 0;
  background: url('login-assets/bg.jpg') no-repeat center center;
  background-size: cover;
}

/* ---------- 云层 ---------- */
/* 目标效果：云从**页面最右侧**飘到**最左侧**，循环不断，并且贯穿整个页面宽度。
   参考站原来只在自己那一小块宽度里循环（平铺位移一张图的宽度），飘动幅度很小。

   做法（按层一个平移层）：
     .login-cloud       = 裁剪窗口，铺满舞台整宽，overflow: hidden
     .login-cloud > i   = 真正飘动的平移层，背景 repeat-x 平铺，宽度 100%
     动画把平移层从 translateX(100%) 推到 translateX(-100%)
   百分比位移是相对**平移层自己的宽度**，而平移层等于视口宽，所以位移量
   刚好是「一个页面宽度」—— 从右边缘外飘到左边缘外，中间始终有云。

   为什么用 transform 而不是像参考站那样动 background-position-x：
   动 transform 由合成器在 GPU 上跑，不触发重绘；动背景位置每帧都要重新
   平铺一张 1600px 的图，在这种大范围的持续动画里差别很明显。 */
.login-cloud {
  position: absolute;
  width: max(100%, calc(100% * var(--cloud-scale)));
  overflow: hidden;
  pointer-events: none;
  /* 云层大小与高度。
     · 宽度：倍数 ≤1 时铺满整宽；>1 时按倍数加宽。tile 宽度 = 铺开宽度 ÷ 片数，
       所以倍数越大每片云越大，而整层仍是横向平铺、无缝、能一路扫过。
     · 高度：必须跟着一起长（背景是 background-size: auto 100%，只加宽不加高会
       把云上下裁掉，看到的是一段横条而不是整片云）。但也不能线性长到 10 倍 ——
       那样厚云会盖住整屏。这里用 min() 收一个上限：长到 2.5 倍封顶。
     · 放大靠宽度而不是 transform: scale：用 transform 会把位移距离一起放大，
       飘动的视觉速度也跟着变，那就成了"整体快放"而不是"云变大"。 */
  --cloud-scale: 1;
  --cloud-ar: 0.1442;
  --cloud-k: 1.206;
  /* 高度上限（设计稿像素）。舞台是 1920x1080 的设计稿，所以 594px ≈ 视口高的 55% */
  --cloud-h-cap: 594px;
  /* tile 基准宽度（视口宽的倍数，由 JS 按各层云图的宽高比算）。
     作用是给每朵云一个"屏幕上的确定大小"：层宽再大，也只是铺出更多朵云。 */
  --cloud-tile-w: 150vw;
  height: min(calc(var(--cloud-k) * var(--cloud-ar) * var(--cloud-scale) * 100vh), var(--cloud-h-cap));
  /* 加宽后左侧会溢出，向左挪回一半，保证整层相对舞台居中 */
  left: calc((1 - max(1, var(--cloud-scale))) * 50%);
}

/* 平移层：背景横向平铺，另外挂**两份副本**（::before 左邻、::after 右邻），
   三者间隔正好一个层宽 W，且每份内部再平铺。

   为什么需要三份：动画把这一层从 translateX(100%) 推到 translateX(-100%)，
   总位移 2W，而层宽 W = 舞台宽 × 倍数。倍数 5 时 W = 5 屏 ≈ 8000px，
   位移范围是 +8000 → -8000：

     位移 +8000 时，本体占 [8000,16000]，完全在画面右侧之外
                 → **画面里一朵云都没有**

   只补一份右邻（::after）解决不了前半程 —— 它在更右边。
   必须在**左边**再补一份（::before，位移 +W 时正好盖住 [0, W]），
   于是任何时刻都至少有一份盖在视口上，云带不再出现空档。 */
.login-cloud > i {
  display: block;
  width: 100%;
  height: 100%;
  background-repeat: repeat-x;
  /* 顺序：宽度 = tile 基准宽（每朵云在屏幕上的大小），高度 = 撑满整层。
     高度用 100% 意味着背景按 (层高 / 图高) 缩放，而层高本身已经乘过缩放倍数，
     所以云会跟着层高一起等比放大 —— 这正是"调大云层"该有的表现。 */
  background-size: var(--cloud-tile-w, 150vw) 100%;
  position: relative;
  will-change: transform;
}

/* 左右两份副本。background: inherit 把图、平铺方式、尺寸全部继承过来，
   所以三份的云完全对位，接缝处不会错位。 */
.login-cloud > i::before,
.login-cloud > i::after {
  content: '';
  position: absolute;
  inset: 0;
  background: inherit;
  background-repeat: inherit;
  background-size: inherit;
}
.login-cloud > i::before { left: -100%; }
.login-cloud > i::after { left: 100%; }

/* 顶层薄云：贴在舞台顶部。
   --cloud-ar = 云图的高/宽 = 200/1600 = 0.125；k 取 0.757 让它 1 倍时
   正好是原来的 175px@900 视口高，不改变默认观感。 */
.login-cloud-top {
  top: -32px;
  --cloud-ar: 0.125;
  --cloud-k: 0.757;
  --cloud-scale: var(--cloud-top-scale, 1);
  --cloud-tile-w: var(--cloud-tile-w-top, 150vw);
}
.login-cloud-top > i {
  background-image: url('login-assets/cloud2.png');
  /* 时长由 JS 按「速度倍数」算好写成 --cloud-top-dur（基准 260s ÷ 速度）。
     这里给 260s 只是兜底，脚本没跑起来时仍是原来的观感。 */
  animation: login-cloud-sweep var(--cloud-top-dur, 260s) linear infinite;
}

/* 底层厚云：底部往下溢出一部分。
   --cloud-ar = 231/1600 ≈ 0.1442；k 取 1.206 让它 1 倍时正好是原来的 284px@900。 */
.login-cloud-bottom {
  bottom: -60px;
  --cloud-ar: 0.1442;
  --cloud-k: 1.206;
  --cloud-scale: var(--cloud-bottom-scale, 1);
  --cloud-tile-w: var(--cloud-tile-w-bottom, 150vw);
}
.login-cloud-bottom > i {
  background-image: url('login-assets/cloud1.png');
  animation: login-cloud-sweep var(--cloud-bottom-dur, 220s) linear infinite;
}

/* 从右到左扫过整个页面宽度。
   位移量固定是「自己这一层的宽度」，而该宽度 = max(100%, 倍数) × 舞台宽，
   所以倍数 >1 时扫过的实际路程也变长 —— 相同时长下视觉速度会随倍数增大而变快。
   「速度」滑杆是在此之上再改时长，两者叠加，最终快慢完全可控。 */
@keyframes login-cloud-sweep {
  0%   { transform: translateX(100%); }
  100% { transform: translateX(-100%); }
}

/* 尊重系统的「减少动态效果」：云还在，只是不动 */
@media (prefers-reduced-motion: reduce) {
  .login-cloud { animation: none !important; }
}

/* ---------- 窄屏的场景横向压缩 ----------
   为什么需要它：桌面端船心在舞台 x=26%，手机上照搬就等于把船推到最左边、
   半个船身出画。

   做法上有个坑：**不能用 JS 写的变量直接做 calc 覆盖**。
   JS 必须把超管存的摆位写到 .login-stage 的内联 style 上（`--boat-x` 等），
   而内联样式优先级高于样式表 —— @media 里再写一次 `--boat-x` 是改不动它的，
   表现就是"手机端怎么调都还是出画"。
   所以这里分两层：
     · JS 只负责写「原始摆位」：--boat-x / --boat-y / --boat-w
     · 本文件负责把它们换算成「实际生效值」：--cx / --cy / --cw
   @media 只改几个换算系数，就能整体缩放/平移，而不是去覆盖 JS 写的那个变量。

   x' = x * k + (1 - k) * 50%   —— 向舞台中心收敛，而不是只按比例缩小。
   只乘 k 的话，船心会从 26% 一路缩到 16%（还是贴着左边）；加上向中心
   收敛的那一项，k=0.62 时船心落在 35%，画面才平衡。
   纵向另用 --scene-ky，因为窄屏要同时「缩放」和「上移避让登录卡」，
   两件事用一个系数表达不了。桌面端 k=ky=1，等价于恒等变换。 */
.login-stage {
  --scene-k: 1;
  --scene-ky: 1;
}

/* ---------- 船 ---------- */
/* left/top = 中心点，width = 舞台宽度的百分比。
   三个值由 JS 从布局数据写成 --boat-x / --boat-y / --boat-w 变量，
   这里换算成实际生效值（见上面 --scene-k 的说明）。 */
.login-boat {
  position: absolute;
  left: calc(var(--boat-x, 26%) * var(--scene-k) + (1 - var(--scene-k)) * 50%);
  top: calc(var(--boat-y, 87%) * var(--scene-ky));
  width: calc(var(--boat-w, 31%) * var(--scene-k));
  height: auto;
  transform: translate(-50%, -50%);
  /* 船身后压一层淡投影，让它和天空/云分层，不至于糊在一起 */
  filter: drop-shadow(0 18px 26px rgba(18, 60, 110, 0.22));
  user-select: none;
  -webkit-user-drag: none;
}

/* ---------- 纸娃娃 ---------- */
/* left/top = 底边中点（百分比）：纸娃娃图自带透明留白，按底边对齐才能让
   角色看起来站在船板上，而不是悬空或陷进甲板。 */
/* 这层是铺满整个舞台的容器，本身不接收指针事件。
   少了这一句它会盖在船上面把 pointerdown 全吃掉 —— 表现是「船明明在，
   编辑态里就是拖不动」，而且拖动时从外面完全看不出是谁挡的。 */
.login-dolls {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.login-doll {
  position: absolute;
  /* 横向位置与船同一套换算（缩放 + 向中心收敛），窄屏下整簇角色会跟着船
     一起收进画面中间，而不是各自留在原来的横向位置上出画。
     宽度**由 JS 按图片原始像素写死**（见 applyDollSizeFromImage）——
     用户明确要求纸娃娃不做任何缩放，所以这里既不设 width，也不参与 --scene-k。 */
  left: calc(var(--doll-x, 18%) * var(--scene-k) + (1 - var(--scene-k)) * 50%);
  /* 左右翻转：scaleX(--doll-flip)，1 是原样、-1 是镜像。
     translate 写在 scale 前面 = 先平移后缩放，缩放围绕元素中心，
     所以「底边中点」这个位置锚点在两种朝向下都是同一个点，不会因为翻转跑位。 */
  transform: translate(-50%, -100%) scaleX(var(--doll-flip, 1));
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
  filter: drop-shadow(0 3px 4px rgba(18, 60, 110, 0.28));
}

/* 被超管在编辑面板里单独隐藏的角色 */
.login-doll[hidden] { display: none; }

/* ---------- 登录卡 ---------- */
/* 位置与宽度都由超管在 ?admin7234187 下拖动决定，存进布局后对所有访客生效。
   三个值由 JS 写成 --card-x / --card-y / --card-w 变量，这里换算成实际生效值
   （换算规则与船一致，见 --scene-k 的说明）。
   默认落在右上区域：x=74.5%、y=21%。 */
.login-card {
  position: absolute;
  left: calc(var(--card-x, 74.5%) * var(--scene-k) + (1 - var(--scene-k)) * 50%);
  top: calc(var(--card-y, 21%) * var(--scene-ky));
  /* 宽度用 JS 换算好的舞台像素值（设计稿百分比 → 舞台 px → ÷ 舞台缩放），
     这样卡片在屏幕上的视觉宽度就是设计稿里的尺寸，不随窗口缩放变形。
     6%~40% 的区间由服务端负责夹取。 */
  width: var(--card-w-px, 340px);
  max-width: none;
  padding: 22px;
  /* 主站 .card 带 overflow: hidden，会把挂在卡片右下角的缩放把手裁掉。
     登录页的卡片里没有需要裁的内容，直接放开。 */
  overflow: visible;
  /* 卡片在舞台内部，而舞台是 pointer-events: none（整层装饰）。
     父元素 none 不会让子元素自动恢复点击，所以必须在这里显式打开 ——
     漏了这句，普通访客连登录表单都点不进去（这个坑踩过一次）。 */
  pointer-events: auto;
  /* 盖住主站 .card 的浅色渐变：风景背景上要半透明毛玻璃，不要白板 */
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid rgba(255, 255, 255, 0.85);
  border-radius: 16px;
  box-shadow:
    0 18px 44px rgba(10, 44, 84, 0.28),
    inset 0 1px 0 rgba(255, 255, 255, 0.9);
  backdrop-filter: blur(8px) saturate(1.1);
  -webkit-backdrop-filter: blur(8px) saturate(1.1);
}

/* 注册表单比登录表单长得多。让卡片以同一「视觉中心」上下生长，
   切换时才不会整块往下窜（用 top 定位 + 微调 translateY 实现）。 */
#login-view.login-card-register .login-card { transform: translateY(-118px); }

#login-view .login-card .login-logo-img { width: 66px; }
#login-view .login-card h1.site-name { font-size: 17px; margin-bottom: 4px; }
#login-view .login-card .subtitle { margin-bottom: 18px; font-size: 13px; }
#login-view .login-card form { gap: 13px; }
#login-view .login-card input { padding: 10px 11px; font-size: 14px; }
#login-view .login-card button[type="submit"] { padding: 11px 16px; }
#login-view .login-card .switch { margin-top: 16px; }

/* ==========================================================================
   编辑态：只有「顶级管理员 + ?admin7234187」会拿到这个 class。
   普通访客下场景层 pointer-events 全关，船和纸娃娃都点不动、也拖不了。
   ========================================================================== */
#login-view.login-editing .login-stage { pointer-events: auto; }

/* ⚠️ 这三条 pointer-events: auto **每一条都是必需的**，别合并简化掉。
   父元素写成 none 之后，子元素并不会自动变回可点 —— 必须显式打开。
   而这里有两个"隐形挡板"：
     · .login-dolls 是铺满整舞台的容器，基础样式里是 pointer-events: none；
       纸娃娃是它的子元素，不显式置 auto 就永远收不到 pointerdown
       （表现：纸娃娃看得见、就是拖不动）。
     · .login-stage 在编辑态被打开成 auto，但 .login-sky / .login-cloud 这些
       铺满整屏的装饰层同样会吃掉事件，所以给"能拖的三个东西"逐个打开。 */
#login-view.login-editing .login-boat,
#login-view.login-editing .login-doll,
#login-view.login-editing .login-card {
  pointer-events: auto;
  outline: 1px dashed rgba(255, 255, 255, 0.9);
  outline-offset: 2px;
  cursor: grab;
  touch-action: none; /* 触屏拖动时不要连带滚动页面 */
}

#login-view.login-editing .login-boat,
#login-view.login-editing .login-doll {
  filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.5));
}

#login-view.login-editing .login-boat:hover,
#login-view.login-editing .login-doll:hover,
#login-view.login-editing .login-card:hover { outline-color: #ffd76a; }

/* 登录卡在编辑态：拖得动，但内部的输入框/按钮要照常能用
   （pointerdown 命中控件时 makeDraggable 会主动放行，见 login-scene.js）。 */
#login-view.login-editing .login-card {
  user-select: none;
}

#login-view.login-editing .is-dragging {
  cursor: grabbing;
  z-index: 60;
}

/* 被单独隐藏的角色在编辑态里仍要看得见（否则没法再拖回来），
   所以这里压低透明度而不是 display:none。 */
#login-view.login-editing .login-doll[hidden] {
  display: block;
  opacity: 0.22;
  outline-color: rgba(255, 120, 120, 0.9);
}

/* 缩放把手 */
.login-handle {
  position: absolute;
  right: -9px;
  bottom: -9px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #ffd76a;
  border: 1px solid #8a5a12;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
  cursor: nwse-resize;
  touch-action: none;
}

/* 船的把手宿主。
   它必须自己就「是」船那个矩形：位置与**尺寸全部由 JS 同步**（见 syncBoatHostBox，
   尺寸用像素写死，不依赖百分比），内层 <img> 填满 100%。
   绝不能让这里退化成 0x0 的盒子 —— 那样它会成为船长宽百分比的解析基准，
   把船压成 0px 直接看不见。
   也**不要**在这里再写一次 width: var(--boat-w)：那会和 JS 算出来的像素值打架，
   两层百分比互相解析，船会被推到画面外（这个坑踩过一次）。 */
.login-handle-host {
  position: absolute;
  transform: translate(-50%, -50%);
}

.login-handle-host .login-boat {
  position: static;
  width: 100%;
  height: auto;
  transform: none;
  display: block;
}

/* ---------- 编辑面板 ---------- */
/* 面板本体：max-height 只用来约束整块，真正滚动的是 .le-body。
   这样折叠之后头部始终留在原位，不会被"滚动到看不见"。
   注意别把 overflow 留在这一层 —— 那会让 sticky 失效、也会让内层滚动条出现两条。 */
.login-editor {
  position: fixed;
  z-index: 1000;
  top: 14px;
  left: 14px;
  width: 320px;
  max-height: calc(100vh - 28px);
  display: flex;
  flex-direction: column;
  padding: 0;
  border-radius: 12px;
  background: rgba(22, 34, 52, 0.94);
  color: #e8f1ff;
  font-size: 13px;
  line-height: 1.6;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.45);
}

.login-editor[hidden] { display: none; }

/* 头部：折叠后只剩这一行。整行都可点（当折叠开关用）。 */
.login-editor .le-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  cursor: pointer;
  user-select: none;
  flex: 0 0 auto;
}
.login-editor .le-head:hover { background: rgba(255, 255, 255, 0.06); border-radius: 12px 12px 0 0; }
.login-editor .le-head h3 {
  margin: 0;
  flex: 1 1 auto;
  font-size: 14px;
  color: #ffd76a;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.login-editor .le-fold {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  padding: 0;
  font-size: 16px;
  line-height: 1;
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.3);
  background: rgba(255, 255, 255, 0.14);
  color: #e8f1ff;
  cursor: pointer;
}
.login-editor .le-fold:hover { background: #ffd76a; color: #3a2a00; border-color: #c99a1e; }

/* 主体：折叠时整块收起（display:none，连 tab 焦点一起收掉）。 */
.login-editor .le-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: 0 14px 14px;
}
.login-editor.le-collapsed .le-body { display: none; }
/* 折叠态：面板变成一个只有标题的小条，不再压住左侧的纸娃娃 */
.login-editor.le-collapsed { width: auto; max-width: min(320px, calc(100vw - 28px)); }
.login-editor.le-collapsed .le-head { border-radius: 12px; }

.login-editor h3 { margin: 0 0 8px; font-size: 14px; color: #ffd76a; }
.login-editor h4 { margin: 0 0 6px; font-size: 13px; color: #ffd76a; }
.login-editor p { margin: 0 0 10px; color: #b9cbe4; }
.login-editor kbd {
  padding: 0 4px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.16);
  font-family: inherit;
  font-size: 12px;
}
.login-editor .le-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.login-editor button {
  flex: 1 1 auto;
  padding: 8px 10px;
  font-size: 13px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(255, 255, 255, 0.12);
  color: #e8f1ff;
  cursor: pointer;
}
.login-editor button:hover { background: rgba(255, 255, 255, 0.22); }
.login-editor button.primary {
  background: #ffd76a;
  border-color: #c99a1e;
  color: #3a2a00;
  font-weight: 700;
}
.login-editor button.primary:hover { background: #ffe08f; }
.login-editor button:disabled { opacity: 0.5; cursor: not-allowed; }
.login-editor button.on { background: #6fb4e6; border-color: #3f8ec7; color: #06263d; font-weight: 700; }
.login-editor .le-status { min-height: 18px; color: #9fe0a0; }
.login-editor .le-status.err { color: #ff9a9a; }
.login-editor .le-hint { margin: 6px 0 0; font-size: 12px; color: #8fa6c2; line-height: 1.5; }

/* 分区：云层 / 上传 / 列表各一块，中间用一条淡分隔线 */
.login-editor .le-sec {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
}

/* 云层大小那一行：标签 + − + 数值 + ＋ */
.login-editor .le-ctl {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}
.login-editor .le-ctl > span { flex: 1 1 auto; color: #b9cbe4; }
.login-editor .le-ctl > b {
  min-width: 46px;
  text-align: center;
  color: #ffd76a;
  font-variant-numeric: tabular-nums;
}
.login-editor .le-ctl button {
  flex: 0 0 auto;
  width: 30px;
  padding: 4px 0;
  font-size: 15px;
  line-height: 1;
}

.login-editor .le-list {
  max-height: 240px;
  overflow: auto;
  margin-top: 4px;
}
.login-editor .le-item { display: flex; align-items: center; gap: 5px; padding: 2px 0; }
.login-editor .le-item img { width: 22px; height: 22px; object-fit: contain; flex: 0 0 auto; }
.login-editor .le-item span { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.login-editor .le-item button { flex: 0 0 auto; padding: 3px 6px; font-size: 12px; }

/* ==========================================================================
   窄屏兜底
   --------------------------------------------------------------------------
   整体缩放会把登录卡在手机上缩到没法用，所以 ≤900px 宽时改成：
   天空/云/船照旧当背景铺开（船缩小并往上挪），登录卡换成底部整宽的正常表单。
   ========================================================================== */
@media (max-width: 900px) {
  .login-stage {
    /* 手机上不再整体等比缩放，改为按视口铺开 */
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    margin: 0;
    transform: none;
  }

  /* 手机上仍按倍数走同一套算式，只是把「舞台宽」换成视口宽 ——
     窄屏下舞台只有 420x860 而云层要横跨整屏，所以用 vw 而不是百分比。
     高度上限也收到 34vh，别让厚云在手机上盖住登录卡。 */
  .login-cloud {
    width: max(100%, calc(100vw * var(--cloud-scale)));
    left: calc((1 - max(1, var(--cloud-scale))) * 50%);
    height: min(calc(var(--cloud-k) * var(--cloud-ar) * var(--cloud-scale) * 100vw), 34vh);
  }
  .login-cloud-top { top: -12px; }
  .login-cloud-bottom { bottom: -20px; }

  .login-stage {
    /* 手机上整体缩放场景，并单独把船压到舞台上方：k=0.62 → 船宽
       31%*0.62 ≈ 19%（420 宽下约 80px），整条船露在卡片顶边之上。

       ⚠️ 这里**不能**去覆盖 --boat-x / --boat-y / --boat-w：
       那三个是 JS 按超管存的摆位写到内联 style 上的，内联样式优先级高于
       样式表（含 @media），重写它们一点效果都没有（这个坑踩过一次）。
       要调纵向位置就调下面这个换算系数 --scene-ky。 */
    --scene-k: 0.62;
    --scene-ky: 0.22;
  }

  .login-card {
    left: 50%;
    top: auto;
    bottom: 4vh;
    transform: translateX(-50%);
    width: min(92vw, 380px);
  }
  #login-view.login-card-register .login-card { transform: translateX(-50%); }

  /* 手机上不再强制整宽：折叠态要能缩成一个小条（否则又会挡住纸娃娃）。
     展开态用 max-width 兜住，与桌面一致。 */
  .login-editor { width: min(320px, calc(100vw - 28px)); }
  .login-editor.le-collapsed { width: auto; max-width: calc(100vw - 28px); }
}

/* ==========================================================================
   背景音乐开关
   --------------------------------------------------------------------------
   固定在右上角。几个约束写下来，都是差点踩到的：

   1) 不能放进 .login-stage。舞台是 pointer-events: none 且被 transform: scale()
      缩放 —— 放进去既要显式恢复点击，又会跟着缩放变形，按钮忽大忽小。
      所以由 login-scene-music.js 动态创建，挂在 #login-view 下（舞台的兄弟节点）。

   2) z-index 要比 .login-editor（1000）高，但别越权盖住底部备案条。
      备案条由 beian.js 注入，position: fixed、z-index 9999、高约 20px。
      本按钮在右上角、跟底部那条不重叠，取 1500 留余量又不越权。

   3) 右上角会压在登录卡片的圆角上（卡片最宽时右边到 ~95vw）——这是**刻意**的。
      按钮要始终可见、位置固定；若为了避让改成随卡片变化，卡片一调宽窄按钮就乱跑。
   ========================================================================== */
.login-bgm {
  position: fixed;
  top: 16px;
  right: 18px;
  z-index: 1500;          /* > .login-editor(1000)，< 备案条(9999) */
  width: 44px;
  height: 44px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.75);
  background: rgba(255, 255, 255, 0.82);
  color: #2b6cb0;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(12, 48, 90, 0.22);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: background 0.15s, transform 0.15s, color 0.15s;
  pointer-events: auto;   /* 登录页整体是只读装饰层，显式打开点击 */
}
.login-bgm:hover { background: #fff; transform: scale(1.06); }
.login-bgm:active { transform: scale(0.96); }
.login-bgm:focus-visible { outline: 2px solid #ffb648; outline-offset: 2px; }

.login-bgm svg { width: 22px; height: 22px; display: block; overflow: visible; }
/* 喇叭本体填充、音波描边 —— 浅色底上线条更清爽 */
.login-bgm .spk { fill: currentColor; stroke: none; }
.login-bgm .wave {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
}
.login-bgm .slash {
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

/* 正在播放：音波可见、斜杠隐藏 */
.login-bgm.is-on { color: #2b6cb0; }
.login-bgm.is-on .wave { opacity: 1; }
.login-bgm.is-on .slash { opacity: 0; }

/* 已静音：音波隐藏、斜杠可见，整体转灰表示"没在响" */
.login-bgm.is-off { color: #7c8794; }
.login-bgm.is-off .wave { opacity: 0; }
.login-bgm.is-off .slash { opacity: 1; }

/* 静音时给一点极轻的呼吸，否则用户容易以为按钮坏了 */
@keyframes login-bgm-breathe {
  0%, 100% { box-shadow: 0 4px 14px rgba(12, 48, 90, 0.22); }
  50%      { box-shadow: 0 4px 18px rgba(12, 48, 90, 0.32); }
}
.login-bgm.is-off { animation: login-bgm-breathe 2.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .login-bgm { animation: none; transition: none; }
}

/* 窄屏：缩小并避开异形屏的圆角/刘海 */
@media (max-width: 900px) {
  .login-bgm {
    top: max(10px, env(safe-area-inset-top, 0px));
    right: max(10px, env(safe-area-inset-right, 0px));
    width: 38px;
    height: 38px;
  }
  .login-bgm svg { width: 19px; height: 19px; }
}

/* ==========================================================================
   微信小程序码（贴在登录卡片左侧）
   --------------------------------------------------------------------------
   位置刻意与卡片用**同一套** --card-* 变量来算，而不是写死 left：
   卡片是可以被超管拖动、改宽的（见 login-scene.js 的 applyCard），
   写死位置的话拖一次卡片就错位了。

   横向：卡片左边缘 = --card-x 换算出的位置；再往左让出「间距 + 码宽」。
   纵向：与卡片垂直居中。卡片在登录/注册两种形态下高度不同（注册表单长得多），
        用 translateY(-50%) 跟着卡片中心走，比写死 top 稳。

   尺寸：由 JS 写 --qr-size（--miniapp-size），因为要保证码不会顶出屏幕左边 ——
        "卡片左边还剩多少地方"只有运行时才知道。见 applyMiniapp()。
   ========================================================================== */
.login-miniapp {
  position: absolute;
  /* 与 .login-card 的 left 完全同一套换算，保证不管卡片怎么拖都对得上 */
  left: calc(var(--card-x, 74.5%) * var(--scene-k) + (1 - var(--scene-k)) * 50%);
  /* 再往左退：自己的宽度 + 间距 */
  margin-left: calc(-1 * (var(--miniapp-size, 200px) + 22px));
  top: calc(var(--card-y, 21%) * var(--scene-ky));
  width: var(--miniapp-size, 200px);
  /* 纵向与卡片**中心**对齐：--miniapp-dy 是「半个卡片高」（JS 实测后写进来的，
     卡片高度由内容撑开，登录/注册两种形态差别很大，CSS 算不出来）。
     没有这个变量时退化成只做 -50%，即与卡片顶边同锚、自身居中。 */
  transform: translateY(calc(-50% + var(--miniapp-dy, 0px)));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px 8px 7px;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid rgba(255, 255, 255, 0.85);
  box-shadow:
    0 18px 44px rgba(10, 44, 84, 0.28),
    inset 0 1px 0 rgba(255, 255, 255, 0.9);
  backdrop-filter: blur(8px) saturate(1.1);
  -webkit-backdrop-filter: blur(8px) saturate(1.1);
  /* 舞台整层是 pointer-events: none，父元素 none 不会让子元素自动恢复点击，
     所以每块要显示的东西都得显式打开（这个坑踩过一次，见 .login-card）。 */
  pointer-events: auto;
}

.login-miniapp img {
  display: block;
  width: 100%;
  height: auto;
  /* 小程序码是正方形，圆角留一点即可；给了 border-radius 后
     某些浏览器会在图片边缘露出底色，所以配一层白底 */
  border-radius: 8px;
  background: #fff;
  /* 拖动/长按不要弹出系统的"保存图片"菜单，免得挡住拖纸娃娃 */
  -webkit-user-drag: none;
  user-select: none;
  -webkit-touch-callout: none;
}

.login-miniapp-cap {
  font-size: 12px;
  line-height: 1.2;
  color: #4a5b70;
  letter-spacing: 0.5px;
  white-space: nowrap;
}

/* 手机上登录卡改成底部整宽、几乎占满竖向空间（注册形态更高），
   卡片两侧和上方都没有稳定的空位了：按原来的 margin-left 摆会被顶出屏幕，
   想塞在卡片上方又会在切到"注册"表单时被卡片压住。
   所以窄屏改成贴在**左上角**固定摆放 —— 那里永远是空的（右下角是卡片，
   右上角是音乐按钮），也是最不容易和其它元素打架的位置。 */
@media (max-width: 900px) {
  .login-miniapp {
    left: max(8px, env(safe-area-inset-left, 0px));
    top: max(8px, env(safe-area-inset-top, 0px));
    /* 复掉桌面那套：桌面是与卡片对齐算的，窄屏卡片位置完全不同 */
    margin-left: 0;
    bottom: auto;
    transform: none;
    width: 84px;
    padding: 4px 4px 4px;
    border-radius: 10px;
    gap: 0;
  }
  /* 窄屏不放文字：一块 84px 的方图在左上角已经很显眼，
     加了标题会让它变宽、更容易碰到卡片。小程序码本身一眼就认得出来。 */
  .login-miniapp-cap { display: none; }
}
