/* ============================ hidden 兜底 ============================ */
/*
 * 浏览器默认样式表的 `[hidden] { display: none }` 优先级极低，
 * 任何一条 `.gs-xxx { display: flex }` 都能把它压掉。
 * 后果：`node.hidden = true` 在 JS 里生效了、在屏幕上什么都没发生。
 * cloud-kitchen 的教学 skip 按钮就是这么"点了没反应"的——
 * 状态机走了、气泡还杵在原地，玩家以为游戏坏了。
 *
 * 组件一律用 hidden 开关显隐，所以这条兜底必须在最前面，且必须 !important：
 * 组件自己的 display 是写在类上的，不加 !important 压不住。
 */
[hidden] {
  display: none !important;
}

/*
 * 游戏 UI 组件样式 —— **只有手机端一套版式**。
 *
 * 组件按手机竖屏设计，手机横屏靠文件末尾那一档 @media 换成左右分栏。
 * 桌面不做自适应：视口比手机宽时，末尾的「手机画布锁定」把整个 #app
 * 收成一块手机画布居中摆着，桌面玩到的就是手机上验收过的那一屏。
 * 所以这里不许再出现按视口宽度放大的规则——那就是第二套设计了。
 */

/* ============================ 满屏插画背景 ============================ */

.gs-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* 背景永远不吃点击，所有交互都在它上面的层 */
  pointer-events: none;
  z-index: 0;
}

.gs-bg__layer {
  position: absolute;
  inset: -4% -4% -4% -4%; /* 留出视差位移的余量，边缘不会露白 */
  width: 108%;
  height: 108%;
  object-fit: cover;
  /* 竖屏上 cover 会把 16:9 的插画裁成正中一条窄带——插画中缝正好落在里面。
     取景点因此是可调的令牌，游戏按自己那张图定（默认仍是正中）。 */
  object-position: var(--gs-bg-focus, 50% 50%);
  will-change: transform;
  transition: transform var(--gs-slow) var(--gs-ease-out);
}

.gs-bg__scrim {
  position: absolute;
  inset: 0;
  /* 压暗，保证前景文字在任何插画上都读得清 */
  background: linear-gradient(
    180deg,
    rgba(10, 6, 24, 0.55) 0%,
    rgba(10, 6, 24, 0.15) 35%,
    rgba(10, 6, 24, 0.35) 70%,
    rgba(10, 6, 24, 0.78) 100%
  );
}

/* ============================ 屏幕骨架 ============================ */

.gs-screen {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  min-height: 100%;
  padding:
    calc(var(--gs-safe-top) + var(--gs-space-4))
    calc(var(--gs-safe-right) + var(--gs-space-4))
    calc(var(--gs-safe-bottom) + var(--gs-space-4))
    calc(var(--gs-safe-left) + var(--gs-space-4));
  gap: var(--gs-space-4);
  color: var(--gs-ink);
}

/* 内容区可滚，底部动作区固定——手机上主按钮永远够得着 */
.gs-screen__body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: var(--gs-space-4);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.gs-screen__foot {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--gs-tap-gap);
}

/* ============================ 发光卡片 ============================ */

.gs-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--gs-space-3);
  padding: var(--gs-space-5) var(--gs-space-4);
  border-radius: var(--gs-radius-lg);
  background: var(--gs-surface);
  border: 1px solid var(--gs-hairline);
  box-shadow: var(--gs-shadow-card);
  backdrop-filter: blur(12px);
}

.gs-card--glow {
  box-shadow: var(--gs-shadow-card), var(--gs-glow-accent);
}

.gs-card__title {
  margin: 0;
  font-size: var(--gs-text-xl);
  line-height: 1.15;
  text-align: center;
}

.gs-card__subtitle {
  margin: 0;
  font-size: var(--gs-text-sm);
  color: var(--gs-ink-soft);
  text-align: center;
}

/* ============================ 大数字 ============================ */

.gs-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gs-space-1);
}

.gs-hero__label {
  font-size: var(--gs-text-sm);
  color: var(--gs-ink-soft);
}

