:root {
  color-scheme: dark;
  font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
  background: #171e46;
  -webkit-tap-highlight-color: transparent;
  /* 系统大字号不得改写排版：首页是像素级对齐的设计稿，
     自动放大会把 12 个入口挤出安全区。 */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* App 化：屏蔽只有浏览器才有的交互。首页是原生 App 的门面，
   长按菜单、选中放大镜、双击缩放都会暴露"这是个网页"。
   原生侧另有对应开关（iOS allowsLinkPreview / Android builtInZoomControls），
   两边都要关：网页侧改完部署即可，原生侧要重新编译。 */
* {
  box-sizing: border-box;
  /* 长按链接/图片弹出"打开、拷贝、存储到照片"菜单 */
  -webkit-touch-callout: none;
  /* 长按选中文字 + 放大镜 + "拷贝/查询/翻译"气泡 */
  -webkit-user-select: none;
  user-select: none;
}

html,
body {
  /* 双击放大；顺带消掉 300ms 点击延迟，按键更跟手 */
  touch-action: manipulation;
}

img {
  /* 长按图片拖出到其它 App（iPad 上尤其明显） */
  -webkit-user-drag: none;
}

/* 卡片和工具按钮都是 <a>：不禁掉的话，桌面上按住一拖就拖出一个链接影子，
   和卡片链自己的拖动直接打架。 */
.landscape-action,
.portrait-action {
  -webkit-user-drag: none;
}

/* 只有真的滚得动才提示「可抓」。给了 grab 却拖不动比不给更糟。 */
[data-chain].is-scrollable {
  cursor: grab;
}

[data-chain].is-scrollable.is-dragging {
  cursor: grabbing;
}

html,
body {
  min-width: 100%;
  min-height: 100%;
  margin: 0;
  background: #171e46;
  scrollbar-width: none;
  overflow-x: hidden;
}

body {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;
  scrollbar-width: none;
}

body::-webkit-scrollbar {
  display: none;
}

html::-webkit-scrollbar {
  display: none;
}

button {
  font: inherit;
}

/* 原生功能与模块入口是真实 <a>：iOS 只把 linkActivated 导航
   识别为 popumidi:// 原生调用，JS 点击的 button 不会触发。 */
a.landscape-action,
a.portrait-action {
  text-decoration: none;
}

.app-stage {
  position: relative;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  isolation: isolate;
  background: #111a46;
}

.portrait-app {
  display: none;
}



.cosmos {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0; /* 旧 WebView(华为 11.0.7) 不识别 inset 简写，上面 longhand 是回退 */
  overflow: hidden;
  background: #182052 url("./assets/app-background.png") center bottom / cover no-repeat;
}

.cosmos__stars,
.cosmos__stars::before,
.cosmos__stars::after {
  position: absolute;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #fff;
  box-shadow:
    5vw 19vh 0 -1px rgb(255 255 255 / 80%),
    12vw 13vh 0 0 rgb(255 255 255 / 92%),
    17vw 31vh 0 -1px #fff,
    23vw 47vh 0 0 rgb(255 246 255 / 86%),
    30vw 8vh 0 -1px #fff,
    36vw 37vh 0 -1px rgb(255 255 255 / 78%),
    43vw 15vh 0 0 #fff,
    51vw 58vh 0 -1px rgb(255 255 255 / 84%),
    58vw 26vh 0 -1px #fff,
    64vw 43vh 0 0 rgb(255 255 255 / 92%),
    71vw 14vh 0 -1px #fff,
    78vw 34vh 0 0 rgb(255 255 255 / 88%),
    86vw 20vh 0 -1px #fff,
    93vw 49vh 0 0 rgb(255 255 255 / 82%);
  content: "";
}

.cosmos__stars {
  top: 6vh;
  left: 7vw;
  filter: drop-shadow(0 0 5px rgb(255 255 255 / 80%));
}

.cosmos__stars::before {
  top: 15vh;
  left: 25vw;
  opacity: 0.72;
  transform: scale(0.68);
  transform-origin: top left;
}

.cosmos__stars::after {
  top: 47vh;
  left: 47vw;
  opacity: 0.65;
  transform: scale(0.45);
  transform-origin: top left;
}

.cosmos__meteors {
  position: absolute;
  z-index: 1;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0; /* 旧 WebView(华为 11.0.7) 不识别 inset 简写，上面 longhand 是回退 */
  overflow: hidden;
  pointer-events: none;
}

.cosmos__shooting-star {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--meteor-length, 160px);
  height: 2px;
  border-radius: 999px;
  background: linear-gradient(90deg, #fff 0%, rgb(255 246 255 / 72%) 8%, rgb(251 238 255 / 26%) 38%, transparent 100%);
  box-shadow: 0 0 7px rgb(255 233 255 / 66%);
  opacity: 0;
  transform: translate3d(var(--meteor-start-x), var(--meteor-start-y), 0) rotate(var(--meteor-angle)) scaleX(0.28);
  transform-origin: left center;
  will-change: transform, opacity;
}

.cosmos__shooting-star.is-flying {
  animation: meteor-crossing var(--meteor-duration) linear var(--meteor-delay, 0ms) forwards;
}

.cosmos__shooting-star::after {
  position: absolute;
  top: 50%;
  left: -3px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 14px 4px rgb(255 228 251 / 72%);
  content: "";
  transform: translateY(-50%);
}

@keyframes meteor-crossing {
  0% {
    opacity: 0;
    transform: translate3d(var(--meteor-start-x), var(--meteor-start-y), 0) rotate(var(--meteor-angle)) scaleX(0.28);
  }

  14% {
    opacity: var(--meteor-opacity, 0.76);
    transform: translate3d(var(--meteor-x-1), var(--meteor-y-1), 0) rotate(var(--meteor-angle)) scaleX(0.76);
  }

  62% {
    opacity: var(--meteor-opacity, 0.76);
    transform: translate3d(var(--meteor-x-2), var(--meteor-y-2), 0) rotate(var(--meteor-angle)) scaleX(1.16);
  }

  100% {
    opacity: 0;
    transform: translate3d(var(--meteor-end-x), var(--meteor-end-y), 0) rotate(var(--meteor-angle)) scaleX(0.64);
  }
}

@media (prefers-reduced-motion: reduce) {
  .cosmos__shooting-star {
    animation: none;
  }
}

.cosmos__planet {
  position: absolute;
  display: block;
  border-radius: 50%;
  background: radial-gradient(circle at 30% 25%, #fed9ec 0%, #bb86c8 31%, #7563bf 68%, #2d3577 100%);
  box-shadow: inset -18px -18px 30px rgb(18 27 84 / 54%), 0 0 28px rgb(220 177 245 / 29%);
}

.cosmos__planet--left {
  bottom: -20vh;
  left: -8vw;
  width: clamp(170px, 27vw, 520px);
  aspect-ratio: 1;
}

.cosmos__planet--left::before {
  position: absolute;
  top: 21%;
  left: -39%;
  width: 176%;
  height: 27%;
  border: clamp(2px, 0.3vw, 5px) solid rgb(255 222 239 / 70%);
  border-right-color: rgb(255 222 239 / 21%);
  border-radius: 50%;
  box-shadow: 0 0 17px rgb(255 216 244 / 20%);
  content: "";
  transform: rotate(-22deg);
}

.cosmos__planet--right {
  right: -8vw;
  bottom: -22vh;
  width: clamp(150px, 24vw, 460px);
  aspect-ratio: 1;
  opacity: 0.9;
}

.cosmos__moon {
  position: absolute;
  top: 25.5%;
  right: 8.1%;
  width: clamp(32px, 4vw, 77px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 26%, #f7c9e7 0%, #c88fc9 33%, #5d4e9e 100%);
  box-shadow: inset -10px -10px 16px rgb(24 25 79 / 47%), 0 0 18px rgb(247 204 243 / 34%);
}

.cosmos__clouds {
  position: absolute;
  right: -8%;
  bottom: -18%;
  left: -8%;
  height: 46%;
  background:
    radial-gradient(ellipse 24% 45% at 16% 82%, rgb(253 230 238 / 82%) 0%, transparent 72%),
    radial-gradient(ellipse 27% 48% at 45% 83%, rgb(239 215 241 / 78%) 0%, transparent 72%),
    radial-gradient(ellipse 28% 48% at 77% 82%, rgb(231 210 244 / 74%) 0%, transparent 72%),
    linear-gradient(180deg, transparent 0%, rgb(244 216 238 / 43%) 60%, rgb(246 224 241 / 91%) 100%);
  filter: blur(clamp(8px, 1.2vw, 20px));
  opacity: 0.88;
}

.cosmos__island {
  position: absolute;
  bottom: 1%;
  width: clamp(48px, 8vw, 154px);
  height: clamp(72px, 13vw, 250px);
  background: linear-gradient(90deg, rgb(100 88 179 / 86%) 0 33%, rgb(143 121 207 / 70%) 33% 66%, rgb(91 81 165 / 78%) 66%);
  box-shadow: 0 0 30px rgb(204 176 243 / 38%);
  opacity: 0.68;
}

.cosmos__island::before {
  position: absolute;
  top: -24%;
  left: 25%;
  width: 53%;
  height: 31%;
  background: rgb(171 148 222 / 72%);
  content: "";
}

.cosmos__island::after {
  position: absolute;
  top: -45%;
  left: 52%;
  width: 0;
  height: 0;
  border-right: clamp(6px, 0.8vw, 16px) solid transparent;
  border-bottom: clamp(22px, 3.2vw, 60px) solid rgb(123 101 192 / 78%);
  border-left: clamp(6px, 0.8vw, 16px) solid transparent;
  content: "";
  transform: translateX(-50%);
}

.cosmos__island--one {
  left: 35%;
}

.cosmos__island--two {
  right: 36%;
  width: clamp(42px, 6.8vw, 132px);
  height: clamp(58px, 10vw, 190px);
}

.landscape-app {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0; /* 旧 WebView(华为 11.0.7) 不识别 inset 简写，上面 longhand 是回退 */
  z-index: 1;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: clamp(14px, 2.4vh, 28px);
  gap: clamp(14px, 2.4dvh, 28px);
  width: 100%;
  max-width: 1760px;
  height: 100%;
  margin: 0 auto;
  /* 横向边距两侧取同一个值：灵动岛只在一侧，但只让那一侧的话，内容会
     整体偏向另一边，翻转手机时布局左右跳动，且岛那侧挖出一条贯穿全高的
     空槽。取两侧较大者做对称边距，内容始终居中。
     --safe-area-* 仍保持文档语义（只有灵动岛那侧非零），这里只是把它
     派生成视觉边距，数据层不受影响。
     4.15vw = 设计稿的 (100% - 91.7%) / 2，无安全区时与原设计等宽。 */
  --edge-gutter: max(
    4.15vw,
    calc(max(var(--safe-area-left), var(--safe-area-right)) + var(--control-gap))
  );

  padding-top: max(20px, var(--safe-area-top));
  padding-right: var(--edge-gutter);
  padding-bottom: max(24px, var(--safe-area-bottom));
  padding-left: var(--edge-gutter);
}

/* ── 桌面宽屏对齐设计稿 ──
   上面那套「width:100% + max-width:1760px + padding:4.15vw」在视口 ≤1760px
   时与设计稿完全等价，但 max-width 一旦生效（视口 >1760px），容器就卡死在
   1760px 不再变宽，而 padding 仍按 vw 继续涨 —— 于是「(视口-1760)/2 的 margin」
   和「4.15vw 的 padding」叠加成两层留白。2400px 宽实测单侧 419.6px，设计稿
   只有 320px，桌面上明显更空。

   这里改回设计稿的宽度语义（width: min(91.7vw, 1760px)，留白全部由
   margin:auto 平分），只在桌面浏览器生效：
     min-width:1761px  —— 只覆盖真正出现偏差的区间，1760px 及以下一行不改；
     pointer:fine + hover:hover —— 必须有鼠标/触控板且支持悬停。触摸设备的
       WebView（iOS/Android/鸿蒙原生壳）报告的是 coarse + none，不会命中，
       所以 App 内的布局与安全区让位逻辑一个像素都不受影响。
   两个条件是「与」关系，超大平板即使宽度越过 1761px 也会被 pointer 挡在外面。 */
@media (min-width: 1761px) and (pointer: fine) and (hover: hover) {
  .landscape-app {
    width: min(91.7vw, 1760px);
    /* 桌面无安全区，这一项恒为 0；保留表达式是为了外接触控屏之类的边缘情况
       仍能让位，且与基线保持同一套语义。 */
    --edge-gutter: max(
      0px,
      calc(
        max(var(--safe-area-left), var(--safe-area-right)) + var(--control-gap) -
          (100vw - min(91.7vw, 1760px)) / 2
      )
    );
  }
}

.landscape-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 0;
}

.landscape-tools {
  display: grid;
  /* 云端 / 蓝牙 / 设置 / OTA / AI助手 五个直达入口。点亮与皮肤临时隐藏，页面就绪后再放开。
     品牌区已撤，工具栏顶到行尾保持原先的右缘对齐。 */
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(8px, 1.4vw, 20px);
  margin-left: auto;
}

.landscape-action {
  position: absolute;
  display: block;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  outline: none;
  color: #fff;
  background: transparent;
  cursor: pointer;
  touch-action: manipulation;
}

.landscape-tool {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 5px;
  color: rgb(255 255 255 / 82%);
  font-size: clamp(11px, 1.9vh, 18px);
  font-size: clamp(11px, 1.9dvh, 18px);
  line-height: 1;
}

.landscape-tool img {
  display: grid;
  width: clamp(38px, 6.4vh, 70px);
  width: clamp(38px, 6.4dvh, 70px);
  height: clamp(38px, 6.4vh, 70px);
  height: clamp(38px, 6.4dvh, 70px);
  padding: clamp(8px, 1.45vh, 15px);
  padding: clamp(8px, 1.45dvh, 15px);
  border: 1px solid rgb(255 255 255 / 17%);
  border-radius: clamp(12px, 1.8vh, 20px);
  border-radius: clamp(12px, 1.8dvh, 20px);
  background: rgb(189 190 226 / 20%);
  box-shadow: inset 0 1px 1px rgb(255 255 255 / 18%);
  object-fit: contain;
}

.landscape-content {
  display: grid;
  grid-template-rows: repeat(2, minmax(0, 1fr));
  min-height: 0;
  gap: clamp(10px, 1.8vh, 20px);
  gap: clamp(10px, 1.8dvh, 20px);
  padding: clamp(12px, 2vh, 22px);
  padding: clamp(12px, 2dvh, 22px);
  border: 1px solid rgb(255 255 255 / 54%);
  border-radius: clamp(22px, 3.1vh, 40px);
  border-radius: clamp(22px, 3.1dvh, 40px);
  background: rgb(229 229 246 / 26%);
  box-shadow: 0 22px 54px rgb(9 14 55 / 24%), inset 0 1px 0 rgb(255 255 255 / 35%);
  backdrop-filter: blur(18px) saturate(120%);
}

.landscape-feature-grid {
  display: grid;
  min-height: 0;
  gap: clamp(10px, 1.4vw, 20px);
}

/* 二级卡片链：卡片绝对定位在槽位上，几何由 chain-geometry.js 计算。
   这里不能用 grid —— grid 会重排卡片，而链条要的是卡片换槽、槽位不动。 */
.landscape-chain,
.portrait-chain {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  /* 自己接管横向拖动；页面本身不滚动，交给浏览器只会变成选中或回弹 */
  touch-action: none;
}

/* 卡片间距沿用设计稿原来两个网格的 gap，由 card-chain.js 读取后计算槽位，
   避免在 JS 里写死一个和设计对不上的数。
   用 column-gap 而不是自定义属性：容器不是 flex/grid，column-gap 没有布局作用，
   但会被解析成 px；自定义属性取到的是未计算的原始字串（"clamp(...)"），
   parseFloat 直接得 NaN。 */
.landscape-chain {
  column-gap: clamp(10px, 1.4vw, 20px);
}

/* 提高到两级选择器：.portrait-action / .landscape-action 在后面的媒体查询里
   设了 position: relative，同为单类选择器会按源码顺序压过 .chain-card。 */
.landscape-chain .chain-card,
.portrait-chain .chain-card {
  position: absolute;
  top: 0;
  left: 0;
  display: grid;
  grid-template-rows: max-content minmax(0, 1fr);
  container-type: inline-size;
  min-height: 0;
  overflow: hidden;
  border: 1px solid rgb(255 255 255 / 69%);
  /* 圆角按方向单独给，见下方规则与 .portrait-action。设计稿里二级卡片和一级
     卡片本来就是同一套圆角（横屏同为 .landscape-card、竖屏同为 .portrait-action），
     链条改造不该拆开。但不能直接并进 .landscape-card 的选择器组 —— 那条规则还带
     position: relative，会覆盖掉绝对定位，卡片退回文档流堆叠。 */
  background: linear-gradient(145deg, var(--card-start), var(--card-end));
  /* 这里不能有 box-shadow：链容器为了裁掉屏幕外的卡片必须 overflow: hidden，
     阴影会在容器边界被直角硬裁，几张卡叠起来就是一块带清晰直边的深色矩形。
     卡片靠白色描边分隔已经足够；要做立体感就用 inset 阴影，别往外投。 */
  /* 宽高与位移由 card-chain.js 逐帧写入 */
  transition: none;
  /* 合成层只在拖动期间提升：常驻 will-change 会让 13 张卡长期各占一层显存，
     反而拖慢整页。拖动开始时由 JS 加 .is-dragging，松手后撤掉。 */
  contain: layout paint;
}

/* 与 .landscape-card 同值 —— 二级卡片在设计稿里就是 .landscape-card */
.landscape-chain .chain-card {
  border-radius: clamp(16px, 2.5vh, 30px);
  border-radius: clamp(16px, 2.5dvh, 30px);
}

.is-dragging .chain-card {
  will-change: transform, width, height;
}

/* 卡片在链上会连续改变大小（小卡升成大卡），所以排版一律用容器单位 cqw，
   字号随卡宽平滑缩放，不需要为两种槽位各写一套。 */
.chain-card > .card-copy {
  /* 留白改用 top/left 给，padding 会连同留白一起算进框里，框就不再等于文字。 */
  position: absolute;
  top: calc(var(--card-w, 31vw) * 0.07);
  top: 7cqw;
  left: calc(var(--card-w, 31vw) * 0.07);
  left: 7cqw;
  z-index: 2;
  width: max-content;
  max-width: calc(100% - var(--card-w, 31vw) * 0.14);
  max-width: calc(100% - 14cqw);
  padding: 0;
  color: rgb(0 0 0 / 65%);
}

.chain-card > .card-copy strong {
  display: block;
  font-size: calc(var(--card-w, 31vw) * 0.11);
  font-size: 11cqw;
  line-height: 1.15;
  white-space: nowrap;
}

.chain-card > .card-copy small {
  display: block;
  margin-top: calc(var(--card-w, 31vw) * 0.015);
  margin-top: 1.5cqw;
  font-size: calc(var(--card-w, 31vw) * 0.07);
  font-size: 7cqw;
  line-height: 1.25;
  opacity: 0.72;
}


.landscape-feature-grid {
  grid-template-columns: 1.32fr 1fr;
}


.landscape-card {
  position: relative;
  display: grid;
  grid-template-rows: max-content minmax(0, 1fr);
  container-type: inline-size;
  overflow: hidden;
  border: 1px solid rgb(255 255 255 / 69%);
  border-radius: clamp(16px, 2.5vh, 30px);
  border-radius: clamp(16px, 2.5dvh, 30px);
  background: linear-gradient(145deg, var(--card-start), var(--card-end));
  box-shadow: 0 12px 24px rgb(16 16 70 / 18%);
}

.landscape-card::before,
.portrait-feature::before,
.portrait-mini::before {
  position: absolute;
  z-index: 0;
  top: -46%;
  right: -14%;
  width: 58%;
  aspect-ratio: 1;
  border: 1px solid rgb(255 255 255 / 34%);
  border-radius: 8px;
  background: linear-gradient(145deg, rgb(255 255 255 / 18%), transparent 68%);
  box-shadow:
    -42px 42px 0 -1px rgb(255 255 255 / 8%),
    -84px 84px 0 -1px rgb(255 255 255 / 5%);
  content: "";
  transform: rotate(34deg);
}

/* 图片层铺满整张卡片。两个关键点：
   1) 网格里绝对定位子元素的包含块是「它的网格区域」，不是整张卡片 ——
      必须先跨满整个网格，inset: 0 才等于整卡；
   2) 不再自己裁切，溢出交给卡片自身的圆角，切出来是圆角而不是直角硬边。 */
.card-art {
  grid-area: 1 / 1 / -1 / -1;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0; /* 旧 WebView(华为 11.0.7) 不识别 inset 简写，上面 longhand 是回退 */
  z-index: 1;
  display: block;
  min-width: 0;
  min-height: 0;
  pointer-events: none;
}

/* 模块图都是正方形，让盒子也恒为正方形：contain 的「限制边」不再会在
   宽和高之间翻转，绘制方块大小从此只由下面各处给的一个尺寸决定。
   构图 = 尺寸(width 或 height) + 水平(right 或 left+translateX) + bottom。 */
.card-art img {
  position: absolute;
  display: block;
  aspect-ratio: 1;
  object-fit: contain;
}


.landscape-card--wide {
  grid-template-columns: minmax(0, 44%) minmax(0, 56%);
  grid-template-rows: minmax(0, 1fr);
}


/* 横屏一级卡：绘制方块右缘贴卡片右缘，底部溢出。两张卡宽不同但卡高相同，
   所以尺寸挂在卡高上，一条规则同时喂饱两张。 */
.landscape-card--wide .card-art img {
  right: 0;
  bottom: -30.7%;
  height: 130%;
}

/* 横屏二级卡（链上 5 张）：绘制方块水平居中，底部溢出。
   bottom 比设计稿反解值（-34.7%）高出约 13px：设计稿里图形顶到文字底还留着
   约 26px，产品要求这段间距收一半，于是整块图上移 13px（卡高 127）。 */
.landscape-chain .chain-card .card-art img {
  left: 50%;
  bottom: -24.5%;
  height: 97.2%;
  transform: translateX(-50%);
}

.landscape-card > .card-copy,
.portrait-feature > .card-copy,
.portrait-mini > .card-copy {
  /* 收缩到文字本身：原先它是被拉满网格单元的网格项，一个 3/4 都是空白的框
     把图片挤出了半张卡片。改成绝对定位后图片才拿得到整张卡。 */
  position: absolute;
  z-index: 2;
  top: clamp(14px, 3.2vh, 34px);
  top: clamp(14px, 3.2dvh, 34px);
  left: clamp(14px, 2.8vw, 36px);
  width: max-content;
  max-width: calc(100% - 2 * clamp(14px, 2.8vw, 36px));
  padding: 0;
  color: rgb(0 0 0 / 65%);
  letter-spacing: 0;
  text-shadow: none;
  text-align: left;
  pointer-events: none;
}

.card-copy strong,
.card-copy small {
  display: block;
  letter-spacing: 0;
  text-wrap: balance;
}

.card-copy strong {
  font-size: clamp(19px, 3.1vh, 31px);
  font-size: clamp(19px, 3.1dvh, 31px);
  font-weight: 700;
  line-height: 1.1;
}

.card-copy small {
  margin-top: clamp(3px, 0.7vh, 6px);
  margin-top: clamp(3px, 0.7dvh, 6px);
  color: rgb(0 0 0 / 35%);
  font-size: clamp(13px, 1.7vh, 16px);
  font-size: clamp(13px, 1.7dvh, 16px);
  font-weight: 500;
  line-height: 1.3;
  white-space: nowrap;
  text-shadow: none;
}

@container (max-width: 145px) {
  .card-copy small {
    white-space: normal;
  }
}

.landscape-card--wide > .card-copy {
  grid-column: 1;
  grid-row: 1;
  align-self: start;
}

.landscape-card--wide .card-copy strong {
  font-size: clamp(24px, 4.1vh, 40px);
  font-size: clamp(24px, 4.1dvh, 40px);
}

.module-card--beginner {
  --card-start: #8fa7ff;
  --card-end: #ded7ff;
}

.module-card--main-course {
  --card-start: #ffc18f;
  --card-end: #ffe4c5;
}

.module-card--library {
  --card-start: #9da4f7;
  --card-end: #d8d5ff;
}

.module-card--super-library {
  --card-start: #f4a7c4;
  --card-end: #ffd9e6;
}

.module-card--webfx {
  --card-start: #86c6e8;
  --card-end: #d3e7ff;
}

.module-card--op1 {
  --card-start: #ffaf9d;
  --card-end: #ffe0c9;
}

.module-card--watermelon {
  --card-start: #b697ef;
  --card-end: #eadcff;
}

.landscape-action::after {
  position: absolute;
  z-index: 3;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0; /* 旧 WebView(华为 11.0.7) 不识别 inset 简写，上面 longhand 是回退 */
  border: 2px solid transparent;
  border-radius: inherit;
  background: rgb(255 255 255 / 0%);
  content: "";
  pointer-events: none;
  transition: border-color 180ms ease-out, background-color 180ms ease-out;
}

.landscape-action:hover::after {
  border-color: rgb(255 255 255 / 72%);
  background: rgb(255 255 255 / 7%);
}

.landscape-card:active,
.portrait-feature:active,
.portrait-mini:active {
  transform: scale(0.985);
  transition: transform 120ms ease-out;
}

.landscape-action:focus-visible::after {
  border-color: #fff;
  box-shadow: 0 0 0 3px #5146d6;
}


.toast {
  position: fixed;
  z-index: 4;
  left: 50%;
  bottom: max(24px, var(--safe-area-bottom));
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: min(90vw, 520px);
  min-height: 48px;
  padding: 12px 18px;
  border: 1px solid rgb(255 255 255 / 56%);
  border-radius: 999px;
  color: #fff;
  background: rgb(26 33 91 / 88%);
  box-shadow: 0 12px 36px rgb(18 21 68 / 35%);
  opacity: 0;
  backdrop-filter: blur(18px) saturate(125%);
  transform: translate(-50%, 18px);
  transition:
    opacity 220ms ease-out,
    transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
  pointer-events: none;
}

.toast.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}

.toast__signal {
  width: 8px;
  height: 8px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: #d8caff;
  box-shadow: 0 3px 12px rgb(183 156 255 / 55%);
}

/* 提示只在「等得到」的时候才出现，圆点持续搏动表示还在加载中。
   动画必须挂在 .is-visible 上：隐藏态的 .toast 只是 opacity:0，圆点若照旧搏动，
   合成器每一帧都得重合成整页——门户上压着 1174×665 的 backdrop-filter，
   真机（F11，55Hz 面板）静止在门户只有 33fps，把这个动画停掉就是满帧 55.5。 */
.toast.is-visible .toast__signal {
  animation: toast-pulse 1.1s ease-in-out infinite;
}

@keyframes toast-pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }

  50% {
    opacity: 0.45;
    transform: scale(1.65);
  }
}

