/* ============================================================================
   pages-store.css —— 原 pages.css 的第 2/6 段（2026-09-19 拆分）

   原文件 3,696 行，把六种板块的样式装在了一起（《模型档位与代码质量_操作准则》
   §5 点名的技术债）。现按文件自己的 ===== 横幅拆成六段，index.html 按
   原顺序引入 —— 层叠顺序与拆分前完全一致（拆分脚本做过逐行相等校验）：

     1 pages-login.css → 2 pages-store.css → 3 pages-practice.css
     → 4 pages-identity.css → 5 pages-practice-fx.css → 6 pages-learn.css

   本段：应用（词本）：商店详情 / 我的库 / 成就页
   ============================================================================ */

/* ==========================================================================
   应用（词本）—— 商店详情 / 我的库详情 / 成就页
   老师 2026-09-18 的信息架构纠正：商店面与个人面必须分开。
   这里按三条线分组，方便对照着改。
   ========================================================================== */

/* 商店里的应用区。刻意排在教材网格之前 ——
   练习是主动行为，比阅读更该被先看到。 */
.store-apps { margin-bottom: 26px; }

/* 真实书封（2026-09-23）—— 叠在程序封面之上，铺满整块 `.card-cover`。
   ★ 下面那层永远画着程序封面：`onerror` 加 `is-missing` 把 img 隐掉，
     于是"这本没封面"就自动回落，**前端不需要知道有没有图**。 */
.cover-img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cover-img.is-missing {
  display: none;
}

/* 条目型下封面缩到 64px 宽 —— 真实书封跟着一起缩（保持封面比例） */
body.view-list .grid .cover-img {
  object-fit: cover;
}

.card-app { position: relative; }

/* 「已在库中」—— 与 CEFR / 单元数 / 词条数**同一排、同一规格**（老师 2026-09-23
   要的"统一标注"）。★ 金色和 `owned-flag` 时代一致，一眼还是那个意思。 */
.badge-owned {
  color: var(--c-gold);
  border-color: rgba(212, 175, 55, 0.5);
}

/* ================= 详情页封面 hero（2026-09-23）=================
   之前详情页"连书籍封面都没有"，只有几行空键值。现在顶部是：
   左边竖版封面 + 右边书名/出版社/作者/词条数/分类。
   ★ 封面图与商店卡片同一套回落（`.bk-hero-img` 盖在程序封面之上、`onerror` 自隐）。 */
.bk-hero {
  display: flex;
  gap: 20px;
  align-items: flex-start;
  padding: 16px;
  margin-bottom: 16px;
  background: linear-gradient(180deg, rgba(28, 44, 62, 0.85), rgba(14, 22, 33, 0.6));
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
}

.bk-hero-cover {
  position: relative;
  flex: none;
  width: 208px;
  aspect-ratio: 210 / 297;
  overflow: hidden;
  border-radius: var(--r-sm);
  background: #0e1621;
}

.bk-hero-cover svg,
.bk-hero-img {
  width: 100%;
  height: 100%;
}

.bk-hero-img {
  position: absolute;
  inset: 0;
  display: block;
  object-fit: cover;
}

.bk-hero-img.is-missing {
  display: none;
}

.bk-hero-info {
  flex: 1 1 auto;
  min-width: 0;
  padding-top: 4px;
}

.bk-title {
  margin: 0 0 6px;
  font-size: var(--fs-xl);
  color: var(--t-bright);
  line-height: 1.25;
}

.bk-series {
  margin-bottom: 10px;
  font-size: var(--fs-md);
  color: var(--t-muted);
}

.bk-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 10px;
  font-size: var(--fs-sm);
  color: var(--t-dim);
}

.bk-meta-item::after {
  content: '·';
  margin-left: 10px;
  opacity: 0.45;
}

.bk-meta-item:last-child::after {
  content: '';
  margin: 0;
}

/* 窄屏：封面缩一档，标题不掉到封面下面 */
@media (max-width: 640px) {
  .bk-hero-cover {
    width: 128px;
  }

  .bk-title {
    font-size: var(--fs-lg);
  }
}

/* ---------------- 封面右上角的两枚状态角标（2026-09-23 统一） ----------------
   老师：「两者应该保持一致，都放到右上角，同时用**高亮的颜色**表示，
   现在那些未入库的都是黑色，很不明显。」
   ★ 两枚**同一位置、同一规格、同一高亮色**（金色）—— 只是文案不同：
     `已在库中` / `暂未开放`。它们互斥，不会同时出现。
   ★ 底色用深色打底（压在封面上要读得清），但**文字与描边是金色** ——
     这就是"高亮"：原来的 `.soon-flag` 文字是暗色 `--t-ink`，压在深封面上等于看不见。 */
