/* ==========================================================================
   style.css —— 无尽单词（Endless Words）
   视觉基线对齐文档 §3.1 / §6.2：
     · 背景是浅色纸张纹理（白底 + 细横线），不是深色
     · 字母块是圆角方块 + 彩色条纹 + 眼睛，小写
     · 槽位是浅灰色空心轮廓 #D8D8D8
     · 舞台固定 1280×720，由 core.js 做 scale 等比缩放
   ========================================================================== */

/* ==========================================================================
   1. 变量与基础
   ========================================================================== */

:root {
  --font-en: 'Baloo 2', 'Trebuchet MS', 'Verdana', system-ui, sans-serif;
  --font-ui: system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;

  --dk:        #3C3765;   /* 统一描边深色 */
  --ink:       #3C3765;
  --ink-soft:  #6B7391;
  --ink-faint: #A7AEC0;

  --paper:     #FFFDF9;   /* 纸张底 */
  --rule:      #E7EBF2;   /* 纸张上的细横线 */
  --line:      #E4E8EE;
  --card:      #FFFFFF;

  --ghost:     #D8D8D8;   /* 文档 §6.2：轮廓槽位色 */
  --sky:       #3B8FE8;
  --sky-d:     #2A6CBB;
  --orange:    #F59D2B;
  --orange-d:  #D97D0E;
  --yellow:    #FFC93C;
  --red:       #E8453C;
  --green:     #5CC15F;
  --green-d2:  #3E9E45;   /* 完成态用的深绿，浅底上比 --green 更清楚 */
}

/* 注意：这里必须是干净的 }，不能写成 };。
   CSS 解析器遇到顶层多余分号会跳过后续规则，
   一旦吞掉这条 border-box，.tile-field 的 width:1280px 就会被内边距撑成 1472。 */
* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;
  background: #F4F6FA;
}

body {
  font-family: var(--font-ui);
  color: var(--ink);
  /* 浅色纸张纹理：细横线叠在极淡的冷白渐变上 */
  background-image:
    repeating-linear-gradient(180deg,
      rgba(60, 55, 101, 0) 0 30px,
      rgba(60, 55, 101, .055) 30px 31px),
    radial-gradient(125% 95% at 50% 0%, #FFFFFF 0%, #FBFCFE 58%, #EFF3F9 100%);
  background-attachment: fixed;
  -webkit-tap-highlight-color: transparent;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ==========================================================================
   2. 舞台与顶栏
   ========================================================================== */

#stage {
  position: fixed;
  left: 50%;
  top: 50%;
  width: 1280px;
  height: 720px;
  transform-origin: center center;
  background: var(--paper);
  background-image:
    repeating-linear-gradient(180deg,
      rgba(60, 55, 101, 0) 0 30px,
      rgba(60, 55, 101, .04) 30px 31px);
  overflow: hidden;
  will-change: transform;
  /* 铺满全屏：不再需要圆角与投影（它们是"卡片浮在背景上"的视觉，
     现在舞台本身就是背景，画在边缘的圆角会被裁掉一半反而难看）。 */
}

.board {
  position: absolute;
  inset: 0;
}

/* ------------------------------ 顶栏 ------------------------------ */

.hud {
  position: absolute;
  left: 0; right: 0;
  /* --safe-top 由 core.js 按「舞台被裁掉多少」算出：
     屏幕铺满后上下被裁时，顶栏跟着往舞台里挪同样的量，于是屏幕上永远贴顶。
     横向同理，左右被裁时靠 --safe-x 把两侧留白补回来。 */
  top: var(--safe-top, 0px);
  height: 88px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 calc(30px + var(--safe-x, 0px));
  z-index: 420;
  pointer-events: none;
}
.hud > * { pointer-events: auto; }

.hud-left { display: flex; align-items: center; gap: 12px; }
.hud-right { display: flex; align-items: center; gap: 12px; }

/* 模式气泡：白底圆角胶囊 + 小尾巴（文档 §3.1 左上角） */
.tag-mode {
  position: relative;
  padding: 9px 26px 10px;
  border-radius: 999px;
  background: var(--card);
  border: 4px solid var(--dk);
  font-family: var(--font-en);
  font-size: 22px;
  font-weight: 800;
  letter-spacing: .5px;
  color: var(--dk);
  box-shadow: 0 4px 0 rgba(60, 55, 101, .16);
}
.tag-mode::after {
  content: "";
  position: absolute;
  left: 30px;
  bottom: -10px;
  width: 15px;
  height: 15px;
  background: var(--card);
  border-right: 4px solid var(--dk);
  border-bottom: 4px solid var(--dk);
  border-bottom-right-radius: 5px;
  transform: rotate(48deg);
}
.tag-mode[data-mode="sentence"] {
  background: #FFF6E2;
  border-color: var(--orange-d);
  color: var(--orange-d);
}
.tag-mode[data-mode="sentence"]::after {
  background: #FFF6E2;
  border-color: var(--orange-d);
}

/* abc 发音开关：蓝色圆角方块白字（文档 §3.1 右上角） */
.abc-btn {
  width: 66px;
  height: 66px;
  border-radius: 18px;
  border: 4px solid var(--sky-d);
  background: var(--sky);
  color: #fff;
  font-family: var(--font-en);
  font-size: 25px;
  font-weight: 800;
  letter-spacing: .5px;
  cursor: pointer;
  box-shadow: 0 5px 0 var(--sky-d);
  transition: transform .12s, box-shadow .12s, background .12s;
}
.abc-btn:active { transform: translateY(4px); box-shadow: 0 1px 0 var(--sky-d); }
.abc-btn.word-mode {
  background: var(--orange);
  border-color: var(--orange-d);
  box-shadow: 0 5px 0 var(--orange-d);
}

.icon-btn {
  width: 62px;
  height: 62px;
  border-radius: 16px;
  border: 4px solid var(--line);
  background: #fff;
  color: var(--ink-soft);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--line);
  transition: transform .12s, box-shadow .12s, background .12s, opacity .12s;
}
.icon-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--line); }
.icon-btn.off { opacity: .4; color: var(--ink-faint); }