.gs-hero__value {
  font-size: var(--gs-text-hero);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--gs-gold);
  text-shadow: var(--gs-glow-gold);
  font-variant-numeric: tabular-nums;
}

.gs-hero__value[data-rolling='true'] {
  transition: transform var(--gs-fast) var(--gs-ease-out);
  transform: scale(1.06);
}

/* ============================ 图标 ============================
 * 图标是 `<img>` 载入的 SVG，里面写的是 currentColor——**在 img 里那只会是黑**。
 * 深色界面上因此整排图标都是黑块。凡是图标 `<img>` 都带 `.gs-icon`，
 * 由这一条统一上色；要金色/主色/警示色的地方覆盖 `--gs-icon-tint` 就行。
 * 不给 img 加通用选择器（`.gs-stat img` 之类）是故意的：
 * 果冻、封面这些**真图片**也是 img，滤镜落到它们身上就毁了。 */
.gs-icon {
  filter: var(--gs-icon-tint, var(--gs-icon-tint-ink));
}

/* ============================ 徽章 ============================ */

.gs-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--gs-space-1);
  padding: var(--gs-space-1) var(--gs-space-3);
  border-radius: var(--gs-radius-pill);
  font-size: var(--gs-text-xs);
  font-weight: 700;
  white-space: nowrap;
  background: var(--gs-gold);
  color: var(--gs-ink-on-light);
  box-shadow: var(--gs-glow-gold);
}

.gs-badge--quiet {
  background: var(--gs-surface-raised);
  color: var(--gs-ink);
  box-shadow: none;
  border: 1px solid var(--gs-hairline);
}

.gs-badge img {
  width: 14px;
  height: 14px;
}

/* ============================ 图标数值行 ============================ */

.gs-stats {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--gs-space-2);
  padding: var(--gs-space-3) var(--gs-space-2);
  border-radius: var(--gs-radius-md);
  background: var(--gs-surface-sunken);
  border: 1px solid var(--gs-hairline);
}

.gs-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gs-space-1);
  min-width: 0;
}

.gs-stat + .gs-stat {
  border-left: 1px solid var(--gs-hairline);
}

.gs-stat__icon {
  width: 26px;
  height: 26px;
  object-fit: contain;
}

.gs-stat__value {
  font-size: var(--gs-text-lg);
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.gs-stat__label {
  font-size: var(--gs-text-xs);
  color: var(--gs-ink-soft);
  text-align: center;
  /* 八种语言长度差很大，标签允许换两行而不是被截断 */
  overflow-wrap: anywhere;
}

/* ============================ 解锁轨道 ============================ */

.gs-track {
  display: flex;
  flex-direction: column;
  gap: var(--gs-space-2);
  /* 不许撑过容器。轨道**自带**横向滚动（下面的 `.gs-track__row`），但那只有在
     轨道本身被限宽时才会生效：作为 flex 子项它默认按内容宽度撑开，
     于是八个解锁节点排成 520px，把 375px 的分区顶破，横滚一次都用不上
     （cloud-kitchen 的班次地图实测：竖屏 +73px、横屏 +115px）。
     `min-width: 0` 是让 flex 子项**允许**被压到内容宽度以下的那一句，缺了它 max-width 不起作用。 */
  max-width: 100%;
  min-width: 0;
}

.gs-track__row {
  display: flex;
  gap: var(--gs-space-2);
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: var(--gs-space-1);
  /* 真的滚起来时，右边界要**看得出还有**。没有这道渐隐的时候，
     第六个节点被分区边缘齐齐切一刀，看起来是「排版把它切掉了」而不是「往右还有」。
     渐隐只吃最右 24px，一屏放得下时那 24px 里本来就没东西，等于没生效。 */
  mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent 100%);
}

.gs-track__row::-webkit-scrollbar {
  display: none;
}

.gs-track__slot {
  position: relative;
  /* 允许压缩：六个 56px 的节点加间距是 376px，手机竖屏的分区只有 358px——
     差 18px，于是最后一个总被切掉一半。让它们一起缩到 38px 下限，
     六个就都排得下；下限之下才交给上面那条横滚。 */
  flex: 0 1 56px;
  min-width: 38px;
  width: 56px;
  aspect-ratio: 1 / 1;
  height: auto;
  display: grid;
  place-items: center;
  border-radius: var(--gs-radius-md);
  background: var(--gs-surface-sunken);
  border: 1px solid var(--gs-hairline);
}