.soon-flag,
.owned-flag {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  padding: 2px 8px;
  border-radius: var(--r-sm);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--c-gold);
  background: rgba(14, 22, 33, 0.9);
  border: 1px solid rgba(212, 175, 55, 0.6);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}

/* `已在库中` 稍微亮一点：它是"好事"，值得更显眼 */
.owned-flag {
  color: #f3d98a;
  border-color: rgba(212, 175, 55, 0.85);
}

/* ---------------- 价格角标（2026-10-16，老师：词本用成就点解锁） ----------------
   老师原话：「学生可以看到**免费入库**的提醒」＋「学科词汇 100/200/300 成就点解锁、
   SSS 与牛津 500」。
   ⇒ 四枚角标共用**同一位置、同一规格**（照 2026-09-23 那轮定下的"统一标注"）：
     暂未开放 / 已在库中 / N 成就点 / 免费入库，两两互斥、不会打架。
   ★ 配色上把**两种价格分开**：要花钱的用**蓝**（"要付代价"），免费的用**绿**
     （"放心点"）—— 只靠文字区分的话，扫一眼是分不出来的。 */
.price-flag,
.free-flag {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  padding: 2px 8px;
  border-radius: var(--r-sm);
  font-size: var(--fs-xs);
  font-weight: 600;
  background: rgba(14, 22, 33, 0.9);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}

/* 要花点 —— 蓝（与全站主色同源，一眼是"要付出"） */
.price-flag {
  color: #8fc7ff;
  border: 1px solid rgba(120, 180, 255, 0.7);
}

/* 免费 —— 绿（老师要的"免费入库提醒"） */
.free-flag {
  color: #8fe3b0;
  border: 1px solid rgba(120, 220, 165, 0.7);
}

/* 条目型下角标也落在**整个条目的右上角**（卡片本身是定位上下文）——
   ★ 条目型的卡片是横排的，`top/right` 相对卡片，所以位置自动就是"整条右上角" ✓ */
body.view-list .grid .soon-flag,
body.view-list .grid .owned-flag,
body.view-list .grid .price-flag,
body.view-list .grid .free-flag {
  top: 6px;
  right: 6px;
}

/* 详情页的**一行成就条**（老师 2026-09-23：已解锁在前、未解锁在后）
   ★ 用"亮/暗"区分，而不是"有/无" —— 未解锁的**也要画出来**（那是"这本书还有哪些
     成就等着你"，是吸引点）；只是压暗、降不透明度。 */
.ach-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
}

.ach-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--r-sm);
  background: var(--c-input);
  border: 1px solid var(--c-line);
  color: var(--t-dim);
  opacity: 0.45;
}

.ach-cell.is-on {
  color: var(--c-gold);
  border-color: rgba(212, 175, 55, 0.55);
  background: rgba(212, 175, 55, 0.12);
  opacity: 1;
}

.ach-cell .icon {
  width: 16px;
  height: 16px;
}

/* 商店详情页的**行动条**（老师 2026-09-23：「这两个按钮都比较小，放到返回商店下面，
   同时做大一点」）。
   ★ 位置本来就在「返回商店」下面（`drawStoreDetail` 里那个 backButton 之后紧接着 `act`），
     这次只动**尺寸**。
   ★ 选择器写成 `.store-action .btn`（比单独的 `.btn` 更具体）—— 不必知道 `.store-action`
     原来在哪儿定义过什么，也能稳稳盖住。 */
.store-action .btn {
  padding: 10px 26px;
  font-size: var(--fs-lg);
  font-weight: 600;
}

/* 「即将推出」（老师 2026-09-23 按 Steam 定稿：**蓝色**）
   ★★ 和可用的绿色主按钮**刻意不同色** —— 绿色 = 现在就能入库；蓝色 = 以后才行。
      Steam 也是这个规矩（绿=购买 / 蓝=预购或即将推出）。
   ★ **不能直接用 `.btn-primary`**：我们这边它是**绿色**的（见 components.css），
     用它就不是老师要的蓝色了。 */
.btn-soon {
  background: var(--c-accent);
  color: #0b1620;
  font-weight: 700;
  cursor: default;
  opacity: 0.9;
}

.btn-soon:disabled {
  opacity: 0.75;
}

/* 详情页信息行里的链接（出版社）——点进去看该社全部教材（老师 2026-09-23） */
.kv-link {
  color: var(--c-accent);
  text-decoration: none;
  border-bottom: 1px dashed rgba(102, 192, 244, 0.45);
}

.kv-link:hover {
  color: var(--t-bright);
}

/* 已入库：加一圈金色描边。不做成「变灰」——
   入库是好事，不该在视觉上被降级。 */
.card-app.is-owned {
  border-color: rgba(212, 175, 55, 0.45);
}

.card-app.is-owned::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--r-md);
  pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(212, 175, 55, 0.28);
}