.level-tag {
  padding: 8px 20px;
  border-radius: 999px;
  background: #EAF3FF;
  border: 4px solid #C6DFF9;
  color: var(--sky-d);
  font-family: var(--font-en);
  font-size: 20px;
  font-weight: 800;
}

/* ==========================================================================
   3. 通用零件：字母块 / 词块 / 槽位 / 箭头 / 提示
   ========================================================================== */

/* ------------------------------ 字母贴纸 ------------------------------ */
/* 字母本身就是一块彩色贴纸（文档 §6.2 的"小写字母 + 彩色条纹"）：
   字形轮廓 + 同色系纹理 + 深色描边 + 柔和投影，没有方块底。
   形状与纹理都在 js/glyphs.js 里生成，这里只管尺寸、投影与交互态。 */

.lblock {
  --lb-base:   #F5A623;
  --lb-stripe: #FFC85C;
  --lb-edge:   #C97A14;
  --lb-ink:    #FFFFFF;

  position: relative;
  display: grid;
  place-items: center;
  width: 112px;
  height: 112px;
  font-family: var(--font-en);
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  flex: 0 0 auto;
}

/* 字形本体。用固定像素而不是百分比：小写字母只有 x-height 高，
   不外扩的话视觉重量比原来的方块轻一大截。取 1.3 × 容器边长。
   （这里刻意不写 130%：grid 里的百分比高度会跟内容高度循环依赖，
     实测会被解析成 1.3 的平方。像素值是确定的。）
   注意这个数值必须和 .slot .ghost svg 保持一致，否则拖过去会对不齐。 */
.lblock .gl-svg {
  width: 146px;          /* 112 × 1.3 */
  height: 146px;
  overflow: visible;
  pointer-events: none;
  filter: drop-shadow(0 5px 2.5px rgba(60, 55, 101, .30));
}

.gl-svg text {
  font-family: var(--font-en);
  font-weight: 800;
  letter-spacing: 0;
  /* 描边先画、填充后画 → 得到干净的外轮廓，不会被填充吃掉 */
  paint-order: stroke fill;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.gl-svg.gl-solid text {
  stroke: var(--lb-edge, #96A3B5);
  stroke-width: 2.9;
}
.gl-svg.gl-ghost text {
  fill: none;
  stroke: var(--ghost);
  stroke-width: 2.5;
}

/* 眼睛：默认不显示，落位或拖起时睁开，制造"字母活过来"的感觉。
   所有字母共用一条基线，x-height 区段也在同一高度，
   所以眼睛统一落在字形上部，矮字母在头顶、高字母在字母身上。 */
.lblock .lb-eye {
  position: absolute;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--dk);
  top: 14px;
  z-index: 2;
  opacity: 0;
  transform: scale(.4);
  transition: opacity .25s, transform .3s cubic-bezier(.3, 1.6, .5, 1);
  pointer-events: none;
}
.lblock .lb-eye.l { left: calc(50% - 26px); }
.lblock .lb-eye.r { left: calc(50% + 9px); }
/* i j l 太窄，两只眼睛并排会飘出字形，一起往中间收 */
.lblock.narrow .lb-eye.l { left: calc(50% - 20px); }
.lblock.narrow .lb-eye.r { left: calc(50% + 3px); }
.lblock .lb-eye::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 4px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--dk);
}
.lblock.placed .lb-eye,
.lblock.lifted .lb-eye {
  opacity: 1;
  transform: scale(1);
}

/* 尺寸变体（字号跟着容器等比走，与 .gl-svg 的 1.3 倍保持一致） */
.lblock.tiny {
  width: 46px;
  height: 46px;
}
/* 字母环：容器仍是 46px（保证点击区与定位计算公式不变），
   但字形放大到 1.35 倍，让环上的字母看起来有分量。
   不宜再大：26 个字母沿弧长均分后中心间距只有 41px，m / w 这类宽字母会开始互相压。 */
.lblock.tiny .gl-svg { width: 62px; height: 62px; filter: drop-shadow(0 2px 1.5px rgba(60, 55, 101, .30)); }
.lblock.tiny .lb-eye { display: none; }

.lblock.small {
  width: 30px;
  height: 30px;
}
.lblock.small .gl-svg { width: 39px; height: 39px; filter: drop-shadow(0 1.5px 1px rgba(60, 55, 101, .28)); }
.lblock.small .lb-eye { display: none; }

.lblock.big {
  width: 132px;
  height: 132px;
}
.lblock.big .gl-svg { width: 172px; height: 172px; filter: drop-shadow(0 7px 3.5px rgba(60, 55, 101, .30)); }

/* 启动页飞行的字母：容器被 JS 压到槽位大小（62px），字形也跟着收 */
.lblock.boot-fly .gl-svg { width: 81px; height: 81px; filter: drop-shadow(0 3px 2px rgba(60, 55, 101, .28)); }

/* ------------------------------ 词块（补句用） ------------------------------ */
/* 和字母贴纸同一套语言：整词也是一块彩色字形贴纸，不再是圆角方块。
   宽度交给字形的 viewBox 宽高比自动撑开，高度定死保持一页内的节奏一致。 */

.wblock {
  --lb-base:   #E8453C;
  --lb-stripe: #FFC93C;
  --lb-edge:   #C42F27;
  --lb-ink:    #FFFFFF;

  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 84px;
  font-family: var(--font-en);
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  flex: 0 0 auto;
}
.wblock .gl-svg {
  height: 100%;
  width: auto;
  overflow: visible;
  pointer-events: none;
  filter: drop-shadow(0 5px 2.5px rgba(60, 55, 101, .30));
}

