/* ============================================================================
   app.css —— 4K 图书馆 全站样式
   设计约束：深色、直角（--radius: 0，全站任何地方都不允许出现圆角）、
   扁平面板、1px 实线边框、信息密度优先；不使用阴影 / 渐变 / 发光等装饰。
   ========================================================================== */

:root {
  --radius: 0;
  --bg: #0e0e10;
  --surface: #16161a;
  --surface-2: #1d1d22;
  --border: #2a2a2e;
  --text: #e8e8ea;
  --muted: #8a8a92;
  --accent: #e8443a;

  --font: system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace;
  --maxw: 1280px;
  --nav-h: 54px;
  --gap: 12px;
}

/* 直角是硬性要求：用通配规则兜底，避免任何组件意外带回圆角 */
*,
*::before,
*::after {
  box-sizing: border-box;
  border-radius: var(--radius);
}

/* hidden 属性必须真的隐藏：作者样式里的 display 会盖掉浏览器默认的 [hidden]{display:none}，
   所以这里显式声明一次，避免 .chip 这类设了 display 的元素把 hidden 顶掉 */
[hidden] { display: none !important; }

input[type="range"]::-webkit-slider-thumb,
input[type="range"]::-webkit-slider-runnable-track,
input[type="range"]::-moz-range-thumb,
input[type="range"]::-moz-range-track,
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
  border-radius: 0;
}

html { background: var(--bg); }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
}

body.is-dragging { user-select: none; cursor: grabbing; }

h1, h2, h3 { margin: 0; font-weight: 650; line-height: 1.25; }
h1 { font-size: 26px; }
h2 { font-size: 17px; }
h3 { font-size: 14px; }
p { margin: 0; }
ul, ol { margin: 0; padding-left: 20px; }
li { margin: 4px 0; }