.gs-track__slot img {
  width: 64%;
  height: 64%;
  object-fit: contain;
}

/* 锁着的节点仍然要**看得见**：先按 .gs-icon 的链上色，再压暗，
   不要直接换掉 filter——那会把上色也一起换掉，图标又变回黑的。 */
.gs-track__slot--locked img {
  filter: var(--gs-icon-tint, var(--gs-icon-tint-ink)) brightness(0.66);
}

.gs-track__slot--unlocked {
  box-shadow: var(--gs-glow-accent);
}

/* 门槛数字压在节点自己的底边**里面**。原来是 -2px，一半悬在格子外，
   下面一行内容离得近的时候两边就叠在一起。 */
.gs-track__at {
  position: absolute;
  bottom: 3px;
  font-size: var(--gs-text-xs);
  font-weight: 700;
  color: var(--gs-ink-soft);
}

/* 进度条：百分比写在条里面，不再单独占一行 */
.gs-meter {
  position: relative;
  height: 22px;
  border-radius: var(--gs-radius-pill);
  background: var(--gs-surface-sunken);
  border: 1px solid var(--gs-hairline);
  overflow: hidden;
}

.gs-meter__fill {
  /* span 默认是 inline，inline 元素不吃 width/height——
     少了这一行进度条就永远是空的，而且没有任何报错。 */
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--gs-cta);
  transition: width var(--gs-slow) var(--gs-ease-out);
}

.gs-meter__text {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  /* 条高是固定的 22px：文字一旦折行就会被截掉半行，而且不报任何错。
     宁可横向挤一点，也不能让"12.4 / 32.0 m"折成两行。 */
  white-space: nowrap;
  font-size: var(--gs-text-xs);
  font-weight: 800;
  /* 文字横跨已填充和未填充两段，任何单一颜色都会有一半读不清。
     所以用白字 + 深描边，两段底色上都成立。 */
  color: #fff;
  text-shadow: 0 1px 2px rgba(6, 3, 18, 0.9), 0 0 6px rgba(6, 3, 18, 0.7);
}

/* ============================ 点状进度 ============================ */

.gs-dots {
  display: flex;
  align-items: center;
  gap: var(--gs-space-2);
  min-width: 0;
}

.gs-dots__row {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  flex: 1 1 auto;
  min-width: 0;
}

.gs-dots__dot {
  display: block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--gs-surface-sunken);
  box-shadow: inset 0 0 0 1px var(--gs-hairline);
  transition: background var(--gs-fast) var(--gs-ease-out);
}

.gs-dots__dot[data-filled='true'] {
  background: var(--gs-accent);
  box-shadow: 0 0 6px var(--gs-accent);
}