.wblock .lb-eye {
  position: absolute;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  border: 4px solid var(--dk);
  top: 21px;
  z-index: 2;
  opacity: 0;
  transform: scale(.4);
  transition: opacity .25s, transform .3s cubic-bezier(.3, 1.6, .5, 1);
  pointer-events: none;
}
.wblock .lb-eye.l { left: calc(50% - 34px); }
.wblock .lb-eye.r { left: calc(50% + 12px); }
.wblock .lb-eye::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 5px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--dk);
}
.wblock.placed .lb-eye,
.wblock.lifted .lb-eye {
  opacity: 1;
  transform: scale(1);
}

/* ------------------------------ 槽位 ------------------------------ */
/* 空心字母轮廓（文档 §4.1）：和字母贴纸共用同一套坐标与基线，
   拖过去的字母会和轮廓严丝合缝地重合，不再有"框"和"块"两层错位。 */

.slot {
  position: relative;
  width: 112px;
  height: 112px;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  transition: transform .2s;
}
.slot .ghost {
  display: block;
  width: 146px;          /* 与 .lblock .gl-svg 同尺寸，两边形状才能重合 */
  height: 146px;
  line-height: 0;
}
.slot .ghost svg { width: 100%; height: 100%; }
.slot.filled .ghost { display: none; }
.slot.filled > .lblock {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  height: 100%;
  margin: 0;
  transform: translate(-50%, -50%);
}

/* 提示引导时的高亮 */
.slot.hint-on .ghost { animation: hintGhost 1.1s ease-in-out infinite; }
.slot.hint-on .ghost text { stroke: var(--sky); }

/* 句子里的缺词槽：空心整词轮廓，形状与词块一一对应 */
.sslot {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 84px;
  transition: transform .2s;
}
.sslot .ghost-word {
  display: block;
  height: 84px;
  line-height: 0;
}
.sslot .ghost-word svg { height: 100%; width: auto; }
/* 用 visibility 而不是 display：轮廓要继续占位撑出词槽宽度，
   词块才有一个确定的框可以填进去 */
.sslot.filled .ghost-word { visibility: hidden; }
.sslot.filled > .wblock {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  height: 100%;
  /* 与 .slot.filled > .lblock 同一套居中方案：placedBounce / grinJump
     两个关键帧里写死了 translate(-50%,-50%)，这里必须配套，
     否则动画播完残留的位移会把词块推离槽位中心（踩过坑）。 */
  transform: translate(-50%, -50%);
}
.sslot.hint-on .ghost-word { animation: hintGhost 1.1s ease-in-out infinite; }
.sslot.hint-on .ghost-word text { stroke: var(--sky); }

/* ------------------------------ 拖拽态 ------------------------------ */

.lblock.dragging,
.wblock.dragging {
  position: absolute !important;
  z-index: 900;
  cursor: grabbing;
  transform: scale(1.08) rotate(-3deg) !important;
  transition: none !important;
}
/* 词块和字母贴纸都没有方块可以投影了，改成让字形自己离纸面更远 */
.lblock.dragging .gl-svg,
.wblock.dragging .gl-svg {
  filter: drop-shadow(0 15px 10px rgba(60, 55, 101, .34));
}

.wblock.dragging { transform: scale(1.06) rotate(-2deg) !important; }

/* 拖拽时字母/词块"抖动"：字形在脚下左右晃 + 轻微缩放，像捏着个果冻。
   动画只作用在内部 .gl-svg 上（块本身 transform 已固定为 scale+rotate，
   不能叠加），视觉上就是字母贴纸在抖。尊重系统"减少动态"偏好。 */
@keyframes blockWobble {
  0%   { transform: rotate(-3.5deg) translateY(-1px) scale(1.02); }
  20%  { transform: rotate(3.5deg)  translateY(0)    scale(1.04); }
  40%  { transform: rotate(-3deg)   translateY(1px)  scale(1.02); }
  60%  { transform: rotate(2.5deg)  translateY(0)    scale(1.04); }
  80%  { transform: rotate(-2deg)   translateY(-1px) scale(1.02); }
  100% { transform: rotate(-3.5deg) translateY(-1px) scale(1.02); }
}
@media (prefers-reduced-motion: no-preference) {
  .lblock.dragging .gl-svg,
  .wblock.dragging .gl-svg {
    animation: blockWobble .4s ease-in-out infinite;
    transform-origin: 50% 80%;
  }
}

/* 占位元素，保证托盘布局在拖走方块后不跳动。
   刻意不画底色：字母本身已经没有方块了，原地再留一个浅灰方块会很突兀。 */
.drag-ph {
  display: inline-block;
  flex: 0 0 auto;
}

/* ------------------------------ 导航箭头 ------------------------------ */

.nav-arrow {
  width: 76px;
  height: 76px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 5px solid var(--sky-d);
  background: rgba(59, 143, 232, .82);
  color: #fff;
  cursor: pointer;
  box-shadow: 0 6px 0 var(--sky-d);
  transition: transform .12s, box-shadow .12s, opacity .12s;
}
.nav-arrow:active:not(:disabled) { transform: translateY(4px); box-shadow: 0 2px 0 var(--sky-d); }
.nav-arrow:disabled { opacity: .3; cursor: not-allowed; }
.nav-arrow .arrow-glyph { font-size: 26px; line-height: 1; }
.nav-arrow.blue { background: var(--sky); }

/* ------------------------------ 提示箭头 ------------------------------ */

.hint-arrow {
  position: absolute;
  width: 0;
  height: 0;
  z-index: 520;
  pointer-events: none;
}
.hint-arrow i {
  position: absolute;
  left: -16px;
  top: -16px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--sky);
  border: 4px solid #fff;
  box-shadow: 0 5px 12px rgba(60, 55, 101, .3);
  animation: hintBob 1s ease-in-out infinite;
}
.hint-arrow.pointing i::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 62px;
  height: 11px;
  margin-top: -5.5px;
  border-radius: 6px;
  background: linear-gradient(90deg, var(--sky), rgba(59, 143, 232, 0));
  transform-origin: 0 50%;
  transform: rotate(var(--point, 0rad));
}