@media (prefers-reduced-motion: reduce) {
  .toast__signal {
    animation: none;
  }
}

.toast__message {
  overflow-wrap: anywhere;
  font-size: 15px;
  line-height: 1.45;
  letter-spacing: 0;
}

@media (prefers-reduced-motion: reduce) {
  .landscape-card img,
  .toast {
    transition: none;
  }
}

@media (orientation: portrait) {
  body {
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    background: #111a46;
  }

  .app-stage {
    position: fixed;
    top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0; /* 旧 WebView(华为 11.0.7) 不识别 inset 简写，上面 longhand 是回退 */
    display: block;
    z-index: 0;
    pointer-events: none;
  }

  .landscape-app {
    display: none;
  }

  .portrait-app {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    /* 列显式收敛到 minmax(0, 1fr)：默认的 auto 列以 min-content 为下限，
       header 里任何一个放不下的元素都会把整张网格连同所有卡片一起撑破。 */
    grid-template-columns: minmax(0, 1fr);
    /* 内容区有高度上限，填不满时富余的空档要上下均分，否则会整体偏上 */
    align-content: center;
    gap: var(--stack-gap);
    width: min(100%, 720px);
    height: 100vh;
    height: 100dvh;
    margin: 0 auto;
    position: relative;
    z-index: 1;
    --edge-gutter: max(
      clamp(12px, 4vw, 16px),
      calc(max(var(--safe-area-left), var(--safe-area-right)) + var(--control-gap))
    );

    /* 上下取同一个值，页面在屏幕里居中。顶部被灵动岛顶到 62px、底部安全区只有
       34px，两边直接用各自的安全区会让内容整体偏上；取较大者作为共同下限。
       圆角让位仍只加在底部那一侧的计算里（见下方 padding-bottom 的注释）。 */
    padding-top: max(
      10px,
      var(--safe-area-top),
      calc(var(--safe-area-bottom) + var(--control-gap))
    );
    padding-right: var(--edge-gutter);
    /* 底部与顶部同值。安全区之外再加一档横向留白：屏幕四角是圆弧，贴着直边量的
       距离在角上会被吃掉一截。圆角半径没有跨平台的公开 API（iOS 的
       _displayCornerRadius 是私有的，Android 12+ 才有公开的 getRoundedCorner），
       所以不猜半径 —— 有底部安全区就说明是圆角全面屏，这个判据由 max() 天然编码。 */
    padding-bottom: max(
      12px,
      var(--safe-area-top),
      calc(var(--safe-area-bottom) + var(--control-gap))
    );
    padding-left: var(--edge-gutter);
  }

  .portrait-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--stack-gap);
    margin: 0;
  }

  .portrait-utilities {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: clamp(4px, 1.5vw, 8px);
    /* 品牌区撤掉后不再拉伸占行：宽度交给内容，顶到行尾保持原先的右缘对齐。 */
    flex: 0 1 auto;
    /* 五个工具 × 48px 图标 + 间隙 ≈ 290px，窄屏放不下时由 max-width 收敛 */
    max-width: min(74vw, 360px);
    margin-left: auto;
  }


  .portrait-action {
    position: relative;
    display: block;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 14px;
    color: #fff;
    background: transparent;
    cursor: pointer;
    overflow: hidden;
    touch-action: manipulation;
  }

  .portrait-action::after {
    position: absolute;
    top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0; /* 旧 WebView(华为 11.0.7) 不识别 inset 简写，上面 longhand 是回退 */
    border: 2px solid transparent;
    border-radius: inherit;
    background: rgb(255 255 255 / 0%);
    content: "";
    pointer-events: none;
    transition:
      border-color 160ms ease-out,
      background-color 160ms ease-out;
  }

  .portrait-action:hover::after,
  .portrait-action:active::after {
    border-color: rgb(255 255 255 / 78%);
    background: rgb(255 255 255 / 10%);
  }

  .portrait-action:focus-visible::after {
    border-color: #fff;
    box-shadow: 0 0 0 3px #4036a6;
  }

  .portrait-utility {
    min-height: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    color: #fff;
    background: transparent;
    box-shadow: none;
  }

  .portrait-tool {
    display: grid;
    justify-items: center;
    gap: 3px;
    color: rgb(255 255 255 / 82%);
    font-size: clamp(10px, 2.6vw, 13px);
    line-height: 1;
  }

  .portrait-tool img {
    display: grid;
    /* 11vw 上限给窄屏收缩用：底板 + 间距放不进 370px 内容区时会把 .portrait-app
       的网格列从 min-content 撑破，整张网格跟着变宽、整页横向溢出。
       402 宽下 11vw = 44.2px 刚好放得下，520px 以上仍取 48。 */
    width: clamp(34px, min(6.4vh, 11vw), 48px);
    width: clamp(34px, min(6.4dvh, 11vw), 48px);
    height: clamp(34px, min(6.4vh, 11vw), 48px);
    height: clamp(34px, min(6.4dvh, 11vw), 48px);
    padding: clamp(7px, 1.45vh, 10px);
    padding: clamp(7px, 1.45dvh, 10px);
    border: 1px solid rgb(255 255 255 / 18%);
    border-radius: clamp(10px, 1.5vh, 14px);
    border-radius: clamp(10px, 1.5dvh, 14px);
    background: rgb(189 190 226 / 20%);
    box-shadow: inset 0 1px 1px rgb(255 255 255 / 18%);
    object-fit: contain;
  }

  .portrait-content {
    display: grid;
    /* 一级卡片区 + 二级卡片链两行；链自己内部再分 Z 字两排 */
    grid-template-rows: 2.4fr 2fr;
    align-content: start;
    /* 填满所在网格行。原本是 min(100%, 100vw * 1.65)：在 402×874 这类比例下
       上限比行高矮 15px，内容顶在上边、底部凭空多出一截空档，于是「顶部到内容」
       62px 而「内容到底部」77px。想靠 align-self: center 补，又会把 header 到
       卡片区的间距顶成 17.6px，破坏统一的 10px 节奏。直接填满最干净。 */
    height: 100%;
    min-height: 0;
    gap: 10px;
  }

  .portrait-feature-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: 1.28fr 1fr;
    min-height: 0;
    gap: 10px;
  }

  .portrait-feature {
    display: grid;
    grid-template-rows: max-content minmax(0, 1fr);
    container-type: inline-size;
    width: 100%;
    height: 100%;
    min-height: 0;
    background: linear-gradient(145deg, var(--card-start), var(--card-end));
    box-shadow: 0 14px 34px rgb(14 19 73 / 24%);
  }

  .portrait-chain {
    column-gap: 10px;
    /* 占满内容网格的第二行；Z 字两排的高度分配在 portraitSlots() 里 */
    min-height: 0;
    height: 100%;
  }

  .portrait-library-grid .portrait-feature {
    height: 100%;
  }

  .portrait-feature > .card-copy,
  .portrait-mini > .card-copy {
    top: clamp(10px, 2.2vh, 18px);
    top: clamp(10px, 2.2dvh, 18px);
    left: clamp(12px, 3.7vw, 18px);
    max-width: calc(100% - 2 * clamp(12px, 3.7vw, 18px));
  }

  .portrait-feature .card-copy strong,
  .portrait-mini .card-copy strong {
    font-size: clamp(18px, 2.9vh, 23px);
    font-size: clamp(18px, 2.9dvh, 23px);
  }

  .portrait-feature .card-copy small,
  .portrait-mini .card-copy small {
    margin-top: 3px;
    font-size: clamp(12px, 1.7vh, 14px);
    font-size: clamp(12px, 1.7dvh, 14px);
    line-height: 1.25;
  }

  .portrait-feature-list .card-copy {
    grid-column: 1;
    grid-row: 1;
    max-width: none;
  }

  .portrait-mini > .card-copy {
    max-width: none;
  }

  .portrait-mini .card-copy strong {
    font-size: clamp(16px, 2.5vh, 20px);
    font-size: clamp(16px, 2.5dvh, 20px);
  }

  .portrait-mini .card-copy small {
    font-size: clamp(11px, 1.55vh, 13px);
    font-size: clamp(11px, 1.55dvh, 13px);
  }

  .portrait-feature-list .portrait-feature {
    grid-template-columns: minmax(0, 54%) minmax(0, 46%);
    grid-template-rows: minmax(0, 1fr);
  }

  /* 竖屏一级卡：绘制方块右缘贴卡片右缘。两张卡宽相同、卡高不同，
     所以尺寸挂在卡宽上，各自的底部溢出量单独给。 */
  .portrait-feature-list .portrait-feature .card-art img {
    right: 0;
    bottom: -38%;
    width: 47.8%;
  }

  .portrait-feature-list .module-card--beginner .card-art img {
    bottom: -32%;
    width: 58.6%;
  }

  /* 竖屏链上卡片：大槽与小槽卡宽不同（339 与 223）但卡高相同，且拖动时同一张卡
     会在两档之间移动 —— 尺寸必须挂在卡高上，两档才拿到一样大的图形。 */
  .portrait-chain .chain-card .card-art img {
    /* -50% 是设计稿反解值，但设计稿在 iPad 竖屏这一档本身就让曲库/AI拆谱的图形
       压住了文字（文字底到图形顶是 -17px 与 -5px）。下移 27px 让最紧的那张也留出
       10px 间距，其余四张跟着下移后仍在安全范围内。 */
    left: 50%;
    bottom: -61.8%;
    height: 133.5%;
    transform: translateX(-50%);
  }

  /* 上面那条是按大槽（曲库/AI拆谱）定的：它们的图形离文字只剩 9~22px，再上移
     就压字了。小槽（下排三张）同一条 bottom 下却还空着 41~61px，得单独上移
     27px 把这段收一半。
     按容器宽度分档而不是按卡片名分：宽度跟着槽位走（大槽 339、小槽 223），
     拖动后卡片换到另一档会自动切换规则；写死卡片名的话，被拖到大槽的
     Web FX 会带着小槽的位置过去。
     阈值 280 夹在两档之间。iPhone 竖屏的小槽（117）也会命中这里，但下面
     520px 断点的同名规则在源码里更靠后，会把它覆盖掉。 */
  @container (max-width: 280px) {
    .portrait-chain .chain-card .card-art img {
      bottom: -50%;
    }
  }

  .portrait-mini {
    display: grid;
    grid-template-rows: max-content minmax(0, 1fr);
    container-type: inline-size;
    height: 100%;
    min-height: 0;
    background: linear-gradient(145deg, var(--card-start), var(--card-end));
    box-shadow: 0 10px 24px rgb(14 19 73 / 18%);
  }

  .toast {
    bottom: max(18px, var(--safe-area-bottom));
    min-height: 46px;
    font-size: 14px;
  }
}