.gs-dots__text {
  flex: 0 0 auto;
  font-size: var(--gs-text-sm);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

/* ============================ 数值胶囊 ============================ */

.gs-chip {
  display: flex;
  align-items: center;
  gap: var(--gs-space-2);
  padding: var(--gs-space-2) var(--gs-space-3);
  border-radius: var(--gs-radius-pill);
  background: var(--gs-surface-raised);
  border: 1px solid var(--gs-hairline);
  box-shadow: var(--gs-shadow-press);
  min-width: 0;
}

.gs-chip__icon {
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
}

.gs-chip__body {
  display: flex;
  flex-direction: column;
  line-height: 1.1;
  min-width: 0;
}

.gs-chip__label {
  font-size: var(--gs-text-xs);
  color: var(--gs-ink-soft);
  white-space: nowrap;
}

.gs-chip__value {
  font-size: var(--gs-text-lg);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.gs-chip[data-tone='gold'] .gs-chip__value {
  color: var(--gs-gold);
  text-shadow: var(--gs-glow-gold);
}

.gs-chip[data-tone='accent'] .gs-chip__value {
  color: var(--gs-accent);
}

.gs-chip[data-bump='true'] {
  animation: gs-chip-bump var(--gs-normal) var(--gs-ease-out);
}

@keyframes gs-chip-bump {
  50% {
    transform: scale(1.12);
  }
}

/* 减少动态时数值照样更新，只是不跳——状态变化本身不能省 */
:root[data-reduced-motion='on'] .gs-chip[data-bump='true'] {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .gs-chip[data-bump='true'] {
    animation: none;
  }
}

/* ============================ 按钮 ============================ */

.gs-btn {
  /* 触控下限写死在组件里，不靠调用方记得加 */
  min-height: var(--gs-tap-min);
  min-width: var(--gs-tap-min);
  padding: var(--gs-space-3) var(--gs-space-5);
  border: 0;
  border-radius: var(--gs-radius-pill);
  font-size: var(--gs-text-md);
  font-weight: 800;
  font-family: inherit;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--gs-fast) var(--gs-ease-out), box-shadow var(--gs-fast) var(--gs-ease-out);
}

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

.gs-btn--primary {
  background: var(--gs-cta);
  color: var(--gs-ink-on-light);
  box-shadow: var(--gs-shadow-button);
}

.gs-btn--secondary {
  background: var(--gs-surface-raised);
  color: var(--gs-ink);
  border: 1px solid var(--gs-hairline);
  box-shadow: var(--gs-shadow-press);
}

.gs-btn--quiet {
  background: transparent;
  color: var(--gs-ink-soft);
  box-shadow: none;
}

.gs-btn--wide {
  width: 100%;
}

/* 按下位移：有厚度的按钮才有游戏感 */
.gs-btn:active:not(:disabled) {
  transform: translateY(4px);
  box-shadow: var(--gs-shadow-press);
}

.gs-btn:disabled {
  opacity: 0.45;
  cursor: default;
}

/* 分段选择。
   之前这个组件只有 JS 没有样式，于是三段按钮各自按 .gs-btn 的 24px 内边距撑开，
   在 375 宽的手机上直接顶出视口（走查报 offscreen-control）。
   分段的本意就是"一行里几个等宽的选项"，所以等分 + 允许收缩 + 窄屏换行。 */
.gs-segmented {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gs-tap-gap);
  min-width: 0;
}

/* `min-width: 0` 会让每一段缩到**比自己的标签还窄**：段还在原地，
   标签整块画到隔壁段上面去（jelly-stack 设置屏实测：「跟随系统」压住「开」）。
   等宽仍然是对的（`flex-basis: 0` 保留），错的是**等宽没有下限**——
   下限就是这一段的标签本身，所以只把 `min-width` 换成 `fit-content`。
   注意别顺手把 basis 也改成 `auto`：那样每段按自己的文字长度排，
   三段长短不一、总宽正好顶到屏幕边（cloud-kitchen 的 Relaxed/Standard/Busy 实测）。
   真的一行放不下时，上面那句 `flex-wrap: wrap` 接手换行——分段的降级是换行，不是叠字。 */
.gs-segmented .gs-btn {
  flex: 1 1 0;
  min-width: fit-content;
  padding-inline: var(--gs-space-3);
  font-size: var(--gs-text-sm);
  white-space: nowrap;
}

/* 选中的那一段必须**看得出来**。
   之前 `aria-pressed` 只有语义没有样式，三段渲染成一模一样的按钮：
   玩家点完哪一段都没有任何可见变化（走查报 control-inert，报得对）。
   不只靠颜色——同时给边框和字重，辨色障碍玩家也读得到（对齐 RingMeter 的双通道做法）。 */
.gs-segmented .gs-btn[aria-pressed='true'] {
  background: var(--gs-accent);
  color: var(--gs-ink-on-light);
  box-shadow: inset 0 0 0 2px var(--gs-ink-on-light), var(--gs-glow-accent);
  font-weight: 900;
}

/* 圆形图标按钮 */
.gs-icon-btn {
  min-width: var(--gs-tap-min);
  min-height: var(--gs-tap-min);
  width: var(--gs-tap-min);
  height: var(--gs-tap-min);
  display: grid;
  place-items: center;
  border: 1px solid var(--gs-hairline);
  border-radius: 50%;
  background: var(--gs-surface-raised);
  cursor: pointer;
  touch-action: manipulation;
  padding: 0;
}