/* .owned-flag 的旧定义已删（2026-09-23 统一到上面那段：与 .soon-flag 同规格、高亮金） */

/* ---------- 线一：商店详情 ---------- */

.detail-sub {
  margin: 6px 0 10px;
  color: var(--t-muted);
  font-size: var(--fs-lg);
}

/* 行动条。商店详情页唯一带个人色彩的东西 ——
   但它只回答「加不加入库」，不回答「我练到哪了」。 */
.store-action {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 14px 0 18px;
  padding: 12px 14px;
  background: linear-gradient(90deg, var(--c-card-hi), var(--c-card));
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
}

.store-action .btn-primary {
  min-height: var(--tap);
  padding: 0 22px;
}

.badge-ok {
  background: rgba(117, 176, 34, 0.16);
  border-color: var(--c-green-a);
  color: #a6d95c;
}

/* 成就体系总览：只给「有多少、怎么分层」，不给「我解锁了几个」 */
.ach-overview { margin-top: 4px; }

.ach-overview-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.ach-overview-head .big-n {
  font-size: 34px;
  font-weight: 700;
  color: var(--t-bright);
  line-height: 1;
}

.ach-overview-head .big-l {
  font-size: var(--fs-md);
  color: var(--t-muted);
}

.tier-bars {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 10px;
}

.tier-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  border-bottom: 1px solid var(--c-line-soft);
}

.tier-bar:last-child { border-bottom: 0; }

.tier-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: 0 0 auto;
}

.tier-name {
  color: var(--t-primary);
  min-width: 68px;
}

.tier-desc {
  color: var(--t-dim);
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tier-n {
  color: var(--t-muted);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}

/* ---------- 线二：我的库 ---------- */

/* 库里的头图刻意矮 —— 「自己看」的页面不该再拿商店那种大图占首屏 */
.lib-hero {
  border-radius: var(--r-md);
  overflow: hidden;
  border: 1px solid var(--c-line);
  margin-bottom: 12px;
}

.lib-hero svg { width: 100%; height: auto; aspect-ratio: 1200 / 200; }

.lib-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px;
  margin: 14px 0 6px;
}

.lib-stat {
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  padding: 10px 12px;
}

.lib-stat .n {
  font-size: var(--fs-xl);
  font-weight: 600;
  color: var(--t-bright);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.lib-stat .l {
  color: var(--t-muted);
  font-size: var(--fs-sm);
  margin-top: 2px;
}

.lib-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.btn-lg {
  min-height: var(--tap);
  padding: 0 24px;
  font-size: var(--fs-lg);
}

/* 进度条 */
.pbar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 4px 0 2px;
}

.pbar-track {
  flex: 1 1 auto;
  height: 6px;
  background: var(--c-input);
  border: 1px solid var(--c-line-soft);
  border-radius: 3px;
  overflow: hidden;
}

.pbar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--c-green-a), var(--c-green-b));
  transition: width 0.25s ease;
}

.pbar-n {
  color: var(--t-muted);
  font-variant-numeric: tabular-nums;
  min-width: 38px;
  text-align: right;
}

/* 库列表 */
.lib-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.lib-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 10px;
  text-align: left;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.lib-row:hover {
  background: var(--c-card-hi);
  border-color: var(--c-hover);
}

.lib-row-cover {
  flex: 0 0 auto;
  width: 132px;
  border-radius: var(--r-sm);
  overflow: hidden;
}

.lib-row-cover svg { width: 100%; height: auto; aspect-ratio: 460 / 215; }

.lib-row-body { flex: 1 1 auto; min-width: 0; }

.lib-row-title {
  color: var(--t-bright);
  font-size: var(--fs-lg);
  margin-bottom: 2px;
}