/* ------------------------------ 对话气泡 ------------------------------ */

.ask-bubble {
  position: absolute;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  padding: 12px 30px;
  border-radius: 999px;
  background: #fff;
  border: 4px solid var(--line);
  font-size: 20px;
  font-weight: 700;
  color: var(--ink-soft);
  white-space: nowrap;
  transition: background .3s, border-color .3s, color .3s, transform .3s;
}
.ask-bubble.cheer {
  background: #FFF6E2;
  border-color: var(--yellow);
  color: var(--orange-d);
  transform: translateX(-50%) scale(1.06);
}

/* ------------------------------ 撒花 ------------------------------ */

.confetti-bit {
  position: absolute;
  top: -24px;
  border-radius: 2px;
  z-index: 640;
  pointer-events: none;
  animation-name: confettiFall;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}

/* ------------------------------ 轻提示 ------------------------------ */

.toast {
  position: fixed;
  left: 50%;
  bottom: 38px;
  transform: translate(-50%, 18px);
  padding: 13px 30px;
  border-radius: 999px;
  background: var(--dk);
  color: #fff;
  font-size: 17px;
  font-weight: 700;
  opacity: 0;
  pointer-events: none;
  transition: opacity .26s, transform .26s;
  z-index: 900;
  max-width: 80vw;
  text-align: center;
}
.toast.on { opacity: .95; transform: translate(-50%, 0); }

/* ==========================================================================
   4. BOOT 启动动画
   ========================================================================== */

.boot-wrap {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 40px;
}

.boot-monsters {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* 怪物从屏幕外跑入，途中怀抱的字母会短暂越过边界；
     舞台整体本来就该是硬边界，裁掉比让它溢出到滚动区好。 */
  overflow: hidden;
}

.boot-carrier {
  position: absolute;
  bottom: 96px;
  transition: transform .4s;
}
.boot-carrier.side-left  { left: 48px; }
.boot-carrier.side-right { right: 48px; }
/* 第二只稍微错开一点，避免两只完全重叠 */
.boot-carrier:nth-child(2) { bottom: 128px; }
.boot-carrier:nth-child(3) { bottom: 72px; left: 132px; }
.boot-carrier:nth-child(4) { bottom: 62px; right: 128px; }

/* 跑入动画只跑一次。别写成依赖别处的 animation-part：
   .monster.is-cheer 带了 infinite，一旦那条规则优先级压到这里，
   怪物就会无限重复"从屏外冲进来"的过程。显式锁死次数最稳。 */
.boot-carrier.on-the-way {
  animation: runIn .7s cubic-bezier(.2,1.2,.4,1) 1 both;
}

.carry-bundle {
  position: absolute;
  left: 50%;
  top: -18px;
  transform: translateX(-50%);
  display: flex;
  gap: 3px;
  animation: bundleBob 1.2s ease-in-out infinite;
}

.boot-stage {
  position: relative;
  z-index: 2;
}

.boot-title {
  display: flex;
  align-items: center;
  gap: 10px;
}
.boot-gap { width: 34px; }

.boot-slot {
  display: grid;
  place-items: center;
  width: 62px;
  height: 62px;
  border-radius: 15px;
  border: 4px dashed #DDE3EC;
  background: rgba(255, 255, 255, .6);
}
/* 字母落位后虚线框收掉，换成和槽位同一形状的彩色字形 */
.boot-slot .gl-svg {
  width: 90px;           /* 比槽位（62px）大一圈，小写字母才撑得满 */
  height: 90px;
  overflow: visible;
  filter: drop-shadow(0 3px 2px rgba(60, 55, 101, .28));
}
.boot-slot.filled {
  border-style: solid;
  border-color: transparent;
  background: transparent;
  animation: slotPop .38s cubic-bezier(.3, 1.6, .5, 1) both;
}

.boot-title.done { animation: titleBounce .7s cubic-bezier(.3, 1.5, .5, 1) both; }

.boot-fly {
  position: absolute;
  z-index: 20;
  margin-left: -31px;
  margin-top: -31px;
  animation: bootFly .5s cubic-bezier(.3, 1.15, .5, 1) forwards;
}

.boot-loading {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.boot-bar {
  width: 300px;
  height: 16px;
  border-radius: 999px;
  background: #EDF1F7;
  border: 3px solid var(--line);
  overflow: hidden;
}
.boot-bar > i {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--yellow), var(--orange));
  transition: width .3s ease-out;
}
.boot-text {
  font-family: var(--font-en);
  font-size: 20px;
  font-weight: 700;
  color: var(--ink-faint);
  letter-spacing: 1px;
}

.boot-skip {
  position: absolute;
  right: 34px;
  bottom: 30px;
  padding: 11px 26px;
  border-radius: 999px;
  border: 4px solid var(--line);
  background: #fff;
  color: var(--ink-soft);
  font-size: 17px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--line);
}
.boot-skip:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--line); }

/* ==========================================================================
   5. SELECT 选词页
   ========================================================================== */

.select-wrap {
  position: absolute;
  inset: 0;
}

.alphabet-ring {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.ring-letter {
  position: absolute;
  margin-left: -23px;
  margin-top: -23px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  pointer-events: auto;
  transition: transform .16s cubic-bezier(.3, 1.6, .5, 1), box-shadow .16s;
}
.ring-letter.dim { opacity: .32; cursor: default; }
.ring-letter:not(.dim):hover { transform: scale(1.18) rotate(-4deg); }
.ring-letter.on { transform: scale(1.3); }
/* 选中态：字母背后垫一圈暖黄光晕（字母已经没有方块底了，
   所以高亮要自己画，不能再靠 box-shadow 撑出方框）。
   直径比按钮容器略小：这个伪元素会跟着 .on 的 scale(1.3) 一起放大。 */
.ring-letter.on::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 42px;
  height: 42px;
  margin: -21px 0 0 -21px;
  border-radius: 50%;
  background: rgba(255, 201, 60, .55);
  box-shadow: 0 0 0 3px rgba(255, 201, 60, .85);
}
.ring-letter.on .gl-svg { position: relative; z-index: 1; }

