/*
 * 果冻叠叠塔 · 皮肤层。
 *
 * 规矩（`shared/game-shell/ui/README.md`）：
 * **只覆盖 `--gs-*` 变量，不覆盖任何 `.gs-*` 选择器**——覆盖选择器会把组件的
 * 触控下限、安全区处理和按下态一起改坏。本文件里除变量之外的部分，全部是
 * 本作自己的 `.js-*` 布局类（分区容器、HUD 排布、画布），与组件库不重名。
 *
 * 配色取自 `art/ART_PLAN.json` 的 neon-toy-arcade：深夜紫蓝的云海、
 * 青绿的稳定读数、暖金的分数与登顶。
 */

:root {
  /* ---- 颜色 ---- */
  --gs-ink: #F4EEFF;
  --gs-ink-soft: rgba(244, 238, 255, 0.76);
  --gs-ink-dim: rgba(244, 238, 255, 0.5);
  --gs-ink-on-light: #1A1038;

  --gs-surface: rgba(30, 20, 62, 0.78);
  --gs-surface-raised: rgba(52, 36, 102, 0.9);
  --gs-surface-sunken: rgba(14, 8, 32, 0.62);
  --gs-hairline: rgba(174, 150, 255, 0.24);

  --gs-accent: #46E8CB;
  --gs-accent-2: #FFAE3C;
  --gs-gold: #FFD257;
  --gs-danger: #FF7A93;

  --gs-glow-accent: 0 0 0 1px rgba(70, 232, 203, 0.45), 0 0 30px rgba(70, 232, 203, 0.3);
  --gs-glow-gold: 0 0 26px rgba(255, 210, 87, 0.5);
  --gs-shadow-card: 0 18px 44px rgba(5, 2, 20, 0.5);
  --gs-shadow-button: 0 6px 0 rgba(24, 8, 52, 0.5), 0 14px 26px rgba(5, 2, 20, 0.45);
  --gs-shadow-press: 0 2px 0 rgba(24, 8, 52, 0.5), 0 6px 12px rgba(5, 2, 20, 0.45);

  --gs-cta: linear-gradient(135deg, #FF63C3, #A15CFF 55%, #5C7BFF);

  /* ---- 圆角：果冻是软的，界面也该是软的 ---- */
  --gs-radius-sm: 12px;
  --gs-radius-md: 20px;
  --gs-radius-lg: 30px;

  /* ---- 本作自己的令牌（`.js-*` 用，不进组件库）---- */
  --js-night: #150C33;
  --js-night-deep: #0B0621;
  --js-plumb: #7FF3FF;
  --js-hud-height: 100%;

  /* 「选中 / 下一个」在**分区里面**的高亮。
     不能用 `--gs-glow-accent`：那一版是 30px 的外发光，而每个 `.js-zone` 都是
     `overflow: hidden`（分区契约）。卡片离分区边只有二十来像素时，外发光被齐齐切成
     一个方角的浅色矩形贴在卡片背后——看起来像渲染坏了，而不是"这张被选中了"。
     选中态改成"一圈实边 + 往里打的光"：光全在卡片自己身上，分区怎么切都切不到它。 */
  --js-select-ring:
    0 0 0 1px rgba(70, 232, 203, 0.55),
    inset 0 0 22px rgba(70, 232, 203, 0.16);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;
  background: var(--js-night-deep);
  color: var(--gs-ink);
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'PingFang SC',
    'Hiragino Sans', 'Noto Sans CJK SC', sans-serif;
  -webkit-font-smoothing: antialiased;
  /* 按住是核心操作：不许触发页面滚动、长按菜单或文本选择（§10.6） */
  touch-action: none;
  overscroll-behavior: none;
  /* 另外四款都有的一条护栏：入口的 viewport 一旦被改坏（少了 width=device-width），
     两家引擎都会开始自作主张放大正文字号，按 px 排好的分区当场撑破。
     判据是 automation/check_mobile.py 的 `text-size-adjust`。 */
  -webkit-text-size-adjust: 100%;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* 安全区在这里一次性扣掉。分区坐标全部是"扣完之后"的比例
   （SCREEN_SPEC.shared_conventions.safe_area）。 */
#app {
  position: fixed;
  inset: 0;
  padding: var(--gs-safe-top) var(--gs-safe-right) var(--gs-safe-bottom) var(--gs-safe-left);
}

.js-stage {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* 画布铺满整个舞台：HUD 旁边的完美连击点、果冻身上的软化，都画在画布上
   （§6.5 登记的两个画布指示），所以画布不能只占游玩分区。 */
.js-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
}

/* 两层：底层是当前场景，上层是浮层。
   上层自成一个层叠上下文，所以底层的按压区（z-index 3）绝不会盖到浮层上面去。
   层本身不吃指针事件——不然它会把整片空白都变成一块看不见的挡板。 */
.js-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.js-layer--base { z-index: 2; }
.js-layer--overlay { z-index: 10; }

/* 分区容器：位置由 zones 决定，一律绝对定位，互不相交。
   `overflow: hidden` 是分区契约的兑现：任何一块都不许溢出到隔壁分区里去。 */
.js-zone {
  position: absolute;
  display: flex;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  pointer-events: auto;
}

/* `js-zone--center` 的 `align-items: center` 会让孩子按**内容宽度**撑开，而不是
   收在分区里。六格解锁轨道因此比分区宽 2px，被上面那句 `overflow: hidden` 齐齐切掉
   一条边（剧本 01 的分区判据两个轴都查，量到的就是这 2px）。
   把孩子的最大宽度钉在分区上，`.gs-track__row` 自带的 `overflow-x: auto`
   就会照它本来的设计接手——行内横向滚动，而不是溢出到分区外面去。 */
.js-zone > * { max-width: 100%; }

.js-zone--col { flex-direction: column; }
.js-zone--center { align-items: center; justify-content: center; }
/* 分区是绝对定位 + `overflow: hidden` 的，可滚分区的下边界因此是一条**硬切线**：
   最后一张卡被从中间齐齐削掉，玩家读到的是"这里坏了"，不是"这里还能滚"。
   两件事一起做：底部留出一整行余量，边界改成渐隐。 */
.js-zone--scroll {
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-bottom: var(--gs-space-5);
  -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 20px), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 20px), transparent 100%);
}
.js-zone--scroll::-webkit-scrollbar { display: none; }
.js-zone--pad { padding: var(--gs-space-3); gap: var(--gs-space-3); }