.gs-icon-btn img {
  width: 22px;
  height: 22px;
}

/* ============================ 底部圆形导航 ============================ */

.gs-nav {
  display: flex;
  /* 撑满分区：作为居中分区的 flex 子项，它默认缩成内容宽度，
     下面每一项的 `flex: 1 1 0` 就没有可分的空间，圆盘之间的间距全靠标签宽度定
     ——四项各 62px，「How to play」于是折成两行。 */
  width: 100%;
  box-sizing: border-box;
  justify-content: center;
  /* 顶端对齐：某一项的标签折成两行时，只有它自己变高，
     其余四项的圆盘不会跟着往下掉半行（对齐是一排圆盘，不是一排文字）。 */
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--gs-space-2) var(--gs-space-4);
  padding-bottom: var(--gs-safe-bottom);
}

.gs-nav__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gs-space-1);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--gs-ink-soft);
  font-family: inherit;
  font-size: var(--gs-text-xs);
  touch-action: manipulation;
  /* 整个「图标 + 标签」是一个可点区域，不是只有图标能点 */
  min-width: var(--gs-tap-min);
  /* 每一项等宽：标签长短不一时（Levels / How to play），
     圆盘之间的间距才不会跟着标签宽度忽大忽小——对齐的是一排圆盘。
     **等宽不是定宽**：写死 76px 时，五项的导航（jelly-stack 首屏）
     总宽 444px > 手机分区的 358px，最后一项直接折到第二行去。
     `flex: 1 1 0` 让它们平分能用的宽度，`max-width` 只是上限。 */
  flex: 1 1 0;
  max-width: 76px;
  /* 标签是这一项自己的宽度上限：长标签折行，不横着压到隔壁那一项上。
     上限比圆盘宽一点，让「How to play」这类两词标签折成两行而不是四行。 */
  text-align: center;
  line-height: 1.2;
  overflow-wrap: anywhere;
}

.gs-nav__disc {
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--gs-surface-raised);
  border: 1px solid var(--gs-hairline);
  box-shadow: var(--gs-shadow-press);
  transition: transform var(--gs-fast) var(--gs-ease-out);
}

.gs-nav__item:active .gs-nav__disc {
  transform: translateY(3px);
}

.gs-nav__disc img {
  width: 30px;
  height: 30px;
}

.gs-nav__item--locked .gs-nav__disc {
  filter: grayscale(1) brightness(0.65);
}

/* ============================ 内嵌横条（非阻塞）============================ */

/*
 * 这个组件的存在就是为了防止「设计里是一条横条，实现做成了全屏模态」。
 * 它没有遮罩、不圈焦点、不阻断下面的内容——从结构上就做不成模态。
 */
.gs-inline-bar {
  display: flex;
  align-items: center;
  gap: var(--gs-space-3);
  padding: var(--gs-space-2) var(--gs-space-3);
  border-radius: var(--gs-radius-pill);
  background: var(--gs-surface-raised);
  border: 1px solid var(--gs-hairline);
  box-shadow: var(--gs-shadow-press);
}

.gs-inline-bar__icon {
  width: 32px;
  height: 32px;
  flex: 0 0 auto;
}

.gs-inline-bar__text {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--gs-text-sm);
  line-height: 1.25;
}

.gs-inline-bar__actions {
  display: flex;
  gap: var(--gs-tap-gap);
  flex: 0 0 auto;
}

.gs-inline-bar .gs-btn {
  padding: var(--gs-space-2) var(--gs-space-4);
  font-size: var(--gs-text-sm);
}

/* 窄屏放不下就换行，但按钮绝不缩到 44px 以下 */
@media (max-width: 380px) {
  .gs-inline-bar {
    flex-wrap: wrap;
    border-radius: var(--gs-radius-md);
  }

  .gs-inline-bar__actions {
    width: 100%;
    justify-content: flex-end;
  }
}

/* ============================ 教学气泡 ============================ */