/* 守卫这一坨整体不接收指针事件：它是 1000px 宽的装饰性容器，
   盒子的左半边会罩住字母环左下方的 a / b / c 几个字母，
   不关掉的话那几个字母点不动（词卡要单独放开，见下）。 */
.guard-figure {
  position: absolute;
  left: 50%;
  top: 112px;
  transform: translateX(-50%);
  width: 1000px;
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
}
.guard-mouth,
.card-rail,
.word-card { pointer-events: auto; }

.guard-topbox { width: 720px; }
.guard-topbox svg { display: block; width: 100%; height: auto; }

/* 嘴腔：词卡真正待的地方，夹在上下唇之间。
   舞台底边被裁时 .select-foot 会上提躲开屏幕下缘，会骑进嘴腔下沿，
   这里让嘴腔收矮同样的量把空间让给底栏。
   但嘴腔自带 overflow:hidden，收太狠会把词卡的英文单词和 Level 标签裁掉，
   所以用 min-height 兜住"一张卡连投影都放得下"的下限（≈112px），
   不够的那部分由 --safe-bottom 折算后的底栏位置自然吸收。 */
.guard-mouth {
  position: relative;
  width: 880px;
  height: max(132px, calc(158px - var(--safe-bottom, 0px)));
  margin-top: -30px;
  overflow: hidden;
}

.guard-bottombox {
  width: 780px;
  height: 152px;
  margin-top: -30px;
  overflow: hidden;
}
.guard-bottombox svg { display: block; width: 100%; height: auto; }

.card-rail {
  display: flex;
  gap: 18px;
  height: 100%;
  padding: 4px 22px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.card-rail::-webkit-scrollbar { display: none; }

.rail-fade {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 46px;
  pointer-events: none;
}
.rail-fade.left  { left: 0;  background: linear-gradient(90deg, var(--paper), rgba(255, 253, 249, 0)); }
.rail-fade.right { right: 0; background: linear-gradient(270deg, var(--paper), rgba(255, 253, 249, 0)); }

.rail-empty {
  display: grid;
  place-items: center;
  width: 100%;
  color: var(--ink-faint);
  font-size: 19px;
  font-weight: 700;
}

.word-card {
  position: relative;
  flex: 0 0 auto;
  scroll-snap-align: center;
  width: 176px;
  padding: 6px 8px 8px;
  border-radius: 20px;
  border: 4px solid var(--line);
  background: #fff;
  cursor: pointer;
  text-align: center;
  box-shadow: 0 5px 0 var(--line);
  transition: transform .18s cubic-bezier(.3, 1.5, .5, 1), border-color .18s, box-shadow .18s;
}
.word-card:hover {
  transform: translateY(-5px);
  border-color: var(--yellow);
  box-shadow: 0 5px 0 #F0D69A;
}
.word-card.picked {
  transform: translateY(-8px) scale(1.06);
  border-color: var(--orange);
  box-shadow: 0 5px 0 var(--orange-d);
}
.word-card.current { border-color: #C6DFF9; }

.wc-art {
  display: block;
  height: 64px;
  overflow: hidden;
}
.wc-art svg { display: block; width: 100%; height: 100%; }

.wc-word {
  display: block;
  font-family: var(--font-en);
  font-size: 25px;
  font-weight: 800;
  color: var(--ink);
  line-height: 1.15;
}
.wc-meta {
  display: block;
  font-size: 13px;
  font-weight: 700;
  color: var(--ink-faint);
  margin-top: 1px;
}
.wc-meta.got { color: var(--orange-d); }
.wc-meta.full { color: var(--green-d2); }

/* 学会进度：实心星 = 已走到那一步，空心星 = 还没走到的闭环环节 */
.wc-stars {
  font-style: normal;
  font-size: 14px;
  letter-spacing: 1.5px;
  color: var(--orange);
  text-shadow: 0 1px 0 rgba(0, 0, 0, .08);
}
.wc-meta.full .wc-stars { color: var(--green-d2); }

.wc-free {
  position: absolute;
  left: 8px;
  top: -10px;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--green);
  color: #fff;
  font-family: var(--font-en);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .6px;
}

.select-foot {
  position: absolute;
  left: 0;
  right: 0;
  /* 铺满全屏时舞台下边缘被裁到屏幕外，foot 原本贴舞台底（bottom:22px）会跟着出屏，
     所以按裁切量把它往上拉回可视区（bottom 变大 = 离底边更远）。
     上提的副作用是会骑到词卡轨道上，而词卡轨道（.guard-mouth）本身不与屏幕绑死，
     所以让嘴腔在裁切时同步变矮（见 .card-rail 的 max-height），两边一起让出空间。 */
  bottom: calc(22px + var(--safe-bottom, 0px));
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 calc(40px + var(--safe-x, 0px));
  gap: 20px;
}

/* 两排：上排 Level 分组（流程图 STEP ①），下排全集 / 免费 */
.filter-chips {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.chip-row {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
}

/* Level chip 比通用 chip 小一号，一排放得下更多级别 */
.chip.lv {
  padding: 8px 18px;
  font-size: 15px;
  border-width: 3px;
  box-shadow: 0 3px 0 var(--line);
}
.chip.lv:active { box-shadow: 0 1px 0 var(--line); }

/* chip 上挂的词数角标 */
.chip-n {
  font-style: normal;
  font-size: 12px;
  font-weight: 800;
  margin-left: 7px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--line);
  color: var(--ink-soft);
}
.chip.on .chip-n { background: rgba(255, 255, 255, .72); color: var(--orange-d); }
.chip {
  padding: 11px 26px;
  border-radius: 999px;
  border: 4px solid var(--line);
  background: #fff;
  color: var(--ink-soft);
  font-size: 17px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--line);
  transition: transform .12s, background .15s, color .15s, border-color .15s;
}
.chip:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--line); }
.chip.on {
  background: var(--yellow);
  border-color: #E5A81C;
  color: var(--dk);
  box-shadow: 0 4px 0 #E5A81C;
}