/* ---- 按压区：整个游玩分区就是它（§6.1）---- */
.js-press {
  position: absolute;
  z-index: 3;
  /* 按压区直接挂在 `.js-layer--base` 上，而层本身是 `pointer-events: none`
     （见上面那条：不然空白处会变成一块看不见的挡板）。分区容器 `.js-zone` 自己
     把指针事件收了回来，按压区不是分区，所以必须在这里显式收回——
     漏了这一句，玩家按下去的是底下那张画布，按压区成了一块画着光标的死板。 */
  pointer-events: auto;
  touch-action: none;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
}

.js-press:focus-visible {
  outline: 3px solid var(--gs-accent);
  outline-offset: -3px;
}

/* ---- HUD ---- */
/* 竖排内边距只有 4px：横屏的 HUD 分区只有 0.12 屏高（375 高时 45px），
   而暂停按钮是写死的 44×44——留 8px 上下就把按钮挤出分区了。 */
.js-hud {
  display: flex;
  align-items: center;
  gap: var(--gs-space-2);
  width: 100%;
  height: 100%;
  /* 底部这 15px 是**画布上的连击点**那条道（`chainDotsBox()`）。
     不留出来的话，画布上的点正好画在「SCORE」那行小字上——
     DOM 量不到这种重叠，因为压上来的那一层根本不在 DOM 里。 */
  padding: var(--gs-space-1) var(--gs-space-3) calc(var(--gs-space-1) + 15px);
  /* HUD 浮在插画天空上，读数要在任何一张天空皮肤上都读得清。
     压暗只压 HUD 这一条，且下沿渐隐到全透明——不许出现一条横杠似的硬边。 */
  background: linear-gradient(
    180deg,
    rgba(11, 6, 33, 0.78) 0%,
    rgba(11, 6, 33, 0.52) 58%,
    rgba(11, 6, 33, 0) 100%
  );
}