@media (orientation: portrait) and (max-width: 520px) {
  .portrait-header {
    /* 与横屏一致：个人中心与工具栏同一行。原本窄屏下改成上下堆叠，
       多占一行高度，观感也和横屏不统一。 */
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--stack-gap);
  }

  .portrait-utilities {
    /* 与个人中心同处一行：列宽取内容、靠右排，末个底板右缘贴住卡片右边线。
       不再占满整行，所以宽度交给内容而不是 100%。加云端入口后共 6 个工具，撤 OTA 后现为 5 个。 */
    grid-template-columns: repeat(5, max-content);
    justify-content: end;
    width: auto;
    max-width: none;
    gap: clamp(6px, 2.2vw, 14px);
  }

  /* 构图只在这个断点重标定。顶部并为一行 + 纵向间距统一 10px 省出的高度全落给了
     卡片，只有窄屏卡片才比设计稿高：hero 从 370x191 变成 390x230，链上卡片从
     117x146 变成 123x170。而 iPad 竖屏不命中本断点，卡片几何与设计稿一致
     （688x309 对 688x305），所以上面通用竖屏那几条不能动。
     参数由设计稿的「图形宽 / 卡片宽」「图形右距」「图形底距」三项反解得到。 */
  .portrait-feature-list .portrait-feature .card-art img {
    bottom: -38%;
    width: 55.8%;
  }

  .portrait-feature-list .module-card--beginner .card-art img {
    bottom: -17.4%;
    width: 60.7%;
  }

  .portrait-chain .chain-card .card-art img {
    /* 「图形顶到卡片底」从 57px 加大一半到 86px，即整块图上移 29px（卡高 170）。
       这一档图形离文字还有 66px 余量，上移后仍不相碰。 */
    bottom: -20.1%;
    /* 两档槽位的目标方块是 142（大槽）与 151（小槽），取让两边偏差相等的中值，
       各差约 3%。挂在卡高上是因为两档卡宽不同、卡高相同。 */
    height: 86.2%;
  }
}