/* ==========================================================================
   6. WORD / SENTENCE 拼词与补句
   ========================================================================== */

.puzzle-wrap {
  position: absolute;
  inset: 0;
}

/* ------------------------------ WORD ------------------------------ */
/* 用 flex 而不是 grid：总高度是固定的 720，flex 能精确分配剩余空间，
   grid 的 1fr 在有内边距时容易把 tile-field 顶出舞台。 */

.puzzle-wrap.is-word {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 88px;
}

.reveal-box {
  position: relative;
  display: grid;
  place-items: center;
  width: 300px;
  height: 200px;
  flex: 0 0 auto;
}
.reveal-art {
  display: block;
  width: 100%;
  height: 100%;
}
.reveal-art svg { display: block; width: 100%; height: 100%; }

.reveal-q {
  position: absolute;
  font-family: var(--font-en);
  font-size: 82px;
  font-weight: 800;
  color: #fff;
  -webkit-text-stroke: 5px #DDE3EB;
  animation: qPulse 2.4s ease-in-out infinite;
}

/* 未揭晓：插画模糊且低饱和，答对后揭开（文档 §4.1 拼完反馈） */
.reveal-box.locked .reveal-art {
  filter: blur(13px) saturate(.35) opacity(.4);
  transform: scale(.94);
  transition: filter .55s ease, transform .55s ease;
}
.reveal-box.revealed .reveal-art {
  filter: none;
  transform: scale(1);
  animation: revealPop .65s cubic-bezier(.3, 1.5, .5, 1) both;
}
.reveal-box.revealed .reveal-q { display: none; }

.slot-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  width: 100%;
  height: 150px;
  flex: 0 0 auto;
}

/* 宽度写死 1280 而不是 100%：舞台本身就是 1280 宽，
   显式给出宽度可以避免内边距把元素撑出边界。
   列间距取 20px、左右内边距取 60px，是为了让最长的 8 字母词
   （elephant）也能排成一行：8×112 + 7×20 = 1036 < 1280-120 = 1160。 */
.puzzle-wrap.is-word .tile-field {
  display: flex;
  flex-wrap: wrap;
  align-content: center;
  justify-content: center;
  gap: 26px 20px;
  width: 1280px;
  flex: 1 1 auto;
  padding: 0 60px 96px;
}

.tile-field .lblock {
  transform: translateY(var(--dy, 0px)) rotate(var(--tilt, 0deg));
}
.tile-field .wblock {
  transform: translateY(var(--dy, 0px)) rotate(var(--tilt, 0deg));
}
.tile-field .lblock.dragging,
.tile-field .wblock.dragging { transform: scale(1.08) rotate(-3deg) !important; }

/* ------------------------------ SENTENCE ------------------------------ */

.puzzle-wrap.is-sentence {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 128px;
  gap: 22px;
}

/* 句子行允许折两行：例句最长 11 个词，一行放不下时不能挤压词块区。
   宽度给到 1200px（舞台 1280）是为了让最长的句子也能把缺词槽排在一行里 ——
   缺词槽现在是按字形宽度撑开的，比原来的固定宽度方块宽不少。 */
.sentence-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px 14px;
  width: 1200px;
  min-height: 268px;
  flex: 0 0 auto;
  align-content: center;
  padding: 0 14px;
}

.stok {
  font-family: var(--font-en);
  font-size: 34px;
  font-weight: 800;
  color: var(--ink);
  cursor: pointer;
  padding: 6px 6px;
  border-radius: 10px;
  transition: background .16s, transform .16s;
}
.stok:hover { background: rgba(255, 201, 60, .28); transform: translateY(-2px); }
.stok:active { transform: translateY(1px); }

.puzzle-wrap.is-sentence .tile-field {
  display: flex;
  flex-wrap: wrap;
  align-content: center;
  justify-content: center;
  gap: 30px 30px;
  width: 1280px;
  flex: 1 1 auto;
  padding: 0 120px 104px;
}

/* ------------------------------ 落位后的庆祝 ------------------------------ */

.slot .lblock.placed,
.sslot .wblock.placed { animation: placedBounce .38s cubic-bezier(.3, 1.6, .5, 1) both; }

/* 拼对后整个字母做鬼脸（文档 §2.1 第 3 条：完成后怪物做个鬼脸） */
.lblock.grin .lb-eye,
.wblock.grin .lb-eye { animation: eyeSquint .6s ease-in-out 2; }
.lblock.grin,
.wblock.grin { animation: grinJump .62s cubic-bezier(.3, 1.5, .5, 1) 2; }

/* 发牌入场 */
.lblock.deal-in,
.wblock.deal-in { animation: dealIn .42s cubic-bezier(.3, 1.5, .5, 1) both; }

/* ==========================================================================
   7. SCRAMBLE 撞散
   ========================================================================== */

.scramble-wrap {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 60px;
  padding-top: 60px;
}

.sc-word {
  display: flex;
  gap: 18px;
  position: relative;
  z-index: 2;
}

.sc-word .lblock.pop-in { animation: dealIn .42s cubic-bezier(.3, 1.5, .5, 1) both; }

.sc-word .lblock.knocked {
  animation: scatter .62s cubic-bezier(.3, .8, .6, 1) both;
}

.sc-runner {
  position: absolute;
  left: -220px;
  top: 50%;
  margin-top: -40px;
  z-index: 3;
  animation: runAcross 1.25s cubic-bezier(.35, .2, .5, 1) .35s both;
}

.sc-tip {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  color: var(--ink-faint);
  animation: fadeUp .5s ease-out 1s both;
}

/* ==========================================================================
   8. SCENE 场景动画
   ========================================================================== */