a { color: var(--text); text-decoration: none; border-bottom: 1px solid var(--border); }
a:hover { border-bottom-color: var(--accent); color: #fff; }

code {
  font-family: var(--mono);
  font-size: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  padding: 0 4px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: #fff;
  padding: 6px 10px;
  border: 0;
  z-index: 100;
}
.skip-link:focus { left: 0; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

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

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.topbar-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  min-height: var(--nav-h);
  padding: 0 16px;
  display: flex;
  align-items: center;
  gap: 18px;
}

.brand { display: flex; align-items: center; gap: 8px; border: 0; flex: none; }
.brand-name { font-weight: 700; letter-spacing: .3px; }

.logo {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  width: 22px;
  height: 22px;
  padding: 3px;
  border: 1px solid var(--border);
  background: var(--surface-2);
}
.logo i { flex: 1; background: var(--text); display: block; }
.logo i:nth-child(1) { height: 55%; }
.logo i:nth-child(2) { height: 100%; background: var(--muted); }
.logo i:nth-child(3) { height: 70%; }
.logo i:nth-child(4) { height: 88%; background: var(--accent); }

.nav { display: flex; align-items: center; gap: 2px; flex: none; }
.nav-link {
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 6px 10px;
  color: var(--muted);
}
.nav-link:hover { color: var(--text); border-bottom-color: var(--border); }
.nav-link.is-active { color: var(--text); border-bottom-color: var(--accent); }

.nav-search { margin-left: auto; flex: 0 1 320px; min-width: 140px; }
.nav-search input,
.search-input {
  width: 100%;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  padding: 7px 9px;
}
.nav-search input::placeholder,
.search-input::placeholder { color: var(--muted); }
.nav-search input:focus,
.search-input:focus { border-color: var(--muted); outline: none; }

.nav-toggle {
  display: none;
  width: 34px;
  height: 30px;
  padding: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  flex-direction: column;
  justify-content: space-between;
  cursor: pointer;
}
.nav-toggle span { display: block; height: 2px; background: var(--text); }

/* -------------------------------------------------------------- 提示条 */

.notice {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: 12px;
  padding: 6px 16px;
  text-align: center;
}
.notice b { color: var(--text); font-weight: 600; }

/* ------------------------------------------------------------------ 布局 */

#main { max-width: var(--maxw); margin: 0 auto; padding: 20px 16px 48px; }

/* 收录进度提示：与 #main 同宽同边距，但它位于 <main> 之外（各视图之上），
   这样在谱面库/曲包页也能看到 —— 用户在那里搜不到包时最需要这条信息。 */
.collect-progress {
  max-width: var(--maxw);
  margin: 12px auto 0;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-left: 2px solid var(--accent);
  background: var(--surface);
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.6;
}
.collect-progress[hidden] { display: none; }
.collect-progress b { color: var(--text); }
@media (max-width: 640px) {
  .collect-progress { margin-left: 16px; margin-right: 16px; }
}

.view { display: block; }
.view[hidden] { display: none; }

.hero {
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 22px 20px;
  margin-bottom: var(--gap);
}
.eyebrow {
  color: var(--accent);
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  margin-bottom: 8px;
}
.hero h1 { font-size: 30px; margin-bottom: 10px; }
.lead { color: var(--muted); max-width: 78ch; }
.lead-sm { color: var(--muted); max-width: 88ch; margin-top: 6px; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }

.view-head { margin-bottom: 14px; }

/**
 * 标题区两栏（说明文字 + 速查面板）。
 *
 * 起因：说明文字有 88ch 的阅读上限（.lead-sm），在 1280px 版心里只占 722px，
 * 右边 500 多像素整片空白 —— 用户指出「这里的描述右侧全是空着的」。
 * 直接把 .lead-sm 的上限去掉不是好解法：那会让行宽到 89 个汉字，
 * 眼睛回行容易串行。改成两栏：左栏保持原来的阅读宽度，右栏放速查面板。
 *
 * 1.45 : 1 的比例是量出来的 —— 左栏恰好落在 720px 上下（= 今天说明文字的宽度），
 * 所以这次排版改动**没有动说明文字本身的行宽**。
 */
.view-head-split {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
/* 860px 以下两栏都放不下：速查面板降到说明文字下方，此时版心窄、不会再有大片空白 */
@media (max-width: 860px) {
  .view-head-split { grid-template-columns: minmax(0, 1fr); }
}

/**
 * 段位写法速查（谱面库标题区右栏）。
 * 视觉语言跟 .collect-progress 一致：1px 边框 + 左侧强调色描边，
 * 让它在标题区里明显是「一块参考面板」而不是又一段正文。
 */
.dan-legend {
  border: 1px solid var(--border);
  border-left: 2px solid var(--accent);
  background: var(--surface);
  padding: 9px 11px;
  font-size: 12.5px;
  line-height: 1.5;
}
.dan-legend-title { color: var(--muted); margin-bottom: 3px; }
/* dt/dd 直接当两列网格项用：一行一组，无需额外的包裹元素 */
.dan-legend dl {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 1px 10px;
  margin: 0;
}
.dan-legend dt { color: var(--muted); white-space: nowrap; }
.dan-legend dd { margin: 0; }
.dan-legend code { font-size: 11.5px; padding: 0 3px; }

.stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--gap);
  margin-bottom: var(--gap);
}
.stat {
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.stat-num { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-label { color: var(--muted); font-size: 12px; }

.block { margin-top: 22px; }
.block-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  border-bottom: 1px solid var(--border);
  padding-bottom: 6px;
  margin-bottom: var(--gap);
}
.more { color: var(--muted); font-size: 13px; border: 0; }
.more:hover { color: var(--accent); }

.howto { color: var(--muted); }
.howto b { color: var(--text); }

/* ------------------------------------------------------------------ 按钮 */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover:not(:disabled) { border-color: var(--muted); }
.btn:disabled { color: var(--muted); cursor: not-allowed; opacity: .55; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.btn-primary:hover:not(:disabled) { border-color: #fff; }
.btn-link { border-style: dashed; }

/* ------------------------------------------------------------- 谱面卡片 */

/**
 * 卡片网格：固定列宽，卡片尺寸**永远一致**。
 *
 * 曾经试过 `flex: 1 1 232px`（末行拉伸填满，不留空格），已回退：
 * 用户看到的是「最后一个怎么不放卡片而是改变大小，这样看着很不规整」——
 * 末行卡片被拉宽后和其它行不一样大，比一块空白更刺眼。
 * **尺寸一致 > 填满末行**，这个优先级是实测出来的，别再改回去。
 *
 * 于是回到 `auto-fill`：保留空轨道，末行不足时右侧留空。
 * 用 `auto-fill` 而不是 `auto-fit` 也有意为之 —— 卡片宽度恒定，
 * 不随"这一行有几张"变化（`auto-fit` 在列宽被 `minmax(min, 1fr)` 拉满时
 * 两行宽度会不同，同样是"改大小"）。
 *
 * 末行的空白靠 `perPage` 与列数对齐来减少（见 manifest.perPage 的注释）。
 * 列数随视口变化（4/5/8 列都可能），无法用单一 perPage 在所有宽度下整除，
 * 已确认接受：空白只是留白，不是坏掉。
 */
.chart-grid,
.pack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  gap: var(--gap);
}

.card {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  cursor: pointer;
  overflow: hidden;
}
.card:hover { border-color: var(--muted); }
.card:focus-visible { border-color: var(--accent); }

.cover-box {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  overflow: hidden;
}
.cover-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.cover-ph {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  gap: 8%;
  padding: 14% 16%;
}
.cover-ph i { flex: 1; background: var(--border); display: block; }
.cover-ph i:nth-child(1) { height: 38%; }
.cover-ph i:nth-child(2) { height: 74%; }
.cover-ph i:nth-child(3) { height: 52%; }
.cover-ph i:nth-child(4) { height: 88%; background: #33333a; }
.card:hover .cover-ph i:nth-child(4) { background: var(--accent); }

.card-body { padding: 10px 11px 11px; display: flex; flex-direction: column; gap: 5px; }

.card-title {
  font-size: 14px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
.card-sub { color: var(--muted); font-size: 11.5px; }
.card-artist { color: var(--muted); font-size: 12.5px; }

.card-tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 2px; }
.badge {
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 11px;
  padding: 1px 6px;
  white-space: nowrap;
}
.badge-diff[data-diff="Challenge"],
.badge-diff[data-diff="Edit"] { border-color: var(--accent); color: var(--accent); }
.badge-diff[data-diff="Hard"],
.badge-diff[data-diff="Medium"] { border-color: var(--muted); }
.badge-diff[data-diff="Beginner"],
.badge-diff[data-diff="Easy"] { color: var(--muted); }

.kv { color: var(--muted); font-size: 11.5px; white-space: nowrap; }
.kv b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- MSD 徽标（Etterna 官方难度评级）----
   MSD 是 Etterna 玩家衡量难度的唯一通用标准，比 1~20 的"等级"精确得多，
   因此给它边框与数字着色，便于在卡片列表里快速扫视难度分布。
   配色按区间分档，仍是直角边、无圆角。 */
.kv-msd { border: 1px solid var(--border); padding: 1px 6px; }
.kv-msd b { color: var(--text); }
.kv-msd-none { opacity: .55; }
.kv-msd.t1 { border-color: #3d6b4f; }
.kv-msd.t1 b { color: #7fc79a; }
.kv-msd.t2 { border-color: #4a6288; }
.kv-msd.t2 b { color: #8fb4e8; }
.kv-msd.t3 { border-color: #8a7a3c; }
.kv-msd.t3 b { color: #e8cf7a; }
.kv-msd.t4 { border-color: #9a5f38; }
.kv-msd.t4 b { color: #f0a970; }
.kv-msd.t5 { border-color: var(--accent); }
.kv-msd.t5 b { color: var(--accent); }

/* ---- 段位徽标（osumania_map_analyser 估算的段位难度）----
   与 MSD 徽标同款方框（1px 边框、同样的内边距），但**边框用该段位图标的渐变配色**
   （用户 2026-10 的要求）。色值不是手调的：tools/dan-icon-colors.py 从
   tools/dan-icons/<档位>.png 采样生成下面 .kv-dan.d-* 的调色板 ——
   要改配色请换图标或重跑脚本，别手改色值。

   每个图标按自己的颜色特性采样，**不是套一套通用彩条**：
   · 单色相（Reform 1~10 的金属铜、Eta/Theta/Iota 的暗红、Kappa 的银灰）——
     渐变本质是明度梯度，按亮度分位取三档；
   · 多色相（Alpha/Epsilon/Gamma/Delta 这几个各自是不同的色相组合，Zeta 是粉蓝双色）——
     先聚出图标上实际有哪几簇色相，再按这些颜色在图标上的位置排序，所以 α 是黄橙红紫蓝、
      γ 是黄绿青紫红、δ 是橙红紫、ε 是蓝靛品红、ζ 是蓝粉橙 —— 各是各的。

   档位键：d-intro / d-1~d-10（Reform）/ d-alpha~d-kappa（希腊字母档）。
   Reform 1~10 的图标是一套玫瑰金铜色（彼此极接近，肉眼几乎分不出），
   具体档位靠标签文本区分，边框负责"一眼看出段位家族"。
   认不出档位的异常标签不加 .d-*，走中性边框（--border）—— 与缺失同一态度：不猜。

   标签可能很长（LN 复合谱面是 "RC || LN" 双段），卡片里按容器宽度裁剪省略，
   全文放 title 提示。 */
.kv-dan {
  --dan-grad: linear-gradient(var(--border), var(--border));
  --dan-text: var(--text);
  border: 1px solid var(--border);
  border-image: var(--dan-grad) 1;
  padding: 1px 6px; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis;
}
.kv-dan b { color: var(--dan-text); }
/* 缺失/无效：回到普通边框（不要渐变），并整体压暗 */
.kv-dan-none { opacity: .55; border-image: none; }

/* 由 tools/dan-icon-colors.py 从 tools/dan-icons/*.png 采样生成，勿手改 */
.kv-dan.d-intro { --dan-grad: linear-gradient(135deg, #f6dbcc, #be8a6c, #4e3729); --dan-text: #f6dbcc; }  /* Reform 1 的图标兼作 Intro 档（图标集没有 Intro） */
.kv-dan.d-1 { --dan-grad: linear-gradient(135deg, #f6dbcc, #be8a6c, #4e3729); --dan-text: #f6dbcc; }
.kv-dan.d-2 { --dan-grad: linear-gradient(135deg, #f4ddcf, #bc8c73, #473022); --dan-text: #f4ddcf; }
.kv-dan.d-3 { --dan-grad: linear-gradient(135deg, #fce0d2, #c69884, #6f5649); --dan-text: #fce0d2; }
.kv-dan.d-4 { --dan-grad: linear-gradient(135deg, #f6d9ca, #b58774, #5b463b); --dan-text: #f6d9ca; }
.kv-dan.d-5 { --dan-grad: linear-gradient(135deg, #fadcce, #c6907a, #554137); --dan-text: #fadcce; }
.kv-dan.d-6 { --dan-grad: linear-gradient(135deg, #f7dfd4, #bf927c, #4a3c34); --dan-text: #f7dfd4; }
.kv-dan.d-7 { --dan-grad: linear-gradient(135deg, #dfb4a0, #c98f73, #481f0c); --dan-text: #dfb4a0; }
.kv-dan.d-8 { --dan-grad: linear-gradient(135deg, #fae2d5, #bb876e, #43322a); --dan-text: #fae2d5; }
.kv-dan.d-9 { --dan-grad: linear-gradient(135deg, #e3c0b0, #b0826e, #4f3120); --dan-text: #e3c0b0; }
.kv-dan.d-10 { --dan-grad: linear-gradient(135deg, #f7d9cc, #c0896f, #47332a); --dan-text: #f7d9cc; }
.kv-dan.d-alpha { --dan-grad: linear-gradient(135deg, #fee300, #fe7500, #fc020e, #af4498, #3b0dc3); --dan-text: #fee300; }
.kv-dan.d-beta { --dan-grad: linear-gradient(135deg, #fef34e, #fba54b, #786102); --dan-text: #fef34e; }
.kv-dan.d-gamma { --dan-grad: linear-gradient(135deg, #acfc00, #04fb29, #00f3fd, #7000fd, #fd001c); --dan-text: #acfc00; }
.kv-dan.d-delta { --dan-grad: linear-gradient(135deg, #fe7002, #cc4939, #8863aa); --dan-text: #fe7002; }
.kv-dan.d-epsilon { --dan-grad: linear-gradient(135deg, #0271ba, #301499, #922786, #fa2e67); --dan-text: #0271ba; }
.kv-dan.d-zeta { --dan-grad: linear-gradient(135deg, #69c1f7, #d16ace, #ef66a3, #fe715a); --dan-text: #69c1f7; }
.kv-dan.d-eta { --dan-grad: linear-gradient(135deg, #a60112, #7f0b10, #6e1011); --dan-text: #bb3e4b; }
.kv-dan.d-theta { --dan-grad: linear-gradient(135deg, #940606, #760e0e, #5e1414); --dan-text: #984949; }
.kv-dan.d-iota { --dan-grad: linear-gradient(135deg, #790d0d, #5d1414, #561717); --dan-text: #7f4f4f; }
.kv-dan.d-kappa { --dan-grad: linear-gradient(135deg, #7c7c7c, #555555, #242424); --dan-text: #7c7c7c; }

.card-pack {
  color: var(--muted);
  font-size: 11.5px;
  border-top: 1px solid var(--border);
  padding-top: 6px;
  margin-top: 3px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pack-banner { aspect-ratio: 16 / 6; }

/* 技能条（曲包页） */
.skill-bars { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
.skill { display: grid; grid-template-columns: 74px 1fr 30px; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); }
.skill-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.skill-bar { display: block; height: 6px; background: var(--surface-2); border: 1px solid var(--border); }
.skill-bar i { display: block; height: 100%; background: var(--muted); }
.skill-val { text-align: right; font-variant-numeric: tabular-nums; }

/* 骨架屏 */
.card-skeleton { cursor: default; }
.sk-line { display: block; background: var(--surface-2); border: 1px solid var(--border); }
.sk-cover { position: absolute; inset: 0; border: 0; }
.sk-title { height: 15px; width: 86%; }
.sk-text { height: 11px; width: 62%; }
.sk-tags { height: 11px; width: 74%; }

/* 空状态 */
.empty {
  /* 独占一整行（跨掉所有列），不要被挤成一张卡片的大小 */
  grid-column: 1 / -1;
  border: 1px dashed var(--border);
  padding: 28px 18px;
  text-align: center;
  color: var(--muted);
}
.empty-title { color: var(--text); font-weight: 600; margin-bottom: 4px; }
.empty-hint { font-size: 12.5px; }

/* --------------------------------------------------------- 搜索与筛选条 */

.toolbar {
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 10px;
  margin-bottom: var(--gap);
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.toolbar-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.search-input { flex: 1 1 260px; }
.filters { gap: 10px; }

.field {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}
.field input,
.field select {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 12.5px;
  padding: 5px 7px;
}
.field input[type="number"] { width: 84px; }
.field input:focus,
.field select:focus { border-color: var(--muted); outline: none; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--accent);
  color: var(--accent);
  font-size: 12px;
  padding: 3px 6px;
}
.chip b { color: var(--text); font-weight: 600; }
.chip button {
  background: none;
  border: 0;
  color: var(--accent);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 2px;
}

.list-status { color: var(--muted); font-size: 12.5px; margin: 0 0 10px; }
.list-status b { color: var(--text); font-weight: 600; }
.list-status .loading { color: var(--accent); }

/* -------------------------------------------------------------- 分页条 */

.pager {
  margin-top: 16px;
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.pager-total { color: var(--muted); font-size: 12.5px; }
.pager-total b { color: var(--text); }
.pager-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.pager-pages { display: flex; flex-wrap: wrap; gap: 4px; }
.page-btn {
  min-width: 32px;
  padding: 5px 7px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 12.5px;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.page-btn:hover { border-color: var(--muted); }
.page-btn.is-current { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.page-gap { color: var(--muted); padding: 5px 2px; }
.pager-jump { display: flex; align-items: center; gap: 6px; margin-left: auto; color: var(--muted); font-size: 12px; }
.pager-jump input {
  width: 68px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 12.5px;
  padding: 5px 6px;
}

/* --------------------------------------------------------- 详情浮层 */

.overlay {
  position: fixed;
  inset: 0;
  background: rgba(8, 8, 10, .62);
  z-index: 40;
}

.detail-panel {
  position: fixed;
  left: 0;
  top: 0;
  width: min(1120px, 96vw);
  max-height: 92vh;
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  z-index: 41;
  will-change: transform;
}
.detail-panel.dragging { border-color: var(--accent); cursor: grabbing; }
.detail-panel.dragging .panel-head { cursor: grabbing; }
/* 面板是浮层容器、不是键盘焦点停靠点：用 tabindex=-1 只是为了让 Esc/快捷键生效，
   给它描一圈强调色边框会跟「拖拽中」的状态混淆，所以这里不画焦点框 */
.detail-panel:focus,
.detail-panel:focus-visible { outline: none; }

.panel-head {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 9px 11px;
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: grab;
  touch-action: none;      /* 触摸拖动时不要触发页面滚动 */
}
.panel-titles { min-width: 0; flex: 1 1 auto; }
.panel-title { font-size: 15px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.panel-sub { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.panel-tools { display: flex; align-items: center; gap: 6px; flex: none; }
.drag-hint { color: var(--muted); font-size: 11.5px; }

/**
 * 窄屏下标题会被工具按钮挤没。
 *
 * 实测 375px 宽时：一行要同时塞下「按住面板空白处或标题栏可拖动」
 * ＋「复位位置」＋「关闭 (Esc)」＋谱面标题 —— 标题被压到 0 宽，
 * 用户看到的面板左上角只剩一撇残影，完全不知道自己在看哪张谱面。
 *
 * 处理：窄屏让标题独占一行，工具按钮折到第二行；
 * 同时把拖动提示藏掉（标题栏整条都可拖，提示是锦上添花，
 * 而且窄屏本来就没有"鼠标拖动"的直觉）。
 */
@media (max-width: 720px) {
  .panel-head { flex-wrap: wrap; row-gap: 6px; }
  .panel-titles { flex: 1 1 100%; }
  .panel-tools { flex: 1 1 100%; justify-content: flex-end; }
  .drag-hint { display: none; }
  .panel-title { font-size: 14px; }
}

/**
 * 两列按**内容**分配宽度，而不是固定比例。
 *
 * 改这里是为了配合播放器的 A4 比例舞台：舞台高度 = 宽度 × 1.414，
 * 所以玩家列的宽度直接决定玩家区有多高。
 *
 * 宽度上限由「高度预算」反推 —— 目的是让**播放器那一列连同控件完整可见**，
 * 不必在面板里滚动才能看到进度条。实测（1440×1000 视口、面板 max-height = 92vh）：
 * 面板本身在 200~440px 的所有列宽下都不溢出（高度由右侧元数据列决定），
 * 所以这里只约束玩家列自己的高度预算。
 *
 * 写法用 `calc(92vh / 1.414 - 155px)` 而不是先减后除：
 * 后者被浏览器化简时会分别处理两项，容易读出与预期不符的值；
 * 前者形式简单、验算直观（92vh/1.414 − X = 目标宽度）。
 * （两者数学上等价：`92vh/1.414 − 155` = `(92vh − 155×1.414)/1.414` = `(92vh − 219.2)/1.414`。）
 *
 * 2026-10 改版：控制区挪到右列底部（用户要求），左列只剩舞台本身，
 * 舞台改为 **flex 撑满列高**、与右列底面对齐 —— 不再靠 aspect-ratio 定高，
 * 列宽预算里的 −155px（控制区预留）随之取消。
 */
.panel-body { display: flex; align-items: stretch; }
/**
 * 播放器列。
 *
 * ⚠️ 选择器必须写成 `.panel-col.player-col`（两个类），不能只写 `.player-col`。
 *
 * 实测踩过：`.player-col` 与 `.panel-col` 优先级相同（都是单类），
 * 而 `.panel-col { flex: 1 1 auto }` 在文件里**排在后面** —— 于是它赢了，
 * 播放器列被拉成 615px（而非预算的 440px）。
 * 后果不只是变宽：舞台高度跟着变，canvas 按 `clientWidth` 放大会跟着变宽 ——
 * 这就是「页面自己往右滚」的成因（画布把文档撑出了横向滚动）。
 *
 * 列宽上限 440px；短视口下按 `92vh / 1.414` 收缩，舞台保持接近 A4 的观感。
 * 列是 flex 纵向容器：#player-slot / .player / .player-stage 层层 flex:1，
 * 舞台最终撑满列高（= 右列内容高度，见 .panel-body 的 stretch）。
 */
.panel-col.player-col {
  flex: none;
  width: min(440px, calc(92vh / 1.414));
  min-width: 230px;
  display: flex;
  flex-direction: column;
}
.panel-col.player-col #player-slot { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.panel-col.player-col .player { flex: 1 1 auto; min-height: 0; }
.panel-col { padding: 11px; min-width: 0; }
.panel-col + .panel-col { border-left: 1px solid var(--border); flex: 1 1 auto; }

/* 矮视口（手机横屏、窗口拖扁）的规则见文件末尾 —— 必须排在所有宽度断点之后 */

/* ------------------------------------------------------------- 播放器 */

.player { display: flex; flex-direction: column; gap: 9px; }

/**
 * 舞台高度 = 撑满左列（左列高 = 右列内容高，见 .panel-body 的 stretch）。
 *
 * 2026-10 改版：控制区挪去右列后，左列只剩舞台 —— 用户要求
 * 「把左侧预览谱面拉长直到对齐右侧底面」，所以不再用 aspect-ratio 定高
 * （那个比例的用途移交给 <860px 的堆叠布局），舞台 flex 撑满即可，
 * canvas 由播放器的 ResizeObserver 按 stage 实际尺寸重设，无需干预。
 * min-height 兜底：右列内容意外变矮时舞台不至于压没。
 */
.player-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 300px;
  background: var(--bg);
  border: 1px solid var(--border);
  overflow: hidden;
}
.player-canvas { display: block; background: var(--bg); }

.player-badge {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--surface);
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 12px;
  padding: 5px 8px;
}

/* 控制区已挪到右列底部（app.js 移动 DOM），与密度图/操作区保持同一 10px 间距 */
.player-controls { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.pc-row { display: flex; align-items: center; gap: 8px; }
.pc-row-wrap { flex-wrap: wrap; gap: 12px; }
.pc-time { color: var(--text); font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.pc-hint { color: var(--muted); font-size: 11.5px; margin-left: auto; }
.pc-field { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; white-space: nowrap; }
.pc-field select {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 12.5px;
  padding: 4px 6px;
}
/**
 * 滑条：细线轨道 + 小矩形滑块（用户 2026-10 指定的风格）。
 * 原生 accent-color 的大圆头粗红条视觉太重，这里完全自绘：
 * 轨道 2px 暗灰细线，滑块 8×16 竖向小条，hover/按住微亮。
 * WebKit 与 Firefox 的伪元素是两套实现，必须分开写。
 */
.pc-field input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 130px;
  height: 18px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.pc-field input[type="range"]::-webkit-slider-runnable-track {
  height: 2px;
  background: #3a3a40;
}
.pc-field input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 8px;
  height: 16px;
  margin-top: -7px;            /* (2px 轨道 − 16px 滑块) / 2，压回细线中心 */
  background: #b8b8be;
  border: none;
  border-radius: 2px;
}
.pc-field input[type="range"]::-moz-range-track {
  height: 2px;
  background: #3a3a40;
}
.pc-field input[type="range"]::-moz-range-thumb {
  width: 8px;
  height: 16px;
  background: #b8b8be;
  border: none;
  border-radius: 2px;
}
.pc-field input[type="range"]:hover::-webkit-slider-thumb,
.pc-field input[type="range"]:active::-webkit-slider-thumb { background: #e8e8ea; }
.pc-field input[type="range"]:hover::-moz-range-thumb,
.pc-field input[type="range"]:active::-moz-range-thumb { background: #e8e8ea; }
.pc-field input[type="range"]:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; }
/* 进度条已移除：定位统一由右侧「整曲音符密度」图承担，见 app.js 的 wireDensitySeek */
.pc-num { color: var(--text); font-family: var(--mono); font-size: 12px; min-width: 34px; text-align: right; }
/**
 * 「升降调」勾选框。
 * 原生 checkbox 在深色底上是一块亮白方块，比周围的滑条/下拉框抢眼得多，
 * 所以只收尺寸并给它 accent-color，让它在控制条里是同一个分量。
 */
.pc-switch { gap: 5px; cursor: pointer; }
.pc-switch input[type="checkbox"] {
  width: 13px;
  height: 13px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* 详情元数据。
 * 双列排布（两组「标签|值」并排）撑满面板宽度 —— 早先是单列 19 行、
 * 值只有几个词，右半边一片空白（用户：「排版基本都塞在左侧了」）。
 * 边框用**顶边**方案而不是底边：双列时最后一视觉行是第 N-1/N 两条，
 * 行数奇偶会让"哪几条是最后一行"变化；顶边框只需固定放过前两条
 * （单列时由 --1col 覆盖为只放第一条），末行闭合于表自身的下边框。 */
.meta-table { margin: 0; border: 1px solid var(--border); display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.meta-table--1col { grid-template-columns: minmax(0, 1fr); }
.meta-row { display: grid; grid-template-columns: 92px minmax(0, 1fr); border-top: 1px solid var(--border); }
.meta-table:not(.meta-table--1col) .meta-row:nth-child(-n+2) { border-top: 0; }
.meta-table--1col .meta-row:first-child { border-top: 0; }
/* 中缝：左列值的右缘。末列右缘与表边框重合，不画。 */
.meta-table:not(.meta-table--1col) .meta-row:nth-child(odd) dd { border-right: 1px solid var(--border); }
.meta-row dt {
  color: var(--muted);
  font-size: 12px;
  padding: 5px 8px;
  background: var(--surface-2);
  border-right: 1px solid var(--border);
}
/* 值塞不下时不换行，由 .meta-val 的往返滚动展示（见 keyframes meta-drift） */
.meta-row dd { margin: 0; padding: 5px 8px; font-size: 12.5px; white-space: nowrap; overflow: hidden; }
.meta-val { display: inline-block; max-width: 100%; vertical-align: bottom; }
.meta-row dd.is-overflow .meta-val { animation: meta-drift var(--meta-dur, 8s) ease-in-out infinite; }
@keyframes meta-drift {
  0%, 12% { transform: translateX(0); }
  46%, 62% { transform: translateX(var(--meta-shift, 0px)); }
  94%, 100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .meta-row dd.is-overflow .meta-val { animation: none; }
}

.meta-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 10px; }
.meta-source { color: var(--muted); font-size: 12px; }
.meta-note { color: var(--muted); font-size: 12px; margin-top: 8px; }

/* 整曲音符密度曲线：让用户即使只看到 15 秒，也能看出整张谱面的形状。
   直角边、无圆角，与全站风格一致；用弱色，它是辅助信息而非主体。 */
.density-block {
  margin-top: 10px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  padding: 7px 9px 5px;
}
.density-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  color: var(--muted);
  font-size: 11.5px;
  margin-bottom: 5px;
}
.density-legend { display: inline-flex; align-items: center; gap: 5px; font-variant-numeric: tabular-nums; }
.dp-swatch {
  display: inline-block;
  width: 9px;
  height: 9px;
  background: var(--accent);
  opacity: 0.35;
  border: 1px solid var(--accent);
}
/**
 * 定位轨道：包住密度曲线的可点击/可拖动区域。
 *
 * 这里是**全站唯一的定位控件**（播放器下方那条独立密度条已移除）——
 * 早期播放器里另有一条密度条与右侧这张图功能完全重叠，
 * 用户指出「把拖动功能移动到图2，这样图3的预览可以拉长了显示更多内容」。
 * 于是这张图同时承担两件事：**展示整曲结构** + **拖动定位**。
 */
.density-track {
  position: relative;
  cursor: grab;
  touch-action: none;          /* 触摸拖动时不要触发页面滚动/缩放 */
}
.density-track.is-dragging { cursor: grabbing; }
.density-track:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.density-svg {
  display: block;
  width: 100%;
  height: 44px;              /* viewBox 高 24，拉高一点让起伏更明显 */
  overflow: visible;
  pointer-events: none;      /* 事件统一由 .density-track 处理，避免落在子元素上丢失 */
}
/* 柱条颜色由 densityProfile 按押型层内联给定（单点深灰→四押白，亮度差拉满），
   这里只管不透明度 —— 给到 1：半透明会把层间对比又抹平了 */
.dp-bars rect { opacity: 1; }
/* 押型图例：四个小色块与柱条同色 */
.dp-key { display: inline-flex; align-items: center; gap: 3px; margin-right: 10px; color: var(--muted); font-size: 11px; }
.dp-key-swatch { width: 8px; height: 8px; display: inline-block; }
/* 预览窗口：半透明色块盖在柱子上，不遮挡形状 */
.dp-win { fill: var(--accent); opacity: 0.22; }
/**
 * 播放头：由 app.js 的 wireDensitySeek 按当前播放时间移动。
 * 用 `left: %` 定位而不是改 SVG 内容 —— 每帧改 DOM 属性会触发重排，
 * 而只需改一个百分比就能跟着 60fps 走。
 */
.dp-head {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--accent);
  pointer-events: none;
}
.dp-head::before {
  content: '';
  position: absolute;
  top: 0;
  left: -4px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--accent);
}
.dp-tip {
  position: absolute;
  top: -20px;
  transform: translateX(-50%);
  background: var(--text);
  color: var(--bg);
  font-family: var(--mono);
  font-size: 11px;
  padding: 1px 5px;
  white-space: nowrap;
  pointer-events: none;
}
.density-axis {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  color: var(--muted);
  font-size: 11px;
  margin-top: 3px;
  font-variant-numeric: tabular-nums;
}
.density-axis > span:nth-child(2) { opacity: 0.75; }

/* 「同一首歌的其他难度」切换器。直角边、无圆角，与卡片风格一致 */
.sib-block { margin-top: 10px; }
.sib-title { color: var(--muted); font-size: 12px; margin: 0 0 6px; }
.sib-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.sib-btn {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  min-width: 84px;
  padding: 5px 9px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.sib-btn:hover:not(:disabled) { border-color: var(--accent); background: var(--surface); }
.sib-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* 当前正在看的那一张：标出来并禁用（避免"点了没反应"） */
.sib-btn.is-current {
  border-color: var(--accent);
  background: var(--surface);
  cursor: default;
}
.sib-btn.is-current .sib-diff { color: var(--accent); }
.sib-diff { font-size: 12px; line-height: 1.3; }
.sib-meta { color: var(--muted); font-size: 11px; line-height: 1.2; font-variant-numeric: tabular-nums; }
/* 「仅预览片段」提示：比普通注释更显眼，但仍保持直角边、无圆角的克制样式 */
.meta-note-preview {
  color: var(--text);
  border-left: 2px solid var(--accent);
  background: var(--surface-2);
  padding: 7px 10px;
  font-size: 12px;
  line-height: 1.6;
}
.meta-error { border: 1px solid var(--accent); color: var(--accent); padding: 8px; font-size: 12.5px; margin-bottom: 8px; }

/* ------------------------------------------------------------------ 关于 */

.prose { border: 1px solid var(--border); background: var(--surface); padding: 18px 20px; max-width: 92ch; }
.prose h2 { margin: 18px 0 6px; font-size: 15px; }
.prose h2:first-child { margin-top: 0; }
.prose p, .prose li { color: var(--muted); }
.prose b { color: var(--text); }

/* ------------------------------------------------------------------ 页脚 */

.site-footer { border-top: 1px solid var(--border); background: var(--surface); }
.footer-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 14px 16px;
  color: var(--muted);
  font-size: 12px;
}
.footer-meta { margin-top: 4px; }

/* ------------------------------------------------------------------ 杂项 */

.toast {
  position: fixed;
  left: 16px;
  bottom: 16px;
  z-index: 60;
  background: var(--surface);
  border: 1px solid var(--accent);
  color: var(--text);
  font-size: 12.5px;
  padding: 8px 12px;
  max-width: 70vw;
}

.boot-warning {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--bg);
  color: var(--text);
  padding: 40px 20px;
  overflow: auto;
}
.boot-warning p { max-width: 70ch; margin-bottom: 10px; color: var(--muted); }
.boot-warning b { color: var(--accent); }

/* -------------------------------------------------------------- 响应式 */

@media (max-width: 980px) {
  /**
   * 窄屏改成上下堆叠（原来是 grid 的单列）。
   * 玩家列宽度改为按视口宽算 —— 堆叠后它不再与元数据列争宽度，
   * 用「视口宽度」和「高度预算允许的宽度」取小，A4 比例保持不变。
   */
  .panel-body { flex-direction: column; }
  .panel-col.player-col {
    width: min(100%, calc(92vh / 1.414 - 155px));
    min-width: 0;
    align-self: center;      /* 堆叠时居中，别贴左留一大片空白 */
  }
  /* 堆叠后左列没有可对齐的右列高度，舞台退回固定 A4 比例（宽度的 1.414 倍） */
  .panel-col.player-col .player-stage {
    flex: 0 0 auto;
    aspect-ratio: 1 / 1.414;
    min-height: 0;
  }
  .panel-col + .panel-col { border-left: 0; border-top: 1px solid var(--border); width: 100%; }
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 860px) {
  .topbar-inner { flex-wrap: wrap; padding: 8px 12px; gap: 10px; }
  .nav-toggle { display: flex; order: 3; margin-left: auto; }
  .nav-search { order: 4; flex: 1 1 100%; margin-left: 0; }
  .nav {
    order: 5;
    flex: 1 1 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    border-top: 1px solid var(--border);
    padding-top: 6px;
    display: none;
  }
  .nav.is-open { display: flex; }
  .nav-link { padding: 9px 4px; border-bottom: 0; border-left: 2px solid transparent; }
  .nav-link.is-active { border-left-color: var(--accent); border-bottom-color: transparent; }
  .hero h1 { font-size: 22px; }
  #main { padding: 14px 12px 40px; }
  .chart-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
  .pack-grid { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
  .skill { grid-template-columns: 62px 1fr 28px; }
  .detail-panel { width: 100vw; max-height: 100vh; }
  .pc-hint { display: none; }
  .meta-row { grid-template-columns: 84px minmax(0, 1fr); }
}

@media (max-width: 520px) {
  .chart-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat-num { font-size: 18px; }
  .pager-jump { margin-left: 0; }
}

/**
 * 矮视口统一处理 —— **放在所有宽度断点之后**。
 *
 * 覆盖手机横屏（可视高度可能只有 277px）、以及桌面把窗口拖扁的情况。
 * 这类视口里纵向空间本就稀缺，坚持 A4 只会两头不讨好：放宽比例，
 * 优先保证轨道宽度与可视时间跨度。
 *
 * 顺序很关键：早期版本把它放在 `max-width: 520px` 之前，
 * 结果手机横屏同时命中两条、被后者按 `vh` 算的窄宽度覆盖，
 * 舞台最后只剩 11×200px。
 */
@media (max-height: 560px) {
  .panel-col.player-col {
    width: min(100%, calc(100vw - 40px));
    min-width: 0;
  }
  /* 矮视口：控制区已在右列，舞台 flex 撑满列高即可，无需再为控制区预留高度
   *（早先的 100vh−245px 显式高度是给"舞台下方还有控制区"的旧布局用的）。 */
}
