/* 邦尼稳拍 / Bunny Tempo —— 深色优先、以谱面为主、可触摸。
   版式的原则：谱面就是页面，走带只占一行，播放时用不到的东西都收进抽屉。

   配色出自品牌规范：主色 #3451D1、品牌浅色 #5B7CFF、邦尼珊瑚 #F47C61（非功能色，
   只给插画）、文字 #241B19、辅助 #6B7280、成功/错误/警告 #16835B/#D92D20/#D97706。
   深色是把同一支蓝压深得到的藏蓝，不是中性黑 —— 依据是品牌规范里的深色 App 图标。
   语义色在浅底上不够 4.5:1 的三个各压深一档，理由写在下面的浅色块里。 */

:root {
  /* 深色 —— **品牌深蓝**，不是中性黑。
     依据是品牌图里那个深色 App 图标和深色手机稿：它们的底是 #3451D1 压深之后
     的藏蓝，而不是灰黑。之前这里是 #14161a（偏冷的中性黑），于是主色蓝是蓝的、
     底是灰的，两者没有血缘关系；换成同一支蓝压深，整个深色主题才像「这个品牌
     的深色」，而不是「随便一个深色皮肤」。
     每个值都实测过 AA（正文 4.5:1）：
       文字 14.64 · 次级 7.78 · 弱化 5.56 · 主色 6.12 · 成功 9.60 · 警告 9.12 · 错误 7.59 */
  --bg: #141a2b; --panel: #1c2439; --line: #2b3550; --paper: #0e121d;
  --fg: #e8ecf5; --dim: #a3aec6; --faint: #8792ac;
  --accent: #7893ff; --accent-ink: #0b1020; --warn: #f0b057; --ok: #5fd6a4;
  --sunk: #101623; --raise: #232d45;
  /* 邦尼的珊瑚橙。品牌图上写得很清楚：**「非功能色」，只给兔子/鼓/插画**。
     它在浅底上只有 2.55:1，一旦当成文字或按钮用就不合格，而那正是
     「品牌色蔓延成交互色」的开始。交互一律用蓝。 */
  --brand: #f47c61; --brand-soft: #fff0ea;
  /* The stage the score sheet sits on — never the same value as the sheet. */
  --stage: var(--paper);
  --accent-soft: #8fa9ff; --red: #ff8a80;
  /* 「在 --warn 上写字」用的墨色。深色下 --warn 是亮琥珀 #f0b057，要深字
     （8.73:1）；浅色下 --warn 已经压深成 #9c5504，同一个深字只有 **2.93:1**，
     读不了。所以它必须是个跟着主题翻的 token，不能写死。 */
  --warn-ink: #2a1c05;
  /* 三条音轨各自的身份色。原来写死在 `.seg button[data-bed=…]` 上，是给深色
     调的三支亮色；浅色主题下它们印在白卡片上是三块浅彩，既认不出也读不了。
     和 --tg-* 一样，各给一版。 */
  --bed-demo: #6ee7a8; --bed-orig: #9ec6ff; --bed-nodrum: #b18cff;
  --r: 8px; --r-lg: 12px;
  --mono: "SF Mono", ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace;
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);
  /* Shadows as a token, because their opacity is the one thing that cannot
     survive a theme flip: black at .5 reads as depth on a dark ground and as
     dirt on a light one. */
  /* The chip hues carry the meaning; only their lightness has to move. */
  --tg-meter: #8fa9ff; --tg-tempo: #5fd6a4; --tg-bars: #b79cff;
  /* The printed page, and what separates it from the stage. */
  --sheet: #f7f7f5; --sheet-edge: transparent;
  --tag-bg: rgba(14,18,29,.9);
  --shadow-lg: 0 18px 48px rgba(4,7,16,.55);
  --shadow-md: 0 8px 24px rgba(4,7,16,.4);
  --scrim: rgba(8,11,20,.62);
}

/* ---- 亮色 -------------------------------------------------------------
   Dark stays the bare `:root`, so nothing changes for anyone until their
   system says otherwise or they pick. Three states, and each needs its own
   block: the media query with `:not([data-theme="dark"])` so an explicit dark
   choice beats a light OS, and the attribute on its own so the toggle wins in
   the other direction.

   The one thing the palette has to protect is that 谱面 is a sheet of paper
   ON something. On the dark ground that is free — white on near-black. In
   light, a white page on a white app is one flat field, and the score stops
   being the object it is. So the light ground is a warm grey the paper sits
   above, not another white. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    /* 浅色 —— **品牌图原值**：页面 #FAFAFC、卡片 #F1F5F9、文字 #241B19、
       主色 #3451D1。之前这里是一套暖白（#fcfbf9 / #e8e4e1 / #6f6864），和品牌图
       给的冷白不是一回事，属于早期的漂移。
       ⚠️ **三个语义色必须压深，这不是偏离品牌而是让它能读**：
         警告 #D97706 在页面底 3.06、卡片底 2.91（正文要 4.5）→ #9C5504（5.42/5.16）
         成功 #16835B 卡片底 4.33 → #137551（5.46/5.19）
         错误 #D92D20 卡片底 4.41 → #C3281C（5.53/5.27）
       辅助文字 #6B7280 在卡片底也只有 4.41，所以正文用的 --dim 压到 #4B5563
       （7.25/6.90），品牌图那个原值留给 --faint（只用于次要提示）。 */
    --bg: #fafafc; --panel: #ffffff; --line: #e2e8f0; --paper: #ffffff;
    --fg: #241b19; --dim: #4b5563; --faint: #656c7a;
    --accent: #3451d1; --accent-ink: #ffffff; --warn: #9c5504; --ok: #137551;
    --sunk: #f1f5f9; --raise: #ffffff;
    --brand: #f47c61; --brand-soft: #fff0ea;
    --stage: #e8edf3;
    --accent-soft: #2942b8; --red: #c3281c;
    --warn-ink: #ffffff;
    --bed-demo: #0f6b49; --bed-orig: #175cd3; --bed-nodrum: #6b3fc0;
    --tg-meter: #3451d1; --tg-tempo: #0f6b49; --tg-bars: #6b3fc0;
    --sheet: #ffffff; --sheet-edge: #e2e8f0;
    --tag-bg: rgba(255,255,255,.94);
    --shadow-lg: 0 16px 40px rgba(20,26,43,.14);
    --shadow-md: 0 6px 18px rgba(20,26,43,.09);
    --scrim: rgba(20,26,43,.38);
  }
}
:root[data-theme="light"] {
  /* 浅色 —— **品牌图原值**：页面 #FAFAFC、卡片 #F1F5F9、文字 #241B19、
     主色 #3451D1。之前这里是一套暖白（#fcfbf9 / #e8e4e1 / #6f6864），和品牌图
     给的冷白不是一回事，属于早期的漂移。
     ⚠️ **三个语义色必须压深，这不是偏离品牌而是让它能读**：
       警告 #D97706 在页面底 3.06、卡片底 2.91（正文要 4.5）→ #9C5504（5.42/5.16）
       成功 #16835B 卡片底 4.33 → #137551（5.46/5.19）
       错误 #D92D20 卡片底 4.41 → #C3281C（5.53/5.27）
     辅助文字 #6B7280 在卡片底也只有 4.41，所以正文用的 --dim 压到 #4B5563
     （7.25/6.90），品牌图那个原值留给 --faint（只用于次要提示）。 */
  --bg: #fafafc; --panel: #ffffff; --line: #e2e8f0; --paper: #ffffff;
  --fg: #241b19; --dim: #4b5563; --faint: #656c7a;
  --accent: #3451d1; --accent-ink: #ffffff; --warn: #9c5504; --ok: #137551;
  --sunk: #f1f5f9; --raise: #ffffff;
  --brand: #f47c61; --brand-soft: #fff0ea;
  --stage: #e8edf3;
  --accent-soft: #2942b8; --red: #c3281c;
  --warn-ink: #ffffff;
  --bed-demo: #0f6b49; --bed-orig: #175cd3; --bed-nodrum: #6b3fc0;
  --tg-meter: #3451d1; --tg-tempo: #0f6b49; --tg-bars: #6b3fc0;
  --sheet: #ffffff; --sheet-edge: #e2e8f0;
  --tag-bg: rgba(255,255,255,.94);
  --shadow-lg: 0 16px 40px rgba(20,26,43,.14);
  --shadow-md: 0 6px 18px rgba(20,26,43,.09);
  --scrim: rgba(20,26,43,.38);
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0; background: var(--bg); color: var(--fg);
  font: 14px/1.55 -apple-system, BlinkMacSystemFont, "PingFang SC",
        "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

/* Author rules that set `display` beat the UA's [hidden] no matter the
   specificity, so restore the attribute's meaning once, globally. */
[hidden] { display: none !important; }

h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }
.meta, .note, .cap { color: var(--dim); }
.note { font-size: 12.5px; line-height: 1.6; margin: 0; }
.cap  { font-size: 12.5px; }
.grow { flex: 1; }
.sep  { width: 1px; height: 20px; background: var(--line); flex: none; }
.err  { color: var(--red); font-size: 12.5px; margin: 0; }
.boot { padding: 64px 24px; color: var(--dim); }

/* ------------------------------------------------------------- controls */
svg { width: 18px; height: 18px; fill: none; stroke: currentColor;
      stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }

button, .btn, .iconBtn { font: inherit; cursor: pointer; color: var(--fg); }

.iconBtn {
  width: 40px; height: 40px; flex: none; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--panel); color: var(--dim);
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none; transition: border-color .12s, color .12s;
}
.iconBtn:hover { border-color: var(--accent); color: var(--fg); }

.btn, .mini, .primary {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  text-decoration: none; white-space: nowrap;
}
.btn {
  height: 38px; padding: 0 14px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--raise);
  transition: border-color .12s;
}
.btn:hover { border-color: var(--accent); }
.btn.on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

.primary {
  height: 46px; padding: 0 18px; border-radius: 10px; border: none;
  background: var(--accent); color: var(--accent-ink);
  font-size: 14.5px; font-weight: 600;
}
/* 禁用的按钮以前是写死的 #2a3038：浅色主题下不翻，白底对话框上一个近黑色的
   块，比旁边可用的按钮还抢眼。用 --line —— 两套配色里它都是「结构灰」。 */
.primary:disabled { background: var(--line); color: var(--faint); cursor: not-allowed; }
.mini { height: 36px; padding: 0 12px; border-radius: 6px; border: 1px solid var(--line);
        background: transparent; color: var(--dim); font-size: 12.5px; }
.mini:hover { border-color: var(--accent); color: var(--fg); }
.ghost { width: 30px; height: 30px; border: none; background: transparent;
         color: var(--faint); display: inline-flex; align-items: center; justify-content: center; }
.ghost:hover { color: var(--fg); }
.ghost svg { width: 15px; height: 15px; }

/* Match by exclusion, not by listing types: `input[type=text]` silently skips
   an <input> with no type attribute, which is how the username box ended up
   with the browser's white default sitting next to a dark password box. */
input:not([type=checkbox]):not([type=radio]):not([type=range]) {
  width: 100%; height: 44px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--sunk); color: var(--fg);
  padding: 0 12px;
  /* 16px keeps iOS from zooming the page on focus. */
  font-size: 16px; font-family: inherit; line-height: 1;
  transition: border-color .12s, box-shadow .12s;
}
input:not([type=checkbox]):not([type=radio]):not([type=range])::placeholder {
  color: var(--faint);
}
/* 🔴 **这两条从来没生效过。**
 *
 * 基础规则是 `input:not(…):not(…):not(…)`，特指度 (0,3,1)；而
 * `input:focus-visible` 和 `input[aria-invalid]` 都是 (0,1,1)，**输**。
 * 实测：键盘聚焦、且 `aria-invalid="true"` 的输入框，边框仍然是 `--line`
 * —— 也就是说「聚焦变主色」和「出错变红」这两个状态全站一次都没画出来过。
 * 注册表单三个字段一起报错时，只有文字是红的，框一个都没变。
 *
 * 修法和下面 `.num` 那条一样：**选择器写成和基础规则同一个形状**。
 * 这已经是同一个坑的第三次（曲库卡片、`.num`、这里），所以
 * `web/tests/specificity.mjs` 的判据也一起补了 —— 它原来只报带后代组合符的
 * 规则，而这两条没有组合符，正好从盲区里漏过去。 */