.scene-wrap {
  position: absolute;
  inset: 0;
  display: grid;
  /* 三行：插画面板 / 例句 / 翻页。
     原实现是第一行 1fr，面板把整屏剩余高度全吃掉，例句被挤到屏幕 71% 处，
     看起来"图太高、句子太低"。现在改成让**面板行参与挤压**：
       ① 面板行上限 410px，不再无限拉伸；
       ② 例句行前面放一段 1fr 的弹性空隙，并且整组贴底——
          这样多出来的高度落在面板**上方**，例句随之上移到屏幕约 65%，
          翻页按钮仍稳稳贴底。
     一句话：面板变矮一点、空白让到上面去，句子就跟着上来了。 */
  grid-template-rows: minmax(0, 380px) 1fr auto auto;
  padding: 96px 40px 20px;
  gap: 12px;
}

.scene-stage {
  position: relative;
  display: grid;
  place-items: center;
  overflow: hidden;
  height: 100%;
  border-radius: 26px;
  background:
    radial-gradient(90% 80% at 50% 30%, #FFFFFF 0%, #F6F9FD 70%, #EDF2F9 100%);
  border: 4px solid var(--line);
  cursor: pointer;
}

.scene-art {
  position: absolute;
  left: 90px;
  top: 50%;
  transform: translateY(-50%);
  /* 插画按面板可用高度自适应：面板上限 410px，减去 4px 边框 ×2，
     再留 8px 内边距，取 380px。用 max-height 而非写死高度，
     这样面板被压低时插画跟着缩，不会溢出到例句上。 */
  width: 520px;
  height: 380px;
  max-height: calc(100% - 16px);
}
.scene-art svg { display: block; width: 100%; height: 100%; }

.scene-actors {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.scene-actor {
  position: absolute;
  /* ⚠️ 不要用 transform 做居中！
     .monster.is-idle 的 monIdle 关键帧写的是 `transform: scaleY() translateY()`，
     与这里的 transform 同为 0-2-0 特异性，源码顺序上 is-idle（下方）胜出，
     于是 translate(-50%,-50%) 被静默覆盖 —— 定位语义从"中心"变成"左上角"，
     怪物整体下沉半个身高、戳出面板被 overflow:hidden 裁掉。
     现在改由 placeActors() 用 left/top 直接给出左上角坐标（减半宽半高），
     transform 完全留给动画，两边不再打架。 */
  will-change: transform;
}
/* 入场动画也不再带 translate，避免和情绪动画抢 transform。 */
.scene-actor.walk-in { animation: actorIn .7s cubic-bezier(.2, 1.2, .4, 1) both; }

.scene-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 12px 26px;
  border-radius: 20px;
  background: #fff;
  border: 4px solid var(--line);
}

.scene-tok {
  border: none;
  background: none;
  padding: 4px 8px;
  border-radius: 10px;
  font-family: var(--font-en);
  font-size: 28px;
  font-weight: 800;
  color: var(--ink);
  cursor: pointer;
  transition: background .16s, transform .16s, color .16s;
}
.scene-tok:hover { background: rgba(255, 201, 60, .3); transform: translateY(-2px); }
.scene-tok.speaking {
  background: var(--yellow);
  color: var(--dk);
  transform: scale(1.12);
}

.scene-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 38px;
  padding-bottom: 18px;
}

.scene-again {
  padding: 12px 30px;
  border-radius: 999px;
  border: 4px solid var(--line);
  background: #fff;
  color: var(--ink-soft);
  font-size: 18px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--line);
}
.scene-again:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--line); }

/* ==========================================================================
   9. 动画
   ========================================================================== */

@keyframes runIn {
  from { transform: translateX(-320px) translateY(20px) rotate(-8deg); opacity: 0; }
  to   { transform: translateX(0) translateY(0) rotate(0); opacity: 1; }
}
/* 这里必须用比 .monster.is-idle（0-2-0）更高的特异性（0-3-0）写全所有 animation 分段。
   否则怪物自带的情绪动画（.is-idle 是 infinite 的 monIdle）会按 cascade 后来居上，
   把跑入动画的 duration 改成 2.6s、iteration 改成 infinite ——
   结果就是台边那两只每隔 2.6 秒从屏幕外冲进来一次，飞到右边界外两百多像素。
   只写 animation-name 不够，必须把时长和次数一起锁死。 */
.boot-carrier.on-the-way.side-left,
.boot-carrier.on-the-way.side-right {
  animation: runIn .7s cubic-bezier(.2,1.2,.4,1) 1 both;
}
.boot-carrier.on-the-way.side-right { animation-name: runInRight; }
@keyframes runInRight {
  from { transform: translateX(320px) translateY(20px) rotate(8deg); opacity: 0; }
  to   { transform: translateX(0) translateY(0) rotate(0); opacity: 1; }
}

@keyframes bundleBob {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(-5px); }
}

@keyframes bootFly {
  from { transform: translate(var(--from-x, -300px), var(--from-y, -200px)) scale(1.12) rotate(-10deg); }
  to   { transform: translate(0, 0) scale(1) rotate(0deg); }
}

@keyframes slotPop {
  0%   { transform: scale(1.3); }
  60%  { transform: scale(.94); }
  100% { transform: scale(1); }
}

@keyframes titleBounce {
  0%   { transform: scale(1) translateY(0); }
  40%  { transform: scale(1.09) translateY(-8px); }
  70%  { transform: scale(.98) translateY(2px); }
  100% { transform: scale(1) translateY(0); }
}

@keyframes qPulse {
  0%, 100% { transform: scale(1); opacity: .9; }
  50%      { transform: scale(1.08); opacity: 1; }
}

@keyframes revealPop {
  0%   { transform: scale(.86); }
  55%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

@keyframes dealIn {
  from { opacity: 0; transform: translateY(34px) scale(.8) rotate(var(--tilt, 0deg)); }
  to   { opacity: 1; transform: translateY(var(--dy, 0px)) scale(1) rotate(var(--tilt, 0deg)); }
}

@keyframes placedBounce {
  0%   { transform: translate(-50%, -50%) scale(1.22); }
  60%  { transform: translate(-50%, -50%) scale(.95); }
  100% { transform: translate(-50%, -50%) scale(1); }
}

@keyframes eyeSquint {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1, .18); }
}