/* 引导期的 HUD 多一行：跳过按钮按 SCREEN_SPEC 落在 hud 分区里，
   和暂停按钮分两行摆，间距天然够（相邻可点元素 ≥8px）。 */
.js-hud__stack {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  gap: var(--gs-space-1);
}

.js-hud__stack .js-hud { height: auto; flex: 1 1 auto; }

.js-hud__second {
  display: flex;
  justify-content: flex-end;
  padding: 0 var(--gs-space-3) var(--gs-space-1);
}

.js-skip { padding-inline: var(--gs-space-4); }

.js-hud__score {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 64px;
}

.js-hud__score b {
  font-size: var(--gs-text-xl);
  color: var(--gs-gold);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  text-shadow: var(--gs-glow-gold);
}

.js-hud__score small {
  font-size: 10px;
  color: var(--gs-ink-dim);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.js-hud__grow { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.js-hud__side { flex: 0 0 auto; display: flex; align-items: center; gap: var(--gs-space-2); }

/* 横屏右侧栏：竖排，同一批读数，不是第二套设计（§9.2） */
.js-rail {
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: var(--gs-space-2);
  padding: var(--gs-space-2);
  border-radius: var(--gs-radius-md);
  background: var(--gs-surface);
  border: 1px solid var(--gs-hairline);
}

/* ---- 内容排布 ---- */
.js-col { display: flex; flex-direction: column; gap: var(--gs-space-2); width: 100%; }
.js-row { display: flex; align-items: center; gap: var(--gs-space-2); width: 100%; }
.js-row--wrap { flex-wrap: wrap; }
.js-fill { flex: 1 1 auto; min-width: 0; }
.js-center { text-align: center; }

/* 浮层标题栏。以前是「标题 flex:1 + 一颗按钮」，标题按**剩下的宽度**居中，
   于是标题越长越贴着按钮，八种语言里迟早撞上（走查在 modes / pause 上量到）。
   两侧各钉一格按钮位，标题在**两格之间**居中：按钮在哪一侧都对称，
   而且标题绝不可能压到按钮上。 */
.js-titlebar {
  display: flex;
  align-items: center;
  gap: var(--gs-space-2);
  width: 100%;
  min-width: 0;
}

.js-titlebar__slot {
  flex: 0 0 var(--gs-tap-min);
  display: flex;
  justify-content: center;
}

.js-titlebar > .js-title {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  overflow-wrap: anywhere;
}

.js-title { margin: 0; font-size: var(--gs-text-xl); line-height: 1.1; }
.js-title--display {
  /* `9vw` 只看视口**宽**，横屏 932×430 上算出 84px，而 brand 分区只有 0.42 屏宽、
     0.3 屏高（129px）——标题折成两行、把下面的「Best 0」整条压出分区外裁掉半截
     （走查在 landscape-932x430 上量到的就是这 16px）。
     改成两个方向取小：横屏受 9vh 约束缩到 ~39px，竖屏与桌面照旧由 9vw / 52px 上限决定，
     两块主场画布上的字号一个像素都没变。 */
  font-size: clamp(30px, min(9vw, 9vh), 52px);
  letter-spacing: 0.01em;
  text-shadow: 0 0 24px rgba(255, 120, 220, 0.5);
}
.js-body { margin: 0; font-size: var(--gs-text-sm); color: var(--gs-ink-soft); line-height: 1.4; }
.js-note { margin: 0; font-size: var(--gs-text-xs); color: var(--gs-ink-dim); line-height: 1.35; }
.js-strong { color: var(--gs-ink); }

.js-panel {
  border-radius: var(--gs-radius-lg);
  background: var(--gs-surface);
  border: 1px solid var(--gs-hairline);
  backdrop-filter: blur(14px);
  padding: var(--gs-space-3);
}

/* 暂停是浮层：背后的局面必须还认得出（模糊 + 压暗，不是纯黑遮罩） */
.js-scrim {
  position: absolute;
  inset: 0;
  background: rgba(11, 6, 33, 0.58);
  backdrop-filter: blur(8px);
  pointer-events: auto;
}

/* ---- 设置行 ---- */
.js-setting {
  display: flex;
  align-items: center;
  gap: var(--gs-space-3);
  min-height: var(--gs-tap-min);
  padding: var(--gs-space-2) var(--gs-space-3);
  border-radius: var(--gs-radius-md);
  background: var(--gs-surface-sunken);
}

/* 一行里塞不下「图标 + 标签 + 三段选择」时，让控件掉到下一行去占满整行。
   `.gs-segmented` 自己会换行，但换行之后三段各自还是很窄；
   给它一整行才是这一档该有的样子。 */
.js-setting--stack { flex-wrap: wrap; row-gap: var(--gs-space-2); }
.js-setting--stack > :last-child { flex: 1 1 100%; }

.js-setting__icon { width: 22px; height: 22px; flex: 0 0 auto; opacity: 0.85; }
.js-setting__label { flex: 1 1 auto; font-size: var(--gs-text-sm); }

.js-switch {
  position: relative;
  flex: 0 0 auto;
  width: 58px;
  height: var(--gs-tap-min);
  border: 0;
  border-radius: var(--gs-radius-pill);
  background: var(--gs-surface-raised);
  cursor: pointer;
}

.js-switch::after {
  content: '';
  position: absolute;
  top: 6px;
  left: 6px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--gs-ink-dim);
  transition: transform var(--gs-normal) var(--gs-ease-out), background var(--gs-normal);
}

.js-switch[aria-checked='true'] { background: rgba(70, 232, 203, 0.3); }
.js-switch[aria-checked='true']::after { transform: translateX(20px); background: var(--gs-accent); }

.js-slider { flex: 1 1 auto; min-width: 90px; height: var(--gs-tap-min); accent-color: var(--gs-accent); }

.js-select {
  flex: 0 0 auto;
  min-height: var(--gs-tap-min);
  padding: 0 var(--gs-space-3);
  border-radius: var(--gs-radius-sm);
  border: 1px solid var(--gs-hairline);
  background: var(--gs-surface-raised);
  color: var(--gs-ink);
  font-size: var(--gs-text-sm);
}

/* ---- 选择卡（模式 / 口味 / 顶纹 / 天空）---- */
.js-choice {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--gs-space-1);
  min-width: var(--gs-tap-min);
  min-height: var(--gs-tap-min);
  padding: var(--gs-space-2);
  border-radius: var(--gs-radius-md);
  border: 2px solid transparent;
  background: var(--gs-surface-sunken);
  color: var(--gs-ink);
  text-align: left;
  cursor: pointer;
  font: inherit;
}