input:not([type=checkbox]):not([type=radio]):not([type=range]):focus-visible {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
input:not([type=checkbox]):not([type=radio]):not([type=range])[aria-invalid="true"] {
  border-color: var(--red);
}
input:not([type=checkbox]):not([type=radio]):not([type=range])[aria-invalid="true"]:focus-visible {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 18%, transparent);
}
/* Same selector shape as the base rule above, on purpose. `.num` alone is
   (0,1,0) and loses to `input:not(…):not(…):not(…)` at (0,3,1), so the width
   never applied and the bar-number boxes stretched to the full width of the
   settings sheet — which pushed 第 / — / 小节 onto lines of their own. The
   same cascade collision as the library cards, in a different room. */
input.num:not([type=checkbox]):not([type=radio]):not([type=range]) {
  width: 74px; text-align: center; font-family: var(--mono);
}

/* 尺寸在这里，样子在 controls.css —— 那边是自绘的，不再依赖 accent-color
   （窗口失焦时系统会把原生控件重绘成灰色，那层盖得过 accent-color）。 */
input[type=range] { height: 22px; width: 108px; }
.chk { display: inline-flex; align-items: center; gap: 7px; color: var(--dim);
       font-size: 13px; cursor: pointer; min-height: 34px; }
.chk input { width: 17px; height: 17px; }

.chip {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  height: 34px; padding: 0 11px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--panel);
  font-size: 12px; color: var(--dim);
}
.chip svg { width: 13px; height: 13px; }
.chip.ok   { color: var(--ok);   border-color: rgba(110,231,168,.32); background: rgba(110,231,168,.09); }
.chip.loop { color: var(--warn); border-color: rgba(255,180,84,.34); background: rgba(255,180,84,.09);
             font-family: var(--mono); }

/* ---------------------------------------------------------------- shell */
/* One rule governs the whole surface: anything you can press has a *surface*
   (a fill, a 1px edge, a radius); anything that only labels has none — 10.5px
   mono, wide tracking, faint. That is what stops a caption reading as a
   button, which is what made the old bed group look like one big control. */
.cap {
  display: block; line-height: 1; font-family: var(--mono);
  font-size: 10.5px; letter-spacing: .09em; color: var(--faint);
}
.cap.dimmer { color: var(--faint); }
.ico { width: 18px; height: 18px; display: block; fill: none; stroke: currentColor;
       stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.divider { width: 1px; align-self: stretch; flex: none; background: var(--line);
           margin: 0 2px; }

#app { display: flex; flex-direction: column; height: 100%; position: relative; }

.top {
  display: flex; align-items: center; gap: 12px; flex: none; height: 64px;
  padding: 0 20px; padding-top: var(--safe-t);
  border-bottom: 1px solid var(--line);
}
.top h1 { font-size: 17px; }
.ident { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ident h1 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.meta { font-size: 12px; color: var(--dim); margin: 2px 0 0;
        display: flex; align-items: center; gap: 7px; overflow: hidden; }
.meta .who { color: var(--fg); font-size: 12.5px; flex: none; }
.meta .tg { padding: 1px 7px; font-size: 10.5px; flex: none; }
.meta .src { color: var(--faint); font-size: 11px; flex: none; }

.calChip {
  display: inline-flex; align-items: center; gap: 7px; flex: none; height: 32px;
  padding: 0 11px; border-radius: 8px;
  border: 1px solid rgba(110,231,168,.30); background: rgba(110,231,168,.08);
  color: var(--ok);
}
.calChip .ico { width: 13px; height: 13px; }
.calLbl { font-size: 11.5px; color: var(--ok); }
.calChip .mono { font-size: 12.5px; }
/* 播放器唯一会说话的地方。一份定义 —— 从前有两份，第二份赢了，只是因为它俩
   碰巧写的一样才没出事。 */
.state { color: var(--warn); font-size: 12px; flex: none;
         display: inline-flex; align-items: center; gap: 8px; }
.state.bad { color: var(--red); }

/* ---------------------------------------------------------------- stage */
.stage { position: relative; flex: 1; min-height: 0; display: flex; background: var(--stage); }

.rail {
  flex: none; width: 150px; padding: 18px 12px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 11px;
  border-right: 1px solid var(--line); background: var(--bg);
}
.railFoot { margin-top: auto; line-height: 1.7; }
.railHead { display: flex; align-items: center; justify-content: space-between; }
.railToggle { width: 26px; height: 26px; padding: 0; display: inline-flex; align-items: center;
              justify-content: center; border-radius: 6px; color: var(--dim); }
.railToggle .ico { width: 16px; height: 16px; }
.railToggle:hover { color: var(--fg); background: var(--sunk); }
/* 收起：只剩一条窄边和那个按钮，箭头掉头。 */
.rail.closed { width: 40px; padding: 18px 7px; }
.rail.closed .railCap, .rail.closed .sections, .rail.closed .railFoot { display: none; }
.rail.closed .railToggle .ico { transform: rotate(180deg); }
/* A rail, not a list of buttons: the thread and the dots say where you are in
   the song even before you read a word. */
.sections { position: relative; display: flex; flex-direction: column; gap: 5px; }
.sections::before {
  content: ""; position: absolute; left: 5px; top: 14px; bottom: 14px;
  width: 1px; background: var(--raise);
}
.sections button {
  position: relative; display: flex; align-items: center; gap: 8px; width: 100%;
  min-height: 38px; padding: 8px 10px 8px 20px; border-radius: 6px;
  border: 1px solid transparent; background: transparent; color: var(--dim);
  font-size: 12.5px; text-align: left;
}
.sections button::before {
  content: ""; position: absolute; left: 1.5px; top: 50%; transform: translateY(-50%);
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--bg); box-shadow: inset 0 0 0 1.5px var(--line);
}
.sections .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.sections .bn { font-family: var(--mono); font-size: 11px; color: var(--faint); }
.sections button.done { color: var(--faint); }
.sections button.done::before { background: var(--faint); box-shadow: none; }
.sections button.on {
  background: color-mix(in srgb, var(--accent) 13%, transparent); border-color: color-mix(in srgb, var(--accent) 42%, transparent); color: var(--accent);
}
.sections button.on::before { background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
.sections button.on .bn { color: var(--accent); }

.scroller {
  position: relative; flex: 1; min-width: 0;
  overflow-y: auto; overflow-x: hidden; scroll-behavior: smooth;
  overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
.sheet {
  position: relative; width: 88%; margin: 22px auto 45vh;
  /* Always the same paper, in both themes: the score is a printed page and a
     printed page is not a theme. What changes is what holds it up — a shadow
     on the dark ground, an edge as well on the light one, where a white page
     on a pale app would otherwise be one flat field. */
  background: var(--sheet); border-radius: 6px;
  box-shadow: var(--shadow-lg); border: 1px solid var(--sheet-edge);
}
.sheet img.page { display: block; width: 100%; border-radius: 6px; }

.hl, .hlNext, .cursor, .loopband, .barTag, .loopTag {
  position: absolute; left: 0; top: 0; pointer-events: none; will-change: transform;
}
/* Now and next differ in three ways at once — fill, edge style, and a labelled
   tag — because at a metre and a half two tints of the same blue are one tint. */
.hl     { background: color-mix(in srgb, var(--accent) 17%, transparent); border-left: 3px solid color-mix(in srgb, var(--accent) 95%, transparent);
          border-radius: 0 3px 3px 0; }
.hlNext { border: 1.5px dashed rgba(158,198,255,.62); border-radius: 3px; }
.barTag {
  display: flex; align-items: center; gap: 5px; height: 19px; padding: 0 7px;
  border-radius: 5px; font-size: 10.5px; white-space: nowrap;
}
.barTag b { font-family: var(--mono); font-size: 11px; }
.barTag.now  { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
/* 预备拍在数：还没开始打，用提醒色，和「正在打」一眼分开。 */
.barTag.now.pre { background: var(--warn); color: var(--warn-ink); }
/* 第几拍：一拍一个点，当前那拍实心放大。点用字色，所以在「正在打」和「预备」上都看得清。 */
.barTag .beats { display: inline-flex; align-items: center; gap: 3px; margin-left: 2px; }
.barTag .beats:empty { display: none; }
.barTag .beats u { width: 5px; height: 5px; border-radius: 50%; background: currentColor;
                   opacity: .35; text-decoration: none; }
.barTag .beats u.on { opacity: 1; transform: scale(1.4); }
.barTag.next { border: 1px dashed var(--accent-soft); color: var(--accent-soft);
               background: var(--tag-bg); }
.cursor { width: 2.5px; background: var(--red); border-radius: 2px; z-index: 2;
          box-shadow: 0 0 11px rgba(255,92,92,.7); }
.loopband { background: rgba(255,180,84,.085); border-radius: 2px;
            border-left: 2px solid rgba(255,180,84,.75);
            border-right: 2px solid rgba(255,180,84,.75);
            border-top: 2px solid rgba(255,180,84,.55); }
.loopTag { display: flex; align-items: center; gap: 6px; height: 20px; padding: 0 8px;
           border-radius: 5px; background: var(--warn); color: var(--warn-ink);
           font-family: var(--mono); font-size: 11px; font-weight: 600; }

/* 颜色走主题：原来写死 rgba(28,31,38)，浅色主题下成了一块黑底配深色字（2026-09-26 用户）。 */
.pickBanner {
  position: absolute; left: 50%; top: 14px; transform: translateX(-50%); z-index: 20;
  display: flex; align-items: center; gap: 12px;
  background: var(--panel); color: var(--fg); border: 1px solid var(--accent);
  border-radius: 22px; padding: 8px 10px 8px 18px; font-size: 13px;
  box-shadow: var(--shadow-lg); max-width: calc(100% - 28px);
}

/* ------------------------------------------------------------ transport */
.transport {
  flex: none; background: var(--panel); border-top: 1px solid var(--line);
  padding: 12px 20px calc(14px + var(--safe-b));
}

.timeline { position: relative; height: 30px; cursor: pointer; touch-action: none; }
.timeline::before {
  content: ""; position: absolute; left: 0; right: 0; top: 11px;
  height: 8px; border-radius: 4px;
  /* 🔴 不能用 --sunk：它是「比面板更深一层」的意思，而这道槽画在 --panel
     上。浅色下 #f1f5f9 on #ffffff = **1.06:1**，那道槽根本不存在，只剩一个
     蓝头在空白里滑。按 --fg 兑一档，两套配色都自动成立（浅 1.45 / 深 1.69，
     深色原来是 1.17）。 */
  background: color-mix(in srgb, var(--fg) 18%, transparent);
}
/* Section marks give the whole song a shape you can read at a glance. */
.tl-ticks i { position: absolute; top: 11px; width: 2px; height: 8px;
              background: color-mix(in srgb, var(--fg) 38%, transparent); }
.tl-played { position: absolute; left: 0; top: 11px; height: 8px; width: 0;
             border-radius: 4px 0 0 4px; background: var(--accent); }
.tl-loop   { position: absolute; top: 9px; height: 12px;
             background: rgba(255,180,84,.28);
             border-left: 2px solid var(--warn); border-right: 2px solid var(--warn); }
.tl-head   { position: absolute; top: 5px; width: 20px; height: 20px; margin-left: -10px;
             border-radius: 50%; background: var(--fg);
             box-shadow: 0 2px 8px rgba(0,0,0,.55); }

/* 🔴 **底栏必须有一条确定的降级路径。**
 *
 * 原来是 `flex` + 每个区块 `flex: none`，宽度不够时既不换行也不能横滚：
 * 实测 1024 宽的 iPad 横屏上内容要 1182 px，「设循环」和「整首重复」直接被切
 * 出屏幕，而「整首重复」在**当前两代 iPad 横屏上都够不到**。手机上那个
 * 「更多」按钮（`.only-phone`）本来就是给这种情况准备的，但在手机断点以上
 * 是 display:none。
 *
 * 两条一起用：
 *   1. 两个滑块区块可压缩（`knobZone`）—— 滑块不需要 182 px，压一压常常就够了，
 *      一行放得下的时候就该放一行。
 *   2. 压到最小仍然不够就**换行**。代价是底栏变两行、谱面矮一截，
 *      但那好过「有个按钮在屏幕外面」。而且换行是确定的：不依赖谁先谁后。
 *
 * 不选「收进更多菜单」：圈小节循环正是这个产品的主张（「圈住难的那几小节」），
 * 把它埋进二级菜单，等于在主力设备上把主功能藏起来。
 * `_layout.mjs` 在 1024/1180/1194 三个宽度上断言底栏每个可交互元素都在屏幕内。 */
.bar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px;
       min-height: 68px; margin-top: 10px; }
.zone { flex: none; display: flex; flex-direction: column; gap: 7px; }
.knobZone { flex: 1 1 130px; min-width: 112px; max-width: 200px; }
.knobZone .knob, .knobZone input[type="range"] { width: 100%; }

.play {
  width: 56px; height: 56px; flex: none; border-radius: 50%; border: none;
  background: var(--accent); color: var(--accent-ink);
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 28%, transparent);
}
.play svg { width: 21px; height: 21px; }
/* One source of truth for what the button shows. */
.play .icoPlay, .play .icoPause, .play .icoWait { display: none; }
.play[data-state="paused"]  .icoPlay  { display: block; }
.play[data-state="playing"] .icoPause { display: block; }
.play[data-state="busy"]    .icoWait  { display: block; }
.play[data-state="busy"] { background: var(--line); box-shadow: none; }
.icoWait { width: 18px; height: 18px; border-radius: 50%;
           border: 2.5px solid rgba(230,233,239,.28); border-top-color: var(--fg);
           animation: spin .7s linear infinite; }

.readout { flex: none; min-width: 150px; display: flex; flex-direction: column;
           gap: 5px; padding-bottom: 2px; }
.roCaps, .roNums { display: flex; align-items: baseline; gap: 10px; }
#pos { font-size: 25px; font-weight: 600; color: var(--accent); line-height: 1; }
#posTotal, .roClock { font-size: 13px; color: var(--dim); line-height: 1; }
.roClock { font-size: 11.5px; }

/* Beds: three plain buttons. The swatch carries the family colour even when
   the button is off, so the set reads as one choice with three positions. */
.seg { display: flex; gap: 8px; }
.seg button {
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
  min-height: 48px; padding: 7px 13px 8px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--sunk);
  white-space: nowrap; text-align: left; transition: border-color .12s, background .12s;
}
.seg .bh { display: flex; align-items: center; gap: 7px; }
.seg .sw { width: 7px; height: 7px; border-radius: 2px; background: var(--c); opacity: .4; }
.seg b { font-size: 13px; font-weight: 500; color: var(--fg); }
.seg i { font-style: normal; font-size: 11px; color: var(--faint); }
.seg button:hover { border-color: color-mix(in srgb, var(--c) 45%, transparent); }
.seg button.on { border-color: color-mix(in srgb, var(--c) 55%, transparent);
                 background: color-mix(in srgb, var(--c) 12%, transparent); }
.seg button.on .sw { opacity: 1; }
.seg button.on b { color: var(--c); }
.seg button.on i { color: color-mix(in srgb, var(--c) 60%, var(--faint)); }
.seg button[data-bed="demo"]   { --c: var(--bed-demo); }
.seg button[data-bed="orig"]   { --c: var(--bed-orig); }
/* Violet, not orange: amber already means "loop" and red means "playhead", so a
   third warm colour would sit between two things it must never be confused with.
   Violet is the one unclaimed region of the palette, and it carries a hint of
   "the last step" — which is what this bed is. */
.seg button[data-bed="nodrum"] { --c: var(--bed-nodrum); }

.knob { width: 182px; display: flex; flex-direction: column; gap: 6px; }
.knobHead { display: flex; align-items: center; justify-content: space-between; }
.knobVal { font-size: 14px; }
/* 旋钮底下有两种东西，长得不能一样：
     `.knobFoot` —— **刻度标签**，钉在滑轨上某一点（`--at`），说的是「这一档
       是标准值」。它以前和读数一个样式、又是个百分数，于是音量 90% 底下写着
       「标准 100%」，看着像第二个读数。
     `.knobNow`  —— **读数**，说的是「你现在在哪」，居中，不钉在任何一点上。 */
/* 刻度标签只写「这一档叫什么」，**不写数字**。以前写的是「标准 100%」，
   而正上方的读数是 90% —— 一个百分数底下再压一个百分数，读起来就是第二个读数。
   位置本身已经说明它对应哪一档了。（还有一个实际原因：这一格只有六十来像素，
   「标准 100%」放不下，一直是折成两行的。） */
.knobFoot { margin-left: var(--at); color: var(--faint); font-size: .92em;
            white-space: nowrap; }
.knobFoot::before { content: "▏"; opacity: .55; margin-right: 1px; }
.knobNow { text-align: center; color: var(--dim); }
.zoomV { font-size: 12.5px; color: var(--dim); min-width: 34px; text-align: right; }
/* 「谱面」一行要放下跟随播放、移动竖线、缩放和读数；滑块收窄一点，不然读数被挤到
   下一行，整个抽屉在 iPad 横屏上长出可视区（_layout.mjs 钉着「重新校准」要看得见）。 */
#zoom { width: 76px; }
.ghost { width: 26px; height: 26px; border: none; background: transparent;
         color: var(--faint); border-radius: 5px;
         display: inline-flex; align-items: center; justify-content: center; }
.ghost:hover { color: var(--fg); }
.ghost .ico { width: 14px; height: 14px; }

/* The mute button carries both icons and shows one, so the glyph always says
   what pressing it will do rather than what state it is in. */
.mute .off, .mute[data-muted="1"] .on { display: none; }
.mute[data-muted="1"] .off { display: block; }
.mute[data-muted="1"] { color: var(--warn); }
.knob input[type=range] { width: 100%; }

.loopZone { align-items: flex-start; }
.loopRow { display: flex; align-items: center; gap: 8px; min-height: 38px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  height: 36px; padding: 0 11px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--panel);
  font-size: 12px; color: var(--dim);
}
.chip .ico { width: 13px; height: 13px; }
.chip.loop { color: var(--warn); border-color: rgba(255,180,84,.34);
             background: rgba(255,180,84,.09); font-family: var(--mono); cursor: pointer; }