@media (orientation: portrait) and (max-width: 420px) {
  .portrait-header {
    align-items: center;
    gap: var(--stack-gap);
  }

  .portrait-utilities {
    /* 与 520px 断点保持同一套：列宽取内容。这里若写回 1fr 会把上面的
       max-content 覆盖掉，工具栏又变回等分列。 */
    grid-template-columns: repeat(5, max-content);
  }
}

.landscape-action.landscape-tool::after,
.portrait-action.portrait-tool::after {
  display: none;
}

.landscape-action.landscape-tool:focus,
.portrait-action.portrait-tool:focus {
  outline: none;
}

.landscape-tool img,
.portrait-tool img {
  transition: background-color 160ms ease-out;
}

/* 旧 WebView(华为 11.0.7) 不识别 :is()，整条规则会被判无效丢弃 —— 触屏上
   :active 是唯一的按压反馈，所以展开成等价的选择器列表。 */
.landscape-tool:hover img,
.landscape-tool:active img,
.portrait-tool:hover img,
.portrait-tool:active img {
  background-color: rgb(189 190 226 / 42%);
}

/* 同样展开；:focus-visible 本身也是新语法，旧 WebView 上仍然不生效，
   但它只画键盘焦点态，触屏设备用不到。 */
.landscape-tool:focus-visible img,
.portrait-tool:focus-visible img {
  filter: brightness(1.16);
}