.js-choice[aria-pressed='true'] { border-color: var(--gs-accent); box-shadow: var(--js-select-ring); }
.js-choice[data-unlocked='false'] { opacity: 0.62; }
/* 以前这几排是 `js-row--wrap`：卡按**内容**撑开，三张卡三个宽度、三个高度，
   图大的那张把自己的标签和解锁行一起顶出卡外，被分区齐齐切掉。
   等宽网格之后，一排里每张卡同宽同高，图 / 名字 / 解锁行三段各有各的位置。 */
.js-choice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: var(--gs-space-2);
  width: 100%;
}

.js-choice { align-items: center; text-align: center; }
.js-choice img { width: 100%; height: 56px; max-height: 56px; object-fit: contain; }
.js-choice > small { line-height: 1.2; overflow-wrap: anywhere; }
.js-choice__lock { display: flex; align-items: center; justify-content: center; gap: 4px; font-size: var(--gs-text-xs); color: var(--gs-ink-dim); }
.js-choice__lock img { width: 14px; height: 14px; }

/* 模式卡：整张卡就是入口按钮，锁着的也照样显示并写清门槛（§5.2 / A59） */
.js-mode {
  width: 100%;
  /* `height: 100%` 会把卡撑成整个分区那么高：内容排完之后卡里剩一大片空白，
     两张卡还各空各的。改成按内容高、以分区为上限，分区自己是居中的。 */
  height: auto;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--gs-space-2);
  padding: var(--gs-space-3);
  border-radius: var(--gs-radius-lg);
  border: 2px solid var(--gs-hairline);
  background: var(--gs-surface);
  color: var(--gs-ink);
  text-align: left;
  cursor: pointer;
  font: inherit;
  overflow: hidden;
}