/*
 * 气泡永远贴着它要指的那个东西，并且自动躲开它——
 * 上一版把教学做成屏幕底部一条固定提示，正好压住玩家要拖的那一行。
 */
.gs-coach {
  position: absolute;
  z-index: 30;
  max-width: min(76vw, 320px);
  display: flex;
  flex-direction: column;
  gap: var(--gs-space-2);
  padding: var(--gs-space-3) var(--gs-space-4);
  border-radius: var(--gs-radius-md);
  background: var(--gs-surface-raised);
  border: 1px solid var(--gs-hairline);
  box-shadow: var(--gs-shadow-card);
  font-size: var(--gs-text-sm);
  line-height: 1.35;
  transition: opacity var(--gs-normal) var(--gs-ease-out);
}

.gs-coach__arrow {
  position: absolute;
  width: 14px;
  height: 14px;
  background: inherit;
  border-left: 1px solid var(--gs-hairline);
  border-top: 1px solid var(--gs-hairline);
  transform: rotate(45deg);
}

.gs-coach[data-side='below'] .gs-coach__arrow {
  top: -8px;
}

.gs-coach[data-side='above'] .gs-coach__arrow {
  bottom: -8px;
  transform: rotate(225deg);
}

.gs-coach__steps {
  display: flex;
  gap: var(--gs-space-1);
  flex: 0 0 auto;
}

/* 跳过 + 下一步。窄屏下"Skip the walkthrough"这种长文案会把气泡撑爆，
   所以允许换行收缩，但两个按钮都不低于 44px 命中高度。 */
.gs-coach__actions {
  display: flex;
  align-items: center;
  gap: var(--gs-space-2);
  flex: 1 1 auto;
  justify-content: flex-end;
  min-width: 0;
}

.gs-coach__actions .gs-btn {
  min-height: 44px;
  padding-block: var(--gs-space-1);
}

/* 跳过的文案在八种语言里长短差很多（英文 "Skip the walkthrough" 就已经
   放不进 375 宽），所以允许它换行，绝不裁字——裁掉的那半句玩家读不到。
   窄到放不下两个按钮时整个底部改成上下排。 */
.gs-coach__skip {
  flex: 0 1 auto;
  min-width: 0;
  white-space: normal;
  text-align: left;
  line-height: 1.25;
}

.gs-coach__next {
  flex: 0 0 auto;
  white-space: nowrap;
}

@media (max-width: 390px) {
  .gs-coach__foot {
    flex-direction: column;
    align-items: stretch;
    gap: var(--gs-space-2);
  }
  .gs-coach__actions {
    justify-content: space-between;
  }
}

.gs-coach__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--gs-ink-dim);
}

.gs-coach__dot[data-active='true'] {
  background: var(--gs-accent);
}

.gs-coach__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gs-space-3);
}

.gs-coach__dot[data-done='true'] {
  background: var(--gs-ink-soft);
}

/* ---- 教学横条（CoachStrip）----
   钉死在调用方给的分区里，所以它不可能盖住棋盘；
   浮动气泡靠算法躲，这一条靠位置本身。 */
.gs-strip {
  display: flex;
  align-items: center;
  gap: var(--gs-space-3);
  width: 100%;
  height: 100%;
  padding: var(--gs-space-2) var(--gs-space-3);
  border-radius: var(--gs-radius-lg);
  background: var(--gs-surface-raised);
  border: 1px solid var(--gs-hairline);
  box-shadow: var(--gs-shadow-card);
  box-sizing: border-box;
  overflow: hidden;
}

.gs-strip__lead {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gs-space-1);
  flex: 0 0 auto;
}