.unpublish-page .landscape-header {
  justify-content: flex-end;
}

.unpublish-page .landscape-tools {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.unpublish-page .landscape-content {
  grid-template-rows: minmax(0, 1fr);
}

.unpublish-module-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  min-height: 0;
  gap: clamp(10px, 1.4vw, 20px);
}

.unpublish-page .unpublish-module .card-copy strong {
  font-size: clamp(30px, 4.8vh, 48px);
  font-size: clamp(30px, 4.8dvh, 48px);
}

.unpublish-page .unpublish-module .card-copy small {
  margin-top: 8px;
  font-size: clamp(16px, 2.2vh, 22px);
  font-size: clamp(16px, 2.2dvh, 22px);
}

.unpublish-page .unpublish-module .card-art img {
  top: 0;
  width: 135%;
  height: 135%;
}

@media (orientation: portrait) {
  .unpublish-page .portrait-header {
    display: flex;
    justify-content: flex-end;
  }

  .unpublish-page .portrait-utilities {
    /* 只有两个入口：沿用主页的「列宽取内容 + 靠右」，末个底板右缘贴住卡片右边线。
       这里若留 1fr 等分列，会被 520px 断点的 max-width: none 拉满整行。 */
    grid-template-columns: repeat(2, max-content);
    justify-content: end;
    width: auto;
  }

  .unpublish-page .portrait-content {
    grid-template-rows: minmax(0, 1fr);
    height: 100%;
  }

  /* 竖屏改纵向堆叠：单列三行，横屏的三列布局在这里被覆盖。 */
  .unpublish-module-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: repeat(3, minmax(0, 1fr));
    gap: clamp(10px, 1.6vh, 20px);
    gap: clamp(10px, 1.6dvh, 20px);
  }

  .unpublish-page .unpublish-module-grid,
  .unpublish-page .unpublish-module {
    height: 100%;
  }
}