.lib-row-sub {
  color: var(--t-dim);
  font-size: var(--fs-sm);
  margin-bottom: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.lib-row-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.lib-row-right {
  flex: 0 0 auto;
  text-align: right;
  padding-left: 8px;
  border-left: 1px solid var(--c-line-soft);
  min-width: 74px;
}

.lib-row-ach {
  color: var(--c-gold);
  font-size: var(--fs-lg);
  font-variant-numeric: tabular-nums;
}

.lib-row-ach-l {
  color: var(--t-dim);
  font-size: var(--fs-xs);
}

/* ---------- 线三：成就页 ---------- */

/* 未入库时的样子。
   ★ 这里必须**不画任何成就 chrome** —— 一旦留出空槽位，
   就等于告诉学生「有东西只是被盖住了」，那道闸就形同虚设。 */
.ach-locked {
  max-width: 520px;
  margin: 40px auto;
  padding: 32px 24px;
  text-align: center;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
}

/* 锁形图标的外壳：不依赖父容器的 text-align，自己就把图标居中 */
.lock-icon { display: flex; justify-content: center; }

.ach-locked .icon-big {
  width: 54px;
  height: 54px;
  margin: 0 auto 14px;
  color: var(--t-dim);
  display: block;
}

.ach-locked .big {
  font-size: var(--fs-xl);
  color: var(--t-bright);
  margin-bottom: 10px;
}

.ach-locked p {
  color: var(--t-muted);
  margin-bottom: 18px;
}

/* 我的解锁 —— 成就页唯一的「我的」数字，sticky 钉在顶部（top 与 store-side-sticky 对齐）。
   原来的三格汇总（参与统计的同学 / 只入库未运行）已删除：那是内部统计口径，
   学生不需要知道；全局信息只以「每个成就已完成多少人」的形式出现在卡片上。 */
/* ---------------- 成就图标：真实图片 + 三态（2026-09-23 实装） ----------------
   图片由 `views/apps.js` 的 `achCard()` 渲染，路径 `assets/img/ach/<成就id>.png`。
   ★ 三态靠 CSS 就能做完，**不需要老师另做一套灰图**：
     未解锁（普通）→ 黑白；未解锁（隐藏）→ 用的是 `_hidden.png` 那张问号；已解锁 → 原色。
   ★ 缺图的成就 **回落成图标字体**（JS 里先垫了图标，图 `onload` 成功才藏起来）——
     所以这里要给图标留个位置，别让它和图打架。 */
.ach-icon {
  position: relative;
}

.ach-icon img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* 图还没加载出来（或压根没有图）时，图标字体先顶着 */
.ach-icon .icon {
  display: block;
  margin: auto;
}

/* 图加载成功 ⇒ 把垫底的图标藏掉（两行叠在一起会糊） */
.ach-icon.has-img .icon {
  display: none;
}

/* ★ 未解锁的**普通**成就：整张图变黑白、再压暗一点。
   ★ 不用 `:not(.is-unlocked) img` 一刀切 —— 因为**隐藏未解锁**用的是那张问号图，
     它本来就该按原样显示（老师说的是"黑白彩色都可以"），压暗了反而看不清。
   ⇒ 只对"非隐藏"的未解锁项做黑白。 */
.ach:not(.is-unlocked):not(.is-secret) .ach-icon img {
  filter: grayscale(1);
  opacity: 0.55;
}

.ach.is-unlocked .ach-icon img {
  filter: none;
  opacity: 1;
}

.ach-mine {
  position: sticky;
  top: 112px;
  z-index: 3;
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 14px 0 10px;
  padding: 10px 16px;
  background: var(--c-card-hi);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
}

.ach-mine-l {
  color: var(--t-muted);
  font-size: var(--fs-sm);
}

.ach-mine-n {
  font-size: var(--fs-xl);
  font-weight: 600;
  color: var(--t-bright);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  margin-left: auto;
}

.cmp-note { margin: 8px 0 14px; }

.ach-section { margin-top: 22px; }

.ach-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: 10px;
  margin-top: 10px;
}

/* ================= 商店视图：一排几本 + 条目型（2026-09-23）=================
   老师要看效果：「现在的尺寸有点过大，一排只显示最多三本…试试 5 本 6 本和 8 本」
   「同理，也可以按照条目来从上到下的显示方式（一种是现在的书籍封面，一种是
   把所有封面都缩小到一行，第二行显示基本信息的条目型）」。

   ★★ 做法：**同一份 DOM**，靠 `body` 上的 class 换排版 —— 卡片本来就是
     「封面 + 卡片体」两段，条目型只是**从竖排改成横排、封面缩小**，
     **不需要第二套渲染代码**（少一套渲染就少一处会漂的地方）。

   ★ 怎么试（定下来之前不用做控件，改地址栏最快）：
       `#/store?cols=5`   `#/store?cols=6`   `#/store?cols=8`
       `#/store?view=list`   也可以一起写：`#/store?cols=6&view=list`
     ★ 默认 `cols=3`（= 现在的样子）。定下来之后我把切换控件做进页面，
       并把这段"试参数"的说明删掉。
   ★ 选择器带 `body.` 前缀 ⇒ 优先级高于 `.grid`（0,2,1 > 0,1,0），
     所以**不必知道 `.grid` 原来定义在哪个文件里**也能覆盖掉它。
   ★ 窄屏兜底：固定列数在手机上会挤成一团 ⇒ 交给 auto-fill。
   ========================================================================= */
/* ★★ 2026-09-23 老师拍板：**封面型 = 一排 5 本**（写死，不再可调）。
   「一排几本」试过了 3/5/6/8，他选 5；试看条只留「封面型 / 条目型」两个按钮。
   ★ 用 `body:not(.view-list)` 而不是 `body.cols-5` —— 密度不再是状态，
     而是封面型**本来**的排版；条目型有自己的排版（见下），两者互斥。 */