.chip.loop:hover { border-color: var(--warn); }
.btn.on { background: rgba(255,180,84,.14); border-color: var(--warn); color: var(--warn); }

/* --------------------------------------------------------------- sheets */
.overlay, .gate, .loading {
  position: fixed; inset: 0; z-index: 60; display: flex;
  align-items: center; justify-content: center; padding: 24px;
  background: rgba(10,12,16,.86); backdrop-filter: blur(3px);
  /* 🔴 **比窗口高的弹层必须还能够得着。**
     原来这里没有 overflow：`position: fixed` + 居中，内容一旦超过窗口高度，
     露在外面的部分**没有任何办法滚到**——底下的按钮就是点不到，而且页面不报错、
     看起来只是「按钮没反应」。注册框加了邮箱那一栏之后，在 720 px 高的窗口上
     就正好越线：`去注册` 掉到了窗口外面（Playwright 的报错原话是
     `element is outside of the viewport`，重试 58 次后超时）。
     笔记本的浏览器窗口比这更矮是常事。 */
  overflow-y: auto; overscroll-behavior: contain;
}
/* 居中要用 `margin: auto` 而不是只靠 `align-items: center`：内容比容器高时，
   居中会把**顶部**切掉，而被切掉的那一截同样滚不回来。`margin: auto` 在放得下
   的时候照样居中，放不下的时候退化成从头开始，两种情况都够得着。 */
.overlay > *, .gate > * { margin: auto; }

/* ---- 弹层的层级 ----------------------------------------------------
 * 🔴 **同层级时是 DOM 顺序说了算，而 DOM 顺序不是给这件事排的。**
 * 付款窗 `#payWin` 写在 `#gate` 前面，两个都是 z-index 60 —— 于是选完套餐、
 * 订单**真的建出来了**，屏幕上却什么都没变，因为付款窗被付费墙整个盖住。
 * 人的自然反应是再点一次，那就是第二张订单。
 *
 * 所以层级写死，不靠 DOM 顺序。顺序即语义：
 *   60  一般弹层（工单、点歌）
 *   61  付费墙 —— 挡住播放器
 *   62  登录框 —— 从付费墙里点「登录/注册」进来，必须盖住它
 *   63  付款窗 —— 从付费墙里选完套餐进来，同理
 *   64  租约断了 / 正在校准 —— 播放已经停了，它们要压住一切
 *   65  恢复码 —— 它是从登录框里长出来的，而且**必须**盖住登录框：那串码只
 *       出现这一次，让它被别的东西盖住等于把它弄丢了
 *   1000 出了点问题，得刷新一下（#failWin，js/fatal.js 按需建，样式在
 *        controls.css）—— 接不下去时唯一的出口，所以连引导气泡（71/72）和后台
 *        预览横幅（999）都要压住
 * web/tests/browser/_layout.mjs 断言这几个弹层的层级互不相同（否则又回到「看
 * DOM 顺序」）；#failWin 的层级由 _fail.mjs 断言。
 * （这里原来写的是「check.sh 断言」—— check.sh 里并没有这条。） */
#gate        { z-index: 61; }
#authOverlay { z-index: 62; }
#payWin      { z-index: 63; }
#leaseWin,
#calOverlay  { z-index: 64; }
#codeWin     { z-index: 65; }
.loading { background: var(--bg); }
.box {
  width: min(400px, 100%); background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: 26px 24px;
  display: flex; flex-direction: column; gap: 14px;
}
/* `position` 是给右上角那个关闭叉的（`.payX` 是绝对定位）。 */
.authBox { gap: 16px; position: relative; }
.field { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; color: var(--dim); }
/* The :not() chain matches the base field rule's specificity; without it
   this never applies. See web/tests/specificity.mjs. */
.field input:not([type=checkbox]):not([type=radio]):not([type=range]) { width: 100%; }
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.sheet-panel {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 50;
  max-height: min(78vh, 620px); overflow-y: auto;
  background: var(--panel); border-top: 1px solid var(--line);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: 18px 20px calc(20px + var(--safe-b));
  box-shadow: 0 -18px 48px rgba(0,0,0,.5);
  display: flex; flex-direction: column; gap: 14px;
}
.sheetHead { display: flex; align-items: center; justify-content: space-between; }
.grp { display: flex; flex-direction: column; gap: 8px; }
.grp h4 { font-size: 12px; color: var(--faint); letter-spacing: .06em; font-weight: 500; }
.hint { color: var(--faint); font-size: 11.5px; margin: 0; }

/* -------------------------------------------------------------- loading */
.loadBox { width: min(420px, 100%); display: flex; flex-direction: column; gap: 22px; }
.loadBox h1 { font-size: 20px; }
.loadBar { height: 6px; border-radius: 3px; background: var(--panel); overflow: hidden; }
.loadFill { width: 0; height: 100%; border-radius: 3px;
            background: linear-gradient(90deg, var(--accent), var(--accent-soft));
            transition: width .18s ease-out; }
.loadNums { display: flex; justify-content: space-between; margin-top: -14px;
            font-family: var(--mono); font-size: 11.5px; color: var(--faint); }
.steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px; }
.steps li { display: flex; align-items: center; gap: 12px; font-size: 13.5px; opacity: .45; }
.steps li span { flex: 1; }
.steps li b { font-family: var(--mono); font-size: 11.5px; color: var(--dim); font-weight: 400; }
.steps li .dot { width: 16px; height: 16px; flex: none; border-radius: 50%;
                 border: 2px solid var(--faint); }
.steps li.doing { opacity: 1; }
.steps li.doing .dot { border-color: var(--accent); border-right-color: transparent;
                       animation: spin .8s linear infinite; }
.steps li.done { opacity: 1; }
.steps li.done .dot { border-color: var(--ok); background: var(--ok); }
@keyframes spin { to { transform: rotate(360deg); } }

/* -------------------------------------------------------------- library */
#library { height: 100%; display: flex; flex-direction: column; }
#library .top { border-bottom: 1px solid var(--line); }
.acct { display: flex; align-items: center; gap: 12px; }

/* The song card, defined once and for every width.
   
   It used to be defined twice: this block, and a redesigned copy inside
   `@media (min-width: 821px)` that never replaced it. On desktop both applied
   and the loser won wherever its selector was longer — `.card .tags` beat
   `.tags`, so the redesign's "chips never wrap" was silently overruled and a
   song with a long tempo came out a row taller than the rest. On a phone only
   this block applied, to markup it predates.
   
   One component, one definition. Only the grid's column count is a question
   the width gets to answer. */
.libScroll { flex: 1; min-height: 0; overflow-y: auto; }
.songs {
  list-style: none; margin: 0;
  padding: 6px 20px 28px; display: grid; gap: 18px; align-content: start;
  grid-template-columns: repeat(auto-fill, minmax(304px, 1fr));
}
.libEmpty { color: var(--dim); padding: 8px 20px 28px; margin: 0; }

.card {
  display: flex; flex-direction: column; min-height: 318px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden; color: inherit; text-decoration: none;
  transition: border-color .14s, transform .14s;
}
a.card:hover { border-color: var(--accent); transform: translateY(-1px); }

/* The thumbnail is a drawn staff, not the real page: it works for a locked
   song too, and costs no request. */