.js-mode[data-unlocked='true'] { border-color: var(--gs-accent); box-shadow: var(--js-select-ring); }
.js-mode[data-unlocked='false'] { color: var(--gs-ink-soft); }
.js-mode__head { display: flex; align-items: center; gap: var(--gs-space-2); }
.js-mode__head img { width: 28px; height: 28px; }
.js-mode__name { font-size: var(--gs-text-lg); }

/* ---- 里程碑轨道 ---- */
.js-milestone {
  display: flex;
  align-items: center;
  gap: var(--gs-space-3);
  padding: var(--gs-space-2) var(--gs-space-3);
  border-radius: var(--gs-radius-md);
  background: var(--gs-surface-sunken);
  border: 1px solid transparent;
}

.js-milestone[data-unlocked='true'] { border-color: rgba(255, 210, 87, 0.45); }
.js-milestone[data-next='true'] { border-color: var(--gs-accent); box-shadow: var(--js-select-ring); }
/* 「150 m」在 58px 上会折成两行、把整行顶高，隔壁的图标和文字跟着错位。
   门槛是一个数字，数字不折行。 */
.js-milestone__at { flex: 0 0 auto; min-width: 58px; white-space: nowrap; font-size: var(--gs-text-lg); color: var(--gs-gold); font-variant-numeric: tabular-nums; }
.js-milestone[data-unlocked='false'] .js-milestone__at { color: var(--gs-ink-dim); }
.js-milestone__icon { width: 40px; height: 40px; flex: 0 0 auto; object-fit: contain; }

/* ---- 帮助卡 ---- */
.js-help-card {
  display: flex;
  gap: var(--gs-space-3);
  padding: var(--gs-space-3);
  border-radius: var(--gs-radius-md);
  background: var(--gs-surface);
  border: 1px solid var(--gs-hairline);
}

.js-help-card img { width: 72px; height: 72px; flex: 0 0 auto; object-fit: contain; }

/* ---- 结算 ---- */
.js-hero-wrap { display: flex; flex-direction: column; align-items: center; gap: var(--gs-space-1); }
.js-replay { position: relative; width: 100%; height: 100%; }
.js-replay canvas { width: 100%; height: 100%; display: block; }

.js-share-card { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.js-share-card canvas {
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--gs-radius-md);
  box-shadow: var(--gs-shadow-card);
}

/* ---- 各屏的零件 ---- */
.js-brand-mark { width: clamp(48px, 16vw, 96px); height: auto; flex: 0 0 auto; }
/* 品牌行：logo 与「标题 + Best」是一组，整组居中；Best 跟着标题居中，
   不再左贴在标题下面看着像掉下来的一行。 */
.js-brand { justify-content: center; }
.js-brand .js-col { width: auto; align-items: center; }
.js-brand .js-row { width: auto; justify-content: center; }
.js-loading-percent { font-size: var(--gs-text-xl); color: var(--gs-accent); font-variant-numeric: tabular-nums; }
.js-tip-icon { width: 48px; height: 48px; flex: 0 0 auto; object-fit: contain; }
.js-title-best { font-size: var(--gs-text-lg); color: var(--gs-accent); font-variant-numeric: tabular-nums; }
.js-preview { width: 100%; height: 100%; display: block; }
.js-rail-thumb { width: 100%; flex: 1 1 auto; min-height: 0; }

/* 引导横条里的对比图示：高块 100 分 vs 扁块 46 分 */
.js-compare { width: 92px; height: 44px; flex: 0 0 auto; }
.js-compare-note { font-size: var(--gs-text-xs); color: var(--gs-ink-soft); white-space: nowrap; }

/* 结算 */
.js-title--result { font-size: var(--gs-text-lg); }
.js-reason { font-size: var(--gs-text-xs); line-height: 1.3; }
.js-freeze { width: 100%; height: 100%; display: block; }
.js-joint-label {
  position: absolute;
  left: var(--gs-space-2);
  top: var(--gs-space-2);
  padding: 2px 8px;
  border-radius: var(--gs-radius-pill);
  background: rgba(255, 174, 60, 0.18);
  border: 1px solid var(--gs-accent-2);
  color: var(--gs-accent-2);
  font-size: var(--gs-text-xs);
}
/* 失败页的 hero 分区只有 0.14 屏高，56px 的巨号数字放不下——
   这里只调变量，不碰组件选择器。 */