body:not(.view-list) .grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }

/* 窄屏兜底：5 列在手机上会挤成一团 ⇒ 交给 auto-fill */
@media (max-width: 760px) {
  body:not(.view-list) .grid {
    grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  }
}

/* ---- 条目型：封面缩到一行、基本信息在右 ---- */
body.view-list .grid {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

body.view-list .grid .card-app,
body.view-list .grid .card {
  flex-direction: row;
  align-items: center;
  gap: 12px;
}

body.view-list .grid .card-cover {
  width: 64px;
  flex: none;
}

body.view-list .grid .card-body {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 12px;
}

/* ★★ S30：排版切换条 —— **画在商店页内**（2026-09-27，老师原话
   「固定到商店页面，而不是现在全平台浮动」）。
   原来的 .density-bar 是 position:fixed 挂在 body 上（右下角悬浮、
   翻页才消失、不点不走）；现在它是页面流里的一行，翻页随页面一起消失。 */
.store-viewbar {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 10px;
  padding: 6px 10px;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
}

.store-viewbar-t {
  margin-right: 2px;
  font-size: var(--fs-xs);
  color: var(--t-dim);
}

.store-viewbar-btn {
  padding: 3px 9px;
  font-size: var(--fs-xs);
  color: var(--t-primary);
  background: var(--c-input);
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  cursor: pointer;
}

.store-viewbar-btn:hover {
  color: var(--t-bright);
  background: var(--c-hover);
}

/* 当前档：描边 + 亮字，一眼看出现在用的是哪一种排版 */
.store-viewbar-btn.is-active {
  color: var(--t-bright);
  border-color: var(--c-accent);
  background: var(--c-hover);
}

/* 成就卡片。左侧 3px 色带 = 层级色，由 JS 内联设置 */
.ach {
  display: flex;
  gap: 10px;
  padding: 10px 12px;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-left: 3px solid var(--c-accent);
  border-radius: var(--r-md);
  /* 未解锁：整体压暗，但文字仍要看得清（要能读条件） */
  opacity: 0.62;
}

.ach.is-unlocked {
  opacity: 1;
  background: var(--c-card-hi);
  border-color: var(--c-hover);
}

.ach.is-secret.is-unlocked {
  background: linear-gradient(135deg, var(--c-card-hi), #241a33);
}

.ach-icon {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--c-input);
  border: 1px solid var(--c-line-soft);
  border-radius: var(--r-sm);
  color: var(--t-dim);
}

.ach.is-unlocked .ach-icon {
  color: var(--c-gold);
  border-color: rgba(212, 175, 55, 0.4);
}

.ach-icon .icon { width: 18px; height: 18px; }

.ach-body { flex: 1 1 auto; min-width: 0; }

.ach-name {
  color: var(--t-bright);
  font-size: var(--fs-lg);
  margin-bottom: 2px;
}

.ach-desc {
  color: var(--t-muted);
  font-size: var(--fs-sm);
  margin-bottom: 4px;
}

.ach-cond {
  color: var(--t-dim);
  font-size: var(--fs-xs);
}

/* ---------------------------------------------------------------- 成就进度条
   2026-10-01 新增（老师：「给成就加上进度条，让学生直观看到进展」）。
   ★ 只出现在**未解锁 + 非隐藏 + 服务端算得出**的卡片上（见 apps.js 的 achCard）。
   ★ 放在名字下面而不是 cond 下面 —— 未解锁时 cond 是被刻意隐藏的（留悬念），
     进度条挂在它后面就会一起消失。
   ★ 视觉上刻意比名字弱：它是辅助信息，不该跟成就名抢视线。 */
.ach-prog {
  margin: 4px 0 5px;
}

.ach-prog-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: var(--fs-xs);
  color: var(--t-dim);
  margin-bottom: 3px;
}

.ach-prog-head .pg-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 数字用等宽数字：进度在动的时候宽度不会跳 */
.ach-prog-head .pg-num {
  flex: 0 0 auto;
  color: var(--t-muted);
  font-variant-numeric: tabular-nums;
}

.pg-track {
  height: 6px;
  border-radius: 3px;
  background: var(--c-input);
  border: 1px solid var(--c-line-soft);
  overflow: hidden;
}

.pg-fill {
  height: 100%;
  border-radius: 3px 0 0 3px;
  /* ★ 与站内其它进度条（components.css 的 .rk-fill / pages-store.css 的 .pbar-fill）
     用**同一套绿渐变**：成就进度条是"完成度"，用绿色比用主题强调色更贴语义。 */
  background: linear-gradient(90deg, var(--c-green-a), var(--c-green-b));
  transition: width 0.25s ease;
}