.thumb { position: relative; height: 190px; background: var(--paper);
         overflow: hidden; border-bottom: 1px solid var(--line); }
.thumb .pg {
  position: absolute; left: 7%; right: 7%; top: 20px; padding: 26px 0 14px;
  background: #f7f7f5; border-radius: 3px; box-shadow: 0 10px 26px rgba(0,0,0,.5);
  display: flex; flex-direction: column; gap: 20px;
}
.thumb .sys { position: relative; height: 26px; margin: 0 12px;
              border-top: 1px solid #c9c9c4; border-bottom: 1px solid #c9c9c4;
              background: repeating-linear-gradient(#c9c9c4 0 1px, transparent 1px 6.5px); }
/* The tempo mark sits where an engraver puts it: above the first system,
   flush left. It is the song's real tempo_display, not decoration. */
.thumb .tmark { position: absolute; left: 12px; top: 8px; font-style: normal;
                font-family: var(--mono); font-size: 11px; color: #4a4a46; }
/* A real time signature on the first system — two digits stacked over the
   middle of the staff, which is what makes one card unmistakably 6/8. */
.thumb .sig { position: absolute; left: 8px; top: -2px; display: flex;
              flex-direction: column; line-height: .92; font-weight: 700;
              font-size: 13px; color: #2f2f2c; }
.thumb .sig em { font-style: normal; }

/* The real cover sits on top of the drawn one and only becomes visible once
   it has decoded, so a slow or missing image degrades to the drawing rather
   than to an empty box. */
.thumb .cov { position: absolute; inset: 0; width: 100%; height: 100%;
              object-fit: cover; object-position: top center;
              opacity: 0; transition: opacity .18s; }
/* Not `.shot`: that is the landing page's drawn screenshot, and sharing the
   class gave every cover its 76px drop shadow — the grey smear under locked cards. */
.thumb.hasCov .cov { opacity: 1; }
.thumb.hasCov .pg { display: none; }
.card.locked .thumb .cov { opacity: .82; filter: saturate(.55); }

/* Every card is the same height because every card is the same SHAPE: one
   line of title, one of artist, one row of chips, one foot. Not because a
   height was pinned on — pinning the box only moves the problem to what gets
   cut off inside it.
   
   Two things used to vary. A song with no artist collapsed that line and came
   out shorter; a long tempo ("♩=86 → 172") wrapped the chips onto a second row
   and came out taller. So the artist line is reserved whether or not there is
   an artist, and the chips never wrap. */
.cbody { flex: 1; display: flex; flex-direction: column; gap: 12px; padding: 15px 17px 16px; }
.cbody h2 { font-size: 17px; white-space: nowrap; overflow: hidden;
            text-overflow: ellipsis; }
/* line-height and min-height are the same number on purpose: a song with no
   artist has to occupy exactly the line a song with one occupies, or the card
   comes out shorter and the row stops lining up. */
.cbody .ar { font-size: 12.5px; color: var(--dim); margin: 3px 0 0;
             line-height: 1.35; min-height: 1.35em;
             white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tags { display: flex; flex-wrap: nowrap; gap: 6px; margin: 0; min-width: 0; }
/* Tags are colour-coded by what they say, not by the song. The hues are the
   player's own: blue reads as position, green as the demo bed, violet as the
   part you play. Duration keeps the neutral chip. */
.tg { font-family: var(--mono); font-size: 11.5px; color: var(--dim);
      background: var(--sunk); border: 1px solid var(--line);
      border-radius: 4px; padding: 3px 8px;
      flex: 0 0 auto; white-space: nowrap; }
/* The tempo is the only chip whose width is not bounded by its own meaning —
   a metre is four characters, a bar count four digits, a duration four. So it
   is the one allowed to shrink, and the only one that can ever ellipsise. The
   full text stays on `title`. */
.tg[data-k="tempo"] { flex: 0 1 auto; min-width: 0; overflow: hidden;
                      text-overflow: ellipsis; }
/* 底色和描边从各自那支色兑出来，别再写死 —— 写死的那三支是给深色调的，
   浅色下和文字色不是一家人。 */
.tg[data-k="meter"] { color: var(--tg-meter);
                      border-color: color-mix(in srgb, var(--tg-meter) 28%, transparent);
                      background: color-mix(in srgb, var(--tg-meter) 10%, transparent); }
.tg[data-k="tempo"] { color: var(--tg-tempo);
                      border-color: color-mix(in srgb, var(--tg-tempo) 26%, transparent);
                      background: color-mix(in srgb, var(--tg-tempo) 9%, transparent); }
.tg[data-k="bars"]  { color: var(--tg-bars);
                      border-color: color-mix(in srgb, var(--tg-bars) 28%, transparent);
                      background: color-mix(in srgb, var(--tg-bars) 10%, transparent); }
/* 🔴 **锁着的歌，这三个 chip 以前是全灰的。**
   那等于让颜色去编码「买没买」，而它本来编码的是「这是哪一类信息」——
   拍号是蓝的、速度是绿的、小节数是紫的，在播放器顶栏里也是这三支色。
   同一个 4/4 在免费歌上是蓝的、在收费歌上是灰的，人得先想一下它们是不是
   一回事。锁没锁由卡片底色、缩略图、和页脚那个「锁」说，不归它们管。 */
.cfoot { margin-top: auto; display: flex; align-items: center;
         justify-content: space-between; gap: 10px; font-size: 11.5px; color: var(--faint); }
.cfoot .free { color: var(--ok); background: rgba(110,231,168,.11);
               border-radius: 4px; padding: 3px 8px; }
.cfoot .lock { color: var(--dim); background: var(--sunk); border-radius: 4px; padding: 3px 8px; }

/* A locked card is the same card. The price chip in its foot is what says
   it is locked; greying the whole card read as dirty, and pushed the tags
   under 4.5:1. The cover is only slightly muted. */
.card.locked .thumb .pg { opacity: .6; }

.only-phone { display: none; }

/* --------------------------------------------------------------- phone */
@media (max-width: 820px) {
  .top { padding-left: 14px; padding-right: 14px; gap: 10px; }
  .transport { padding: 10px 14px calc(12px + var(--safe-b)); }

  /* The rail becomes a scrolling chip strip under the score. */
  .stage { flex-direction: column; }
  .rail {
    order: 2; width: auto; flex: none; border-right: none;
    border-top: 1px solid var(--line); background: var(--bg);
    padding: 10px 14px; overflow-x: auto; overflow-y: hidden;
  }
  .railCap { display: none; }
  /* 窄屏上段落本来就是底下一排，不用收起。 */
  .railHead { display: none; }
  .rail.closed { width: auto; padding: 10px 14px; }
  .rail.closed .sections { display: flex; }
  .sections { flex-direction: row; gap: 8px; }
  /* 段落名一个字一行地折下去（竖屏截图里的「数/拍」）：不许折。 */
  .sections button, .sections button * { white-space: nowrap; }
  .sections button { min-height: 40px; border-radius: 20px; padding: 0 16px;
                     display: inline-flex; align-items: center; }
  .scroller { order: 1; }
  .sheet { width: 96%; margin: 12px auto 30vh; }

  /* One line at a time: a full page scaled to 390px makes a system ~35px
     tall, with bar hit targets a few pixels wide. */
  .sheet.oneSystem { margin-bottom: 12px; overflow: hidden; }

  .bar { gap: 10px; flex-wrap: wrap; }
  .readout { min-width: 0; flex: 1; }
  .pos { font-size: 17px; }
  .pod { display: none; }
  .only-phone { display: inline-flex; }
  .play { width: 60px; height: 60px; }
  .songs { grid-template-columns: 1fr; padding: 18px 14px 32px; }
  .hint { display: none; }
}

/* ------------------------------------------------------------- auth form */
/* `.payX` 是绝对定位到盒子右上角的，所以标题这一行不用再给它留位置。 */
/* 「三个格子都填对了，按钮还是灰的」—— 缺的那一步得看得见。 */
.agree.needs { color: var(--warn); }
.agree.needs input[type=checkbox] { outline: 2px solid var(--warn); outline-offset: 3px; }
.authHead { display: flex; align-items: center; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field > span { font-size: 12.5px; color: var(--dim); }
.fieldMsg { display: block; min-height: 15px; font-size: 11.5px; font-style: normal;
            color: var(--faint); }
.fieldMsg.ok  { color: var(--ok); }
.fieldMsg.bad { color: var(--red); }
.fieldMsg.ok::before { content: "✓ "; }

/* 验证码那一行：输入框和「发验证码」并排。按钮不收缩，否则在窄屏上它会被压成
   两行字，看起来像坏了。 */
.codeRow { display: flex; gap: 8px; align-items: center; }
.codeRow input:not([type=checkbox]):not([type=radio]) { flex: 1 1 auto; min-width: 0; }
.codeRow .btn { flex: 0 0 auto; white-space: nowrap; }
/* 个人中心里的绑定邮箱那一块。 */
.mailRow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.mailRow input:not([type=checkbox]):not([type=radio]) { flex: 1 1 220px; min-width: 0; }

.pwWrap { position: relative; display: block; }
/* Room for the show/hide button. Needs the same :not() chain to outrank
   the base `padding: 0 12px`, or the password runs under the eye. */
.pwWrap input:not([type=checkbox]):not([type=radio]):not([type=range]) { padding-right: 44px; }
.pwEye {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 36px; height: 36px; border: none; background: transparent;
  color: var(--faint); display: inline-flex; align-items: center; justify-content: center;
}
.pwEye:hover, .pwEye.on { color: var(--accent); }
.pwEye svg { width: 17px; height: 17px; }

.linkBtn { border: none; background: transparent; color: var(--accent);
           font-size: 13px; padding: 6px; min-height: 34px; }
.linkBtn:hover { text-decoration: underline; }
/* 夹在一句话里的那种：不该有按钮的行高和内边距，否则那一行会被撑开。 */
.linkBtn.inline { padding: 0; min-height: 0; font-size: inherit;
                  text-decoration: underline; }

/* ---------------------------------------------------------------- picking */
.stage { position: relative; }
.btn.on { background: rgba(255,180,84,.14); border-color: var(--warn); color: var(--warn); }
.chip.loop { cursor: pointer; }
.chip.loop:hover { border-color: var(--warn); }

/* The play button's third state: mid-switch is neither playing nor stopped. */
.play[data-state="busy"] { background: var(--line); }
.icoWait { width: 18px; height: 18px; border-radius: 50%;
           border: 2.5px solid rgba(230,233,239,.28); border-top-color: var(--fg);
           animation: spin .7s linear infinite; }

/* ------------------------------------------------------------------ tour */
.tourMask { position: fixed; inset: 0; z-index: 70; background: rgba(8,10,13,.62); }
.tourPop {
  position: fixed; z-index: 71; width: min(320px, calc(100vw - 32px));
  background: var(--panel); border: 1px solid var(--accent); border-radius: var(--r-lg);
  padding: 16px 18px; box-shadow: 0 20px 50px rgba(0,0,0,.6);
  display: flex; flex-direction: column; gap: 8px;
}
.tourPop b { font-size: 15px; }
.tourPop p { margin: 0; color: var(--dim); font-size: 13px; line-height: 1.65; }
.tourFoot { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.tourStep { font-family: var(--mono); font-size: 11.5px; color: var(--faint);
            white-space: nowrap; }
.tourNext { height: 38px; padding: 0 16px; font-size: 13.5px; }
.tourSpot { position: relative; z-index: 72;
            box-shadow: 0 0 0 3px var(--accent), 0 0 0 9px color-mix(in srgb, var(--accent) 22%, transparent);
            border-radius: 10px; }

/* The settings sheet used to cover the whole screen, so you could not see what
   any of it applied to. On a wide screen it now docks to the side. */
@media (min-width: 821px) {
  /* Anchored under whatever opened it: a panel summoned from the top-right
     that rises from the bottom of the screen reads as unrelated to the press. */
  .sheet-panel.anchored {
    left: auto; bottom: auto; width: 400px;
    /* 内容必须**装得下**，不能靠滚动：macOS 和 iPadOS 的悬浮滚动条不显示，
       所以被裁掉的部分看上去就是「面板到这儿就完了」。裁掉的偏偏是
       「节拍器对齐」那一节的滑块和「重新校准」按钮 —— 而它上面那句话正写着
       「拖一下这个，或者重测一次」，指着一个看不见的控件。
       `_layout.mjs` 断言那个按钮在可视区内。 */
    max-height: min(76vh, 600px); border-radius: var(--r-lg);
    border: 1px solid var(--line);
    box-shadow: 0 18px 48px rgba(0,0,0,.55);
  }
  .sheet-panel:not(.anchored) {
    left: auto; right: 16px; bottom: 92px; width: 400px;
    max-height: min(64vh, 540px); border-radius: var(--r-lg);
  }
}

/* ============================================================== library ==
   A catalogue you can scan: each card shows the shape of the score, and the
   facts a drummer picks on — metre, tempo, how long, how many bars.
   ======================================================================= */
#library { height: 100%; display: flex; flex-direction: column; }
.libHead { display: flex; align-items: baseline; gap: 12px; }
.libHead h1 { font-size: 17px; }
.libTag { font-size: 12px; color: var(--dim); }

.libBar {
  flex: none; display: flex; align-items: center; gap: 10px;
  padding: 16px 20px 14px;
}
.search {
  display: flex; align-items: center; gap: 9px; width: 300px; height: 44px;
  padding: 0 12px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--sunk); color: var(--faint);
}
.search .ico { width: 17px; height: 17px; flex: none; }
/* The :not() chain is not decoration — it is the only way this wins.
   The base input rule is `input:not([type=checkbox]):not([type=radio])
   :not([type=range])`, which scores (0,3,1); a plain `.search input` scores
   (0,1,1) and loses, so the input kept its own 44px border inside the search
   box's border and drew a box inside a box. */
.search input:not([type=checkbox]):not([type=radio]):not([type=range]) {
  border: none; background: none; height: auto; padding: 0; flex: 1; width: auto;
}
/* 聚焦环画在**外面这个框**上，不是里面那个输入框上。里面的 input 是无边框的，
   给它画环等于在框里再套一个环——和上面「框里套框」是同一个毛病换了个形状。
   原来这里只有 `.search input:focus-visible { box-shadow: none }`，(0,1,1)，
   输给基础规则；它一直没生效也没被发现，是因为基础的聚焦规则当时也没生效，
   两个都不画，看起来就对了。现在基础规则修好了，这条必须跟着写成同一形状，
   否则搜索框一聚焦就冒出个环。 */
.search input:not([type=checkbox]):not([type=radio]):not([type=range]):focus-visible {
  box-shadow: none; border-color: transparent;
}
.search:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

/* One strip per dimension, stacked: "4/4" and "中等" must never share a line,
   or they read as alternatives to each other. */
.filters { display: flex; flex-direction: column; gap: 8px; }
.fstrip { display: flex; gap: 8px; flex-wrap: wrap; }
.fchip {
  height: 36px; padding: 0 13px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--panel); color: var(--dim);
  font-size: 12.5px; display: inline-flex; align-items: center; gap: 6px;
}
.fchip .mono { font-size: 11px; color: var(--faint); }
.fchip:hover { border-color: var(--accent); }
.fchip.on { background: color-mix(in srgb, var(--accent) 13%, transparent); border-color: color-mix(in srgb, var(--accent) 45%, transparent);
            color: var(--accent); }
.fchip.on .mono { color: var(--accent); }



.acct { display: flex; align-items: center; gap: 12px; flex: none; }
.acctIn { display: flex; align-items: center; gap: 12px; }
/* 🔴 **这是全站进个人中心的唯一入口，而它以前就是一段粗体字** —— 没有下划线、
   没有边框、没有 hover，谁也想不到那里能点。所以给它一个能点的样子：
   一个有边界的小块，右边一个尖角。 */
.acctWho { display: flex; align-items: center; gap: 7px; text-align: right;
           line-height: 1.3; color: var(--fg); text-decoration: none;
           padding: 4px 8px 4px 10px; border-radius: var(--r);
           border: 1px solid transparent; }
.acctWho:hover { border-color: var(--line); background: var(--sunk); }
.acctWho:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.acctWho::after { content: "›"; color: var(--faint); font-size: 15px;
                  line-height: 1; }
/* 在个人中心里它指向自己，所以不是链接 —— 但名字和套餐还在，位置也不变。
   以前那里换成了一个不可点的 <span>，连「免费」那个 chip 都一起没了，
   于是同一个位置在两页之间少了一样东西。 */
.acctWho.here { border-color: var(--line); background: var(--sunk); }
.acctWho.here::after { content: none; }
.acctWho b { font-size: 13.5px; }
.acctPlan { display: block; color: var(--ok); font-size: 11.5px; }

@media (max-width: 820px) {
  .libBar { flex-wrap: wrap; padding: 12px 14px; }
  .search { width: 100%; }
  .libBar .divider, #libStats { display: none; }
  .songs { grid-template-columns: 1fr; padding: 6px 14px 28px; }
  .libTag { display: none; }
}

/* ============================================================== landing ==
   The front door. States the offer in one sentence, shows the thing, and asks
   for one action — repeated at the foot, not competing at the top.
   ======================================================================= */
#landing { position: relative; height: 100%; overflow-y: auto; overscroll-behavior: contain; }
.glow {
  position: absolute; left: 50%; top: -260px; width: 900px; height: 520px;
  margin-left: -450px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 10%, transparent), transparent);
}

.lTop { position: relative; max-width: 1180px; margin: 0 auto;
        padding: calc(20px + var(--safe-t)) 28px 20px;
        display: flex; align-items: center; justify-content: space-between; gap: 16px; }
/* ---- 品牌标识 ------------------------------------------------------
 * 一个组件，全站复用：落地页导航、曲库/个人中心/播放器的顶栏、登录框、
 * 付费墙、加载页、四个法务页、404/50x。**每一处都是同一段标记、同一套尺寸**，
 * 因为「logo 在每一页都在同一个位置、同样大小」正是它读起来像一家公司
 * 而不是一堆页面的原因。
 *
 * 三档：
 *   .brand            —— 图标 + 字，默认尺寸，用在顶栏
 *   .brand.lg         —— 大一号，用在落地页（那里它是主角）
 *   .brand.stack      —— 竖排居中，用在登录框 / 加载页 / 404 这类居中的版面
 * 只要图标就给 <a class="brand mark-only">，字用 <b> 包着由 CSS 隐藏。
 */
/* 品牌图上的是**横版组合**：兔子 + 邦尼稳拍 + BUNNY TEMPO，是一整块，
   不是「一个小图标加两个字」。所以这里的默认形态也是那一块 —— 中文名和
   英文名上下叠在图标右边，图标给足尺寸。

   尺寸走一个变量，几何只在一条规则里写。三档各写一遍 width/height 的话，
   是三条规则争同一组属性，靠特指度排序才对；那种写法能跑，但下一个人加
   第四档时很容易写出一条赢不了的规则，而它的表现是「改了没反应」。
   check.sh 的 UI 检查就是拦这个的。 */
.brand { --mark: 40px;
         display: inline-flex; align-items: center; gap: 11px; font-size: 17px;
         color: var(--fg); text-decoration: none; }
.brand.lg { --mark: 52px; gap: 13px; font-size: 20px; }
.brand.stack { --mark: 64px; flex-direction: column; gap: 10px; font-size: 18px;
               text-align: center; }
/* 名字那一叠：中文在上，英文压小、加字距在下 —— 品牌图上就是这个关系。 */
.brand .bWord { display: flex; flex-direction: column; gap: 1px; line-height: 1.15; }
.brand .bWord b { font-weight: 700; letter-spacing: -.01em; }
.brand .bWord i { font-style: normal; font-size: .58em; font-weight: 600;
                  letter-spacing: .2em; color: var(--faint);
                  text-transform: uppercase; }
.brand.stack .bWord { align-items: center; gap: 3px; }
.brand.markOnly .bWord { display: none; }
/* 顶栏里 logo 和页面标题之间的竖线 —— 分开「这是谁」和「这是哪一页」。 */
.brandSep { width: 1px; height: 20px; background: var(--line); flex: none; }
/* 窄屏上只留图标：顶栏里还有返回、主题、报错等按钮，字先让位。 */
@media (max-width: 720px) {
  .top .brand .bWord { display: none; }
  .top .brandSep { display: none; }
}
/* 居中版面里的竖排标识：登录框、付费墙、付款窗口、加载页。
   `align-self: center` 而不是给父级加 align-items —— 那几个框里的表单
   本来是左对齐的，动父级会把每一个输入框也拽到中间。 */
.brand.stack { align-self: center; margin: 0 0 16px; }
.loadBrand { margin-bottom: 20px; }
.paybox .brand.stack { margin-top: 6px; }
/* 导航栏那个 logo 是一张全彩的图（img/icon.svg），不是描边图标，
   所以这里不给它染色 —— `color` 对 <img> 没有作用，留着只会让下一个人
   以为改这里能改 logo 的颜色。
   🔴 用的是 icon.svg（自带奶油色底板）而不是 mark.svg：这套 logo 的兔子
   身体没有填充（71% 的像素透明），在深色导航栏上会糊成一团。底板不是装饰，
   是这张图成立的前提。圆角对上底板自己的 rx，免得看出两层。 */
.brand .ico { width: var(--mark); height: var(--mark);
              border-radius: calc(var(--mark) * .22); }
.lNav { display: flex; align-items: center; gap: 8px; }

.hero { position: relative; max-width: 1180px; margin: 0 auto;
        padding: clamp(20px, 4vh, 44px) 28px 0; }
.heroText { max-width: 780px; margin: 0 auto; text-align: center; }
.hero .heroCta { justify-content: center; }
.hero .heroFacts { display: flex; flex-direction: column; align-items: center; }
.hero .hfSongs { justify-content: center; }
/* 演示图单独一块，宽一点，居中。 */
.shotWrap { position: relative; max-width: 1000px; margin: 0 auto;
            padding: clamp(28px, 5vh, 52px) 28px 0; }
.heroText h1, .heroH { font-size: clamp(23px, 3vw, 32px); line-height: 1.24;
               letter-spacing: -.022em; text-wrap: balance; margin: 0 0 14px; }
/* ---- 第一屏：一块地方回答四个问题 -------------------------------
 * 顺序是定死的，因为这就是一个人做决定的顺序：
 *   标识（这是谁）→ eyebrow（这是什么、在哪用）→ 标题（它替我做什么）
 *   → 一句机制（凭什么信）→ 按钮（怎么开始）→ 一行「不会亏」
 *   → 曲名和价格（有没有我要的、多少钱）
 * 按钮**紧跟在机制那一句后面**，而不是压在最底下：读到那儿就是决定的时刻。 */
/* 居中的第一屏：标识 → 名字 → 标语 → 说明 → 按钮 → 事实。
   标语排在产品说明之前，因为它本身是一句主张（「稳住节奏，跟上每一拍」），
   不是装饰；而下面那一串仍然按做决定的顺序走。 */
/* 深色下不再垫浅色底板：theme.js 会把 src 换成 logo-dark.svg —— 那一版是
   米色线稿，直接站在深底上（见 scripts/make-dark-logo.py）。 */
.heroMark { width: clamp(104px, 13vw, 148px); height: auto; display: block;
            margin: 0 auto 18px; }
.heroName { font-size: clamp(34px, 5.6vw, 54px); line-height: 1.06; margin: 0;
            letter-spacing: .015em; font-weight: 800; }
.heroEn { margin: 9px 0 0; font-size: clamp(11px, 1.4vw, 14px); font-weight: 600;
          letter-spacing: .34em; text-transform: uppercase; color: var(--faint); }
.heroTag { margin: 16px 0 0; font-size: clamp(16px, 2.1vw, 21px);
           color: var(--fg); letter-spacing: .06em; font-weight: 600; }
/* 标语和产品说明之间的分隔：一条短线，说明「上面是它是谁，下面是它做什么」。 */
.heroTag + .eyebrow { margin-top: 34px; padding-top: 30px; position: relative; }
.heroTag + .eyebrow::before { content: ""; position: absolute; top: 0; left: 50%;
  transform: translateX(-50%); width: 44px; height: 1px; background: var(--line); }

.eyebrow { margin: 0 0 12px; font-size: 13px; font-weight: 600;
           letter-spacing: .04em; color: var(--accent); }
.lede b { color: var(--fg); }

/* 主按钮要大。它是这一页唯一要人做的动作，做小了等于把决定往后推。 */
.primary.big, .btn.big { font-size: 16px; padding: 14px 26px; border-radius: 11px; }

/* 曲名和价格。曲名做成可点的小片 —— 看见自己想练的那首，比看见「5 首」有用。 */
.heroFacts { margin: 20px 0 0; }
.hfSongs { display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
           font-size: 12.5px; }
.hfLead { color: var(--faint); }
.hfSong { color: var(--dim); text-decoration: none; border: 1px solid var(--line);
          border-radius: 999px; padding: 3px 10px; background: var(--panel); }
.hfSong:hover { border-color: var(--accent); color: var(--accent); }
.hfSong.free { border-color: color-mix(in srgb, var(--ok) 45%, transparent);
               color: var(--ok); }
.hfPrice { margin: 10px 0 0; font-size: 12.5px; color: var(--faint); }

/* 四块特点的序号：它们是**一次练习的先后**，不是并列的功能点，
   所以给编号，让人从左往右读。 */
.featN { position: absolute; top: 14px; right: 16px;
         font-size: 12px; font-weight: 700; color: var(--faint);
         font-variant-numeric: tabular-nums; }

/* 收尾的按钮。四块之后、解释之前 —— 那是说服刚好结束的位置。 */
.closing { position: relative; max-width: 720px; margin: 0 auto;
           padding: clamp(40px, 6vh, 64px) 28px 0; text-align: center; }
.closing p { margin: 0 0 18px; color: var(--dim); font-size: 15px; }

/* 差异化那一段。窄一点、居中，读起来像一句解释而不是又一个模块。 */
.why { position: relative; max-width: 720px; margin: 0 auto;
       padding: 52px 28px 0; text-align: center; }
.why p { margin: 0 0 10px; color: var(--dim); font-size: 14.5px; line-height: 1.85; }
.why p b { color: var(--fg); }
.why p:first-child { font-size: 16px; }
/* 🔴 **`margin` 简写要写在 `margin-left/right` 后面就等于把它们抹掉。**
   这条原来是：`max-width:700px; margin-left:auto; margin-right:auto;
   margin:18px 0 0; max-width:44ch` —— 简写把左右两个 auto 重置成 0，于是这一段
   在居中的 hero 里**整块靠左**，实测偏 175px。两个 max-width 也是重复的。
   一条声明只写一次，居中靠 `margin: 18px auto 0` 里那个 auto。 */
.lede { color: var(--dim); font-size: 15.5px; line-height: 1.8;
        max-width: 44ch; margin: 18px auto 0; text-wrap: pretty; }
.heroCta { display: flex; gap: 10px; margin-top: 28px; }
.heroCta .primary { height: 50px; padding: 0 26px; font-size: 15px; }
.heroBtn { height: 50px; padding: 0 22px; }
.fine { color: var(--faint); font-size: 12.5px; margin: 14px 0 0; }

/* Drawn, not screenshotted: no asset to host and it cannot fall out of date. */
.shot { background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
        overflow: hidden; box-shadow: 0 30px 76px rgba(0,0,0,.55); }
.shotBar { display: flex; gap: 6px; padding: 11px 14px; border-bottom: 1px solid var(--line); }
.shotBar i { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.shotBody { display: flex; height: 246px; background: var(--paper); }
.shotRail { flex: none; width: 78px; padding: 14px 10px; background: var(--bg);
            border-right: 1px solid var(--line);
            display: flex; flex-direction: column; gap: 6px; }
.shotRail i { height: 20px; border-radius: 5px; background: var(--raise); }
.shotRail i.on { background: color-mix(in srgb, var(--accent) 20%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 50%, transparent); }
.shotPaper { position: relative; flex: 1; margin: 14px 16px; padding: 10px 0;
             background: #f7f7f5; border-radius: 4px; box-shadow: 0 10px 26px rgba(0,0,0,.4);
             display: flex; flex-direction: column; justify-content: space-around; }
.shotPaper .sys { height: 22px; margin: 0 14px;
                  border-top: 1px solid #c9c9c4; border-bottom: 1px solid #c9c9c4;
                  background: repeating-linear-gradient(#c9c9c4 0 1px, transparent 1px 5.5px); }
.shotLoop { position: absolute; left: 6%; top: 44%; width: 90%; height: 19%;
            background: rgba(255,180,84,.09);
            border-left: 2px solid rgba(255,180,84,.7); border-right: 2px solid rgba(255,180,84,.7); }
.shotNow  { position: absolute; left: 51.4%; top: 45.5%; width: 21.9%; height: 16%;
            background: color-mix(in srgb, var(--accent) 18%, transparent); border-left: 3px solid color-mix(in srgb, var(--accent) 90%, transparent); }
.shotNext { position: absolute; left: 73.3%; top: 45.5%; width: 21.9%; height: 16%;
            border: 1.5px dashed rgba(158,198,255,.6); border-radius: 3px; }
.shotCur  { position: absolute; left: 61.3%; top: 43.5%; width: 2px; height: 20%;
            background: var(--red); box-shadow: 0 0 9px rgba(255,92,92,.7); }
.shotFoot { padding: 12px 16px 15px; border-top: 1px solid var(--line); }
.shotTl { position: relative; height: 8px; border-radius: 4px; background: var(--sunk); }
.shotTl .p { position: absolute; left: 0; width: 34%; height: 8px;
             border-radius: 4px 0 0 4px; background: var(--accent); }
.shotTl .l { position: absolute; left: 30%; width: 14%; height: 8px;
             background: rgba(255,180,84,.3);
             border-left: 2px solid var(--warn); border-right: 2px solid var(--warn); }
.shotTl .h { position: absolute; left: 34%; top: -5px; margin-left: -9px;
             width: 18px; height: 18px; border-radius: 50%; background: var(--fg); }
.shotCtl { display: flex; align-items: center; gap: 12px; margin-top: 14px; }
.shotPlay { width: 40px; height: 40px; flex: none; border-radius: 50%;
            background: var(--accent); color: var(--accent-ink);
            display: inline-flex; align-items: center; justify-content: center; }
.shotPlay svg { width: 15px; height: 15px; }
.shotRead { display: flex; flex-direction: column; gap: 3px; flex: none; }
.shotRead b { font-size: 17px; font-weight: 600; color: var(--accent); line-height: 1; }
.shotRead b em { font-style: normal; font-size: 11px; color: var(--dim); font-weight: 400; }
.shotRead i { font-style: normal; font-size: 10px; color: var(--faint); line-height: 1; }
.shotBeds { display: flex; gap: 6px; }
.shotBeds u { display: block; width: 46px; height: 30px; border-radius: 7px;
              border: 1px solid var(--line); background: var(--sunk); }
.shotBeds u.a { border-color: rgba(110,231,168,.5); background: rgba(110,231,168,.12); }

.feats { position: relative; max-width: 1180px; margin: 0 auto; padding: 56px 28px 0;
         display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(238px, 1fr)); }
.feat { position: relative; background: var(--panel); border: 1px solid var(--line);
        border-radius: var(--r-lg); padding: 18px; }
.feat .ico { width: 20px; height: 20px; color: var(--accent); }
.feat h3 { font-size: 15px; margin: 12px 0 6px; }
.feat p { margin: 0; color: var(--dim); font-size: 13px; line-height: 1.68; }

.lFoot { position: relative; max-width: 1180px; margin: 0 auto;
         padding: 48px 28px calc(56px + var(--safe-b)); text-align: center; }

@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; gap: 30px; padding-top: 14px; }
  .lTop, .hero, .feats, .lFoot { padding-left: 18px; padding-right: 18px; }
  .heroCta .primary, .heroBtn { flex: 1; }
  .feats { padding-top: 40px; }
  .glow { display: none; }
}

/* 付费 */
/* 🔴 **付费墙必须在 1024×768 里装得下**，那是最小支持尺寸（iPad 横屏）。
 *
 * 原来盒子沿用 `.box` 的 400 px 宽，四张套餐卡于是落成 2×2，整块高 1184 px
 * —— 在任何一种 iPad 横屏上都装不下，被切掉的偏偏是「登录 / 注册」那个按钮，
 * 也就是转化漏斗上最关键的一次点击。而两边全是空白：1440 宽的屏上这个 400 px
 * 的盒子左右各空五百多。
 *
 * 所以最省的一刀是版式而不是删功能：把盒子放宽，四张卡排成一行，
 * 高度立刻掉一半。`_layout.mjs` 在四个尺寸上断言整块都在屏幕里。 */
#gate .box { width: min(960px, 100%); gap: 10px; padding: 20px 24px; }
/* 两个码并排。仍然是两个独立的框（各自的语义不一样），只是不再各占一行。 */
/* 付费墙标题里的歌手名：比歌名弱一档，但要在同一行 —— 「买的是哪一首」是
   一个事实，拆成两行读起来像两件事。 */
.gateWho { font-size: .72em; font-weight: 400; color: var(--dim); margin-left: 6px; }
.codeRows { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.codeRows .redeemRow { margin-top: 0; }
/* 退款规则在这一屏里是最高的一块（164 px）。放宽之后行数自然少，
   再收一点行距 —— 内容一个字没删，法律上要说的话都还在。 */
#gate .terms { margin-top: 0; line-height: 1.55; padding: 8px 12px; }
.plans { display: grid; gap: 8px; margin: 14px 0 0;
         grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
#gate .plans { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.plan { display: grid; gap: 3px; text-align: left; cursor: pointer;
        padding: 12px 13px; border-radius: 9px; color: inherit;
        background: var(--sunk); border: 1px solid var(--line);
        transition: border-color .14s, transform .14s; }
.plan:hover { border-color: var(--accent); transform: translateY(-1px); }
.plan:disabled { opacity: .5; cursor: default; transform: none; }
.plan b { font-size: 14px; }
.plan .price { font-family: var(--mono); font-size: 19px; color: var(--accent); }
.plan .dur { color: var(--faint); }
/* 从支付宝回来时角上那条「正在确认付款…」。不是弹窗：人已经付完了，不该再挡着页面。 */
/* 个人中心分区：左边一栏，右边一块（和后台同一个做法）。窄屏上一栏变成顶上一排。 */
.meLayout { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 28px; align-items: start; }
.meNav { position: sticky; top: 0; display: flex; flex-direction: column; gap: 4px; }
.meNav button { text-align: left; padding: 9px 12px; border-radius: 8px; border: 0; cursor: pointer;
                background: transparent; color: var(--dim); font: inherit; font-size: 14px; }
.meNav button:hover { background: var(--sunk); color: var(--fg); }
.meNav button.on { background: var(--sunk); color: var(--fg); font-weight: 600; }
.meMain { min-width: 0; }
@media (max-width: 760px) {
  .meLayout { grid-template-columns: 1fr; gap: 14px; }
  .meNav { flex-direction: row; overflow-x: auto; position: static; }
  .meNav button { white-space: nowrap; flex: none; }
}
.meBuyRow { display: flex; flex-wrap: wrap; gap: 8px; }
.meBuyRow b { margin-left: 4px; font-family: var(--mono); color: var(--accent); }
.paidToast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 200;
             max-width: calc(100vw - 32px); padding: 10px 16px; border-radius: 9px;
             background: var(--sunk); border: 1px solid var(--accent); color: var(--fg);
             font-size: 13.5px; box-shadow: 0 6px 24px rgba(0,0,0,.25); }
.redeemRow { display: flex; gap: 8px; margin-top: 12px; }
.redeemRow input { flex: 1; min-width: 0; }

/* A locked card is still a link, so it must still look like one. */
a.card.locked:hover { border-color: var(--warn); }
.cfoot .lock { color: var(--warn); background: rgba(255,180,84,.10); }

/* 付款窗口 */
.paybox { position: relative; max-width: 360px; text-align: center; }
/* 🔴 **每一个用 .payX 的盒子都必须自己定位。**
   它是 `position: absolute`，所以参照的是最近的已定位祖先 —— 盒子没定位的话
   就一路找到那个 `position: fixed` 的遮罩，于是关闭的叉落在浏览器窗口的角上，
   离对话框两百多像素。`.reqBox` 就是这么漏的：实测那个 × 在 (1970, 28)，
   而盒子从 y=257 才开始。check.sh 里有一条盯着这件事。 */
.payX { position: absolute; top: 8px; right: 8px; }
.payAmt { margin: 6px 0 2px; display: grid; gap: 2px; }
/* 价钱在全站是同一个颜色。付费墙上是主色，这里原来是 --ok（绿）——
   同一笔 ¥6，选的时候一个色，付的时候另一个色，读起来像两个数。
   而绿色在这一屏还有别的意思（「已开通」），更不该拿来当价钱。 */
#payAmount { font-family: var(--mono); font-size: 30px; color: var(--accent); }
.qrWrap { margin: 14px 0 4px; display: grid; gap: 10px; justify-items: center; }
.qrTabs { display: flex; gap: 6px; }
.qrTab { background: var(--sunk); color: var(--dim); border: 1px solid var(--line);
         border-radius: 6px; padding: 5px 14px; font-size: 12.5px; cursor: pointer; }
.qrTab.on { color: var(--fg); border-color: var(--accent);
            background: color-mix(in srgb, var(--accent) 10%, transparent); }
.qrImg { width: 208px; height: 208px; object-fit: contain; background: #fff;
         border-radius: 8px; padding: 8px; }
.paybox .row { justify-content: center; margin-top: 14px; }

/* 恢复码只出现这一次 —— 服务端只存哈希。所以这一屏没有关闭按钮，也不响应点
   外面：关掉它就等于把这个账号唯一的备份扔了，而代价要到某个忘了密码的日子
   才显出来。出口只有「我存好了」那一个勾。 */
.codeBox { width: min(460px, 100%); display: flex; flex-direction: column; gap: 14px; }
.codeVal {
  font-family: var(--mono);
  font-size: clamp(17px, 4.2vw, 23px); letter-spacing: .1em; text-align: center;
  padding: 16px 12px; border-radius: var(--r); margin: 0;
  background: var(--sunk); border: 1px dashed var(--line); color: var(--fg);
  /* 这串码是要被手抄下来的：点一下就整串选中，省掉一次「拖不准」。 */
  user-select: all; -webkit-user-select: all; word-break: break-all;
}
.codeBox .row { display: flex; gap: 10px; }

/* ------------------------------------------------------------- 个人中心 */
#me { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.meBody { flex: 1; min-height: 0; overflow-y: auto;
          padding: 22px 20px calc(48px + var(--safe-b));
          width: 100%; max-width: 1040px; margin: 0 auto; }
.meLoad { padding: 40px 0; }
.meBody section { margin-bottom: 28px; }
.meBody h2 { font-size: 13px; color: var(--dim); font-weight: 500;
             letter-spacing: .02em; margin: 0 0 9px; }
.secHead { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
.secHead h2 { margin: 0; }
.empty { color: var(--faint); padding: 2px 0 4px; }

/* The one thing worth reading first: what you can play right now. */
.statusStrip {
  display: flex; align-items: center; gap: 22px; flex-wrap: wrap;
  padding: 18px 20px; margin-bottom: 22px;
  background: var(--panel); border: 1px solid var(--line);
  border-left: 3px solid var(--line); border-radius: var(--r-lg);
}
.statusStrip.on { border-left-color: var(--ok); }
.ssMain { display: grid; gap: 2px; flex: 1; min-width: 180px; }
.ssMain b { font-size: 19px; letter-spacing: -.01em; }
.statusStrip.on .ssMain b { color: var(--ok); }
.ssNums { display: flex; gap: 24px; }
.ssNums div { display: grid; gap: 1px; text-align: right; }
.ssNums b { font-family: var(--mono); font-size: 19px; }

.banner { margin: -8px 0 22px; padding: 10px 14px; border-radius: var(--r);
          font-size: 12.5px; color: var(--warn);
          background: rgba(255,180,84,.09); border: 1px solid rgba(255,180,84,.3); }

/* One row shape for permissions, orders and threads: title on the left, its
   numbers under it in mono, state on the right. Three lists that look alike
   is three lists you only have to learn once. */
.rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px;
        background: var(--line); border: 1px solid var(--line);
        border-radius: var(--r-lg); overflow: hidden; }
.rows li {
  background: var(--panel); padding: 12px 15px;
  display: grid; gap: 2px 12px; align-items: baseline;
  grid-template-columns: minmax(0, 1fr) auto;
}
.rTitle { font-size: 14px; grid-column: 1; }
.rTitle i { font-style: normal; color: var(--dim); }
.rTitle i::before { content: '·'; margin: 0 6px; color: var(--faint); }
.rMeta { grid-column: 1; font-size: 11.5px; color: var(--faint); }
.rows .tag { grid-column: 2; grid-row: 1 / span 2; align-self: center;
             justify-self: end; }
.rHint { grid-column: 1 / -1; color: var(--warn); margin-top: 4px; }
.rows li.needs { background: var(--sunk); }
.btn.tiny, .primary.tiny { font-size: 11.5px; padding: 4px 10px; height: auto; }
/* Scoped to a row. Unscoped, these grid properties leaked onto the same button
   sitting in the flex section header and pushed it out of alignment. */
.rows .btn.tiny { grid-column: 1 / -1; justify-self: start; margin-top: 7px; }
.threads .said, .threads .replied {
  grid-column: 1 / -1; margin: 8px 0 0; font-size: 12.5px; line-height: 1.6;
  white-space: pre-wrap; padding-left: 11px; border-left: 2px solid var(--line);
  color: var(--dim);
}
.threads .replied { border-left-color: var(--ok); color: var(--fg); }
.threads .replied b { color: var(--ok); margin-right: 8px; font-weight: 500; }

.tkBox { position: relative; max-width: 460px; display: grid; gap: 12px; }
.tkBox .lbl { font-size: 12.5px; color: var(--dim); }
.tkBox textarea, .tkBox select {
  width: 100%; background: var(--sunk); color: var(--fg); font: inherit;
  font-size: 15px; border: 1px solid var(--line); border-radius: var(--r);
  padding: 9px 12px;
}
.tkBox textarea { line-height: 1.6; resize: vertical; }
.tkTag { color: var(--accent); }

a.acctWho { text-decoration: none; color: inherit; display: block; }
a.acctWho:hover b { color: var(--accent); }

@media (max-width: 560px) {
  .statusStrip { gap: 14px; }
  .ssNums { gap: 18px; }
}

/* 付款记录：一笔一张卡，该有的字段一个不少 */
.orders { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.ordCard { background: var(--panel); border: 1px solid var(--line);
           border-radius: var(--r-lg); padding: 14px 16px; }
.ordCard.needs { border-color: rgba(255,180,84,.34); }
.ordTop { display: flex; align-items: center; gap: 10px; }
.ordName { font-size: 14.5px; flex: 1; min-width: 0; }
.ordName i { font-style: normal; color: var(--dim); }
.ordName i::before { content: '·'; margin: 0 6px; color: var(--faint); }
.ordAmt { font-size: 17px; }

/* Label / value pairs, aligned. A record where the values do not line up
   reads as a paragraph, not a receipt. */
.ordFacts, .payFacts {
  display: grid; grid-template-columns: max-content minmax(0, 1fr);
  gap: 5px 14px; margin: 11px 0 0; font-size: 12.5px;
}
.ordFacts dt, .payFacts dt { color: var(--faint); }
.ordFacts dd, .payFacts dd { margin: 0; color: var(--dim);
                             display: flex; align-items: center; gap: 8px; }
.ordId { color: var(--fg); word-break: break-all; }
.payFacts { justify-content: center; text-align: left; margin: 10px auto 0; }
.payFacts dd { color: var(--fg); }

.ordActs { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.ordActs .btn { font-size: 12px; padding: 5px 12px; height: auto; }
.btn.primaryish { border-color: var(--accent); color: var(--accent);
                  background: color-mix(in srgb, var(--accent) 10%, transparent); }
.btn.primaryish:hover { background: color-mix(in srgb, var(--accent) 17%, transparent); }

/* 退款规则要出现在买之前，不是买完才去翻的一页。 */
.terms { font-size: 11.5px; line-height: 1.65; color: var(--dim); margin: 12px 0 0;
         padding: 10px 12px; background: var(--sunk); border: 1px solid var(--line);
         border-radius: var(--r); text-align: left; }
.terms b { color: var(--fg); display: block; margin-bottom: 3px; font-weight: 500; }
.banner.urgent { color: var(--bad); background: rgba(255,92,92,.08);
                 border-color: rgba(255,92,92,.34); }
.banner a { color: inherit; }

/* 同意条款：勾选框和文字对齐，链接要明显能点 */
.agree { display: flex; align-items: flex-start; gap: 9px; font-size: 12.5px;
         color: var(--dim); line-height: 1.6; cursor: pointer; }
.agree input[type=checkbox] { width: 16px; height: 16px; margin: 2px 0 0;
                              flex: none; cursor: pointer; }
.agree a { color: var(--accent); }
.lLegal { font-size: 12px; color: var(--faint); margin: 14px 0 0; }
.lLegal a { color: var(--dim); }
.meLegal { margin-top: 34px;
           font-size: 12px; color: var(--faint); display: flex; gap: 10px;
           flex-wrap: wrap; align-items: center; }
.meLegal a { color: var(--dim); }


/* Preview: an operator looking at an unpublished song through the real player.
   Loud on purpose — the whole risk of a preview mode is forgetting you are in
   one and concluding the song is live. */
.previewbar { position: fixed; inset: 0 0 auto 0; z-index: 999; text-align: center;
  background: var(--warn); color: var(--warn-ink); font-size: 13px;
  padding: 6px 12px; }
/* Being signed out by a login elsewhere is a statement of fact, not a warning
   about this page — so it is the neutral panel colour, not the preview amber. */
.previewbar.signedout { background: var(--raise); color: var(--fg);
  border-bottom: 1px solid var(--line); padding: 9px 12px; }
.previewbar .barx { margin-left: 10px; font: inherit; color: var(--accent);
  background: none; border: 0; padding: 0 2px; cursor: pointer; }
.previewbar .barx:hover { text-decoration: underline; }

/* 对拍 — only ever rendered on the build machine (see wireTapping).
   A flex child of #app, not a fixed overlay: the transport lives at the
   bottom of that column and a floating bar simply sat on top of it. */
.tapbar {
  flex: none; background: var(--sunk); color: var(--fg);
  border-top: 2px solid var(--accent);
  padding: 10px 20px calc(12px + var(--safe-b));
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
}
.tapsay { flex: 1 1 340px; font-size: 13px; line-height: 1.6; }
.tapsay b { color: var(--fg) }
.taprow { display: flex; align-items: center; gap: 10px }
.tapbar .dim { color: var(--dim); font-size: 13px; min-width: 42px }
.tapgo { padding: 9px 16px; border-radius: 7px; border: 1px solid var(--line);
  background: var(--panel); color: var(--fg); font-size: 14px; cursor: pointer }
.tapgo:disabled { opacity: .4; cursor: default }
/* Wide and tall, because it gets hit a hundred and seventeen times. */
.taphit { width: 190px; height: 58px; border-radius: 10px; font-size: 19px;
  border: 2px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, var(--panel));
  color: var(--fg); cursor: pointer }
.taphit i { font-style: normal; opacity: .65; font-size: 13px; margin-left: 6px }
.taphit.flash { background: var(--accent) }
.shiftrow { border-left: 1px solid var(--line); padding-left: 16px; margin-left: 4px }
.shiftrow button[data-sh] { width: 42px; padding: 7px 0; border-radius: 6px;
  border: 1px solid var(--line); background: var(--panel); color: var(--fg);
  cursor: pointer }
.shiftrow output { min-width: 62px; text-align: center; font-variant-numeric: tabular-nums }

/* ---- 手机拦截层（ADR-23）------------------------------------------------
   Replaces the player on a phone rather than warning over it. Its own block
   because it is the one screen that must lay out correctly on the device the
   rest of the app is not built for. */
/* `body` is `overflow: hidden` for the player's sake; this screen is the one
   that must still scroll, because a short phone plus a large system font can
   put the last line under the fold with no way to reach it. */
.phonegate { min-height: 100dvh; display: flex; align-items: center;
  justify-content: center; padding: 28px 22px; overflow-y: auto; }
.phonegate .pg { max-width: 30rem; }
.phonegate h1 { font-size: 21px; line-height: 1.4; margin: 0 0 14px;
  letter-spacing: -.01em; }
.phonegate p { color: var(--dim); font-size: 14.5px; line-height: 1.75;
  margin: 0 0 12px; }
.phonegate b { color: var(--fg); font-weight: 500; }
.phonegate .pg-ok { color: var(--ok); }
.phonegate .pg-go { display: flex; gap: 10px; flex-wrap: wrap; margin: 22px 0 0; }
.phonegate .pg-go a { padding: 11px 18px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--panel); color: var(--fg);
  text-decoration: none; font-size: 15px; }
.phonegate .pg-go a.key { border-color: var(--accent); color: var(--accent); }
.phonegate .pg-fine { color: var(--faint); font-size: 12.5px; margin-top: 26px; }
.phonegate .pg-fine button { font: inherit; color: var(--dim); background: none;
  border: 0; border-bottom: 1px solid var(--line); padding: 0 1px; cursor: pointer; }
.phonegate .pg-fine button:hover { color: var(--accent); border-color: var(--accent); }

/* ---- 工单里的一轮 ---------------------------------------------------- */
.threads .tkAct { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
.threads .tkMore {
  width: 100%; min-height: 54px; resize: vertical; font: inherit;
  border-radius: var(--r); border: 1px solid var(--line);
  background: var(--sunk); color: var(--fg); padding: 9px 11px; line-height: 1.6;
}
.threads .tkMore:focus-visible { outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent); }
.threads .tkBtns { display: flex; gap: 8px; }
.threads .tkErr { margin: 0; }

/* ---- 注销账号 ----------------------------------------------------------
   Its own visual register, and folded away behind a summary. Not because it
   should be hard to find — the privacy policy promises it — but because an
   irreversible button sitting open next to "兑换码" is a button someone
   eventually presses while meaning to press something else. */
.danger { border-top: 1px solid rgba(255,92,92,.25); }
.danger h2 { color: var(--red); }
/* 「有件事你现在最好处理一下」——注销那一节和恢复码那一节都用它。
   原来只定义在 `.danger` 里面，于是同一个类名在别处静默地没有样式。 */
.warnline { color: var(--warn); }
.danger .delbox { margin-top: 12px; }
.danger .delbox summary { cursor: pointer; color: var(--dim); font-size: 13px;
  list-style: none; display: inline-flex; align-items: center; gap: 6px; }
.danger .delbox summary::-webkit-details-marker { display: none; }
.danger .delbox summary { list-style: none; }
.danger .delbox summary::marker { content: ''; }
.danger .delbox summary::before { content: "▸"; color: var(--faint); }
.danger .delbox[open] summary::before { content: "▾"; }
.danger .delbox summary:hover { color: var(--red); }
.danger .delform { display: flex; flex-direction: column; gap: 8px;
  max-width: 22rem; margin-top: 14px; padding: 14px;
  border: 1px solid rgba(255,92,92,.3); border-radius: var(--r);
  background: rgba(255,92,92,.05); }
.danger .delform label { font-size: 12.5px; color: var(--dim); }
.btn.danger { border-color: color-mix(in srgb, var(--red) 50%, transparent);
              color: var(--red); align-self: flex-start; }
.btn.danger:hover { border-color: var(--red); background: rgba(255,92,92,.12); }
.btn.danger:disabled { opacity: .5; cursor: not-allowed; }

/* ---- 继续上次练习 ------------------------------------------------------ */
.resume {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(96px + var(--safe-b)); z-index: 60;
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px 8px 14px; border-radius: 999px;
  background: var(--raise); border: 1px solid var(--line);
  box-shadow: 0 6px 20px rgba(0,0,0,.35); font-size: 13px;
}
.resume span { color: var(--dim); white-space: nowrap; }

/* ---- 练习记录 ---------------------------------------------------------- */
.rows.practice li { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px;
  align-items: baseline; }
.rows.practice .rTitle { color: var(--fg); text-decoration: none; }
.rows.practice .rTitle:hover { color: var(--accent); }
.rows.practice .rMeta { color: var(--faint); font-size: 12px; white-space: nowrap; }

/* 权益明细 —— 一句 blurb 说不清「我到底买了什么」。 */
.plan .perks { list-style: none; margin: 8px 0 0; padding: 0; display: grid;
  gap: 4px; text-align: left; }
.plan .perks li { color: var(--dim); font-size: 11.5px; line-height: 1.5;
  padding-left: 14px; position: relative; }
.plan .perks li::before { content: "·"; position: absolute; left: 4px;
  color: var(--faint); }
.plan .perks b { color: var(--fg); font-weight: 500; }

/* 折后价：原价划掉留在旁边。三个数里只显示一个，就没法验算。 */
.plan .price.cut s { color: var(--faint); font-weight: 400; margin-right: 6px;
  font-size: .82em; }

/* 还在后台载入的底轨：留在原位、置灰、说明白。让它「载好了再冒出来」会把
   按钮从别人手指底下挪走。 */
.seg button.loading { opacity: .55; cursor: progress; }
.seg button.loading i { color: var(--faint); font-style: normal; }

/* ---- 配色开关（紧凑版）------------------------------------------------
   The full three-button 分段控件 lives in 练习设置, where there is room for
   words. Every other header gets this: one button that steps 跟随系统 →
   深色 → 浅色, showing the state it is *currently in* rather than the one it
   would move to — a control that shows its own next state is a control nobody
   can read.

   `data-theme-cycle` carries the mode, so the icon is chosen by attribute
   rather than by three class toggles that can disagree with each other. */
.themeBtn { position: relative; }
.themeBtn .ico { position: absolute; }
.themeBtn .tAuto, .themeBtn .tDark, .themeBtn .tLight { display: none; }
.themeBtn[data-theme-cycle="auto"]  .tAuto  { display: block; }
.themeBtn[data-theme-cycle="dark"]  .tDark  { display: block; }
.themeBtn[data-theme-cycle="light"] .tLight { display: block; }
/* An explicit choice is a state worth seeing at a glance; 跟随系统 is the
   default and stays quiet. */
.themeBtn[data-theme-cycle="dark"], .themeBtn[data-theme-cycle="light"] {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
}
.lNav .themeBtn { width: 36px; height: 36px; }

/* 段落循环的两行。第一行是「一边放一边标」，第二行是「照着谱子敲号码」——
   它们是两种动作，所以是两行。挤在一个 .row 里的时候，flex-wrap 把它折成了
   四段没有含义的碎行：`第`「框」`–`「框」`小节` 和「应用」全都各自换行。 */
.barPick { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
/* Only `flex`. Width and alignment come from the `input.num:not(…)` rule
   further up — restating them here at (0,2,0) loses to that (0,4,1) selector,
   and `specificity.mjs` fails the build for exactly that, which is how this
   line got written twice. A declaration that silently never applies is worse
   than not writing it. */
.barPick .num { flex: none; }
.barPick .cap { flex: none; }
.barPick .btn { flex: none; margin-left: auto; }

/* 配色那一行：按钮、当前是哪一种、怎么换。 */
.themeRow b { font-size: 13.5px; font-weight: 500; }
.themeRow .cap { color: var(--faint); }

/* 数拍：在段落之前，不属于段落。用一条细线把它和曲子本身分开——它是开演之前
   的那几小节节拍器，不是第八段主歌。 */
#sections .lead { border-bottom: 1px solid var(--line); padding-bottom: 10px;
                  margin-bottom: 4px; }
#sections .lead .bn { color: var(--faint); font-family: var(--sans);
                      font-size: 11px; letter-spacing: 0; }

/* 「怎么把它关掉」要写在脸上。第一次进来的人，第一下点的多半是别的东西——
   那一下被蒙版吃掉，看上去就是「这个按钮坏了」。 */
/* 自己占一行 —— 挤在按钮那一行里放不下（气泡只有 320 px）。 */
.tourHint { font-size: 11px; color: var(--faint); margin-top: -2px; }

/* ---- 点歌 (L-02) -----------------------------------------------------
   列表在输入框下面，是有意的：先看见「已经有 7 个人想要」，再决定要不要
   自己写一遍。反过来排，票会散在七行上，而这张表存在的唯一意义是排序。 */
.reqBox { position: relative; max-width: 560px; }
.reqList { margin-top: 18px; display: flex; flex-direction: column; gap: 8px;
           max-height: 42vh; overflow-y: auto; }
.reqHead { padding-bottom: 4px; border-bottom: 1px solid var(--line); }
.reqItem { display: flex; align-items: center; gap: 12px;
           padding: 9px 11px; border-radius: var(--r);
           background: var(--sunk); }
.reqItem.mine { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent); }
.reqWho { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.reqWho b { font-size: 13.5px; font-weight: 500; }
.reqN { flex: none; font-size: 12px; color: var(--dim); }
.reqItem .tiny { flex: none; }
.reqReply { font-size: 12px; color: var(--ok); }

/* 首页上的真数字 (L-04)。默认 hidden：够不到门槛服务端不给，这里就不画——
   「累计练习 3 小时」是真的，但它说的是「这里没人」。 */
.heroStats { display: flex; gap: 28px; margin-top: 22px; }
.heroStats div { display: flex; flex-direction: column; gap: 2px; }
.heroStats b { font-size: 22px; font-weight: 600; letter-spacing: -.01em;
               font-variant-numeric: tabular-nums; }

/* 页脚里等主体的那三栏 (A-29)。备案号、经营主体、联系方式是法律要求的固定
   内容，等到有了再往一个没给它留位置的版式里塞，就只能塞成一坨——所以位置
   先留着，`hidden` 到有内容为止。 */
.lEntity { display: flex; flex-wrap: wrap; justify-content: center;
           gap: 6px 26px; margin-top: 18px; padding-top: 16px;
           border-top: 1px solid var(--line); font-size: 12px; color: var(--faint); }
.lEntity div { display: flex; gap: 7px; align-items: baseline; }
.lEntity b { font-weight: 500; color: var(--faint); }
.lEntity span { color: var(--dim); }
.lEntity a { color: var(--dim); }
@media (max-width: 560px) { .lEntity { flex-direction: column; align-items: center; } }



/* 页脚的品牌名和标语。中英并置是品牌规范里的写法（「邦尼稳拍 / BUNNY TEMPO」），
   英文压小、加字距、降饱和 —— 它是注脚不是第二个标题。 */
.lFootName { margin: 0 0 6px; font-size: 17px; font-weight: 700;
             letter-spacing: -.01em; }
.lFootName span { font-size: 11.5px; font-weight: 600; letter-spacing: .18em;
                  color: var(--faint); margin-left: 8px;
                  text-transform: uppercase; vertical-align: 2px; }
.lFootTag { margin: 0 0 22px; color: var(--dim); font-size: 14px;
            letter-spacing: .02em; }

/* 加载页上的「点一下开始」。它要显眼 —— 这一步不点，后面什么都不会发生。 */
.prime { margin: 18px 0 4px; text-align: center; }
.primeAsk { margin: 0 0 12px; color: var(--fg); font-size: 15px; }
.primeWhy { margin: 12px auto 0; color: var(--faint); font-size: 12.5px;
            line-height: 1.7; max-width: 380px; }

/* ======================================================= library sidebar ==
   左边一栏分类，右边卡片。分类原来是搜索框右边一维一行的按钮 —— 曲库一大就把卡片
   往下挤；手机上它变回顶上一条横着滑的带子，和播放器的段落栏同一个做法。
   ======================================================================= */
.libBody { flex: 1; min-height: 0; display: grid; grid-template-columns: 224px minmax(0, 1fr); }
.libMain { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.libSide {
  min-height: 0; overflow-y: auto; border-right: 1px solid var(--line);
  padding: 16px 12px 20px; display: flex; flex-direction: column; gap: 14px;
}
.sideGroups { display: flex; flex-direction: column; gap: 16px; }
.sideGroup { display: flex; flex-direction: column; gap: 2px; }
.sideCap { color: var(--faint); letter-spacing: .06em; padding: 0 10px 4px; }
.sideItem {
  display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 10px;
  border: 1px solid transparent; border-radius: var(--r); background: none;
  color: var(--dim); text-align: left; font-size: 14px;
}
.sideItem .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sideItem .n { font-size: 11.5px; color: var(--faint); }
.sideItem:hover { background: var(--sunk); color: var(--fg); }
.sideItem:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sideItem.on { background: color-mix(in srgb, var(--accent) 13%, transparent); color: var(--accent); font-weight: 600; }
.sideItem.on .n { color: var(--accent); }
.resumeCard {
  display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--panel); color: var(--fg); text-decoration: none;
}
.resumeCard b { font-size: 14.5px; line-height: 1.35; }
.resumeCard:hover { border-color: var(--accent); }
.resumeCard:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.libSide .sideFoot {
  margin-top: auto; padding: 12px 10px 0; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
}
@media (max-width: 820px) {
  .libBody { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
  .libSide {
    flex-direction: row; align-items: center; overflow-x: auto; overflow-y: hidden;
    border-right: 0; border-bottom: 1px solid var(--line); padding: 10px 14px; gap: 8px;
  }
  .sideGroups { flex-direction: row; gap: 10px; }
  .sideGroup { flex-direction: row; align-items: center; gap: 6px; }
  .sideCap { padding: 0 2px 0 6px; white-space: nowrap; }
  .sideItem { flex: none; min-height: 38px; white-space: nowrap; border-color: var(--line); background: var(--panel); }
  .resumeCard { flex: none; flex-direction: row; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 12px; }
  .resumeCard .dimmer { display: none; }
  .libSide .sideFoot { flex: none; flex-direction: row; align-items: center; margin: 0; padding: 0 0 0 6px; border-top: 0; }
  .libSide .sideFoot .cap { display: none; }
}

/* 设置里的节拍器音色 */
.voiceSel { display: inline-flex; align-items: center; gap: 8px; }
.voiceSel select { min-width: 0; }