.js-zone--tight { --gs-text-hero: 36px; }

/* 结算页的 hero / stats / unlock 三个分区：**分区高度是 S2 冻结的比例，内容却按
   八种语言排版**，`.gs-stat__label` 本身就声明了标签可以换两行。剧本 01 在 375×812
   上量到 stats 溢出 9px、unlock 溢出 13px——英文单行就已经装不下，西语、葡语再换一行
   只会更糟。分区不许动，所以把内容这边的留白与间距收紧，留出一整行的余量。

   只调变量与自己的 `.js-` 选择器，不碰任何 `.gs-*` 组件选择器：
   分区自己的 padding/gap 直接改，组件内部（.gs-stats 的内边距、.gs-track 的行距）
   靠把 `--gs-space-*` 收小来影响，作用域仅限这三个分区。 */
.js-zone--dense {
  padding: var(--gs-space-2);
  gap: var(--gs-space-2);
  --gs-space-3: 8px;
  --gs-space-2: 6px;
  --gs-space-1: 3px;
}

/* 错误屏插画：空钩爪 + 孤零零坐在云台上的果冻 */
.js-error-art { position: relative; width: 100%; height: 100%; }
.js-error-art img { position: absolute; object-fit: contain; }
.js-error-claw { right: 16%; top: 0; width: 22%; }
.js-error-pad { left: 26%; bottom: 4%; width: 48%; }
.js-error-jelly { left: 36%; bottom: 22%; width: 28%; }
.js-error-face { left: 42%; bottom: 32%; width: 16%; }

/* 模式卡里的假按钮：整张卡才是真按钮，这一枚只是 CTA 的样子 */
.js-mode__cta {
  min-height: 32px;
  /* 两张卡的说明行数不一样，CTA 还是要落在同一条线上 */
  margin-top: auto;
  align-self: stretch;
  display: grid;
  place-items: center;
  padding: var(--gs-space-1) var(--gs-space-3);
  font-size: var(--gs-text-sm);
  pointer-events: none;
}
.js-mode__lock { width: 18px; height: 18px; }
.js-mode .js-body { font-size: var(--gs-text-sm); }
.js-milestone__lock { width: 16px; height: 16px; flex: 0 0 auto; }
.js-choice__blank { display: block; height: 40px; }

/* ---- 触控间距兜底：相邻可点元素 ≥8px（平台基线）---- */
.js-row > .gs-btn + .gs-btn,
.js-col > .gs-btn + .gs-btn { margin-left: 0; }
.js-row { gap: max(var(--gs-tap-gap), var(--gs-space-2)); }

/* ---- 手机横屏：分区变成"宽而矮"，里面的东西要跟着改排布 ----
 *
 * 横屏的分区表（S2 冻结）把结算屏的 `actions` 定成 0.96 屏宽 × 0.16 屏高：
 * 932×430 上就是 833×62。而分区容器默认竖排，三件东西（主按钮 44 + 一行 46 + 次按钮 44）
 * 叠起来是 122px——比分区高 60px，底下那颗「返回标题」直接被挤出屏幕。
 * 走查在 landscape-932x430 上一次报了 3 条 zone-overflow + 1 条 offscreen-control，
 * 根都是这一件事：**宽而矮的分区里必须横排**。
 *
 * 只在矮视口生效，桌面走的是锁出来的竖屏画布，不会碰到这一档。 */