@keyframes grinJump {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  35%      { transform: translate(-50%, -62%) scale(1.07) rotate(-3deg); }
  70%      { transform: translate(-50%, -50%) scale(.98) rotate(2deg); }
}

@keyframes scatter {
  0%   { transform: translate(0, 0) rotate(0) scale(1); opacity: 1; }
  100% {
    transform: translate(var(--kx, 120px), var(--ky, -60px)) rotate(var(--kr, 90deg)) scale(.9);
    opacity: 0;
  }
}

@keyframes runAcross {
  0%   { transform: translateX(0) translateY(0) rotate(0); }
  50%  { transform: translateX(760px) translateY(-14px) rotate(4deg); }
  100% { transform: translateX(1520px) translateY(0) rotate(0); }
}

@keyframes actorIn {
  from { opacity: 0; transform: translateY(24px) scale(.7) rotate(-10deg); }
  to   { opacity: 1; transform: translateY(0) scale(1) rotate(0); }
}

@keyframes hintPulse {
  0%, 100% { border-color: var(--sky); background: rgba(59, 143, 232, .10); }
  50%      { border-color: #8FC6FF; background: rgba(59, 143, 232, .22); }
}

/* 字母槽位的提示：轮廓整体呼吸，颜色交给 .hint-on 的描边规则 */
@keyframes hintGhost {
  0%, 100% { opacity: .62; transform: scale(1); }
  50%      { opacity: 1;   transform: scale(1.06); }
}

@keyframes hintBob {
  0%, 100% { transform: translateY(0) scale(1); }
  50%      { transform: translateY(-8px) scale(1.08); }
}

@keyframes confettiFall {
  from { transform: translateY(0) rotate(0); opacity: 1; }
  to   { transform: translateY(760px) rotate(var(--spin, 240deg)); opacity: .15; }
}

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* 怪物自身的待机与情绪动画（文档 §6.1 的 7 组基础动画） */
.monster { display: block; }
.monster .mon-face .m-open,
.monster .mon-face .m-grin { display: none; }

.monster.is-idle  { animation: monIdle 2.6s ease-in-out infinite; }
.monster.is-run   { animation: monRun .42s ease-in-out infinite; }
.monster.is-carry { animation: monCarry .9s ease-in-out infinite; }
.monster.is-surprised { animation: monPop .5s cubic-bezier(.3, 1.6, .5, 1) both; }
.monster.is-cheer { animation: monCheer .6s cubic-bezier(.3, 1.5, .5, 1) infinite; }
.monster.is-eat   { animation: monEat .7s ease-in-out infinite; }
.monster.is-grin  { animation: monGrin .6s cubic-bezier(.3, 1.5, .5, 1) infinite; }

.monster.is-cheer .mon-face .m-smile,
.monster.is-grin .mon-face .m-smile,
.monster.is-eat .mon-face .m-smile { display: none; }
.monster.is-cheer .mon-face .m-open { display: block; }
.monster.is-eat .mon-face .m-open { display: block; }
.monster.is-grin .mon-face .m-grin { display: block; }

@keyframes monIdle {
  0%, 100% { transform: scaleY(1) translateY(0); }
  50%      { transform: scaleY(1.035) translateY(-4px); }
}
@keyframes monRun {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-12px) rotate(3deg); }
}
@keyframes monCarry {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50%      { transform: translateY(-6px) rotate(4deg); }
}
@keyframes monPop {
  0%   { transform: scale(.8); }
  60%  { transform: scale(1.14); }
  100% { transform: scale(1); }
}
@keyframes monCheer {
  0%, 100% { transform: translateY(0) scale(1); }
  45%      { transform: translateY(-16px) scale(1.07); }
}
@keyframes monEat {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.05, .95); }
}
@keyframes monGrin {
  0%, 100% { transform: rotate(0); }
  30%      { transform: rotate(-6deg) scale(1.05); }
  70%      { transform: rotate(6deg) scale(1.05); }
}

/* ==========================================================================
   10. 竖屏提示
   ========================================================================== */

.rotate-tip {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1000;
  align-items: center;
  justify-content: center;
  padding: 24px;
  box-sizing: border-box;
  background: var(--paper);
  color: var(--ink-soft);
  text-align: center;
}

.rotate-tip-inner { max-width: 320px; }

.rotate-tip-icon {
  width: 120px;
  height: 120px;
  color: var(--ink-soft);
  margin-bottom: 4px;
  /* 手机轻轻摇摆，暗示"转一下" */
  animation: tipTilt 2.4s ease-in-out infinite;
}

@keyframes tipTilt {
  0%, 100% { transform: rotate(-6deg); }
  50%      { transform: rotate(6deg); }
}

.rotate-tip-title {
  margin: 0 0 8px;
  font-size: 21px;
  font-weight: 800;
  line-height: 1.4;
}

.rotate-tip-sub {
  margin: 0;
  font-size: 15px;
  line-height: 1.6;
  opacity: .72;
}

.rotate-tip-hint {
  margin: 18px 0 0;
  font-size: 13.5px;
  line-height: 1.7;
  opacity: .55;
}

.rotate-tip-hint b { font-weight: 700; }

@media (orientation: portrait) and (max-width: 820px) {
  .rotate-tip { display: flex; }
}

/* 极矮的竖屏（小屏手机浏览器工具栏占位）：收掉副提示，省出空间 */
@media (orientation: portrait) and (max-width: 820px) and (max-height: 560px) {
  .rotate-tip-icon { width: 84px; height: 84px; }
  .rotate-tip-hint { display: none; }
}

/* 系统偏好减少动效：把装饰性动画全部压掉 */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}