.gs-strip__counter {
  font-size: var(--gs-text-lg);
  color: var(--gs-gold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* 图示位：放图形，不放说明文字。一次只引入一个新规则，图看得懂就不需要读。 */
.gs-strip__figure {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gs-space-2);
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
}

.gs-strip__figure img,
.gs-strip__figure canvas {
  height: min(100%, 46px);
  width: auto;
}

.gs-strip .gs-coach__skip {
  flex: 0 0 auto;
  min-height: var(--gs-tap-min);
  min-width: var(--gs-tap-min);
  white-space: nowrap;
}

/* 教学横条里的文字位：只有 withCopy 时才存在 */
.gs-strip__copy {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  justify-content: center;
}

.gs-strip__title {
  font-size: var(--gs-text-md);
  color: var(--gs-ink);
  line-height: 1.15;
}

.gs-strip__body {
  margin: 0;
  font-size: var(--gs-text-sm);
  color: var(--gs-ink-soft);
  line-height: 1.25;
}

/* ---- 环形余量表（RingMeter）：颜色 + 粗细双通道 ----
   只靠颜色的话，辨色障碍玩家看不出自己快输了。 */
.gs-ring {
  position: relative;
  flex: 0 0 auto;
  display: block;
}

.gs-ring__svg {
  display: block;
  width: 100%;
  height: 100%;
}

.gs-ring__track {
  stroke: var(--gs-surface-sunken);
}

.gs-ring__fill {
  stroke-linecap: round;
  transition: stroke-dasharray var(--gs-normal) var(--gs-ease-out),
              stroke-width var(--gs-normal) var(--gs-ease-out);
}

/* 环里的文字只有**内接正方形**那么大的地方（边长 = 直径 / √2，
   也就是左右各让出 14.65%）。按整个方框排的话，"STABLE MARGIN" 这种两词标签
   会横着画出圆外、压在环的描边和隔壁读数上（走查在游玩 HUD 上量到）。
   让出这一圈之后标签自己换行——环形表的降级是折行，不是溢出。 */
.gs-ring__body {
  position: absolute;
  inset: 0;
  padding-inline: 14.65%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  pointer-events: none;
}

.gs-ring__value {
  font-size: min(var(--gs-text-md), calc(var(--gs-ring-size, 72px) * 0.26));
  color: var(--gs-ink);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.gs-ring__label {
  text-transform: uppercase;
  color: var(--gs-ink-dim);
  line-height: 1.1;
  /* 八种语言里这个标签长短差一倍，宁可折两行也不许画到圆外面去。
     字号跟着环的直径走（`--gs-ring-size` 由组件写进行内样式）：56 的环上
     写死 9px 就会连词都拆开，读出来是 "MARGI / N"。 */
  font-size: min(9px, calc(var(--gs-ring-size, 72px) * 0.125));
  letter-spacing: 0.02em;
  text-align: center;
  /* `anywhere` 会把词从中间劈开，先按词换行，实在放不下才劈 */
  overflow-wrap: break-word;
  max-width: 100%;
}

.gs-ring[data-level='warn'] .gs-ring__value { color: var(--gs-accent-2); }
.gs-ring[data-level='critical'] .gs-ring__value { color: var(--gs-danger); }

/* 刻度环：一格一个单位。格数由调用方从规则常量传，组件不猜。 */
.gs-ring__seg {
  stroke: var(--gs-surface-sunken);
  stroke-linecap: butt;
  transition: stroke var(--gs-normal) var(--gs-ease-out),
              stroke-width var(--gs-normal) var(--gs-ease-out);
}

.gs-ring__seg[data-on='true'] {
  stroke: var(--gs-accent);
}

.gs-ring__seg[data-on='true'][data-warn='true'] {
  stroke: var(--gs-danger);
}

/* 高亮圈：把玩家要操作的那块从压暗层里挖出来 */
.gs-spotlight {
  position: absolute;
  z-index: 29;
  border-radius: var(--gs-radius-md);
  box-shadow: 0 0 0 9999px rgba(6, 3, 18, 0.62);
  pointer-events: none;
  transition: all var(--gs-normal) var(--gs-ease-out);
}

/* ============================ 屏幕可读性辅助 ============================ */

.gs-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ============================================================
 * 手机画布锁定
 *
 * 这里原来是「宽屏自适应」：桌面另有一套放大版版式。那套东西每款游戏都要
 * 重调一次，而且**没有人在桌面上验收**——分区表是按手机比例写死的，
 * 一套到桌面 16:10 的视口上就互相压住，报上来就是「电脑端显示很乱」。
 *
 * 现在的规矩：**游戏只有手机端一套版式。** 视口比手机宽的时候不重排，
 * 而是把整个 `#app` 收成一块手机画布居中摆着，两侧留黑边。
 * 桌面上玩到的就是手机上验收过的那一屏，不用再单独调一遍。
 *
 * 三档行为：
 * - 手机竖屏：一点不变，满屏。`min()` 的上限比手机自己还宽，永远不生效。
 * - 矮视口（高 ≤ 560px，也就是手机横过来）：一点不变，满屏走横屏分区。
 *   横屏是**手机的**一个朝向，不是桌面，所以保留。
 * - 其余（桌面、平板、平台 iframe）：宽度封顶到 9:16，画布再封顶到 540×960
 *   ——设计画布 1080×1920 在 2× DPR 下的 CSS 尺寸，也就是「一台大手机」。
 *   不封顶的话，2560×1440 的显示器上会得到一块 810×1440 的画布：
 *   比例还是竖的，但字号是按手机定死的 px，摊在那么大的画布上就散了。
 *
 * 为什么锁 `#app`：所有游戏入口页的根容器都是 `<div id="app">`，
 * 三份 theme.css 也都把它钉成 `position: fixed; inset: 0`。锁在这里而不是
 * 各自的 theme.css，是为了只留一份真值——三份互相漂移的画布尺寸比没有更糟。
 *
 * `contain: paint` 做两件事：把画布外的东西剪掉；让 `#app` 成为
 * `position: fixed` 子元素的包含块。第二件是必须的——cloud-kitchen 的插画
 * 背景层是 fixed 的，不这样做背景会铺满整个桌面窗口，而 UI 只剩中间一条。
 *
 * **下面这几个数字的唯一真值是 `shared/phone-canvas.json`。**
 * 同一组数字还被平台舞台、三款游戏的横屏 @media、旋转剧本和打包闸门用到；
 * CSS 里没法 import，所以由 `tests/phone-canvas-contract.test.mjs` 机器核对——
 * 改了这里没改那份 JSON（或反过来），`npm test` 当场红。
 * ============================================================ */

@media (min-height: 561px) {
  #app {
    width: min(100%, calc(100dvh * 9 / 16));
    margin-inline: auto;
    contain: paint;
  }
}

/* 视口比一台手机还宽 → 画布封顶到 540×960，上下也居中 */
@media (min-width: 541px) and (min-height: 561px) {
  #app {
    width: min(100%, calc(100dvh * 9 / 16), 540px);
    height: min(100%, 960px);
    margin: auto;
  }
}