/* 旧 WebView(华为 11.0.7) 的最后两处回退。这里没沿用本文件其余地方的成对声明
   写法，是因为这两项的兜底值不是「构造上等价」的同义写法，直接并排会改动新引擎
   的渲染；关进 @supports 后支持这两个特性的引擎完全看不到这一段。 */

/* aspect-ratio 需要新引擎。装饰方块的宽是百分比(58% 父宽)，height:58% 会按父高
   解析、并不等价；padding 的百分比恰恰按父宽解析，配合全局 box-sizing:border-box
   总高即 58% 父宽，与 aspect-ratio:1 同果。
   .card-art img 不需要同样处理：模块图源文件全是正方形且 CSS 只给单边尺寸，
   旧引擎按 <img> 的内在比例算出来就是正方形。 */
@supports not (aspect-ratio: 1) {
  .landscape-card::before,
  .portrait-feature::before,
  .portrait-mini::before {
    height: 0;
    padding-top: 58%;
  }
}

/* 容器查询同样不被支持，两个 @container 块整体失效。卡宽由 card-chain.js 在写
   --card-w 的同一处打成属性，这里按属性复现，阈值与那边一一对应。
   只覆盖链上卡片 —— 其余几个 container 的宽度没有 JS 在跟，也没窄到会命中。 */
@supports not (container-type: inline-size) {
  .chain-card[data-narrow] > .card-copy small {
    white-space: normal;
  }

  .portrait-chain .chain-card[data-sub280] .card-art img {
    bottom: -50%;
  }
}