@media (orientation: landscape) and (max-height: 560px) {
  /* 动作区横排：主按钮与次按钮并排，各占一份宽度 */
  .js-zone[data-zone="actions"] {
    flex-direction: row;
    align-items: center;
    gap: var(--gs-space-3);
  }
  .js-zone[data-zone="actions"] > * { flex: 1 1 0; min-width: 0; }
  /* 横屏整体把间距收紧一档。
     改的是**组件库的间距变量**，不是组件库的选择器——皮肤层只允许覆盖 `--gs-*`
     （`shared/game-shell/ui/README.md`），覆盖 `.gs-*` 会把组件的触控下限和安全区
     处理一起改坏。收一档之后，结算右列（大数字 + 三格数据 + 一行小字）和各浮层
     底栏在 430px 高上都塞得回自己的分区，字号一个都没动。 */
  :root {
    --gs-space-3: 8px;
    --gs-space-4: 12px;
    --gs-space-5: 16px;
  }
  .js-zone[data-zone="hero"],
  .js-zone[data-zone="stats"] { gap: var(--gs-space-1); }
  /* 结算大数字那一块也改横排：竖着叠是 大数字+标签+徽章+两行原因+纪录行 ≈ 137px，
     而横屏这个分区只有 0.28 屏高 = 120px，玩家看到的是**被削掉一截的分数**
     （走查在 landscape-932x430 上量到 +17px）。
     左边一栏放数字与徽章，右边放说明与纪录行，一条信息都不删。 */
  .js-zone[data-zone="hero"] {
    flex-direction: row;
    align-items: center;
    gap: var(--gs-space-3);
    padding-block: 0;
  }
  .js-zone[data-zone="hero"] > .js-hero-wrap { flex: 0 0 auto; }
  .js-zone[data-zone="hero"] > * { min-width: 0; }
  /* 数据条与它下面那行小字改成并排：横屏的分区是**宽而矮**的，
     竖着叠是 87+16+间距 = 107px，塞不进 94px 的分区；并排之后高度就是 87。
     这正是平台基线说的"竖排改横排"，信息一条没少、字号一个没动。 */
  .js-zone[data-zone="stats"] {
    flex-direction: row;
    align-items: center;
    gap: var(--gs-space-2);
    /* 分区自带的上下内边距在 430px 高上就是最后那 2px 的来源 */
    padding-block: 0;
  }
  /* 数据条吃掉剩下的宽度，小字固定一栏。
     不钉这一栏，两边会互相抢宽度：小字换行→数据条变窄→三格标签也跟着换行，
     高度反而从 87 涨到 104，比不改还糟（量过）。 */
  .js-zone[data-zone="stats"] > .gs-stats { flex: 1 1 auto; min-width: 0; }
  .js-zone[data-zone="stats"] > .js-note { flex: 0 0 96px; }
  .js-hero-wrap { gap: 0; }
  /* 浮层底栏（帮助 / 里程碑 / 口味）差 3–9px，同样收间距，不改字号 */
  .js-zone[data-zone="foot"] { gap: var(--gs-space-1); padding-block: 0; }
  /* 动作区中间那一格放着两颗次按钮，宽度给少了标签会折成两行、把分区顶破 */
  .js-zone[data-zone="actions"] > .js-row { flex: 2 1 0; gap: var(--gs-space-2); }
  /* 设置行在竖屏是「控件掉下一行」，横屏不能这么做：横屏缺的就是高，
     多掉一行就多一行被分区切掉。横屏改回同一行——不会再叠字，因为
     `.gs-segmented .gs-btn` 的下限已经钉在自己的标签上了（game-shell/ui/ui.css）。 */
  .js-setting--stack { flex-wrap: nowrap; }
  .js-setting--stack > :last-child { flex: 0 1 auto; }
}

/* 手机横屏且屏够宽：放大安全区、字号升一档。
   **桌面不走这里**——`max-height: 560px` 是这一档的全部意义所在：
   桌面现在跑的是共用库锁出来的 540px 宽手机画布（`game-shell/ui/ui.css`
   末尾「手机画布锁定」），再按视口宽度放大字号，就是拿桌面的尺子去量
   一块手机大小的画布，分区当场撑破。 */
@media (min-width: 900px) and (max-height: 560px) {
  :root {
    --gs-text-sm: 15px;
    --gs-text-md: 17px;
    --gs-text-lg: 22px;
    --gs-text-xl: 30px;
  }

  #app {
    padding: max(var(--gs-safe-top), 20px) max(var(--gs-safe-right), 32px)
             max(var(--gs-safe-bottom), 20px) max(var(--gs-safe-left), 32px);
  }
}