/* 手机横屏且屏够宽：字号略升一档。
   条件里的 `max-height: 560px` 是新加的——没有它，这一档会在桌面上生效，
   而桌面现在跑的是 540px 宽的手机画布，放大的字号会直接把分区撑破。 */
@media (min-width: 700px) and (max-height: 560px) {
  :root {
    --gs-text-md: 17px;
    --gs-text-lg: 22px;
    --gs-text-xl: 30px;
    /* 大数字**反而要收**，不是放大。
       这一档的触发条件是"屏够宽"，可手机横过来之后缺的是**高**：
       932×430 上 68px 的分数占掉屏高的 16%，结算屏的大数字连同它下面的说明
       一起顶破分区，玩家看到的是被削掉一截的数字（jelly-stack 实测 +17px）。
       比竖屏的 56px 再小一档，才是"横屏该有的样子"。 */
    --gs-text-hero: 44px;
  }

  .gs-screen {
    padding-top: var(--gs-space-6);
    padding-bottom: var(--gs-space-6);
  }

  .gs-track__slot {
    width: 64px;
    height: 64px;
  }
}

/* 横屏矮屏（手机横过来）：竖排的头尾改成左右分栏，别让内容被挤没 */
@media (orientation: landscape) and (max-height: 560px) {
  .gs-screen {
    flex-direction: row;
    align-items: stretch;
    max-width: none;
    gap: var(--gs-space-5);
  }

  .gs-screen__body {
    flex: 1 1 60%;
  }

  .gs-screen__foot {
    flex: 0 0 34%;
    justify-content: center;
  }

  .gs-nav {
    flex-wrap: wrap;
  }
}