/* 窄屏：进度条本身是流式的，只需要让数字不被挤掉 */
@media (max-width: 640px) {
  .ach-prog-head { font-size: var(--fs-xs); }
}

.ach-times {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px dashed var(--c-line-soft);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

.ach-times .me { color: var(--c-accent); }
.ach-times .peer { color: var(--c-gold); }

.ach-side {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  text-align: right;
}

.ach-side .rar {
  font-size: var(--fs-xs);
  padding: 1px 6px;
  border-radius: var(--r-sm);
  border: 1px solid var(--c-line);
  color: var(--t-muted);
}

/* 稀有度色：和 achievements.json 的 rarity 表对应 */
.r-common    { }
.r-uncommon  { color: #7ec850; border-color: rgba(126, 200, 80, 0.4); }
.r-rare      { color: #5aa9e6; border-color: rgba(90, 169, 230, 0.45); }
.r-epic      { color: #c78ff0; border-color: rgba(199, 143, 240, 0.45); }
.r-legendary { color: #d4af37; border-color: rgba(212, 175, 55, 0.5); }

.ach-side .pts {
  color: var(--t-dim);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

/* 全站完成人数 —— 学生能看到的唯一全局信息，刻意压低存在感，
   不跟稀有度/privacy 抢视线；只给人数不给百分比。 */
.ach-side .done {
  color: var(--t-dim);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

.ach-side .tick {
  color: #a6d95c;
  font-size: var(--fs-xs);
}

/* 窄屏：卡片改竖排，色带留在左边 */
@media (max-width: 640px) {
  .lib-row { flex-wrap: wrap; }
  .lib-row-cover { width: 100%; }
  .lib-row-right {
    border-left: 0;
    padding-left: 0;
    text-align: left;
  }
  .ach-grid { grid-template-columns: 1fr; }
  .lib-hero svg { aspect-ratio: 3 / 1; }
}




/* ---------------- 应用区按种类分段（2026-09-23） ----------------
   老师要求：默认每类只显示 5 本（一行），右下角「查看更多」每次 +5。
   ★ 用 is-row5 把一行钉成 5 列 —— 原来的 .grid 是自适应列数，
     5 张卡很可能挤成两行，那就不叫"一行"了。
   ★ 窄屏退回自适应（5 列在手机上会挤成豆腐块）。 */
.app-group {
  margin-top: 18px;
}

.sec-head.is-sub {
  margin-top: 14px;
  padding-bottom: 6px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}

.group-foot {
  display: flex;
  justify-content: flex-end;      /* ★ 按钮靠右下角 */
  margin-top: 10px;
}

.grid.is-row5 {
  grid-template-columns: repeat(5, minmax(0, 1fr));
}

@media (max-width: 900px) {
  .grid.is-row5 {
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  }
}


/* ---------------- 全校词库搜索（2026-09-30） ----------------
   老师：「把目前我们所有的单词，做到主页里，做成一个查询单词的接口……
         学生一次查询，返回查询的单词的所有信息」。
   ★ 这一块**不带任何词库数据** —— 它显示的每一个字都是那一次查询的返回，
     所以样式按"一个词一张卡、卡里按来源词本分行"来排。
   ★ 类名统一 ws-（word search）前缀，与 views/store.js 和
     core/wordsearch.js 里的类名逐字对应（教师挑词界面共用同一套 render）。 */

.store-words { margin-bottom: 26px; }

.ws-lib {
  margin: 0 0 8px;
  font-size: var(--fs-xs);
  color: var(--t-dim);
}

.ws-item {
  padding: 12px 14px;
  margin-bottom: 10px;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
}

/* 精确命中的那一条（学生敲了一整个词）—— 描边亮一点，一眼知道"就是它" */
.ws-item.is-exact {
  border-color: var(--c-accent-dim);
  background: linear-gradient(180deg, rgba(28, 44, 62, 0.9), rgba(14, 22, 33, 0.65));
}

.ws-item-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.ws-word {
  font-size: var(--fs-xl);
  color: var(--t-bright);
  line-height: 1.2;
  word-break: break-word;
}

/* 教师挑词：词头本身就是按钮（点了选中这个词） */
.ws-pick {
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--fs-xl);
  color: var(--c-accent);
  cursor: pointer;
  text-align: left;
}

.ws-pick:hover { text-decoration: underline; }

.ws-phon {
  color: var(--t-muted);
  font-size: var(--fs-md);
  word-break: break-word;
}

.ws-badge {
  display: inline-flex;
  align-items: center;
  height: 20px;
  padding: 0 7px;
  border-radius: var(--r-sm);
  background: #223449;
  color: var(--t-dim);
  font-size: var(--fs-xs);
  white-space: nowrap;
}

/* 精确命中给金色（与"已在库中"同一档重量）；其余匹配一律压暗 */
.ws-match-exact {
  background: rgba(212, 175, 55, 0.18);
  border: 1px solid var(--c-gold);
  color: var(--c-gold);
}

/* 词形推断命中（difficulties → difficulty）：**推出来的**，与"精确命中"分开着色 */
.ws-match-lemma {
  background: rgba(46, 160, 110, 0.16);
  border: 1px solid #2ea06e;
  color: #63d3a2;
}

/* 变形关系那一行：difficulties = difficulty（复数） */
.ws-inflect {
  margin-top: 5px;
  font-size: var(--fs-sm);
  color: var(--t-primary);
}
.ws-inflect-surface { color: #63d3a2; font-weight: 700; }
.ws-inflect-arrow { color: var(--t-dim); }
.ws-inflect-lemma { color: var(--t-bright); font-weight: 700; }
.ws-inflect-rel { color: #63d3a2; }
.ws-inflect-alt { margin-top: 2px; font-size: var(--fs-xs); color: var(--t-dim); }

/* 拼错时的「你是不是想查…」（服务端一编辑建议） */
.ws-suggest {
  margin-top: 8px;
  font-size: var(--fs-sm);
  color: var(--t-primary);
}
.ws-suggest-t { color: var(--t-dim); }
.ws-suggest-w { color: #63d3a2; }
.ws-suggest-l { color: var(--t-dim); font-size: var(--fs-xs); }
.ws-suggest-sep { color: var(--t-dim); }

.ws-count {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--t-dim);
  white-space: nowrap;
}

/* 发音：没录音就退回系统朗读；两条路都不通时 JS 会明确提示，不留"点了没反应" */
.ws-play {
  height: 24px;
  padding: 0 10px;
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  background: var(--c-card-hi);
  color: var(--t-primary);
  font-size: var(--fs-xs);
  cursor: pointer;
}

.ws-play:hover { background: var(--c-hover); }

.ws-sum {
  margin-top: 6px;
  font-size: var(--fs-md);
  color: var(--t-primary);
}

.ws-pos {
  margin-right: 6px;
  color: var(--c-accent);
  font-style: italic;
}

.ws-cn { color: var(--t-primary); }
.ws-cn.is-dim { color: var(--t-dim); }

.ws-en {
  margin-top: 4px;
  font-size: var(--fs-md);
  color: var(--t-muted);
}

/* 例句：左边一条竖线，与释义分开；出处（example_src）跟在后面 */
.ws-ex {
  margin-top: 6px;
  padding-left: 8px;
  border-left: 2px solid var(--c-line);
  font-size: var(--fs-md);
  color: var(--t-muted);
  word-break: break-word;
}

.ws-ex.is-sm { font-size: var(--fs-sm); }

.ws-ex-src {
  margin-left: 6px;
  font-size: var(--fs-xs);
  color: var(--t-dim);
}

.ws-en.is-sm { font-size: var(--fs-sm); }

/* 各词本分行 —— 老师要的"这个词在所有词本里的信息，一条不合并" */
.ws-books {
  margin: 10px 0 0;
  padding: 8px 0 0;
  border-top: 1px dashed var(--c-line-soft);
  list-style: none;
}

.ws-book { padding: 6px 0; }
.ws-book + .ws-book { border-top: 1px solid var(--c-line-soft); }

.ws-book-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
}

.ws-book-name {
  color: var(--c-accent);
  font-size: var(--fs-sm);
}

.ws-book-unit {
  color: var(--t-dim);
  font-size: var(--fs-xs);
}

.ws-book-def {
  margin-top: 2px;
  font-size: var(--fs-md);
}

.ws-hint {
  margin-top: 6px;
  font-size: var(--fs-xs);
  color: var(--t-dim);
}

/* 查询失败 —— 与"没找到"是两件事，红色框把这句话顶到脸上 */
.ws.is-failed {
  padding: 12px 14px;
  border: 1px solid rgba(192, 57, 43, 0.55);
  border-radius: var(--r-md);
  background: rgba(192, 57, 43, 0.1);
}

.ws-fail-title {
  color: #e88b7c;
  font-size: var(--fs-lg);
  margin-bottom: 4px;
}

.ws-fail-msg { color: var(--t-primary); }

.ws-fail-hint {
  margin-top: 6px;
  font-size: var(--fs-xs);
  color: var(--t-dim);
}

/* 空态在搜索区里不需要整页那么大的留白 */
.ws .empty { padding: 26px 16px; }

/* ===========================================================================
   我的库：视图切换（列表 / 封面）+ 星标（老师 2026-10-01）

   老师原话：「把我的库这个的可视化也加上**显示词本封面的按钮**，现在这样也保留，
   让用户自己挑选，同时做一个**加星标的按钮**，加了星标的词本会排在前面。」

   ★ 这套样式一直住在 pages-store.css（`.lib-list` / `.lib-row` 就在这里）——
     没另起一个文件，免得"同一个页面的样式分两处"。
   =========================================================================== */

/* 视图切换器：两颗小按钮，放在「我的库」标题那一行右侧 */
.lib-view-switch {
  display: flex;
  gap: 6px;
  margin-left: auto;          /* 顶到那一行的最右边 */
}

.lib-view-btn {
  padding: 4px 12px;
  font-size: var(--fs-sm);
  font-family: inherit;
  color: var(--t-dim);
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  cursor: pointer;
}

.lib-view-btn:hover {
  color: var(--t-primary);
  background: var(--c-card-hi);
}

.lib-view-btn.is-on {
  color: var(--t-bright);
  background: var(--c-hover);
  border-color: var(--c-accent-dim);
}

/* ---------- 星标 ---------- */
.lib-star {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  padding: 0;
  font-size: 17px;
  line-height: 1;
  color: var(--t-dim);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
}

.lib-star:hover {
  color: var(--c-gold);
  background: var(--c-card-hi);
}

.lib-star.is-on {
  color: var(--c-gold);       /* 打上就是金色实心星 */
}

.lib-star[aria-busy='true'] {
  opacity: 0.55;
  cursor: progress;
}

/* 打了星的那一条/那一张：左边一条金线，扫一眼就知道哪些排前面了 */
.lib-row.is-starred,
.lib-card.is-starred > .lib-card-cover {
  border-color: var(--c-gold);
}

/* ---------- 封面视图 ---------- */
.lib-cards {
  display: grid;
  /* 自适应列数：封面卡比列表行窄得多，用 minmax 让它随窗口自己排 */
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px;
}

.lib-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.lib-card-cover {
  position: relative;
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  overflow: hidden;
  /* 书封的常见比例。★ 用 3/4（≈0.75）而不是 16/9 —— 那是**单词卡**的比例，
     书封是竖的，套 16/9 会把封面压扁。 */
  aspect-ratio: 3 / 4;
  background: var(--c-card);
}

/* 真封面：铺满，`object-fit: cover` 让它自己裁（封面本来就有各种比例） */
.lib-card-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ★ 没有真实封面时 onerror 加 is-missing → 自隐，露出下面程序生成的图案。
   与教材详情页 `.bk-hero-img` 同一套回落。 */
.lib-card-img.is-missing { display: none; }

/* 整张封面就是"打开这本书"的按钮（透明覆盖层），
   星标按钮 z-index 比它高，点星不会误开书。 */
.lib-card-open {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  background: transparent;
  border: 0;
  cursor: pointer;
  z-index: 1;
}

.lib-card-cover .lib-star {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 2;                 /* ★ 盖在 open 之上，否则点不到 */
  background: rgba(14, 22, 33, 0.72);
  backdrop-filter: blur(2px);
}

.lib-card-title {
  color: var(--t-bright);
  font-size: var(--fs-sm);
  line-height: 1.35;
  /* 书名最多两行，超出省略 —— 长书名不能把网格撑歪 */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.lib-card-sub {
  color: var(--t-dim);
  font-size: var(--fs-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- 列表行：为了塞进星标，改成"覆盖层 + 星标"的结构 ---------- */
/* 行本身不再是 <button>（星标是里面另一个按钮，button 不能嵌套）——
   open 是一层透明盖板，星标在它之上。 */
.lib-row {
  position: relative;
}

.lib-row-open {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  background: transparent;
  border: 0;
  cursor: pointer;
  z-index: 1;
}

.lib-row-right {
  position: relative;
  z-index: 2;                 /* 成就数字与星标在盖板之上 */
  display: flex;
  align-items: center;
  gap: 10px;
}

/* 窄屏：星标别把这一行挤变形 */
@media (max-width: 620px) {
  .lib-view-switch { margin-left: 0; }
  .lib-cards { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 10px; }
}



/* ===========================================================================
   ★★ 2026-10-16（老师）：库/商店页的搜索框从**顶栏**搬到**侧栏「种类」下面**。
   原因：它挂顶栏右侧时，学生窄屏（1024 / 390）会把左侧导航最后几项
   （我的词库 / 班级 / 我的 / 成就商店 / 任务 / 后台）挤住点不到。
   顶栏那份已经撤掉（core/ui.js 不再挂载），全页只有这一个 #global-search。
   文案已改「搜索本库」。下面两条都加了 .side 作用域 ——
   layout.css 里 .searchbox input { width: 220px } 与窄屏媒体查询的 130/96px
   靠**选择器更具体**压住，不改顶栏那份定义（别的地方将来还要用）。
   =========================================================================== */
.side .searchbox {
  display: block;
  width: 100%;
  margin: 10px 0 0;
}

.side .searchbox input {
  width: 100%;
  box-sizing: border-box;
}