/* ── Self-hosted fonts ── */
@font-face {
  font-family: 'Shippori Mincho';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/ShipporiMincho-Regular.ttf') format('truetype');
}
@font-face {
  font-family: 'Shippori Mincho';
  font-style: normal; font-weight: 500; font-display: swap;
  src: url('fonts/ShipporiMincho-Medium.ttf') format('truetype');
}
@font-face {
  font-family: 'Shippori Mincho';
  font-style: normal; font-weight: 600 700; font-display: swap;
  src: url('fonts/ShipporiMincho-SemiBold.ttf') format('truetype');
}
@font-face {
  font-family: 'Noto Serif JP';
  font-style: normal; font-weight: 100 900; font-display: swap;
  src: url('fonts/NotoSerifJP-VF.ttf') format('truetype-variations');
}
@font-face {
  font-family: 'Noto Sans JP';
  font-style: normal; font-weight: 100 900; font-display: swap;
  src: url('fonts/NotoSansJP-VF.ttf') format('truetype-variations');
}
@font-face {
  font-family: 'Noto Serif TC';
  font-style: normal; font-weight: 100 900; font-display: swap;
  src: url('fonts/NotoSerifTC-VF.ttf') format('truetype-variations');
}
@font-face {
  font-family: 'Noto Sans TC';
  font-style: normal; font-weight: 100 900; font-display: swap;
  src: url('fonts/NotoSansTC-VF.ttf') format('truetype-variations');
}
@font-face {
  font-family: 'Newsreader';
  font-style: normal; font-weight: 200 800; font-display: swap;
  src: url('fonts/Newsreader-VF.ttf') format('truetype-variations');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal; font-weight: 100 800; font-display: swap;
  src: url('fonts/JetBrainsMono-VF.ttf') format('truetype-variations');
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root,
:root[data-theme="dark"] {
  color-scheme: dark;

  /* ── Background ── */
  --bg-base:     #16130F;
  --bg-surface:  #1F1B16;
  --bg-subtle:   #1A1612;
  --bg-elevated: #2A241D;

  /* ── Border ── */
  --border-default: #3E342B;
  --border-subtle:  #2D2620;

  /* ── Text ── */
  --text-primary:   #F5F1EA;
  --text-secondary: #B8AFA1;
  --text-muted:     #7A7268;



  /* ── Accent ── */
  --accent:            #FF6B4A;
  --accent-hover:      #FF856B;
  --accent-soft:       rgba(255, 107, 74, 0.12);
  --accent-soft-hover: rgba(255, 107, 74, 0.18);
  --accent-border:     rgba(255, 107, 74, 0.30); /* intentional: from article-learning prototype; used by suggestion-card hover / romaji-card-speak / level-badge--matched */
  --accent-highlight:  rgba(255, 107, 74, 0.25); /* D-2B: SnippetMark bg — heavier than accent-soft to pop against card bg */
  --accent-ink:        #FFFFFF;

  /* ── Status ── */
  --status-correct:           #4FB892;
  --status-correct-bg:        rgba(79, 184, 146, 0.10);
  --status-partial:           #E5A24E;
  --status-partial-bg:        rgba(229, 162, 78, 0.10);
  --status-partial-bg-subtle: rgba(229, 162, 78, 0.04); /* intentional: D-4 Phase 3 token; conj-card-header amber 4% tint */
  --status-partial-border:    rgba(229, 162, 78, 0.35); /* intentional: D-4 Phase 3 token; verb-mode composer border */
  --status-wrong:             #C7553F;
  --status-wrong-bg:          rgba(199, 85, 63, 0.10);

  /* ── Shadow ── */
  --shadow-card:     0 1px 2px rgba(0,0,0,0.30), 0 8px 24px rgba(0,0,0,0.20);
  --shadow-misc-1:   0 2px 14px rgba(35,26,15,0.08);
  --shadow-misc-2:   0 2px 16px rgba(35,26,15,0.2);
  --shadow-elevated: 0 4px 24px rgba(0,0,0,0.32), 0 16px 40px rgba(0,0,0,0.28);

  /* ── Glass ── */
  --glass-hairline:   rgba(255,255,255,0.10);
  --glass-bg:         rgba(31, 27, 22, 0.72);
  --glass-divider:    rgba(255,255,255,0.18);
  --glass-hover:      rgba(255,255,255,0.22);
  --glass-border:     rgba(255, 255, 255, 0.06);
  --overlay-warm:     rgba(35,26,15,0.4);
  --shadow-glass:     0 8px 32px rgba(0,0,0,0.32), inset 0 1px 0 rgba(255,255,255,0.04);
  --bg-hover-overlay: rgba(255,255,255,0.04); /* intentional: D-3 Phase 3 token; tooltip-btn hover */

  /* ── Radius ── */
  --radius-sm:    6px;
  --radius-md:    12px;
  --radius-lg:    16px;
  --radius-xl:    24px;
  --radius-full:  9999px;
  --radius-misc-1: 4px;
  --radius-pill: 20px;
  --radius-misc-3: 8px;

  /* ── 等級藥丸的視覺置中補償 ──────────────────────────────────────────────
     等級藥丸（導覽列的「你的等級」、內容區的 `.level-badge`）內容永遠是「N3」這種
     **大寫＋數字**，沒有下伸部；但字體的 descent 仍佔著位子，於是 `align-items:center`
     這種「盒子置中」會讓墨跡整團偏上。上下 padding 一加一減這個量，就把墨跡推回正中，
     **總高度不變**。
     0.11em 是掃描出來的零點（±0.08 偏上 0.30／**±0.11 為 0.01**／±0.14 偏下 0.33），
     不是手感值。用 em 讓它隨字級縮放。
     ⚠️ **抽成單一來源的理由**：2026-08-16 先只修了 `.level-badge`，導覽列那顆仍偏上
        1.30px —— 同一條規則存在兩份，修了一份、另一份沒有任何東西會提醒。
     ⚠️ 換字體或改字級要重跑那個掃描（量法見 `.level-badge` 註解）。 */
  --pill-cap-nudge: 0.11em;

  /* `.credit-tag__n`／`.credit-receipt b` 的數字視覺置中補償——與 `--pill-cap-nudge`
     同一招、不同數值。`align-items: center` 對齊的是 line-box，而數字（Newsreader）
     的墨跡在自己的 line-box 裡**偏上** 0.955px、「點」（Noto Sans TC）的墨跡在自己的
     line-box 裡**偏下** 0.945px ⇒ 兩團墨跡差 1.90px，把數字往**下**推回來。
     ⚠️ **正值＝往下**。第三輪把符號寫反成負值，等於把數字再往上推 1.82px，
        使用者回報「數字歪的很離譜」（見 `.credit-tag__n` 上方的完整說明）。
     實測（2026-08-26，像素級量測）：13/12px 需 1.900px、12.5/11.5px 需 1.824px，
     換算成數字自己的 em 都是 0.146 ⇒ 單一來源、兩個字級共用。 */
  --credit-num-nudge: 0.146em;

  /* 「解析文章」按鈕的文字視覺置中補償——**第三個**同一種病的實例
     （前兩個：`--pill-cap-nudge` 等級藥丸、`--credit-num-nudge` 成本標數字）。
     `align-items: center` 置中的是 line-box，而 `--font-jp-sans`（Noto Sans JP）
     的墨跡在 line-box 裡**偏下** 0.567px（13px 字級實測：上留白 4.07／下留白 2.94）
     ⇒ 按鈕看起來文字偏下（使用者 2026-08-26 回報）。
     補償方式是**下方多加一點內距**（比照 `--pill-cap-nudge`：按鈕是 flex 容器、
     `box-sizing: border-box` 且高度固定，加下內距只把內容往上推、**總高不變**）。
     值 ＝ 需要位移量的兩倍（0.567 × 2 ÷ 13px ≈ 0.087em）。
     ⚠️ **這個值不能跨字級沿用**：`line-height: normal` 的行框高度是**取整**的
        （13px 與 12.5px 都算成 19px），所以 em 比例在不同字級並不相同
        （實測 13px→0.0436em、12.5px→0.0577em、16px→0.058em）。
        這個 token 是為「13px 的 jp-sans 按鈕」量的，別的字級要重量。
     ⚠️ 兩個變體（一般 40px／`url-bar-compact` 32px）字級都是 13px，共用同一個值；
        但 compact 的 `padding` 簡寫特異性較高會整條蓋掉，所以**兩處都要寫**。
     ⚠️ 載入中（`解析中...` ＋ `.parse-spinner`）時整組內容一起上移 0.567px，
        轉圈圖示因此也偏移同樣的量——它是幾何圖形、本來就置中，這 0.567px
        是為了文字付的代價。實測放大十倍仍看不出來，接受。 */
  --btn-cap-pad: 0.087em;

  /* ── Font family ── */
  --font-jp-sans:    'Noto Sans JP', 'Hiragino Sans', sans-serif;
  --font-jp-serif:   'Shippori Mincho', 'Noto Serif JP', 'Hiragino Mincho ProN', 'Yu Mincho', serif;
  --font-zh-sans:    'Noto Sans TC', 'PingFang TC', 'Microsoft JhengHei', sans-serif;
  --font-zh-serif:   'Noto Serif TC', 'Source Han Serif TC', 'Songti TC', serif;
  --font-en-display: 'Newsreader', 'GT Sectra', Georgia, serif;
  --font-en-mono:    'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace;

  /* ── Type scale ── */
  --text-display:  48px;
  --text-h1:       32px;
  --text-h2:       24px;
  --text-h3:       19px;
  --text-body-lg:  17px;
  --text-body:     15px;
  --text-sm:       13px;
  --text-xs:       11px;

  /* ── Misc text sizes (需人工確認) ── */
  --text-misc-10: 38px;
  --text-misc-11: 9px;

  /* ── Line heights ── */
  --lh-display:  1.15;
  --lh-h1:       1.25;
  --lh-h2:       1.30;
  --lh-h3:       1.40;
  --lh-body-lg:  1.70;
  --lh-body:     1.60;
  --lh-tight:    1.50;
  --lh-label:    1.40;

  /* ── Spacing ── */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;
  --space-9: 96px;

  /* 兩個練習單位（中翻日／影子跟讀）在分頁列底下的起始節奏。
     🔴 **兩邊必須是同一個值**：閃卡蓋住整個單位面板時，`.panel-header` 會被藏起來，
        中翻日的容器就直接接在分頁列下面 —— 與影子跟讀的 `.sh-session` 位置完全相同。
        值不同的話，同一張閃卡在兩個單位會長在不同高度（2026-08-30 使用者截圖回報）。
     ⚠️ 不是 `--space-*` 的級距（22 不在音階上），但**只有一份**，
        所以它不會分歧 —— 見 `.sh-session` 與 `.exercises-list` 兩個消費者。 */
  --unit-panel-top: 22px;   /* intentional: 設計 ShadowingSession 的 22 節奏 */

  /* ── Component ── */
  /* 按鈕水平內距。刻意不在 spacing scale 上（最近的是 space-3=12 / space-4=16）——
     設計的 ActionBtn 與既有多數按鈕都是 14px，硬塞進 scale 會讓全站按鈕位移 2px。
     收斂為元件層 token，服務 .btn-secondary / .btn-ghost。 */
  --btn-pad-x: 14px;
  /* 設計系統的 Button 尺寸階梯（Primitives.jsx）：
       sm  height 32 / font 13（= --text-sm，按鈕預設）
       md  height 40 / font 14（modal footer 用）
     14px 同樣不在 type scale 上（13 / 15），故收為元件層 token。
     height 直接寫在 size 變體裡 —— 規則 12 禁的是 font-size / spacing / radius，不含 height。 */
  --btn-font-md: 14px;

  /* ── Motion ── */
  --duration-fast:  120ms;
  --duration-base:  200ms;
  --duration-entry: 240ms;
  --duration-slow:    320ms;
  --duration-breath: 1400ms;   /* looping breathing dots (題庫 generating) */
  --ease-entry:     cubic-bezier(0.2, 0, 0, 1);
  --ease-out:       cubic-bezier(0.16, 1, 0.3, 1);

  /* ── Misc color tokens ── */
  --color-context-btn-hover: #8a5010;  /* paywall save/copy btn hover */
  --color-misc-19: #f0c060;            /* paywall cmd code text */
  --tag-blue-bg:    rgba(59, 130, 246, 0.15);
  --tag-blue-text:  #93c5fd;
  --tag-green-bg:   rgba(34, 197, 94, 0.15);
  --tag-green-text: #86efac;
  --tag-orange-bg:  rgba(251, 146, 60, 0.15);
  --tag-orange-text: #fdba74;
}

/* ── 後台（控制台）把 `--text-muted` 提到可讀標準 ─────────────────────────────
   🩸 使用者 2026-08-26 回報後台「看起來很暗」。查下去發現不只是那張被調暗的卡：
      **`--text-muted` 本身在任何背景上都不到可讀門檻**（4.5）。實測：

        深色  base 3.91 / surface 3.62 / subtle 3.80 / elevated 3.24
        淺色  base 3.54 / surface 3.75 / subtle 3.24

      `--text-primary`（13.6–16.5）與 `--text-secondary`（7.1–8.5）都沒問題，
      **只有 muted 這一個**。而後台幾乎所有欄位標籤、樣本數、註腳都用它。

   🔴 **只改後台，不動學習者畫面。** 使用者要的是「後台共用顏色設定」；
      前台的資訊密度與閱讀情境不同，一起改是擅自擴大範圍。作用域用
      `.admin-body`（只有 `admin_shadow.html` 帶這個 class）。
   🔴 **值是解出來的不是調出來的**：沿明度軸往白位移，取「**最差的那個背景**
      （`--bg-elevated` #2A241D）也達 4.5」的最小位移量（18.4%），
      得 `#928C84` —— 四個背景實測 4.61 / 5.14 / 5.40 / 5.56，全部達標。
   ⚠️ **只給深色**：後台是 `<html data-theme="dark">` 寫死的，沒有主題切換
      （沒載 `_theme_init`、沒載 `nav-chrome.js`）。多寫一份淺色＝
      「存在但從沒被執行到」，這個專案吃過那個虧。改成可切換主題時要一起補。
   ⚠️ 唯一的非文字用途是 `.ap-job__st.muted` 的狀態圓點底色（「尚無紀錄」＝中性），
      變亮一點不影響它的語意。
   🔒 `tests/test_admin_contrast.py` —— 釘的是「**對比達標**」這個關係，
      不是某個 hex，所以哪天背景色調整了它會在真的變不可讀時才紅。 */
.admin-body {
  --text-muted:    #928C84;   /* 原 #7A7268（3.24）→ 4.61 */
}

/* 全部量過（四個背景 × 各自的底色 tint）：
     --text-primary 13.63 ✅ ／ --text-secondary 7.08 ✅
     --status-correct 4.76 ✅ ／ --status-partial 5.82 ✅
     --text-muted 3.24 ❌（已覆寫）／ --status-wrong 2.96 ❌（**刻意不覆寫**，見下）

   🔴 **`--status-wrong` 維持原值 `#C7553F` —— 使用者裁決 2026-08-26。**
      我算出來的 `#D78575`（4.63）在數字上達標，但使用者實際看過兩版之後說
      「原本的比較清楚」。**UI 的 ground truth 是畫面，不是公式。**
      而且他的觀察有依據：WCAG 的對比公式**只看明度、完全不看色相**，
      而深色暖底上的飽和紅還帶著色相對比 —— 往白位移會同時稀釋掉那一份，
      公式上升、視覺上反而變糊。這是公式的已知盲點，不是使用者看錯。
   ⚠️ **這是一個被記錄下來的例外，不是漏做。** 由
      `tests/test_admin_contrast.py::test_the_alert_red_is_a_deliberate_exception`
      釘住 —— 哪天有人「順手把它修達標」，那條會紅並指回這段。
   ⚠️ `--text-muted` 的覆寫保留：那個是純灰，沒有色相可稀釋，
      往白位移的效果與公式一致（使用者也確認變清楚了）。 */


:root[data-theme="light"] {
  color-scheme: light;

  /* ── Background ── */
  --bg-base:     #FAF8F4;
  --bg-surface:  #FFFFFF;
  --bg-subtle:   #F2EEE7;
  --bg-elevated: #FFFFFF;

  /* ── Border ── */
  --border-default: #D6CFC2;
  --border-subtle:  #E8E2D8;

  /* ── Text ── */
  --text-primary:   #1A1612;
  --text-secondary: #5C544A;
  --text-muted:     #8A8378;

  /* ── Accent ── */
  --accent:            #D94F30;
  --accent-hover:      #C7401F;
  --accent-soft:       #FCEEE8;
  --accent-soft-hover: rgba(217, 79, 48, 0.15);
  --accent-border:     rgba(217, 79, 48, 0.30);
  --accent-highlight:  rgba(199, 122, 31, 0.20); /* D-2B: SnippetMark bg light mode */
  --accent-ink:        #FFFFFF;

  /* ── Status ── */
  --status-correct:           #2D7D5F;
  --status-correct-bg:        #E8F4EE;
  --status-partial:           #C77A1F;
  --status-partial-bg:        #FAF0DE;
  --status-partial-bg-subtle: rgba(199, 122, 31, 0.06);
  --status-partial-border:    rgba(199, 122, 31, 0.30);
  --status-wrong:             #9F3722;
  --status-wrong-bg:          #F7E5DF;

  /* ── Shadow ── */
  --shadow-card:     0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.04);
  --shadow-misc-1:   0 2px 14px rgba(0,0,0,0.04);
  --shadow-misc-2:   0 2px 16px rgba(0,0,0,0.08);
  --shadow-elevated: 0 4px 24px rgba(0,0,0,0.08), 0 16px 40px rgba(0,0,0,0.06);

  /* ── Glass ── */
  --glass-hairline:   rgba(0,0,0,0.08);
  --glass-bg:         rgba(255, 255, 255, 0.65);
  --glass-divider:    rgba(0,0,0,0.12);
  --glass-hover:      rgba(0,0,0,0.06);
  --glass-border:     rgba(0, 0, 0, 0.06);
  --overlay-warm:     rgba(26,22,18,0.35);
  --shadow-glass:     0 8px 32px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.80);
  --bg-hover-overlay: rgba(0,0,0,0.04);

  /* ── Misc color tokens ── */
  --color-context-btn-hover: #9A5F00;
  --color-misc-19:           #8B5200;
  --tag-blue-text:           #1d4ed8;
  --tag-green-text:          #15803d;
  --tag-orange-text:         #c2410c;
}

/* ── Glass utility — Phase 2 shared overlay token
   Source: docs/design-system/prototype/article-learning/index.html className="glass"
   Scenes: FAB / context banner / selection tooltip / dropdown / modal scrim ── */
.glass {
  background: var(--glass-bg);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-glass);
}

body {
  font-family: var(--font-jp-sans);
  background: var(--bg-base);
  color: var(--text-primary);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
  font-size: var(--text-body);
  transition: background-color var(--duration-base) ease, color var(--duration-base) ease;
}

/* ── Header ── */
/* ── Navbar ── */
.navbar {
  height: 64px;
  background: var(--bg-surface);
  border-bottom: 1px solid var(--glass-hairline);
  display: flex;
  align-items: center;
  padding: 0 24px;
  gap: 0;
  position: sticky;
  top: 0;
  z-index: 10;
  flex-shrink: 0;
}
.nav-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  flex-shrink: 0;
}
.nav-mark {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  display: block;
}
.nav-wordmark {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
}
.nav-wordmark-ja {
  font-family: var(--font-zh-serif);
  font-size: 22px;
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: -0.01em;
  line-height: 1;
}
.nav-wordmark-romaji {
  font-family: var(--font-en-display);
  font-size: 16px;
  font-weight: 400;
  font-style: italic;
  color: var(--text-secondary);
  letter-spacing: 0.02em;
  line-height: 1;
}
.nav-divider {
  width: 1px;
  height: 22px;
  background: var(--border-subtle);
  margin: 0 20px;
  flex-shrink: 0;
}
/* 餘額 chip 與其餘 chrome 之間的分界（設計 `Navbar.jsx`：`width:1, height:18,
   margin: 0 4px`）。⚠️ 與上面那條**不同尺寸、不同用途** —— 上面是「品牌 ↔ 分頁」
   的大分界（22px／margin 20），這條是右側元件之間的小分界。
   共用同一條的話，改其中一個用途會連帶動到另一個。
   🔒 `tests/test_navbar_shared.py::TestTheCreditChipMatchesTheDesign` */
.nav-divider--chip {
  height: 18px;                /* intentional: Navbar.jsx */
  margin: 0 4px;               /* intentional: Navbar.jsx */
}
.nav-tabs {
  display: flex;
  align-items: stretch;
  height: 100%;
  gap: 0;
}
.nav-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  /* intentional: Navbar.jsx 右側群組 gap 6（原本是既有的 4）。
     ⚠️ 2026-08-25 對齊設計時一併修 —— 加了餘額 chip 之後右側的結構
     （chip · 分隔線 · 等級 · 三顆 icon）與設計完全一致，間距沒有理由再差 2px。
     影響：等級↔主題、主題↔設定、設定↔頭像 各 +2px。 */
  gap: 6px;
}
/* ══ 導覽列的藥丸：等級與餘額 chip **共用同一個外框** ══
   🔴 使用者裁決 2026-08-25：「點數外匡改成和等級一樣元件」。
      ⇒ 邊框、圓角、文字色、高度只有**一份**。改藥丸外觀時兩個一起變，
        不可能有一個走鐘（而走鐘的那個不會有人發現，因為沒人會把兩顆並排量）。
   ⚠️ **這是刻意與設計稿不同**（設計的 `CreditChip` 是 30px 高、radius 8、
      `--border-subtle`、數字 14px、符號 16px）。裁決的理由是讓它與導覽列既有的
      等級藥丸成為**同一種東西** —— 兩顆並排時，「像不像同一家的」比
      「像不像設計稿」重要。完整紀錄見 `docs/ROADMAP.md` §💠 點數系統〈與設計交付的偏離〉。 */
.nav-pill {
  height: 23px;
  box-sizing: border-box;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  line-height: 1;
  text-decoration: none;
}

.nav-level {
  font-family: var(--font-en-display);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* 視覺置中：與 `.level-badge` 用**同一個**補償量（見 `--pill-cap-nudge`）。
     ⚠️ 尺寸刻意不與 `.level-badge` 統一 —— 導覽列常駐、要一眼看清（11px / 23px 高），
        內容徽章混在 12.5px 的 meta 列裡（10.5px / 20.5px），大小差異是有理由的。
        收斂的是「怎麼置中」這條規則，不是外觀。
     上下 padding 一加一減，**總高度不變**（實測 23.0 → 23.0）。 */
  /* ⚠️ 邊框／圓角／色／高度在 `.nav-pill`，這裡只放**字**的事。 */
  padding: calc(5px + var(--pill-cap-nudge)) 11px calc(5px - var(--pill-cap-nudge));
  /* 🩸 **`margin-right: 6px` 已移除（2026-08-25）。** 它是 `.nav-right` 的 gap
     還是 4px 時，用來補出等級與 icon 之間距離的**補償**。gap 對齊設計改成 6 之後
     它就變成多出來的 6px —— 實測「等級 → 主題」12px，設計是 6。
     ⚠️ 這是「加了新規則但舊的補償沒拿掉」的典型：兩層各自合理，疊起來就錯，
        而且**只有量了才看得出來**（差 6px 用眼睛判不了）。 */
}
.nav-icon-btn {
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--text-secondary);
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: background var(--duration-base), color var(--duration-base);
}
.nav-icon-btn:hover {
  background: var(--bg-subtle);
  color: var(--text-primary);
}
.nav-icon-btn svg { width: 18px; height: 18px; }

/* ── 帳號 avatar + dropdown（登入後 navbar，Google OAuth v1，規格 §4）── */
/* avatar 觸發鈕複用 .nav-icon-btn（34×34、radius、hover 完全一致，規格 §4.1「以現行實作為準」）。 */
.nav-account { position: relative; }
.nav-avatar-btn { padding: 0; }
.nav-avatar { position: relative; width: 26px; height: 26px; border-radius: 50%;
              display: flex; align-items: center; justify-content: center; }
.nav-avatar-img { position: absolute; inset: 0; width: 26px; height: 26px; border-radius: 50%; object-fit: cover; }
.nav-avatar-fallback { width: 26px; height: 26px; border-radius: 50%; background: var(--bg-subtle);
                       border: 1px solid var(--border-default); display: flex; align-items: center;
                       justify-content: center; font-family: var(--font-zh-serif); font-weight: 500;
                       font-size: 12px; color: var(--text-secondary); line-height: 1; }
/* 開啟態＝其他 icon 按鈕的 hover（複用 --bg-subtle），保持到關閉。 */
.nav-icon-btn.is-open { background: var(--bg-subtle); color: var(--text-primary); }

.nav-account-menu { position: absolute; top: calc(100% + 10px); right: 0; min-width: 148px; padding: 6px;
                    border-radius: var(--radius-md); background: var(--glass-bg);
                    backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);
                    border: 1px solid var(--glass-border); box-shadow: var(--shadow-glass); z-index: 40;
                    display: none; transform-origin: top right; }
.nav-account-menu.open { display: block; animation: accountMenuPop var(--duration-base) var(--ease-out); }
@keyframes accountMenuPop { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
.nav-account-item { width: 100%; display: flex; align-items: center; gap: 10px; padding: 10px 12px;
                    background: transparent; border: 0; border-radius: 8px; cursor: pointer;
                    font-family: var(--font-zh-sans); font-size: 13.5px; color: var(--text-primary);
                    line-height: 1; text-align: left; }
.nav-account-item:hover { background: var(--bg-subtle); }
.nav-account-item svg { width: 15px; height: 15px; color: var(--text-secondary); flex-shrink: 0; }

/* ── 認證流程落地進場「紙頁上移」（取代規格 §2 的 splash）──
   splash 原定位是「session 建立中的等待畫面」，但 session 在 /auth/callback 已於伺服器端建好，
   splash render 出來時無事可等 = 純粹自己加的 1.6s 延遲（且呼吸階段在 1.6s 跳轉下從未播出）。
   改為把過場做成**目的頁的進場**：發生在頁面本來就要載入的時間內，零額外等待。
   只在認證落地時播一次（session 一次性旗標 auth_landing），一般重整不播。
   只動內容區、navbar 保持穩定 —— 否則每次進站整條列都在動。 */
/* 淡入與位移**刻意拆開**：opacity 320ms 先完成，位移續跑到 800ms ——
   先看得見紙、再看著它落下。兩者同步的話只會是普通淡入，做不出紙張感。 */
@keyframes pageFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes pageRise { from { transform: translateY(20px); } to { transform: none; } }
body.page-enter .tab-panel.active > main {
  /* intentional: 設計系統的 --ease-entry(0.2,0,0,1) / --ease-out(0.16,1,0.3,1) 都是 swift-out
     曲線（前 25% 就走完 61%~83%），適合選單彈出、不適合「從靜止落下」。實測現用曲線在 420ms
     裡 200ms 就實質結束，後段只移動 1.5px（看不見）。此處需要慢起步 + 長尾，無對應 token，
     故用字面值；來源：紙頁上移手感對照 mock，使用者選定 B 案。 */
  animation: pageFade 320ms cubic-bezier(0.4, 0, 0.2, 1) both,
             pageRise 800ms cubic-bezier(0.45, 0.05, 0.15, 1) both;
}

/* ── 法務頁（隱私權政策 / 使用條款，OAuth v1）— 單欄 680px、ZH serif 長讀 ──
   **nav 與品牌 lockup 不在這裡** —— 法務頁直接複用 app 既有的 `.navbar` + `.nav-brand` /
   `.nav-mark` / `.nav-wordmark(-ja/-romaji)`（見上方 Navbar 一節）。
   原本移植 prototype legal.css 時另寫了一份 `.legal-nav .brand/.ja/.romaji`，立刻與 app 走鐘：
   日々 字體 Shippori Mincho vs var(--font-zh-serif)、mark→日々 12 vs 10、日々→hibi 12 vs 8。
   **勿再為法務頁另寫品牌樣式**；品牌 chrome 全站只有一份。 */
.legal { max-width: 680px; margin: 0 auto; padding: 72px 28px 96px; }
.legal h1 { font-family: var(--font-zh-serif); font-size: var(--text-h1); font-weight: 500; }
.legal .meta { font-family: var(--font-zh-sans); font-size: var(--text-sm); color: var(--text-muted); margin-top: 12px; }
.legal .lead { font-family: var(--font-zh-serif); font-size: var(--text-body-lg); line-height: 1.9; color: var(--text-secondary); margin: 28px 0 0; text-wrap: pretty; }
.legal section { margin-top: 48px; }
.legal h2 { font-family: var(--font-zh-serif); font-size: var(--text-h3); font-weight: 500; margin-bottom: 16px; }
.legal p { font-family: var(--font-zh-sans); font-size: var(--text-body); line-height: 1.85; color: var(--text-secondary); margin: 0 0 14px; }
.legal p strong { color: var(--text-primary); font-weight: 500; }
.legal a { color: var(--accent); text-decoration: none; }
.legal a:hover { text-decoration: underline; text-underline-offset: 3px; }
.legal-foot { margin-top: 72px; padding-top: 24px; border-top: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 16px; font-family: var(--font-zh-sans); font-size: var(--text-sm); }
.legal-foot a { color: var(--text-muted); text-decoration: none; }
.legal-foot a:hover { color: var(--text-primary); }
.legal-foot .dot { width: 1px; height: 12px; background: var(--border-subtle); }

/* ═══════════════════════════════════════════════════════════════════════════
   共用按鈕系統 — .btn-secondary（outlined）/ .btn-ghost（無框）

   收斂自六份幾乎相同的規則：
     outlined  #reload-btn · .followup-btn · .qb-action-btn
     ghost     .reveal-btn · .retry-btn · .qb-action-btn--subtle

   語意 class（.reveal-btn 等）保留，但只當 JS 的行為 hook，不再帶樣式 ——
   樣式與行為分離，所以 app.js 的 querySelector 完全不用動。
   佈局（如 .followup-btn 的 margin-left: auto）屬於「使用處」，不進元件。
   ═══════════════════════════════════════════════════════════════════════════ */

.btn-secondary,
.btn-ghost,
.btn-primary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
  height: 32px;
  padding: 0 var(--btn-pad-x);
  border-radius: var(--radius-md);
  background-color: transparent;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 400;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out),
              color var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-out);
}
/* Icon 尺寸依設計系統的 Button 基底（Primitives.jsx: <Icon size={16} />）。
   收斂前是不一致的：#reload-btn 吃 HTML 的 width="16"，而 .followup-btn /
   .retry-btn 被 CSS 壓成 12px。全部回到 16px。 */
.btn-secondary svg,
.btn-ghost svg,
.btn-primary svg { width: 16px; height: 16px; flex-shrink: 0; }

.btn-secondary:disabled,
.btn-ghost:disabled,
.btn-primary:disabled { opacity: 0.45; cursor: default; }

/* Size ladder — the base is sm (32 / 13). md is what modal footers use.
   （設計系統 Primitives.jsx 的 Button：heights = {sm:32, md:40, lg:48}） */
.btn--md {
  height: 40px;
  font-size: var(--btn-font-md);
}

/* Outlined */
.btn-secondary {
  border: 1px solid var(--border-default);
  color: var(--text-primary);
}
.btn-secondary:hover:not(:disabled) {
  border-color: var(--text-secondary);
  background-color: var(--bg-subtle);
}

/* Ghost — transparent border keeps the box the same size as .btn-secondary,
   so the two never jump against each other when sitting in the same row. */
.btn-ghost {
  border: 1px solid transparent;
  color: var(--text-muted);
}
.btn-ghost:hover:not(:disabled) {
  color: var(--text-secondary);
  background-color: var(--bg-subtle);
}

/* Louder ghost — 「參考回答」比「重新回答」在語意上更主要。這是層級，不是不一致。 */
.btn-ghost--strong { color: var(--text-secondary); }
.btn-ghost--strong:hover:not(:disabled) { color: var(--text-primary); }

/* Quieter secondary — modal footer 的取消鈕（「先不要」「取消」）。仍是 outlined，
   但字色退一階：它在對話框裡是「不要做那件事」，不該和主鈕爭注意力。 */
.btn-secondary--quiet { color: var(--text-secondary); }
.btn-secondary--quiet:hover:not(:disabled) { color: var(--text-primary); }

/* Primary — 唯一填 accent 的按鈕。modal 的確認鈕。 */
.btn-primary {
  border: 1px solid transparent;
  background-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 500;
}
/* 主鈕 hover 微上推（設計 BankControls.jsx 的 ConfirmModal 主鈕）。只有 primary
   有 —— 取消鈕（outlined）不上推。transform 已在基底 transition 列表，同步過渡。 */
.btn-primary:hover:not(:disabled) {
  background-color: var(--accent-hover);
  transform: translateY(-1px);
}

/* ── Settings Modal ── */
/* ── Settings Modal ── */
.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: var(--overlay-warm);
  z-index: 100;
  align-items: center;
  justify-content: center;
  padding: 24px;
  backdrop-filter: blur(6px) saturate(140%);
  -webkit-backdrop-filter: blur(6px) saturate(140%);
}
.modal-overlay.open {
  display: flex;
  animation: settingsScrim var(--duration-entry) var(--ease-entry) both;
}
.modal {
  background: var(--bg-elevated);
  border-radius: var(--radius-xl);
  width: 100%;
  max-width: 560px;
  box-shadow: var(--shadow-elevated);
  overflow: hidden;
  border: 1px solid var(--border-subtle);
  display: flex;
  flex-direction: column;
  animation: settingsPop var(--duration-entry) var(--ease-entry) both;
}
/* ══ 純文字 modal 的節奏（`.modal:not(.modal--form)`）══
   只有幾段文字與按鈕。設計＝`BankControls.jsx` 的 `ConfirmModal`：
   **一個 `padding: 28px 30px 24px` 的盒子**，段落靠自己的 margin 排，沒有分隔線。

   ⚠️ **判準是「有沒有東西要你填」，不是「哪一個尺寸變體」。**
      第一版寫成 `.modal--compact .xxx` —— 那是把**節奏**綁在**寬度**上。
      實際後果：許願 modal 的「已送出」狀態也是純文字（三行 p），
      但它不是 compact，於是拿不到緊湊節奏、留白照樣過大（使用者截圖回報）。
      現在節奏與分隔線**吃同一個訊號**（`.modal--form`），
      一個 modal 不可能出現「沒有線但很鬆」或「有線但很緊」這種半套狀態。

   實作沿用 `.modal-header / -body / -footer` 三個容器（REFACTOR_AUDIT 3.3 裁決 A：
   複用既有 modal 系統的 scrim／圓角／動畫），所以這裡把那三塊的內距**收回設計的節奏**。
   ⚠️ 不另外做一個「純文字 modal」的容器 —— 那會讓同一組 padding 存在兩份，遲早分歧，
      而且分歧的那份不會有人發現（沒人會把兩個 modal 並排量）。

   設計的字面值（inline style，無 cascade，讀值即渲染值）與本處**一行對一個數字**：

     eyebrow → 標題      12px
     標題 → 內文         14px
     段 → 段              8px
     內文 → 按鈕列       24px   ← body padding-bottom 8 + footer padding-top 16

   ⚠️ **間距寫在「產生它的那個 margin」上，不要用三條 padding 相加湊。**
      第一版就是那樣寫的（header padding-bottom 6 + body padding-top 8 = 14），
      結果實測 17.6px —— 因為 `.modal-title` 自帶 `margin: 4px 0`，而算式裡沒有它。
      跨三條規則的加法沒有人驗得動，改一條就得重算全部。

   ⚠️ 改前是 45px／33px。那兩個大洞原本是**替分隔線留的呼吸空間** ——
      線在 2026-08-15 拿掉之後它們就只是洞，標題與內文讀起來像兩個不相干的區塊
      （使用者截圖比對回報）。動 `.modal--form` 的分隔線時要一起想到這一層。
   ⚠️ 帶 `.modal--form` 的維持基礎那組較鬆的節奏 —— 有分隔線、有欄位分區時它是對的。 */
.modal:not(.modal--form) .modal-eyebrow { margin-bottom: 12px; }
.modal:not(.modal--form) .modal-title   { margin: 0 0 14px; }
.modal:not(.modal--form) .modal-header  { padding-bottom: 0; }
.modal:not(.modal--form) .modal-body    { padding-top: 0; gap: var(--space-2); }
.modal:not(.modal--form) .modal-footer  { margin-top: 0; padding-top: 16px; }

/* 寬度是**另一件事**，只管寬度。設計的 ConfirmModal 是 480px；
   設定 modal 內容多得多（等級五宮格 + textarea），維持基礎的 560px。 */
.modal--compact { max-width: 480px; }
/* ⚠️ **分隔線不屬於這個容器，屬於 `.modal--form`。** 見下方 `.modal--form` 的說明。 */
.modal-header {
  padding: 24px 28px 18px;
}
/* 中文優先（docs/design-handoff/o1/CLAUDE.md 硬規則）：eyebrow 一律中文、用 var(--font-zh-sans)，
   不得把 font-en-display + uppercase 套在中文字串上 —— 這條是關**字體/大小寫**，與顏色無關。
   顏色 = **accent 橘紅**：設計稿與 prototype（SettingsModal.jsx / WishModal.jsx）的 eyebrow 都是
   var(--accent)，三個 modal（設定/更換教材/許願）統一。
   （舊註解誤稱「設計是 text-muted」，與實際 JSX + 設計稿相反，2026-07-29 更正為 accent。）*/
.modal-eyebrow {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  color: var(--accent);
  margin: 0;
  font-weight: 500;
}
.modal-title {
  font-family: var(--font-zh-serif);
  /* 設計是 21px —— 不在 type scale 上（19 / 24）。取最近的 --text-h3，2px 的差異
     視覺上很輕微，不值得為單一用途再開一個 token。 */
  font-size: var(--text-h3);
  color: var(--text-primary);
  margin: 4px 0;
  font-weight: 500;
  line-height: 1.3;
}
.modal-subtitle {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0;
  line-height: 1.55;
}
.modal-body {
  padding: 24px 28px 8px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.modal-section {}
.modal-field-label {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 12px;
}
.modal-field-label label {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: 0.01em;
}
.modal-field-optional {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-weight: 400;
}
.modal-field-hint {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin: 0 0 10px 2px;
  line-height: 1.5;
}
.level-btns {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 6px;
}
.level-btn {
  border: 1px solid var(--border-default);
  background: var(--bg-subtle);
  border-radius: var(--radius-md);
  padding: 12px 0;
  font-size: var(--text-body-lg);
  font-weight: 500;
  cursor: pointer;
  transition: all 120ms var(--ease-out);
  color: var(--text-secondary);
  font-family: var(--font-en-display);
  letter-spacing: 0.04em;
  line-height: 1;
}
.level-btn:hover {
  background: var(--bg-elevated);
  border-color: var(--text-secondary);
  color: var(--text-primary);
}
.level-btn.selected {
  border: 2px solid var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}
.level-desc {
  margin: 12px 2px 0;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.55;
}
.level-desc-label {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  margin-right: 8px;
}
.level-change-notice {
  /* 收合態：上下 padding + 上下 border 一併收 0 —— 否則 max-height:0 收不掉它們，殘留 22px
     幽靈高度把下方（soon 註腳）往下推、間距歪掉。橫向 padding/border 保留（不影響垂直高度）。*/
  padding: 0 12px;
  border-radius: var(--radius-md);
  background: var(--status-partial-bg);
  border: 1px solid var(--status-partial);
  border-top-width: 0;
  border-bottom-width: 0;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.55;
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  margin-top: 0;
  transition: max-height 380ms var(--ease-entry),
              opacity 280ms ease 80ms,
              margin-top 380ms var(--ease-entry),
              padding 380ms var(--ease-entry);
}
.level-change-notice.is-visible {
  max-height: 80px;
  opacity: 1;
  margin-top: 12px;
  padding: 10px 12px;
  border-top-width: 1px;
  border-bottom-width: 1px;
}
.notice-icon {
  color: var(--status-partial);
  flex: 0 0 auto;
  margin-top: 1px;
}

/* ══ N2／N1 尚未開放 × 許願區（見 handoff/N2N1-未開放許願區-開發交付.md）══ */
/* 未開放等級：未選時虛線框 + muted；選中與其他等級一致（「你點的是這個」要看得出來）*/
.level-btn.is-soon:not(.selected) { border-style: dashed; color: var(--text-muted); }
.level-btn.is-soon:not(.selected):hover { border-color: var(--text-muted); color: var(--text-secondary); }
/* hairline 註腳（取代卡片式提示，接在等級說明後）—— 無底色/邊框/圓角/icon */
.level-soon-note {
  margin: 12px 0 0; padding-top: 10px; border-top: 1px solid var(--border-subtle);
  font-family: var(--font-zh-sans); font-size: 12px;   /* intentional: spec §三 註腳字級，text-xs(11)/sm(13) 間無對應 token */
  color: var(--text-muted); line-height: 1.7; display: none;
}
.level-soon-note.is-visible { display: block; animation: settingsDescFade 220ms var(--ease-entry) both; }
.lv-en { font-family: var(--font-en-display); letter-spacing: 0.04em; }
/* 「許願區」是 button 但長得像連結 */
.wish-link {
  background: transparent; border: 0; padding: 0; cursor: pointer;
  font-family: var(--font-zh-sans); font-size: 12px; color: var(--accent);
  text-decoration: underline; text-underline-offset: 3px;
}
/* 補充說明一併停編 */
.modal-section.is-soon-disabled { opacity: 0.5; transition: opacity 140ms ease; }
.modal-section.is-soon-disabled .modal-textarea { cursor: not-allowed; }
/* footer 已送出許願回報 */
.settings-wish-sent {
  margin-right: auto; display: none; align-items: baseline; gap: 6px;
  font-family: var(--font-zh-sans); font-size: 11.5px; color: var(--text-muted); line-height: 1.5;
}
.settings-wish-sent.is-visible { display: inline-flex; }
.settings-wish-sent .chk { color: var(--status-correct); }

/* ── 許願 modal 內容（chrome 走 .modal-overlay/.modal，與設定一致）── */
.wish-level-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
.wish-level-card {
  cursor: pointer; text-align: left; padding: 13px 14px; border-radius: var(--radius-md);
  border: 1px solid var(--border-default); background: transparent;
  display: flex; flex-direction: column; gap: 5px; transition: all 140ms ease;
}
.wish-level-card:hover:not(.selected) { border-color: var(--text-secondary); }
.wish-level-card.selected { border-color: var(--accent); background: var(--accent-soft); }
.wish-level-card .lv { font-family: var(--font-en-display); font-size: 17px; letter-spacing: 0.04em; font-weight: 500; line-height: 1; color: var(--text-primary); }
.wish-level-card.selected .lv { color: var(--accent); font-weight: 600; }
.wish-level-card .lv-desc { font-family: var(--font-zh-sans); font-size: 11.5px; line-height: 1.5; color: var(--text-muted); }
.wish-field-hint { margin: 6px 2px 10px; font-family: var(--font-zh-sans); font-size: 11.5px; color: var(--text-muted); line-height: 1.5; }   /* 下緣 10px = 對齊設定 .modal-field-hint 與設計（否則 hint 貼死 textarea）*/
/* 已送出態 */
.wish-sent-line { margin: 0; display: flex; align-items: baseline; gap: 8px; font-family: var(--font-zh-sans); font-size: 13.5px; font-weight: 500; color: var(--text-primary); line-height: 1.5; }
.wish-sent-line .chk { color: var(--status-correct); }
.wish-sent-sub { margin: 8px 0 0; font-family: var(--font-zh-sans); font-size: 12.5px; color: var(--text-secondary); line-height: 1.75; }
.wish-sent-echo { margin: 14px 0 0; padding-top: 12px; border-top: 1px solid var(--border-subtle); font-family: var(--font-zh-serif); font-size: 13.5px; color: var(--text-muted); line-height: 1.8; }

/* ── 設定⇄許願 連動轉場（左出右入、scrim 不重播）── */
/* 「紙靜 · 綿長」進場 —— 純淡入 + 垂直微升、無橫移（沉靜、少 app 彈進感，使用者於動畫實驗室定案）。
   非方向性：設定/許願的 enter 同一款、leave 同一款（不再分左右）。leave 比 enter 快一半、接得順。*/
@keyframes paperEnter { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
@keyframes paperLeave { from { opacity: 1; transform: translateY(0); }     to { opacity: 0; transform: translateY(-7px); } }
.modal-overlay.is-chained { animation: none; }   /* 換手期間維持同一層暗底、不重播 fade */
/* enter 在前、leaving 在後 —— 萬一同時殘留，同 specificity 由後者(leaving)勝（安全排序，見 goWish 殘留 bug）*/
.modal.is-enter-left,
.modal.is-enter-right   { animation: paperEnter 740ms cubic-bezier(0.33, 0, 0, 1) both; }
.modal.is-leaving-left,
.modal.is-leaving-right { animation: paperLeave 355ms cubic-bezier(0.4, 0, 0.6, 1) both; pointer-events: none; }

/* ── 全站 modal 出場（鏡像紙靜：卡片下沉淡出 + 遮罩淡出）——單一控制點 closeModalAnimated 收口 ──
   descendant 選擇器 specificity(0,3,0) 蓋過殘留的 is-enter-*(0,2,0)，所以關閉一定播出場、不被進場殘留卡住。*/
@keyframes paperExit { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(10px); } }
@keyframes scrimOut  { from { opacity: 1; } to { opacity: 0; } }
.modal-overlay.is-closing { animation: scrimOut 260ms cubic-bezier(0.4, 0, 0.6, 1) both; }
.modal-overlay.is-closing .modal { animation: paperExit 320ms cubic-bezier(0.4, 0, 0.6, 1) both; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .modal-overlay.is-closing, .modal-overlay.is-closing .modal { animation: none; }
}
.notice-accent {
  color: var(--accent);
  font-weight: 500;
}
.modal-textarea {
  width: 100%;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  padding: 12px 14px;
  font-size: var(--text-body);
  font-family: var(--font-zh-serif);
  resize: vertical;
  min-height: 92px;
  outline: none;
  transition: border-color var(--duration-base);
  background: var(--bg-subtle);
  color: var(--text-primary);
  line-height: 1.65;
}
.modal-textarea:focus {
  border-color: var(--accent);
}
/* ══ 分隔線只給「要你填東西」的 modal ══
   （2026-08-15，使用者截圖回報：純文字的確認框長出兩條線）

   規則（使用者原話）：**內文是純文字的 dialog 不該有分隔線，只有裡面有設定選項或
   輸入框的才有。** 所以 border **不掛在 `.modal-header` / `.modal-footer` 上** ——
   那兩個容器的職責是版面分區與 padding，任何 modal 都需要；分隔線是「內容被分段了、
   需要視覺切開」才有的東西。

   ⚠️ 設計本來就分得很清楚，是實作把兩種合成一個殼：
     · SettingsModal.jsx  → header `borderBottom`、footer `borderTop`   ✅ 有線
     · BankControls.jsx 的 `ConfirmModal` → **一個 padding 盒子，沒有任何 border**
   題庫確認框複用 `.modal` 系統時（REFACTOR_AUDIT 3.3 裁決 A），裁決的範圍是
   scrim／圓角／動畫，**沒盤點到這個殼還夾帶設定型專屬的 chrome**。

   ⚠️ **預設是「沒有線」**，不是「有線但可以關掉」。反過來的話，下一個純文字 modal
      要「記得加一個關閉用的 class」—— 那是靠記憶的規則，而靠記憶正是這個 bug 的病因。
      漏標的後果因此落在安全的那一側（少一條細線），不是又長出一條錯的線。
   ⚠️ 不用 `:has(input, textarea)` 讓瀏覽器自己判：規則會藏進選擇器，讀的人看到設定
      modal 有線得先想到有那條規則；而且哪天純文字 modal 塞一個隱藏 input 就無聲長出線。
   🔒 「有輸入控制項 ⟺ 帶 .modal--form」由 tests/test_modal_divider.py 從 DOM 內容反推把關。 */
.modal--form .modal-header { border-bottom: 1px solid var(--border-subtle); }
.modal--form .modal-footer { border-top: 1px solid var(--border-subtle); }

.modal-footer {
  padding: 18px 28px 22px;
  margin-top: 8px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}
/* .modal-cancel-btn → .btn-secondary .btn-secondary--quiet .btn--md
   .modal-save-btn   → .btn-primary .btn--md
   （見「共用按鈕系統」。兩個 class 只剩 JS 行為 hook 的身分。）

   收斂前它們是第三份重複的按鈕 CSS，而且與設計不符：取消鈕沒有邊框（設計是
   outlined）、兩顆都是 34px/15px（設計是 40px/14px）。 */
@keyframes settingsScrim {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes settingsPop {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes settingsDescFade {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── Main Layout ──
   app 的左右兩欄版面規則**只掛 `.tab-panel > main`**（見下方 ARTICLE LEARNING PANEL 前一節）。
   這裡原本有一條**未限定範圍**的 `main { display:grid; 60fr 40fr; … }`，與 scoped 那條逐字相同
   （9 條宣告零差異），對 app 完全多餘（三個 <main> 都是 .tab-panel 直接子元素），卻會**認領任何
   新增的獨立 <main> 頁面** —— 法務頁 `<main class="legal">` 就被它排成左右交錯的兩欄。
   **勿再新增無範圍限定的 `main` 版面規則**；獨立頁面請自帶容器樣式。 */

/* ── Left: Exercises Panel ── */
.exercises-panel {
  display: flex;
  flex-direction: column;
  /* 與右側 coach 面板同高（同 .article-panel / .chat-panel 公式）→ 左欄撐滿，
     錯誤/空狀態的 .article-err-state（flex:1 + justify-content:center）才能上下置中。
     正常 10 題內容超過一屏、依內容變更高，不受此下限影響。 */
  min-height: calc(100vh - 112px);
}

.panel-header {
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--border-default);
  flex-shrink: 0;
  position: relative;
}
.exercises-panel .panel-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  padding: 20px 20px 20px 24px;
  border-bottom: 1px solid var(--border-subtle);
}
/* 🔴 影子跟讀模式要把中翻日的標題列一起收掉（`header.hidden = true`）。
   ⚠️ **這條必須排在 `.exercises-panel .panel-header` 之後，而且特異度不能輸它**：
      `.panel-header[hidden]` 與 `.exercises-panel .panel-header` 同為 (0,2,0)，
      平手時後出現的贏 —— 第一版寫在前面，結果標題列**照樣顯示**
      （2026-08-20 實測：清單藏起來了、標題還在）。 */
.panel-header[hidden],
.exercises-panel .panel-header[hidden] { display: none; }
.panel-header-left { display: flex; flex-direction: column; }

/* ── Practice Header —— 頁面標題，**在兩個單位之上、不隨切換消失** ──
   設計 `Shadowing.jsx` 的 TodayOverview 註解：「常駐 tab：標題與進度不隨切換消失」。
   ⚠️ 它**不帶 `.panel-header`** —— 那個 class 會被 `shSwitchUnit` 藏起來
      （那是中翻日自己的區段標題）。兩者混用的話，切到影子跟讀就沒有標題了。 */
.practice-header {
  padding: 20px 20px 16px 24px;   /* 左右與既有 .exercises-panel .panel-header 對齊 */
  flex-shrink: 0;
}

/* 中翻日**自己的**區段標題（設計的第二層，比頁面標題小一階） */
.drill-header__title {
  font-family: var(--font-zh-serif);
  font-size: var(--text-h2);
  font-weight: 500;
  line-height: 1.2;
  color: var(--text-primary);
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.drill-header__title .practice-header__num { font-size: var(--text-h2); }

/* ── Practice Header — h1 title ── */
.practice-header__title {
  font-family: var(--font-zh-serif);
  font-size: 30px; /* intentional: D-1A prototype value */
  font-weight: 500;
  line-height: 1.2;
  color: var(--text-primary);
  margin: 0;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 14px;
  letter-spacing: 0.01em;
}
.practice-header__divider {
  font-family: var(--font-en-display);
  font-size: 28px; /* intentional: D-1A prototype value */
  font-weight: 300;
  color: var(--text-muted);
  letter-spacing: 0.02em;
}
.practice-header__num {
  font-family: var(--font-en-display);
  font-size: 30px; /* intentional: D-1A prototype value */
  font-weight: 400;
  color: var(--text-primary);
  font-feature-settings: "lnum" 1;
  letter-spacing: -0.01em;
}

/* ── Practice Header — date row ── */
.practice-header__date {
  font-family: var(--font-zh-sans);
  font-size: 13px; /* intentional: D-1A prototype value */
  line-height: 1.5;
  color: var(--text-muted);
  margin: 14px 0 0;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 20px;
  opacity: 0;
  transition: opacity 240ms ease-out;
}
.practice-header__date-label {
  font-family: var(--font-en-display);
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
/* 「約 30 分鐘」—— **固定文案**（使用者裁決 2026-08-21），不由題數推算。
   ⚠️ 分隔點用 ::before 而不是寫進 HTML：日期還沒載入時（`opacity:0`）
      不會先出現一個孤零零的點。 */
.practice-header__est::before {
  content: "·";
  margin-right: 10px;
  color: var(--text-muted);
}

/* ── Chat Panel Header (scoped — does not affect exercises panel) ── */
.chat-panel .panel-header,
.article-chat-panel .panel-header {
  background: var(--bg-surface);
  padding: 20px 24px 16px;
  border-bottom: 1px solid var(--border-subtle);
}
.chat-panel-eyebrow {
  font-family: var(--font-en-display);
  font-size: 11px; /* intentional: D-2A prototype value */
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 500;
  margin: 0 0 6px;
}
.chat-panel .chat-panel-heading,
.article-chat-panel .chat-panel-heading {
  font-family: var(--font-zh-serif);
  font-size: 22px; /* intentional: D-2A prototype value */
  font-weight: 500;
  color: var(--text-primary);
  margin: 0 0 4px;
  display: flex;
  align-items: baseline;
  gap: 12px;
}
.chat-panel-heading__divider {
  font-family: var(--font-en-display);
  font-size: 14px; /* intentional: D-2A prototype value */
  font-weight: 300;
  color: var(--text-muted);
  letter-spacing: 0.02em;
}
.chat-panel-heading__sub {
  font-family: var(--font-zh-serif);
  font-size: 16px; /* intentional: D-2A prototype value */
  font-weight: 400;
  color: var(--text-secondary);
}
.chat-panel-subtitle {
  font-family: var(--font-zh-sans);
  font-size: 12.5px; /* intentional: D-2A prototype value */
  color: var(--text-muted);
  margin: 0;
  line-height: 1.6;
}

/* 🔴 影子跟讀切換時會把中翻日整塊藏起來（`list.hidden = true`），
   而 `.exercises-list` 宣告了 `display:flex` —— `[hidden]` 靠的是 UA stylesheet 的
   最低優先權，被 author 的 display 蓋掉就**完全沒作用**。
   2026-08-20 使用者截圖回報：切到影子跟讀之後，中翻日的 1948px 題目卡照樣
   佔滿畫面，session 被擠到看不見的下方。 */
.exercises-list[hidden] { display: none; }
.exercises-list {
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex: 1;
}

/* ── Exercise Card ── */
.exercise-card {
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 22px 24px 20px;
  position: relative;
  transition: background 240ms var(--ease-entry), border-color var(--duration-fast) var(--ease-out);
}
.exercise-card:hover { border-color: var(--border-default); }
.exercise-card.correct { background: var(--status-correct-bg); }
.exercise-card.partial { background: var(--status-partial-bg); }
.exercise-card.wrong   { background: var(--status-wrong-bg); }

.exercise-card-bar {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 4px;
  background: transparent;
  border-top-left-radius: var(--radius-lg);
  border-bottom-left-radius: var(--radius-lg);
  pointer-events: none;
  transition: background 240ms var(--ease-entry);
}
.exercise-card.correct .exercise-card-bar { background: var(--status-correct); }
.exercise-card.partial .exercise-card-bar { background: var(--status-partial); }
.exercise-card.wrong   .exercise-card-bar { background: var(--status-wrong); }

.card-header {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 16px;
}
.card-num {
  font-family: var(--font-en-display);
  /* intentional: structural value between --text-h1 (32) and --text-body-lg (17);
     en-display numeral hierarchy for practice card. See Phase 1 D-1. */
  font-size: 28px;
  font-weight: 400;
  line-height: 1;
  color: var(--text-primary);
  font-feature-settings: "lnum" 1, "tnum" 1;
  min-width: 32px;
  flex: 0 0 auto;
  height: 32px;
  display: flex;
  align-items: center;
}
.card-divider {
  width: 1px;
  height: 18px;
  background: var(--border-subtle);
  flex: 0 0 auto;
  align-self: center;
}
.card-meta-group {
  display: flex;
  align-items: baseline;
  gap: 4px;
  flex: 0 0 auto;
}
.card-meta-label {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--text-muted);
  flex: 0 0 auto;
  line-height: 1;
}
.card-meta-dot {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
  opacity: 0.5;
  flex: 0 0 auto;
  line-height: 1;
}
.card-level-badge {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  color: var(--text-muted);
  flex: 0 0 auto;
  line-height: 1;
}
/* 複習題：接在等級 badge 後的「複習 文法」標籤 —— 與 meta 同一系（muted、xs、zh-sans）。
   不外包括號（文法名本身常帶括號，再包會巢狀）；沿用 group 的 4px gap 與等級分隔。 */
.card-review-note {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
  flex: 0 0 auto;
  line-height: 1;
}
.card-header-action {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  align-self: center;
}
.card-status-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 12px;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
}
.card-status-pill svg { width: 14px; height: 14px; flex: 0 0 auto; }
.exercise-card.correct .card-status-pill { color: var(--status-correct); }
.exercise-card.partial .card-status-pill { color: var(--status-partial); }
.exercise-card.wrong   .card-status-pill { color: var(--status-wrong); }
.exercise-card .feedback-box .card-status-pill {
  padding: 0;
  height: auto;
  margin-bottom: 8px;
}
.exercise-card .feedback-box .card-status-pill[hidden] { display: none; }
.comp-graded-body .card-status-pill { padding: 0; height: auto; margin-bottom: 8px; }

.card-question {
  font-family: var(--font-zh-serif);
  /* intentional: 1px above --text-body-lg (17), maintains prompt hierarchy
     in practice card. Mirrors ReadingCard kanji-row decision (Overlay D-6).
     See Phase 1 D-2. */
  font-size: 18px;
  line-height: 1.6;
  color: var(--text-primary);
  letter-spacing: 0.005em;
  text-wrap: pretty;
  margin: 0 0 16px;
}
.card-input-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}
.card-input-row .answer-input {
  height: 44px;
  padding: 0 16px;
}
.answer-input {
  flex: 1;
  background: var(--bg-base);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  font-family: var(--font-jp-sans);
  font-size: var(--text-body);
  color: var(--text-primary);
  outline: 0;
  transition: border-color var(--duration-fast) var(--ease-out);
}
.answer-input:focus { border-color: var(--accent); }
.answer-input:disabled { background: transparent; opacity: 0.85; cursor: default; }

.check-btn {
  height: 44px;
  padding: 0 20px;
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
  flex: 0 0 auto;
  white-space: nowrap;
}
.check-btn:hover { background: var(--accent-hover); }
.check-btn:disabled { opacity: 0.45; cursor: default; }
.check-btn[hidden] { display: none; }

/* .reveal-btn 的樣式已收斂進 .btn-ghost--strong（見「共用按鈕系統」）；
   這個 class 只剩 JS 行為 hook 的身分。 */

.feedback-box {
  margin: 0 16px 0 52px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  line-height: 1.6;
  display: none;
}
.exercise-card .feedback-box {
  margin: 18px 0 0;
  padding: 18px 0 0;
  border-radius: 0;
  display: block;
}
.exercise-card .feedback-box[hidden] { display: none; }
.feedback-box.correct { background: var(--status-correct-bg); color: var(--status-correct); border-left: 3px solid var(--status-correct); }
.feedback-box.partial  { background: var(--status-partial-bg); color: var(--status-partial); border-left: 3px solid var(--status-partial); }
.feedback-box.wrong    { background: var(--status-wrong-bg); color: var(--status-wrong); border-left: 3px solid var(--status-wrong); }

.feedback-divider,
.answer-reveal-divider {
  display: block;
  width: 100%;
  height: 1px;
  background: var(--border-subtle);
  margin-bottom: 14px;
}
.feedback-text {
  display: block;
  font-family: var(--font-zh-serif);
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--text-secondary);
  text-wrap: pretty;
}

.card-footer {
  display: flex;
  gap: 10px;
  margin-top: 16px;
  align-items: center;
}
.card-footer[hidden] { display: none; }

/* .retry-btn → .btn-ghost，.followup-btn → .btn-secondary（見「共用按鈕系統」）。
   兩個 class 只剩 JS 行為 hook 的身分。

   ⑤ margin-left: auto 是「把追問鈕推到卡片右側」的佈局意圖，不是按鈕本身的樣式 ——
   所以它歸使用處（.card-footer）管，元件不帶它。否則題庫、理解題等其他地方一用
   .followup-btn 就會莫名其妙被推到右邊。 */
.card-footer .followup-btn { margin-left: auto; }

/* ── Chat context banner (exercises tab) — mirrors .selection-context-bar glass layout ── */
.context-banner {
  display: none;
  margin-bottom: 8px;
  border-radius: var(--radius-md);
  padding: 10px 12px 10px 14px;
  align-items: center;
  gap: 10px;
}
.context-banner.active { display: flex; }
/* exercises banner text is Chinese → zh-serif */
.context-banner .sel-context-text { font-family: var(--font-zh-serif); }

/* ── Chat context cards (QuestionContextCard / SelectionContextCard) ── */
.msg.context-msg { align-self: stretch; max-width: 100%; }

/* QuestionContextCard */
.question-ctx-card {
  border: 1px solid var(--accent);
  background: var(--accent-soft);
  border-radius: var(--radius-md); /* D-1B: radius-md 12px */
  padding: 12px 16px 4px;
  width: 100%;
}
.question-ctx-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.question-ctx-header__label {
  font-family: var(--font-zh-sans);
  font-size: 11.5px; /* intentional: matches banner eyebrow convention */
  font-weight: 600;
  color: var(--accent);
  letter-spacing: 0.04em;
}
.question-ctx-header__sep { opacity: 0.4; color: var(--accent); }
.question-ctx-header__num {
  font-family: var(--font-zh-sans);
  font-size: 11.5px; /* intentional */
  font-weight: 400;
  color: var(--accent);
  letter-spacing: 0.04em;
  opacity: 0.92;
}
.question-ctx-lede {
  font-family: var(--font-zh-serif);
  font-size: var(--text-sm);
  line-height: 1.65;
  color: var(--text-primary);
  margin: 0 0 10px;
}
.question-ctx-diff {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-left: 10px;
  border-left: 2px solid var(--accent);
  margin: 0 0 4px;
}
.question-ctx-diff-label {
  font-family: var(--font-zh-sans);
  font-size: 10.5px; /* intentional: between text-xs 11px and smaller */
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin: 0 0 2px;
}
.question-ctx-diff-value {
  font-family: var(--font-jp-serif);
  font-size: 13.5px; /* intentional */
  line-height: 1.6;
  margin: 0;
  letter-spacing: 0.02em;
}
.question-ctx-diff-value--muted  { color: var(--text-secondary); }
.question-ctx-diff-value--primary { color: var(--text-primary); }
.question-ctx-disclosure {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding: 6px 0;
  background: transparent;
  border: 0;
  color: var(--text-muted);
  font-family: var(--font-zh-sans);
  font-size: 12px;
  letter-spacing: 0.02em;
  cursor: pointer;
  width: 100%;
  justify-content: flex-start;
}
.question-ctx-chevron {
  transition: transform 180ms ease;
  flex-shrink: 0;
}
.question-ctx-chevron--open { transform: rotate(90deg); }
.question-ctx-disclose-body {
  padding: 8px 0 10px;
  border-top: 1px dashed var(--border-default);
  margin-top: 2px;
  animation: ctxExpand 200ms var(--ease-entry) both;
}
.question-ctx-disclose-text {
  font-family: var(--font-zh-serif);
  font-size: 13px;
  line-height: 1.75;
  color: var(--text-secondary);
  margin: 0;
  letter-spacing: 0.005em;
}

/* SelectionContextCard */
.selection-ctx-card {
  border: 1px solid var(--accent);
  background: var(--accent-soft);
  border-radius: var(--radius-md); /* D-1B: consistent with QuestionContextCard */
  padding: 10px 14px;
  width: 100%;
}
.selection-ctx-inner {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  row-gap: 4px;
}
.selection-ctx-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  font-family: var(--font-zh-sans);
  font-size: 11.5px; /* intentional */
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--accent);
  line-height: 1;
}
.selection-ctx-snippet {
  flex: 1;
  min-width: 0;
  font-size: 14.5px; /* intentional */
  line-height: 1.6;
  color: var(--text-primary);
  font-weight: 500;
}
.selection-ctx-snippet[lang="ja"] { font-family: var(--font-jp-serif); letter-spacing: 0.02em; }
.selection-ctx-snippet[lang="zh-Hant"] { font-family: var(--font-zh-serif); letter-spacing: 0.005em; }

@keyframes ctxExpand {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: translateY(0); }
}

.answer-reveal {
  margin-top: 18px;
  padding-top: 18px;
}
.answer-reveal[hidden] { display: none; }
.answer-reveal-label {
  display: block;
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.answer-reveal-text {
  display: block;
  font-family: var(--font-jp-serif);
  font-size: var(--text-h3);
  color: var(--text-primary);
  letter-spacing: 0.02em;
  line-height: 1.6;
  margin-bottom: 4px;
}
.answer-reveal-reading {
  display: block;
  font-family: var(--font-jp-sans);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ── Loading / Error States ── */
.loading-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
  color: var(--text-secondary);
  gap: 12px;
}
.spinner {
  width: 36px; height: 36px;
  border: 2.5px solid var(--border-default);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.85s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Practice Loading Simple (LS-1 / LS-2) ── */
.practice-loading-simple {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--accent);
}
.practice-loading-simple__spinner {
  flex-shrink: 0;
  animation: stagedSpin 900ms linear infinite;
}

/* ── Staged Loading Utility (LS-3 練習題生成 + LS-4 理解題生成 共用) ── */
.staged-loading {
  padding: var(--space-7) var(--space-7) var(--space-8);
  display: flex;
  flex-direction: column;
  min-height: 520px;
}
.staged-loading__eyebrow {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 18px;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--accent);
}
.staged-loading__eyebrow-spinner {
  flex-shrink: 0;
  animation: stagedSpin 900ms linear infinite;
}
.staged-loading__title {
  font-family: var(--font-zh-serif);
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  font-weight: 500;
  color: var(--text-primary);
  margin: 0 0 10px;
  letter-spacing: -0.005em;
}
.staged-loading__lede {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0 0 36px;
  max-width: 440px;
  line-height: var(--lh-body-lg);
}
.staged-loading__steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-width: 520px;
}
.staged-loading__step {
  display: grid;
  grid-template-columns: 28px 1fr;
  column-gap: 14px;
  position: relative;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  background: transparent;
  transition: background var(--duration-slow) var(--ease-out);
  animation: stagedStepIn var(--duration-slow) var(--ease-entry) both;
}
.staged-loading__step--active {
  background: var(--accent-soft);
}
.staged-loading__step-marker-col {
  grid-column: 1;
  grid-row: 1 / span 2;
  position: relative;
  display: flex;
  justify-content: center;
}
.staged-loading__connector {
  position: absolute;
  top: 22px;
  bottom: -14px;
  left: 50%;
  width: 1px;
  transform: translateX(-50%);
  background: var(--border-subtle);
}
.staged-loading__step-marker {
  display: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 2px;
  align-items: center;
  justify-content: center;
}
.staged-loading__step--pending .staged-loading__step-marker--pending {
  display: inline-flex;
  border: 1.5px solid var(--border-default);
  background: transparent;
}
.staged-loading__step--active .staged-loading__step-marker--active {
  display: inline-flex;
  border: 1.5px solid var(--accent);
  animation: stagedRingPulse 1600ms var(--ease-out) infinite;
}
.staged-loading__step--done .staged-loading__step-marker--done {
  display: inline-flex;
  background: var(--status-correct);
  color: var(--bg-base);
}
.staged-loading__step-marker-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}
.staged-loading__step-content {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.staged-loading__step-label-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.staged-loading__step-label {
  font-family: var(--font-zh-sans);
  font-size: var(--text-body);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-secondary);
}
.staged-loading__step--active .staged-loading__step-label {
  font-weight: 600;
  color: var(--text-primary);
}
.staged-loading__step--done .staged-loading__step-label {
  color: var(--text-muted);
}
.staged-loading__step-pulse {
  display: none;
  gap: 3px;
  align-items: center;
}
.staged-loading__step--active .staged-loading__step-pulse {
  display: inline-flex;
}
.staged-loading__step-pulse span {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--accent);
  animation: stagedPulse 1200ms ease-in-out infinite;
}
.staged-loading__step-pulse span:nth-child(2) { animation-delay: 180ms; }
.staged-loading__step-pulse span:nth-child(3) { animation-delay: 360ms; }
.staged-loading__step-done-badge {
  display: none;
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  color: var(--status-correct);
  font-weight: 500;
}
.staged-loading__step--done .staged-loading__step-done-badge {
  display: inline;
}
.staged-loading__step-sub {
  margin: 0;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  line-height: var(--lh-tight);
  color: var(--text-muted);
}
.staged-loading__step--active .staged-loading__step-sub {
  color: var(--text-secondary);
}
.staged-loading__step--done .staged-loading__step-sub {
  opacity: 0.65;
}
.staged-loading__step-bar {
  display: none;
  margin-top: var(--space-2);
  height: 3px;
  border-radius: var(--radius-full);
  width: 60%;
  background: linear-gradient(
    90deg,
    var(--accent-soft) 0%,
    var(--accent)      50%,
    var(--accent-soft) 100%
  );
  background-size: 200% 100%;
  animation: stagedBarShimmer 1600ms ease-in-out infinite;
}
.staged-loading__step--active .staged-loading__step-bar {
  display: block;
}
.staged-loading__coach-voice {
  margin-top: var(--space-6);
  display: flex;
  align-items: center;
  gap: 10px;
  height: 20px;
}
.staged-loading__coach-dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.7;
  flex-shrink: 0;
  animation: stagedPulse 1400ms ease-in-out infinite;
}
.staged-loading__coach-text {
  margin: 0;
  font-family: var(--font-zh-serif);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-style: italic;
  animation: stagedCoachIn var(--duration-entry) var(--ease-entry) both;
}

/* ── Staged Loading Keyframes ── */
@keyframes stagedSpin {
  to { transform: rotate(360deg); }
}
@keyframes stagedStepIn {
  from { opacity: 0; transform: translateX(-6px); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes stagedRingPulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--accent-soft); }
  50%      { box-shadow: 0 0 0 6px transparent; }
}
@keyframes stagedPulse {
  0%, 100% { transform: scale(1);   opacity: 0.7; }
  50%      { transform: scale(1.6); opacity: 0.25; }
}
@keyframes stagedBarShimmer {
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}
@keyframes stagedCoachIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── Right: Q&A Chat Panel ── */
.chat-panel {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: 16px; /* intentional: D-1A prototype value */
  box-shadow: var(--shadow-card);
  position: sticky;   /* 也是 .coach-drop-overlay(inset:0) 的 containing block；勿改成 relative（見 §拖放覆蓋層註解） */
  top: 80px;          /* sticky 吸附偏移；position 一旦被打成 relative，這裡會變成向下位移 80px → 面板偏下 */
  height: calc(100vh - 112px);
  min-height: 640px;
}

/* ── 面板標題列的「重設這個面板」按鈕（單一元件）────────────────────────────
   🔴 **四個面板的重設鈕共用這一個**（2026-08-29 使用者裁決「收斂成同一個」）：
     · 教練面板「重新提問」（`#chat-reset-btn`）
     · 文章教練面板「重新提問」（`#article-chat-reset-btn`）
     · 中翻日「重新生成 N 題」（`#reload-btn`，原本是 `btn-secondary`）
     · 影子跟讀「重新生成」（`#sh-regen`，原本是 `sh-textbtn`）
   它們角色相同（重設整個面板）、位置相同（面板標題列右側），改之前卻用了
   **三種不同的元件** —— 同一件事三個長相。

   ⚠️ **不收「錯誤重試」那幾顆**（朗讀失敗、出題失敗、對話錯誤區塊、
      理解題「再答一次」）：那是「這次壞了、再試一次」，與「重設整個面板」
      是兩件事。長得一樣會讓使用者分不出來。

   🩸 **`display:none` 與 `position:absolute` 從這裡搬走了。** 它們是**教練面板
      專屬的行為**（對話還沒開始就不顯示、浮在面板右上角），不是這個按鈕的
      長相。留在共用件裡的話，另外兩顆一掛上去就會**消失並且定位錯亂**
      —— 正是〈借了元件、沒借它的容器〉那個坑。現在那兩件事在
      `.chat-reset-btn--floating`，只有教練面板用。 */
.chat-reset-btn {
  display: inline-flex;
  padding: 6px 12px;
  border-radius: var(--radius-full);
  font-family: var(--font-zh-sans);
  /* intentional: 12.5px — between --text-xs (11) and --text-sm (13)（使用者裁決
     2026-08-29）。⚠️ **不用最接近的 token 頂替**：專案規則明訂「設計有明確數值、
     但無對應 token 時，用 literal ＋ intentional 註解」，1–2px 的偏差會累積成
     可見的不協調。同一個值在專案已有多處先例（`.analysis-empty__eyebrow-label`、
     UrlBar D-3a、D-2A／D-4A prototype 值）。
     🔴 **四顆面板重設鈕共用這一條** —— 改這裡就是四顆一起改，不會有一顆落單。 */
  font-size: 12.5px;
  color: var(--text-primary);
  letter-spacing: 0.02em;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  transition: opacity var(--duration-base) var(--ease-out);
  white-space: nowrap;
}
.chat-reset-btn:hover { opacity: 0.75; }

/* 教練面板專屬：浮在面板右上角，而且**對話開始之後才出現**
   （`showResetBtn` 加 `.visible`）。另外兩顆常駐在標題列的 flex 排版裡，
   不吃這一組。 */
.chat-reset-btn--floating {
  display: none;
  position: absolute;
  top: 18px;
  right: 18px;
}
.chat-reset-btn--floating.visible { display: inline-flex; }

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 18px 22px; /* intentional: D-6B prototype value (was 20px) */
  display: flex;
  flex-direction: column;
  gap: 24px; /* intentional: D-6B prototype value — both ChatPanel.jsx prototypes use 24px */
}

.msg {
  display: flex;
  gap: 10px;
}
.msg.user      { align-self: flex-end;   flex-direction: row-reverse; max-width: 80%; }  /* intentional: D-6B user max-width 80% per prototype */
.msg.assistant { align-self: flex-start; max-width: 92%; }

.avatar {
  width: 28px; height: 28px; /* intentional: D-6B prototype value (was 30px) */
  border-radius: 50%;
  border: 1px solid var(--border-default);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  flex: 0 0 28px;
  margin-top: 2px; /* intentional: D-6B CoachPanel.jsx prototype marginTop:2 */
}
.msg.assistant .avatar { color: var(--accent); } /* intentional: D-6B prototype + reply-actions-spec v1.0 — accent 橘色 */

.bubble {
  padding: 10px 14px;
  border-radius: var(--radius-lg);
  max-width: 100%;
}
.msg.user .bubble {
  background: var(--accent-soft); /* intentional: D-6B D-B, accent-soft per prototype (was accent) */
  color: var(--text-primary);
  border-radius: 16px 16px 6px 16px; /* intentional: D-6B D-C, chat bubble cut corner per prototype */
  font-family: var(--font-zh-sans);
  font-size: 14px; /* intentional: D-6B D-F, prototype value; --text-sm=13px --text-body=15px, no exact token */
  line-height: 1.55; /* intentional: D-6B, user bubble tighter than assistant (1.7); prototype explicit value */
}
.msg.assistant .bubble {
  padding: 0; /* intentional: D-6B prose wrapper has no padding in prototype; user bubble keeps padding */
}

.chat-wait-dots {
  display: inline-flex;
  gap: 3px;
  align-items: center;
  padding: 2px 0;
}
.chat-wait-dots span {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--accent);
  animation: stagedPulse 1200ms ease-in-out infinite;
}
.chat-wait-dots span:nth-child(2) { animation-delay: 180ms; }
.chat-wait-dots span:nth-child(3) { animation-delay: 360ms; }

/* .chat-welcome removed — article panel now uses .chat-empty (see EmptyStates refactor) */
/* .coach-welcome is the exercise panel component, still uses chat-welcome class as a DOM hook */

/* Zero padding from .chat-messages when CoachWelcome is the sole child */
.chat-messages:has(> .coach-welcome:only-child) {
  padding: 0;
}

/* CoachWelcome panel-fill — overrides .chat-welcome card styling */
.coach-welcome {
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  text-align: left;
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: var(--space-6) 28px var(--space-5);
}
.coach-welcome h3 {
  font-family: var(--font-zh-serif);
  font-size: var(--text-h3);
  color: var(--text-primary);
  font-weight: 500;
  letter-spacing: 0.005em;
  margin: 0 0 12px;
}
.coach-welcome > p {
  font-family: var(--font-zh-serif);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.85;
  text-wrap: pretty;
}

/* Coach mark */
.welcome-coach-mark {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border-default);
  background: var(--bg-subtle);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 18px;
  color: var(--accent);
  flex-shrink: 0;
}

/* R1: Skeleton grid */
.suggestion-skeleton-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
  margin-top: 28px;
}
.suggestion-skeleton {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: 14px 14px 16px;
  min-height: 78px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.skeleton-line {
  height: 11px;
  border-radius: var(--radius-misc-1);
  background: linear-gradient(90deg, var(--bg-subtle) 0%, var(--border-subtle) 50%, var(--bg-subtle) 100%);
  background-size: 200% 100%;
  animation: stagedBarShimmer 1800ms ease-in-out infinite;
}

/* R1: Analyzing hint pinned to bottom */
.welcome-analyzing-hint {
  margin-top: auto;
  padding-top: var(--space-5);
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.welcome-analyzing-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--text-muted);
  flex-shrink: 0;
  animation: stagedPulse 1400ms ease-in-out infinite;
}

/* R2: Curriculum summary paragraphs */
.curriculum-summary {
  font-family: var(--font-zh-serif);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.85;
  text-wrap: pretty;
  margin-bottom: 6px;
}
.curriculum-summary:last-of-type { margin-bottom: 0; }

/* R2: Suggestion card grid */
.suggestion-card-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
  margin-top: 28px;
}
.suggestion-card {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--bg-subtle);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: 12px 14px 13px;
  min-height: 78px;
  font-family: var(--font-zh-serif);
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-secondary);
  text-wrap: pretty;
  cursor: pointer;
  transition: background 160ms var(--ease-out), border-color 160ms var(--ease-out), color 160ms var(--ease-out);
  animation: stagedCoachIn 360ms var(--ease-entry) both;
}
.suggestion-card:hover {
  background: var(--accent-soft);
  border-color: var(--accent-border);
  color: var(--text-primary);
}
.suggestion-card:disabled,
.suggestion-card:disabled:hover {
  opacity: 0.4;
  cursor: default;
  background: var(--bg-subtle);
  border-color: var(--border-subtle);
  color: var(--text-secondary);
}

/* R2 entry fade-in */
.coach-welcome.welcome-ready {
  animation: welcomeFadeIn 240ms var(--ease-entry) both;
}
@keyframes welcomeFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* R2: Refresh footer pinned to bottom */
.welcome-footer {
  margin-top: auto;
  padding-top: var(--space-5);
}
.curriculum-refresh-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  background: transparent;
  border: 0;
  border-radius: var(--radius-misc-3);
  color: var(--text-muted);
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.curriculum-refresh-btn:hover {
  color: var(--text-secondary);
  background: var(--bg-subtle);
}

/* .rec-sites / .rec-site-* removed — superseded by .site-card system (see EmptyStates refactor) */

/* ── Mode Switcher (segmented pill) ── */
.mode-switcher {
  display: inline-flex;
  align-self: flex-start;
  background: var(--bg-subtle);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  padding: 3px;
  gap: 2px;
}
.mode-switcher-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: var(--radius-full);
  border: 0;
  cursor: pointer;
  background: transparent;
  color: var(--text-secondary);
  font-family: var(--font-zh-sans);
  font-size: 12.5px; /* intentional: D-4A prototype value */
  font-weight: 400;
  line-height: 1;
  box-shadow: none;
  transition: all 200ms ease;
  white-space: nowrap;
}
.mode-switcher-btn.is-active {
  background: var(--bg-surface);
  font-weight: 500;
  box-shadow: 0 1px 2px rgba(0,0,0,0.20); /* intentional: D-4A prototype active tab shadow */
}
.mode-switcher-btn.is-active.is-qa   { color: var(--accent); }
.mode-switcher-btn.is-active.is-conj { color: var(--status-partial); }

/* ── Composer Wrapper ── */
.composer-wrapper {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--bg-subtle);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md); /* 12px, intentional: D-5A prototype value */
  padding: 6px 8px 6px 8px;
  transition: border-color 200ms ease;
}
/* Verb mode: amber border on wrapper, amber send button, hide image upload */
.composer-wrapper.verb-mode {
  border-color: var(--status-partial-border);
}
.composer-wrapper.verb-mode #send-btn,
.composer-wrapper.verb-mode #article-send-btn {
  background: var(--status-partial);
}
.composer-wrapper.verb-mode .img-upload-btn {
  display: none;
}

/* ── Conjugation Card (in chat area) ── */
.conj-card {
  background: var(--bg-base);
  border: 1px solid var(--border-default);
  /* intentional: left border 3px amber (status-partial) editorial tool-output accent; right/top/bottom 1px default. See Stage 2.5-C D-1. */
  border-left: 3px solid var(--status-partial);
  border-radius: 10px;
  overflow: hidden;
  /* intentional: flex-shrink 0 — overflow:hidden sets min-height:0 on flex children, allowing card to collapse to 0px when chat-messages is full. See Stage 2.5-C D-1. */
  flex-shrink: 0;
  box-shadow: var(--shadow-card);
}
.conj-card-header {
  padding: 14px 18px 12px;
  border-bottom: 1px solid var(--border-subtle);
  background: var(--status-partial-bg-subtle);
}
.conj-header-top {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.conj-tool-icon {
  color: var(--status-partial);
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.conj-tool-label {
  font-family: var(--font-zh-sans);
  font-size: 11.5px;
  letter-spacing: 0.2em;
  color: var(--status-partial);
  font-weight: 500;
}
.conj-type {
  font-family: var(--font-en-display);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-left: auto;
}
.conj-header-info {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
}
.conj-verb {
  font-family: var(--font-jp-serif);
  font-size: 22px;
  font-weight: 500;
  color: var(--text-primary);
  line-height: 1.2;
}
.conj-reading {
  font-family: var(--font-jp-sans);
  font-size: 13px;
  color: var(--text-secondary);
  letter-spacing: 0.04em;
}
.conj-input-hint {
  font-family: var(--font-zh-sans);
  font-size: 12px;
  color: var(--text-muted);
}
.conj-input-hint .conj-input-form {
  font-family: var(--font-jp-serif);
  color: var(--text-secondary);
}
.conj-input-hint .conj-matched-label {
  color: var(--status-partial);
  font-weight: 500;
}
.conj-groups {
  padding: 4px 18px 16px;
  display: flex;
  flex-direction: column;
}
.conj-group {
  margin-top: 14px;
}
.conj-group-label {
  font-family: var(--font-en-display);
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 8px;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 8px;
}
.conj-group-label::before {
  content: '';
  display: inline-block;
  width: 10px;
  height: 1px;
  background: var(--border-default);
}
.conj-rows {
  display: grid;
  grid-template-columns: 78px 1fr auto;
  gap: 0;
}
.conj-row {
  display: contents;
}
.conj-row > * {
  padding: 7px 10px;
  align-self: baseline;
}
/* Highlight: matched conjugation form */
.conj-row.is-highlight > * {
  background: var(--status-partial-bg);
}
.conj-row.is-highlight .conj-label { color: var(--status-partial); font-weight: 500; }
.conj-row.is-highlight .conj-value { font-weight: 500; }
/* Left indicator bar for highlight row */
.conj-row.is-highlight .conj-label {
  border-left: 2px solid var(--status-partial);
  padding-left: 8px;
}
.conj-label {
  font-family: var(--font-zh-sans);
  font-size: 11.5px;
  color: var(--text-muted);
  white-space: nowrap;
}
.conj-value {
  font-family: var(--font-jp-serif);
  font-size: 15px;
  color: var(--text-primary);
}
.conj-kana {
  font-family: var(--font-jp-sans);
  font-size: 11.5px;
  color: var(--text-muted);
  letter-spacing: 0.04em;
  text-align: right;
}

/* ── Chat Input ── */
.chat-input-area {
  padding: 12px 16px 16px;
  border-top: 1px solid var(--border-default);
  background: var(--bg-surface);
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
#chat-input, #article-chat-input {
  flex: 1;
  border: 0;
  outline: 0;
  resize: none;
  padding: 8px 4px;
  font-size: 14.5px; /* intentional: D-5A prototype value */
  font-family: var(--font-zh-serif); /* QA mode default: zh-serif per prototype D-5A */
  min-height: 40px;
  max-height: 120px;
  background: transparent;
  color: var(--text-primary);
  line-height: 1.6;
  transition: font-family 120ms;
}
/* Conjugate mode: jp-serif font + extra left padding */
.composer-wrapper.verb-mode #chat-input,
.composer-wrapper.verb-mode #article-chat-input {
  font-family: var(--font-jp-serif);
  padding: 8px 4px 8px 12px; /* intentional: D-5A extra left pad in conjugate mode */
}
/* 教練面板 composer 的成本標（`coach`，問與答與動詞活用共用同一個收費單位）。
   🔴 **`quiet: true` 只免除同意層 popover 與逐次回執，不免除成本標**——
      設計 `CreditGuard` 明講「高頻微額動作雖然預設不問，但成本要**常駐**在
      按鈕旁」。這一塊與反白工具列是**同一次漏做**（2026-08-26 使用者兩次
      截圖回報），治法也相同：把成本標放進畫面、費率由後端送。
   ⚠️ **字級用一般成本標的預設（13/12px），不是工具列那組 12.5/11.5px**——
      後者是設計對玻璃工具列的明確指定（`size={11.5}`），composer 是一般表面，
      沿用它等於憑空多出第三種字級。
   ⚠️ 不隨模式切換：兩個模式都扣 `coach`，切模式改 spec 只會讓數字閃一下。 */
.composer-cost {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  /* 左右各 8px ＋ `.composer-wrapper` 自己的 `gap: 6px` ＝ **兩側各 14px**。
     ⚠️ 第一版只給左邊 4px（右側只剩 gap 的 6px），成本標整個貼在送出鈕上，
        與設計稿不符（使用者 2026-08-26 對照回報）。設計稿量到的間隙約 13–15px。 */
  padding: 0 var(--space-2);
}
#send-btn, #article-send-btn {
  background: var(--accent);
  color: var(--accent-ink);
  border: none;
  border-radius: 8px; /* intentional: D-6A prototype value, between --radius-sm(6) and --radius-md(12) */
  width: 34px; height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color 200ms ease;
  flex-shrink: 0;
}
#send-btn:hover, #article-send-btn:hover { background: var(--accent-hover); }
#send-btn:disabled, #article-send-btn:disabled { opacity: 0.45; cursor: default; }

/* ── API Key Warning ── */
.api-warning {
  background: var(--status-partial-bg);
  border: 1px solid var(--status-partial);
  border-radius: var(--radius-md);
  padding: 10px 14px;
  font-size: var(--text-sm);
  color: var(--status-partial);
  margin: 14px;
  display: none;
}

/* ── Scrollbar ── */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--bg-elevated); }

/* ── Responsive ── */
@media (max-width: 768px) {
  main,
  .tab-panel > main {
    grid-template-columns: 1fr;
    gap: 0;
    padding: 16px 16px 40px;
  }
  .chat-panel,
  .article-chat-panel {
    position: static;
    height: auto;
    min-height: 480px;
  }
}

/* ══════════════════════════════════════════
   TAB NAVIGATION
══════════════════════════════════════════ */
.tab-btn {
  /* ⚠️ `.tab-btn` 同時服務 `<button>`（app 內）與 `<a>`（獨立頁面的導覽列）——
     連結有 UA 預設底線，不清掉的話兩頁的分頁**長得不一樣**（使用者一眼看得出來）。
     🔒 `tests/test_navbar_shared.py` */
  text-decoration: none;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 0 16px;
  height: 100%;
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color var(--duration-base), border-color var(--duration-base), font-weight var(--duration-base);
}
.tab-btn:hover { color: var(--text-primary); }
.tab-btn.active {
  color: var(--text-primary);
  font-weight: 500;
  letter-spacing: 0.04em;
  border-bottom-color: var(--accent);
}
.tab-icon {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
}

.tab-panel { display: none; }
.tab-panel.active { display: flex; flex-direction: column; }

/* ══ 全站內容框（page frame）—— **唯一一份** ══
   🔴 使用者裁決 2026-08-25：「點數頁左右撐滿的寬度和其他頁不一致，應一致。
      先調查是否有共用 frame 元件，**應要有才對**」。
   🩸 **調查結果：本來沒有。** 寬度與內距寫死在 `.tab-panel > main` 裡 ——
      它綁死在 app 的分頁結構上，**別的頁面（我的點數）根本用不到**，
      於是新頁面只能自己再寫一份，而那一份必然與既有的分歧（實際差了 240px）。
   ⇒ 抽成 class：每一個內容框都掛 `.page-frame`，單欄的再加 `--single`。
   ⚠️ **frame 只管「寬度與內距」** —— grid 兩欄是版面的事，留在 `.tab-panel > main`。
   🔒 `tests/test_page_frame.py`（含「每個 <main> 都要掛」的護欄 —— 忘了掛會撐滿） */
.page-frame {
  width: 100%;
  margin: 0 auto;
  max-width: 1280px;
  padding: 32px 32px 80px;
}
/* 單欄頁（題庫、我的點數）。⚠️ 比兩欄窄是刻意的：單欄拉到 1280 一行會太長。 */
.page-frame--single {
  max-width: 1080px;
  padding: var(--space-7) var(--space-6) var(--space-9);
}

.tab-panel > main {
  /* ⚠️ 寬度與內距在 `.page-frame`，這裡只放**兩欄版面**的事。 */
  display: grid;
  grid-template-columns: minmax(0, 60fr) minmax(0, 40fr);
  gap: 32px;
  align-items: flex-start;
}

/* ══════════════════════════════════════════
   ARTICLE LEARNING PANEL
══════════════════════════════════════════ */
.article-panel {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: 16px; /* intentional: matches chat-panel D-1A */
  box-shadow: var(--shadow-card);
  min-height: calc(100vh - 112px); /* matches article-chat-panel height formula */
}

/* URL input bar — page-level chrome, shown only on article tab via .url-bar-visible */
.url-input-bar {
  display: none;
  background: var(--bg-base);
  border-bottom: 1px solid var(--border-default); /* intentional: border-default (not border-subtle per prototype); stronger separation desired. See UrlBar D-2. */
}
.url-input-bar.url-bar-visible {
  display: block;
}
/* Compact mode: S2/S3/S4 (phase !== S1) — triggered by analyzeUrl() start */
.url-input-bar.url-bar-compact .url-input-bar__inner {
  padding: 8px var(--space-6);
}
.url-input-bar.url-bar-compact .url-input-wrapper {
  height: 30px;
  border-radius: 8px; /* intentional: 8px compact (vs radius-md 12px full); matches prototype compact spec. See UrlBar D-8. */
  padding: 0 10px;
}
.url-input-bar.url-bar-compact .url-input-bar__label {
  font-size: 11.5px; /* intentional: 11.5px compact (vs 12.5px full); matches prototype compact spec. See UrlBar D-8. */
}
.url-input-bar.url-bar-compact #url-input {
  font-size: 12px; /* intentional: 12px compact (vs --text-sm 13px full); matches prototype compact spec. See UrlBar D-8. */
}
.url-input-bar.url-bar-compact #analyze-btn {
  height: 32px;
  /* ⚠️ 這條的特異性 (1,2,0) 高於 `#analyze-btn` (1,0,0)，`padding` 簡寫會整條蓋掉
     —— 補償量必須在這裡再寫一次，不能只寫在上面那條。 */
  padding: 0 14px var(--btn-cap-pad);
}
.url-input-bar__inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 14px var(--space-6); /* intentional: 14px vertical (between --space-3 12px and --space-4 16px); matches prototype breathing. See UrlBar D-9. */
  display: flex;
  align-items: center;
  gap: 12px;
}
.url-input-bar__label {
  font-family: var(--font-zh-sans);
  font-size: 12.5px; /* intentional: 12.5px (not --text-sm 13px); matches prototype exactly. See UrlBar D-3a. */
  color: var(--text-muted);
  letter-spacing: 0.02em;
  flex: 0 0 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 120px;
}
.url-input-wrapper {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--bg-subtle);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  padding: 0 14px;
  height: 40px;
  transition: border-color var(--duration-base), box-shadow var(--duration-base);
}
.url-input-wrapper:focus-within {
  border-color: var(--accent);
}
.url-input-search-icon {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  color: var(--text-muted);
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
#url-input {
  flex: 1;
  border: none;
  outline: none;
  background: transparent;
  color: var(--text-primary);
  font-size: var(--text-sm);
  font-family: var(--font-en-mono);
  letter-spacing: 0.01em;
}
#analyze-btn {
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  height: 40px;
  padding: 0 20px var(--btn-cap-pad);  /* 下內距＝視覺置中補償，見 --btn-cap-pad */
  font-size: var(--text-sm);
  font-weight: 400;
  font-family: var(--font-jp-sans); /* intentional: font-jp-sans (not font-zh-sans per prototype); app-wide button convention. See UrlBar D-6. */
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--duration-base);
  letter-spacing: 0.02em;
  display: flex;
  align-items: center;
  gap: 6px;
}
#analyze-btn:hover { background: var(--accent-hover); }
#analyze-btn:disabled {
  opacity: 0.6; /* intentional: 0.6 (aligns with prototype loading state, was 0.45). See UrlBar D-7. */
  pointer-events: none;
}
.parse-spinner {
  display: inline-flex;
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
}
.parse-spinner svg {
  animation: spin 900ms linear infinite;
  stroke: currentColor;
  fill: none;
  stroke-width: 2.4;
  stroke-linecap: round;
}

/* Article body */
.article-body {
  padding: 22px 26px;
  font-size: var(--text-sm);
  line-height: 1.85;
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
}
/* .article-body table/th/td removed — superseded by .prose table rules. See AnalysisChunks D-3/D-4. */

/* ============================================================
   PROSE — Markdown content system
   Three sizes: .prose-sm (13/1.5) · .prose (15/1.7) · .prose-lg (17/1.8)
   Used by: #analysis-panel (.prose-lg) · AI chat replies (.prose.ai-prose)
   ============================================================ */

.prose, .prose-sm, .prose-lg {
  font-family: var(--font-zh-serif);
  color: var(--text-primary);
}
.prose-sm { font-size: var(--text-sm); line-height: var(--lh-tight); }
.prose    { font-size: var(--text-body); line-height: var(--lh-body-lg); }
/* intentional: line-height 1.8 above --lh-body-lg (1.7); long-form reading needs extra breathing room. See Phase 2 prose D-2. */
.prose-lg { font-size: var(--text-body-lg); line-height: 1.8; }

.prose[data-lang="ja"], .prose-sm[data-lang="ja"], .prose-lg[data-lang="ja"],
.prose[lang="ja"],      .prose-sm[lang="ja"],      .prose-lg[lang="ja"] {
  font-family: var(--font-jp-serif);
}

/* Paragraph */
.prose p, .prose-sm p, .prose-lg p {
  margin: 0 0 1em 0;
  text-indent: 0;
}
.prose p:last-child, .prose-sm p:last-child, .prose-lg p:last-child { margin-bottom: 0; }

/* Strong */
.prose strong, .prose-sm strong, .prose-lg strong {
  font-weight: 600;
  color: var(--text-primary);
}
.prose .callout strong, .prose-sm .callout strong, .prose-lg .callout strong {
  color: var(--accent);
}
.prose blockquote strong, .prose-sm blockquote strong, .prose-lg blockquote strong,
.prose blockquote code,   .prose-sm blockquote code,   .prose-lg blockquote code {
  color: inherit;
  background: transparent;
  padding: 0;
}

/* Inline code */
.prose code, .prose-sm code, .prose-lg code {
  font-family: var(--font-en-mono);
  font-size: 0.9em;
  background: var(--bg-subtle);
  padding: 2px 6px;
  border-radius: var(--radius-misc-1);
  color: var(--status-partial);
}

/* Blockquote */
.prose blockquote, .prose-sm blockquote, .prose-lg blockquote {
  margin: 1em 0;
  padding: 2px 0 2px var(--space-4);
  border-left: 2px solid var(--accent);
  font-style: italic;
  color: var(--text-secondary);
}
.prose blockquote[data-lang="ja"], .prose-sm blockquote[data-lang="ja"], .prose-lg blockquote[data-lang="ja"] {
  font-family: var(--font-jp-serif);
  font-style: normal;
}

/* Unordered list */
.prose ul, .prose-sm ul, .prose-lg ul {
  list-style: none;
  padding-left: 0;
  margin: 0 0 1em 0;
}
.prose ul li, .prose-sm ul li, .prose-lg ul li {
  position: relative;
  padding-left: 1.4em;
  margin-bottom: 4px;
}
.prose ul li::before, .prose-sm ul li::before, .prose-lg ul li::before {
  content: '・';
  position: absolute;
  left: 0.2em;
  color: var(--text-secondary);
  font-family: var(--font-zh-serif);
}

/* ⚠️ 批改回饋掛上 .prose-sm 後，必須把它自己的排版值搶回來（2026-08-07）。
   兩個批改容器加的是**同一個 class，結果卻不同** —— 純粹因為在本檔的位置：

     .feedback-text  在第 1187 行（**在 .prose-sm 之前**）→ 特異性相同、prose 在後 → **被 prose 蓋掉**
     .comp-feedback  在第 4015 行（**在 .prose-sm 之後**）→ 特異性相同、它在後 → 自己贏，不必處理

   會被蓋掉的是 line-height（1.7 → --lh-tight 1.50）與 color（secondary → primary）。
   掛 .prose-sm 的目的**只是要拿清單樣式**（`::before` 自繪符號、符號留在盒內不溢出），
   不是要改字級行高顏色 —— 所以在這裡原值釘回去。

   為什麼不把 .feedback-text 整塊搬到 prose 之後：搬動 CSS 區塊會連帶改變它與其他規則的
   先後關係，影響面比補兩行大得多。 */
.feedback-text.prose-sm {
  line-height: 1.7;
  color: var(--text-secondary);
}

/* Ordered list */
.prose ol, .prose-sm ol, .prose-lg ol {
  list-style: none;
  padding-left: 0;
  margin: 0 0 1em 0;
  counter-reset: prose-counter;
}
.prose ol li, .prose-sm ol li, .prose-lg ol li {
  position: relative;
  padding-left: 2em;
  margin-bottom: 4px;
  counter-increment: prose-counter;
}
.prose ol li::before, .prose-sm ol li::before, .prose-lg ol li::before {
  content: counter(prose-counter) '.';
  position: absolute;
  left: 0;
  font-family: var(--font-en-display);
  font-weight: 500;
  color: var(--text-secondary);
}

/* Headings inside prose */
/* intentional: h3 font-size 18px between --text-body-lg (17) and --text-h3 (19); section-header weight for prose. See Phase 2 prose D-2. */
.prose h3, .prose-sm h3, .prose-lg h3 {
  font-family: var(--font-zh-serif);
  font-size: 18px;
  font-weight: 500;
  margin: 1.4em 0 0.5em;
  color: var(--text-primary);
  display: flex;
  align-items: center;
  gap: 10px;
}
/* 標題內容的唯一 flex item（由 renderMarkdown 的 _wrapHeadingBody 產生）。
   ⚠️ `min-width: 0` 是必要的：flex item 預設 `min-width: auto`，不能收縮到比
      內容的最小寬度還窄 —— 遇到長網址或英文長字時標題會撐破容器。
   ⚠️ 這個 span 存在的理由見 app.js `_wrapHeadingBody` 的註解（標題 flex 版面的不變式）。 */
.prose h3 > .heading-body, .prose-sm h3 > .heading-body, .prose-lg h3 > .heading-body {
  min-width: 0;
}
.prose h3.with-rule::before, .prose-sm h3.with-rule::before, .prose-lg h3.with-rule::before {
  content: '';
  display: inline-block;
  width: 18px;
  height: 2px;
  background: var(--accent);
  flex: 0 0 18px;
}
/* intentional: article analysis (.prose-lg) h3 always shows accent rule for stronger section hierarchy;
   chat (.prose) keeps .with-rule opt-in to avoid decorating every grammar explanation heading. */
.prose-lg h3::before {
  content: '';
  display: inline-block;
  width: 18px;
  height: 2px;
  background: var(--accent);
  flex: 0 0 18px;
}
/* intentional: forward-compatible hook; AI prompt currently outputs ## not #.
   Article title is injected via JS (_injectAnalysisHeader). See Analysis 基底 D-5. */
.prose h1, .prose-sm h1, .prose-lg h1 {
  font-family: var(--font-jp-serif);
  font-size: 26px; /* intentional: editorial jp-serif article title; distinct from --text-h1 (32px). See Analysis 基底 D-1. */
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.005em;
  color: var(--text-primary);
  margin: 0 0 12px;
}
/* intentional: jp-serif 18px for Pattern head (句型名稱); affects all .prose h4. See AnalysisChunks D-6 B. */
.prose h4, .prose-sm h4, .prose-lg h4 {
  font-family: var(--font-jp-serif);
  font-size: 18px;
  font-weight: 600;
  margin: 1.2em 0 0.4em;
  color: var(--text-primary);
  letter-spacing: 0.01em;
}
/* intentional: prose h2 not in prototype spec (prototype uses SectionHeader component);
   app補 h2 because markdown ## renders as <h2> vs prototype SectionHeader element — architecture gap;
   long-term solution is AI outputting structured JSON → SectionHeader component. See Phase 2 prose D-8. */
.prose h2, .prose-sm h2, .prose-lg h2 {
  font-family: var(--font-zh-serif);
  font-size: 22px;
  font-weight: 500;
  margin: 1.6em 0 0.6em;
  color: var(--text-primary);
}

/* ── SectionHeader (generated by renderMarkdown from ## 01 一、 pattern) ── */
.prose-section-header {
  display: flex;
  align-items: baseline;
  gap: var(--space-4); /* 16px */
  /* intentional: marginTop 40px — no token; structural breathing between sections. See AnalysisChunks D-1. */
  margin-top: 40px;
  /* intentional: marginBottom 18px — between --space-4(16) and --space-5(24). See AnalysisChunks D-1. */
  margin-bottom: 18px;
  padding-top: 8px;
}
.section-num {
  font-family: var(--font-en-display);
  /* intentional: 30px en-display decorative size; distinct from --text-h1(32px). See AnalysisChunks D-1. */
  font-size: 30px;
  color: var(--accent);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.01em;
  flex-shrink: 0;
}
.prose-section-header h2 {
  font-family: var(--font-zh-serif);
  /* intentional: 22px section title; matches prototype SectionHeader h2. See AnalysisChunks D-1. */
  font-size: 22px;
  line-height: 1.3;
  font-weight: 500;
  color: var(--text-primary);
  margin: 0;
}

/* JP inline span (generated by renderMarkdown from 《》 marker) */
/* 日文片段（_wrapJapaneseRuns 產生）—— **只換字體**，讓 `、。` 落在正確位置。
   ⚠️ 刻意不沿用 .jp-inline：那是《》強調用的，帶 font-weight:500 + letter-spacing，
      套到整段日文例句上會讓它變粗，與周圍中文說明失衡。
   ⚠️ 這條規則是必要的 —— 只輸出 lang="ja" 沒有用：全站既有規則是
      `.prose[lang="ja"]`（容器層）與 `.jp-inline`（class），**都不匹配 prose 內的子 span**。
      實測踩過：原始碼層測試全綠，畫面上 6 個標點仍有 4 個是中文字體。
   🔴 **`display: contents` 是必要的，不是可有可無的整潔。**
      這個 span 的職責只有「換字體」，它**不該產生版面盒子**。少了這行，它在 flex 容器裡
      會變成一個 flex item：`.prose h3` 是 `display:flex; gap:10px`（為了排 ::before 那條
      橘色橫線），於是 `〜ても大丈夫` 被包住的前半段成了獨立 item，**中間硬生生多出 10px**
      —— 使用者 2026-08-15 回報「大丈 夫 中間有個空格」。實測：間隙 10px → 0px，
      而日文字形**沒有被關掉**（「、」墨跡中心 0.95 靠下 vs 中文 0.62 置中，套用前後相同）。
      ⚠️ 同一個坑對任何塞進 h3 的行內元素都成立 —— `_plainifyHeadings` 把 h3 裡的
      code/strong/em 剝掉就是同一個問題的既有繞法。治本（改用 ::before margin 取代 flex gap）
      影響 5 種 prose 容器的所有標題，另立一輪，見 ROADMAP。
   🔒 tests/test_jp_punctuation_font.py::test_jp_run_does_not_participate_in_layout */
.jp-run { font-family: var(--font-jp-serif); display: contents; }

.jp-inline {
  font-family: var(--font-jp-serif);
  font-weight: 500;
  letter-spacing: 0.01em;
}

/* Table */
.prose table, .prose-sm table, .prose-lg table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-zh-sans);
  /* intentional: 14px table content; between --text-sm(13) and --text-body(15). See AnalysisChunks D-3. */
  font-size: 14px;
  line-height: 1.6;
  margin: 0 0 1em;
}
.prose thead tr, .prose-sm thead tr, .prose-lg thead tr {
  background: var(--bg-subtle);
  border-bottom: 1px solid var(--border-default);
}
.prose th, .prose-sm th, .prose-lg th {
  text-align: left;
  padding: 12px 14px;
  font-family: var(--font-en-display);
  /* intentional: 11px uppercase editorial header; no token match. See AnalysisChunks D-3. */
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-secondary);
  font-weight: 500;
}
.prose td, .prose-sm td, .prose-lg td {
  padding: 12px 14px;
  border-bottom: 1px solid var(--border-subtle);
}
.prose tbody tr:nth-child(odd), .prose-sm tbody tr:nth-child(odd), .prose-lg tbody tr:nth-child(odd) {
  background: var(--bg-subtle);
}

/* Hairline divider */
.prose hr, .prose-sm hr, .prose-lg hr {
  border: 0;
  height: 1px;
  background: var(--border-subtle);
  margin: 1.4em var(--space-6);
}

/* Links */
.prose a, .prose-sm a, .prose-lg a {
  color: var(--accent);
  text-decoration: none;
}
.prose a:hover, .prose-sm a:hover, .prose-lg a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.prose a[href^="http"]::after, .prose-sm a[href^="http"]::after, .prose-lg a[href^="http"]::after {
  content: '';
  display: inline-block;
  width: 0.8em; height: 0.8em;
  margin-left: 0.25em;
  vertical-align: -0.05em;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M7 17L17 7M9 7h8v8'/></svg>") center/contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M7 17L17 7M9 7h8v8'/></svg>") center/contain no-repeat;
}

/* JP example block */
.jp-example { margin: 0.6em 0; }
.jp-example .main {
  font-family: var(--font-jp-serif);
  font-size: 1em;
  line-height: 1.5;
  color: var(--text-primary);
}
.jp-example .reading {
  font-family: var(--font-jp-sans);
  font-size: 0.85em;
  line-height: 1.4;
  color: var(--text-muted);
  margin-top: 2px;
  letter-spacing: 0.04em;
}
.jp-example .gloss {
  font-family: var(--font-zh-serif);
  font-size: 0.9em;
  color: var(--text-secondary);
  margin-top: 2px;
}
/* Label icon hook for jp-example (D-4) */
.prose .jp-example .label-icon,
.prose-lg .jp-example .label-icon,
.prose-sm .jp-example .label-icon {
  display: inline-block;
  width: 13px;
  height: 13px;
  vertical-align: -2px;
  margin-right: 4px;
}

/* Compare callout */
.compare {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  margin: 1em 0;
  /* intentional: padding 14px between --space-3 (12) and --space-4 (16); compare block visual breathing. See Phase 2 prose D-2. */
  padding: 14px;
  background: var(--bg-subtle);
  border-radius: var(--radius-md);
  border: 1px solid var(--border-subtle);
}
/* intentional: item gap 6px structural gap between compare item tag and content. See Phase 2 prose D-2. */
.compare .item { display: flex; flex-direction: column; gap: 6px; }
.compare .item .tag {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  align-self: flex-start;
}
.compare .item.good .tag { color: var(--status-correct); }
.compare .item.bad  .tag { color: var(--status-wrong); }
.compare .item .main {
  font-family: var(--font-jp-serif);
  font-size: 1em;
  color: var(--text-primary);
  line-height: 1.5;
}
.compare .item .reading {
  font-family: var(--font-jp-sans);
  font-size: 0.82em;
  color: var(--text-muted);
  letter-spacing: 0.04em;
}
.compare .item .gloss {
  font-family: var(--font-zh-serif);
  font-size: 0.88em;
  color: var(--text-secondary);
}

/* Emoji normalization */
.prose .emoji-status { font-style: normal; font-size: 1em; }
.prose .emoji-correct { color: var(--status-correct); }
.prose .emoji-partial { color: var(--status-partial); }
.prose .emoji-wrong   { color: var(--status-wrong);   }
.prose .traffic-light { font-size: 0.85em; vertical-align: 0.05em; }

/* Inline icon-with-text helper */
.prose .icon-tag, .prose-sm .icon-tag, .prose-lg .icon-tag {
  display: inline-flex; align-items: center; gap: 6px; color: var(--accent);
  font-family: var(--font-zh-sans); font-weight: 500; font-size: 0.95em;
}
.prose .icon-tag svg, .prose-sm .icon-tag svg, .prose-lg .icon-tag svg { flex: 0 0 auto; }

/* AI reply prose variant */
/* intentional: font-size 14px between --text-sm (13) and --text-body (15); AI reply compact reading. See Phase 2 prose D-2. */
/* intentional: line-height 1.75 tighter than prose-lg (1.8) but looser than prose (1.7); AI reply readability balance. See Phase 2 prose D-2. */
.ai-prose {
  font-size: 14px;
  line-height: 1.75;
}
/* intentional: ai-prose h3 always shows accent rule (opt-in with-rule is for generic .prose only).
   Stage 2.5-C: AI responses now open with ### headings; rule makes them visually anchored. See Stage 2.5-C D-2. */
.ai-prose h3::before {
  content: '';
  display: inline-block;
  width: 18px;
  height: 2px;
  background: var(--accent);
  flex: 0 0 18px;
}
.bubble.ai-prose {
  /* intentional: remove top padding so first h3 aligns with avatar.
     .bubble has padding:10px 14px; 10px top was pushing h3 down, misaligning ::before with avatar. */
  padding-top: 0;
}
.bubble.ai-prose > p:empty {
  /* intentional: renderMarkdown wraps output in <p>; browser auto-closes <p> before block <h3>,
     creating an empty <p></p> with margin-bottom:1em that pushes h3 down. Hide it to fix alignment. */
  display: none;
}
.bubble.ai-prose h3 {
  /* intentional: (0,2,1) beats .prose h3 (0,1,1); margin-top:0 overrides 1.4em so h3 starts flush.
     align-items:flex-start overrides center so ::before stays at first-line level when h3 wraps. */
  margin-top: 0;
  margin-bottom: 0.6em;
  align-items: flex-start;
}
.bubble.ai-prose h3::before {
  /* intentional: margin-top centers the 2px dash within the first line when align-items is flex-start.
     h3 inherits line-height:1.75 from .ai-prose → line box = 18px × 1.75 = 31.5px → center at 15.75px.
     margin-top:15px puts ::before center at y=16px, matching avatar center (margin-top:2px + 14px = 16px).
     With bubble padding-top:0, h3 top is flush with .msg top, so this aligns perfectly. */
  margin-top: 15px;
}

/* .article-placeholder removed — #article-body now uses .analysis-empty (see EmptyStates refactor) */

/* ── Paywall card ── */
.paywall-card {
  background: var(--status-partial-bg);
  border: 1.5px solid var(--status-partial);
  border-radius: var(--radius-lg);
  padding: 28px 24px;
  max-width: 460px;
  margin: 32px auto;
}
.paywall-icon { font-size: var(--text-h1); text-align: center; margin-bottom: 10px; }
.paywall-card h3 {
  font-family: var(--font-jp-sans);
  font-size: var(--text-body);
  font-weight: 700;
  color: var(--text-primary);
  text-align: center;
  margin-bottom: 12px;
  letter-spacing: 0.04em;
}
.paywall-card > p {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.75;
  margin-bottom: 14px;
}
.paywall-steps {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.9;
  padding-left: 18px;
  margin-bottom: 18px;
}
.paywall-steps li { margin-bottom: 2px; }
.paywall-steps kbd {
  background: var(--bg-subtle);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-misc-1);
  padding: 1px 6px;
  font-size: var(--text-xs);
  font-family: var(--font-en-mono);
}
.paywall-cookie-input {
  width: 100%;
  border: 1.5px solid var(--status-partial);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  font-size: var(--text-sm);
  font-family: var(--font-en-mono);
  resize: vertical;
  min-height: 72px;
  max-height: 140px;
  background: var(--bg-surface);
  color: var(--text-primary);
  outline: none;
  margin-bottom: 12px;
  transition: border-color var(--duration-base);
}
.paywall-cookie-input:focus { border-color: var(--status-partial); }
.paywall-cookie-input::placeholder { color: var(--text-muted); font-family: inherit; }
.paywall-actions {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}
.paywall-save-btn {
  background: var(--status-partial);
  color: var(--accent-ink);
  border: none;
  border-radius: var(--radius-sm);
  padding: 9px 18px;
  font-size: var(--text-sm);
  font-weight: 600;
  font-family: var(--font-jp-sans);
  cursor: pointer;
  transition: background var(--duration-base);
  white-space: nowrap;
}
.paywall-save-btn:hover { background: var(--color-context-btn-hover); }
.paywall-save-btn:disabled { opacity: 0.45; cursor: default; }
.paywall-domain-tag {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  background: var(--bg-subtle);
  border-radius: var(--radius-pill);
  padding: 4px 12px;
  border: 1px solid var(--border-default);
}
.paywall-hint {
  margin-top: 12px;
  font-size: var(--text-xs);
  color: var(--status-partial);
  line-height: 1.6;
}
.paywall-step-block {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  margin-bottom: 16px;
}
.paywall-step-num {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--status-partial);
  color: var(--accent-ink);
  font-size: var(--text-sm);
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 2px;
}
.paywall-step-body {
  flex: 1;
  font-size: var(--text-sm);
  line-height: 1.75;
  color: var(--text-secondary);
}
.paywall-step-body strong { color: var(--text-primary); }
.paywall-cmd-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 8px 0 4px;
  background: var(--bg-subtle);
  border-radius: var(--radius-sm);
  padding: 8px 12px;
}
.paywall-cmd-row code {
  flex: 1;
  font-family: var(--font-en-mono);
  font-size: var(--text-sm);
  color: var(--color-misc-19);
  background: none;
  border: none;
  padding: 0;
  user-select: all;
}
.paywall-copy-btn {
  flex-shrink: 0;
  background: var(--status-partial);
  color: var(--accent-ink);
  border: none;
  border-radius: var(--radius-misc-1);
  padding: 4px 10px;
  font-size: var(--text-xs);
  font-family: var(--font-jp-sans);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--duration-fast);
}
.paywall-copy-btn:hover { background: var(--color-context-btn-hover); }
.paywall-visual-hint { margin: 10px 0 4px; }
.paywall-visual-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
}
.paywall-tag {
  border-radius: var(--radius-sm);
  padding: 2px 8px;
  font-size: var(--text-xs);
  font-weight: 600;
  white-space: nowrap;
}
.tag-blue   { background: var(--tag-blue-bg);   color: var(--tag-blue-text); }
.tag-green  { background: var(--tag-green-bg);  color: var(--tag-green-text); }
.tag-orange { background: var(--tag-orange-bg); color: var(--tag-orange-text); }
.tag-red    { background: var(--accent-soft); color: var(--accent); }
.paywall-arrow { color: var(--status-partial); font-size: var(--text-sm); }
.paywall-icon-inline {
  width: 14px;
  height: 14px;
  vertical-align: -2px;
  display: inline-block;
  color: var(--text-secondary);
}
.paywall-ext-link {
  color: var(--accent);
  text-decoration: underline;
  font-size: var(--text-sm);
  display: inline-block;
  margin-top: 4px;
}
.paywall-ext-link:hover { color: var(--accent-hover); }
.paywall-manual-section {
  margin-top: 18px;
  border-top: 1px solid var(--border-subtle);
  padding-top: 14px;
}
.paywall-manual-toggle {
  font-size: var(--text-sm);
  color: var(--text-muted);
  cursor: pointer;
  user-select: none;
  list-style: none;
}
.paywall-manual-toggle::-webkit-details-marker { display: none; }
.paywall-manual-toggle::before { content: "▶ "; font-size: var(--text-xs); }
details[open] .paywall-manual-toggle::before { content: "▼ "; }
.paywall-manual-body { margin-top: 12px; }
.paywall-manual-note {
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.7;
  margin-bottom: 10px;
}
.paywall-save-manual-btn {
  background: var(--bg-subtle);
  color: var(--text-secondary);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: 8px 16px;
  font-size: var(--text-sm);
  font-weight: 600;
  font-family: var(--font-jp-sans);
  cursor: pointer;
  transition: background var(--duration-base);
  white-space: nowrap;
}
.paywall-save-manual-btn:hover { background: var(--bg-elevated); }
.paywall-save-manual-btn:disabled { opacity: 0.45; cursor: default; }

/* Text selection tooltip */
.selection-tooltip {
  position: fixed;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 10px; /* hardcode: D-7 known gap (--radius-md app=10px vs design-system=12px) */
  padding: 4px;
  display: none;
  align-items: stretch;
  gap: 0;
  z-index: 100;
  box-shadow: var(--shadow-glass);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
}
.tooltip-btn {
  background: transparent;
  border: 0;
  padding: 7px 12px;
  /* intentional: tooltip-internal radius, between --radius-sm (6) and
     --radius-md (12); chosen to stay tighter than the tooltip container's
     own 10px. See Phase 1 D-6. */
  border-radius: 8px;
  font-family: var(--font-zh-sans); /* F-1: zh-sans replaces jp-sans */
  font-size: var(--text-sm);
  line-height: 1;
  color: var(--text-primary);
  font-weight: 400;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--duration-fast), color var(--duration-fast);
}
.tooltip-btn:hover { background: var(--bg-hover-overlay); }
.tooltip-btn--primary {
  color: var(--accent);
  font-weight: 500;
}
.tooltip-btn.active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
}
.tooltip-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
.tooltip-divider {
  width: 1px;
  background: var(--glass-border);
  align-self: stretch;
  margin: 4px 0;
  flex-shrink: 0;
}
/* 記憶訣竅按鈕：容器 class 控制（jp-only 沿用既有 JS 方式，word-only 用 CSS）*/
.selection-tooltip:not(.tooltip-word-only) .tooltip-btn[data-action="mnemonic"],
.selection-tooltip:not(.tooltip-word-only) .tooltip-word-only-divider {
  display: none;
}
/* 群組分隔線：AI 對話組 ↔ 快速查詢組，左右各 4px 留白（也borrow給成本標前的分隔線） */
.tooltip-divider.group-sep {
  margin: 4px 4px;
}
/* 工具列成本標（設計 SelectionTooltip.jsx：`padding: 0 9px 0 3px`，
   `CreditCost size={11.5}` —— 比一般成本標（size 12）小一階，因為擠在按鈕列裡）。
   拿不到費率時 `.credit-tag` 是空的 `<span>`，這裡的 padding 不會露出多餘留白
   （flex 容器裡空 inline-flex 元素不佔額外視覺空間）。 */
.tooltip-cost {
  display: inline-flex;
  align-items: center;
  padding: 0 9px 0 3px;
}
.tooltip-cost .credit-tag__n { font-size: 12.5px; }
.tooltip-cost .credit-tag__u { font-size: 11.5px; }
.tooltip-caret {
  position: absolute;
  left: 50%;
  bottom: -5px;
  transform: translateX(-50%) rotate(45deg);
  width: 8px;
  height: 8px;
  background: var(--glass-bg);
  border-right: 1px solid var(--glass-border);
  border-bottom: 1px solid var(--glass-border);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
}

/* Chinese-to-Japanese translate card */
.translate-card {
  position: fixed;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 10px; /* hardcode: D-7 */
  padding: 12px 16px;
  z-index: 6;
  box-shadow: var(--shadow-glass);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  display: none;
  flex-direction: column;
  gap: 4px;
  min-width: 200px;
  max-width: 320px;
}
.translate-card.visible { display: flex; }
.translate-card-row-1 {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.translate-card-eyebrow {
  flex: 1;
  margin: 0;
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
  letter-spacing: 0.04em;
  line-height: 1.4;
}
.translate-card-speak {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent-soft);
  border: 1px solid var(--accent-border);
  color: var(--accent);
  cursor: pointer;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 1px;
  transition: background var(--duration-fast);
}
.translate-card-speak:hover { background: var(--accent-soft-hover); }
.translate-card-speak svg { width: 13px; height: 13px; }
.translate-card-eyebrow-plain {
  margin: 0;
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
  letter-spacing: 0.04em;
  line-height: 1.4;
}
.translate-card-original {
  font-family: var(--font-zh-serif); /* F-2 */
  font-size: var(--text-sm);
  color: var(--text-muted);
  letter-spacing: 0.02em;
  line-height: 1.4;
  margin: 0 0 4px;
}
.translate-card-result {
  font-family: var(--font-jp-serif); /* F-2 */
  font-size: var(--text-body-lg);
  color: var(--text-primary);
  letter-spacing: 0.02em;
  line-height: 1.5;
  margin: 0;
}
.translate-card-reading {
  margin: 0;
  font-family: var(--font-jp-sans);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  letter-spacing: 0.04em;
  line-height: 1.4;
}
.translate-caret {
  position: absolute;
  left: 50%;
  bottom: -5px;
  transform: translateX(-50%) rotate(45deg);
  width: 8px;
  height: 8px;
  background: var(--glass-bg);
  border-right: 1px solid var(--glass-border);
  border-bottom: 1px solid var(--glass-border);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
}

/* Romaji result card */
.romaji-card {
  position: fixed;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  padding: 14px 16px;
  z-index: 110;
  box-shadow: var(--shadow-glass);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  display: none;
  flex-direction: column;
  gap: 6px;
  max-width: 360px;
}
.romaji-card.visible { display: flex; }
.romaji-card-row-1 {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.romaji-card-original {
  flex: 1;
  margin: 0;
  font-family: var(--font-jp-serif); /* F-4 */
  font-size: 18px; /* hardcode: intentional hero hierarchy vs translate-card 17px, see D-6 */
  font-weight: 500;
  color: var(--text-primary);
  line-height: 1.4;
}
.romaji-card-hiragana {
  margin: 0;
  font-family: var(--font-jp-sans);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  letter-spacing: 0.04em;
  line-height: 1.4;
}
.romaji-card-reading {
  margin: 0;
  font-family: var(--font-en-mono); /* F-3 */
  font-size: var(--text-xs);
  color: var(--text-muted);
  letter-spacing: 0.02em;
  line-height: 1.4;
}
.romaji-card-speak {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent-soft);
  border: 1px solid var(--accent-border);
  color: var(--accent);
  cursor: pointer;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 1px;
  transition: background var(--duration-fast);
}
.romaji-card-speak:hover { background: var(--accent-soft-hover); }
.romaji-card-speak svg { width: 13px; height: 13px; }
.romaji-caret {
  position: absolute;
  left: 50%;
  bottom: -5px;
  transform: translateX(-50%) rotate(45deg);
  width: 8px;
  height: 8px;
  background: var(--glass-bg);
  border-right: 1px solid var(--glass-border);
  border-bottom: 1px solid var(--glass-border);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
}

/* Analysis CTA (D-5 B: injected inside #analysis-panel by _injectAnalysisCta()) */
.analysis-cta {
  margin-top: 40px;
}
.analysis-cta__hr {
  border: none;
  border-top: 1px solid var(--border-default);
  margin: 0 0 20px;
}
.analysis-cta__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.analysis-cta__desc {
  flex: 1;
  margin: 0;
  font-family: var(--font-zh-serif);
  font-size: 14px; /* intentional: matches prototype, between --text-sm/base */
  color: var(--text-muted);
  line-height: 1.5;
  min-width: 180px;
  /* max-width: 380px omitted — prototype value assumes wider panel; app panel is narrower and
     clips the text to two lines. Re-evaluate when article-panel width is adjusted. */
}
.analysis-cta__action {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
.analysis-cta__status {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.analysis-cta-btn {
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid transparent;
  border-radius: 12px; /* intentional: matches AnalysisFooterCta prototype, between --radius-sm/md */
  padding: 0 22px;
  height: 44px;
  font-size: 14px; /* intentional: prototype hardcode, between --text-sm/base */
  font-weight: 400;
  font-family: var(--font-zh-sans);
  cursor: pointer;
  transition: background var(--duration-base), color var(--duration-base), border-color var(--duration-base);
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
.analysis-cta-btn:hover { background: var(--accent-hover); }
.analysis-cta-btn:disabled { opacity: 0.45; cursor: default; }
.analysis-cta-btn.is-done,
.analysis-cta-btn.is-imported {
  background: var(--bg-subtle);
  color: var(--text-primary);
  border-color: var(--border-default);
}
.analysis-cta-btn.is-done:hover,
.analysis-cta-btn.is-imported:hover { background: var(--bg-surface); }
.analysis-cta-btn svg { width: 14px; height: 14px; }

/* Article sources chip bar ─────────────────────────────────────────────────── */

/* Base: hidden when no mode class (non-article tabs) */
.sources-bar {
  position: relative;
  background: var(--bg-subtle);
  border-bottom: none;
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  flex-shrink: 0;
  transition: max-height var(--duration-entry) var(--ease-out), opacity var(--duration-fast) ease, border-color var(--duration-entry);
}

/* ── Collapsed mode ── */
.sources-bar.mode-collapsed {
  max-height: 0;
  opacity: 0;
  border-bottom: none;
  overflow: hidden;
}
.sources-bar.mode-collapsed.has-sources {
  max-height: 200px;
  opacity: 1;
  border-bottom: 1px solid var(--border-subtle);
}
.sources-bar.mode-collapsed.peek-open { overflow: visible; }

/* ── Full mode ── always visible regardless of has-sources */
.sources-bar.mode-full.has-sources {
  max-height: 200px;
  opacity: 1;
  border-bottom: 1px solid var(--border-subtle);
}

/* ── Mode visibility toggles ── */
.sources-bar.mode-full .sources-strip-inner { display: none; }
.sources-bar.mode-full .sources-peek { display: none; }
.sources-bar.mode-full .sources-full-inner { display: block; }
.sources-bar.mode-collapsed .sources-full-inner { display: none; }
.sources-bar.mode-collapsed .sources-strip-inner { display: block; }

/* ── Collapsed: strip inner ── */
.sources-strip-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0 var(--space-6);
}
.sources-strip {
  width: 100%;
  height: 36px;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: default;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-family: inherit;
  text-align: left;
  color: inherit;
}
.sources-bar.mode-collapsed.has-sources .sources-strip { cursor: pointer; }

/* ── Full mode: inner layout ── */
.sources-full-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 12px var(--space-6);
}
.sources-full-row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 30px;
}

/* ── Empty hint (full mode, chips=0) ── */
.sources-empty-hint {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
}
.sources-empty-hairline {
  width: 14px;
  height: 1px;
  background: var(--border-default);
  flex: 0 0 14px;
}
.sources-empty-text {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.5;
}

/* ── Full mode chips scroll row ── */
.sources-full-chips {
  flex: 1;
  display: flex;
  gap: 8px;
  overflow-x: hidden;
  flex-wrap: nowrap;
  scrollbar-width: none;
  min-width: 0;
}
.sources-full-chips::-webkit-scrollbar { display: none; }
/* Expanded: chips wrap to second row */
.sources-bar.full-expanded .sources-full-chips {
  flex-wrap: wrap;
  overflow-x: visible;
  row-gap: 8px;
}

/* ── +N overflow indicator ── */
.sources-overflow-indicator {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  color: var(--text-muted);
  align-self: center;
  flex: 0 0 auto;
  white-space: nowrap;
}

/* ── Full mode expand toggle ── */
.sources-full-expand {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-misc-3);
  background: transparent;
  border: 1px solid var(--border-subtle);
  color: var(--text-secondary);
  cursor: pointer;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.sources-full-expand:hover {
  border-color: var(--border-default);
  color: var(--text-primary);
}
.sources-full-expand svg { transition: transform var(--duration-base) var(--ease-out); }
.sources-bar.full-expanded .sources-full-expand svg { transform: rotate(180deg); }

/* ── Shared: label + count (both modes) ── */
.sources-label {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
  flex: 0 0 auto;
}
.sources-bar.has-sources .sources-label { color: var(--text-secondary); }
.sources-count {
  font-size: var(--text-xs);
  font-weight: 500;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  white-space: nowrap;
  background: transparent;
  flex: 0 0 auto;
}
/* Collapsed count: padding 1px 7px */
.sources-strip-inner .sources-count { padding: 1px 7px; }
/* Full mode count: padding 2px 8px */
.sources-full-row .sources-count { padding: 2px 8px; }
.sources-count-num {
  font-family: var(--font-en-display);
  font-feature-settings: "lnum" 1, "tnum" 1;
  color: var(--text-muted);
}
.sources-count-unit {
  font-family: var(--font-zh-sans);
  margin-left: 2px;
  color: var(--text-muted);
}
.sources-bar.has-sources .sources-count { border-color: var(--accent); }
.sources-bar.has-sources .sources-count-num { color: var(--accent); }
.sources-bar.has-sources .sources-count-unit { color: var(--accent); }

/* ── Collapsed: summary + expand hint ── */
.sources-summary {
    /* 內容是日文文章摘要 —— 字體決定 、。 的位置（實測：中文字體置中 0.49、日文字體靠下 0.87）。
     ⚠️ 這裡與 .qb-row__title 的方向**相反**，不可一律換成同一種。2026-08-07 */
  font-family: var(--font-jp-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
}
.sources-expand-hint {
  margin-left: auto;
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  white-space: nowrap;
}
.sources-expand-chevron { transition: transform var(--duration-base) var(--ease-out); }
.sources-strip[aria-expanded="true"] .sources-expand-chevron { transform: rotate(180deg); }

/* ── Peek popover (collapsed mode) ── */
.sources-peek {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: var(--bg-subtle);
  border-bottom: 1px solid var(--border-subtle);
  box-shadow: var(--shadow-card);
  z-index: 20;
}
.sources-peek.open { display: block; }
.sources-peek-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--space-3) var(--space-6);
}
.sources-peek-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ── Chip ── */
.source-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 5px 6px 5px 12px;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  max-width: 260px;
  transition: border-color var(--duration-fast);
  cursor: pointer;
}
.source-chip--entering { animation: chipEnter var(--duration-entry) var(--ease-out) both; }
@keyframes chipEnter {
  from { clip-path: inset(0 100% 0 0 round 999px); }
  to   { clip-path: inset(0 0% 0 0 round 999px); }
}
.source-chip:hover { border-color: var(--border-default); }
.source-chip--active,
.source-chip--active:hover { border-color: var(--accent); }
.source-chip--active .source-chip-star { color: var(--accent); }
.source-chip-star { color: var(--text-muted); flex: 0 0 auto; }
.source-chip-body {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}
.source-chip-title {
    /* 內容是日文文章標題 —— 字體決定 、。 的位置（實測：中文字體置中 0.49、日文字體靠下 0.87）。
     ⚠️ 這裡與 .qb-row__title 的方向**相反**，不可一律換成同一種。2026-08-07 */
  font-family: var(--font-jp-sans);
  font-size: var(--text-sm);
  color: var(--text-primary);
  max-width: 150px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
.source-chip-date {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  color: var(--text-muted);
  flex: 0 0 auto;
  font-feature-settings: "lnum" 1, "tnum" 1;
}
.source-chip-del {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: transparent;
  border: 0;
  cursor: pointer;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  padding: 0;
  transition: all var(--duration-fast) var(--ease-out);
}
.source-chip-del:hover {
  background: var(--bg-base);
  color: var(--text-primary);
}

/* Article chat panel */
.article-chat-panel {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: 16px; /* intentional: D-1A prototype value */
  box-shadow: var(--shadow-card);
  position: sticky;   /* 也是 .coach-drop-overlay(inset:0) 的 containing block；勿改成 relative（見 §拖放覆蓋層註解） */
  top: 80px;          /* sticky 吸附偏移；position 一旦被打成 relative，這裡會變成向下位移 80px → 面板偏下 */
  height: calc(100vh - 112px);
  min-height: 640px;
}

/* ── Selection context bar ── */
.selection-context-bar {
  display: none;
  margin-bottom: 8px;
  /* glass: intentional D-1A —吃 .chat-input-area 的 18px horizontal padding */
  border-radius: var(--radius-md); /* intentional: 12px, matches prototype */
  padding: 10px 12px 10px 14px;   /* intentional: D-1A, left extra 2px for eyebrow balance */
  align-items: center;
  gap: 10px;
}
.selection-context-bar.active { display: flex; }
.sel-context-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-en-display);
  font-size: 10.5px; /* intentional D-1A: prototype value, tighter than --text-xs 11px */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 500;
  flex: 0 0 auto;
}
.sel-context-eyebrow svg { flex-shrink: 0; }
.sel-context-text {
  flex: 1;
  font-family: var(--font-jp-serif);
  font-size: 13.5px; /* intentional D-1A: prototype value, jp-serif body */
  color: var(--text-primary);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3; /* intentional: max 3-line clamp per spec */
  -webkit-box-orient: vertical;
}
.sel-clear-btn {
  width: 22px; height: 22px;       /* intentional D-1A: prototype value */
  border-radius: 50%;
  background: transparent; border: none;
  color: var(--text-muted); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  flex: 0 0 auto; padding: 0;
  transition: color var(--duration-fast);
}
.sel-clear-btn:hover { color: var(--text-primary); }
/* Remove composer border-top when context bar is visible — intentional D-3A */
.chat-input-area.has-context { border-top: 0; }

/* ── Image upload button (D-7A: transparent + no border, flush inside composer-wrapper) ── */
.img-upload-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px; /* intentional: enlarged from prototype 34px for usability */
  height: 40px;
  flex-shrink: 0;
  background: transparent;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  color: var(--text-muted);
  transition: color var(--duration-fast);
}
.img-upload-btn:hover { color: var(--text-secondary); }

/* ── Coach 截圖附件（O-2/O-3，數值照設計交付 §三）── */

/* 待送出附件列（composer 上方、ModeSwitcher 下方）。多圖 flex-wrap。 */
.pending-strip { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 8px; }
.pending-thumb { position: relative; flex: 0 0 auto; }
.pending-thumb img {
  height: 56px; width: auto; max-width: 120px;
  object-fit: cover; display: block;
  border-radius: 10px; border: 1px solid var(--border-default);
}
.pending-thumb__x {
  position: absolute; top: -6px; right: -6px;
  width: 18px; height: 18px; border-radius: 9999px;
  background: var(--bg-base); border: 1px solid var(--border-default);
  color: var(--text-secondary); cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.pending-thumb__x:hover { color: var(--text-primary); }

/* 訊息內縮圖（user bubble）。有圖時 bubble padding 由 .has-image 調整。 */
.msg.user .bubble.has-image { padding: 10px 10px 12px; }
.msg-image-row { display: flex; gap: 8px; flex-wrap: wrap; }
.msg-image-row + .msg-text { padding: 0 6px; margin-top: 10px; }
.msg-image {
  max-width: 220px; max-height: 150px; width: auto; height: auto;
  display: block; border-radius: 10px;
  border: 1px solid var(--border-default); cursor: zoom-in;
}

/* 拖放覆蓋層（印章式）。玻璃屬 modal scrim 類，允許清單內。 */
.coach-drop-overlay {
  position: absolute; inset: 0; z-index: 30;
  display: none; flex-direction: column; align-items: center; justify-content: center;
  gap: 16px; pointer-events: none;
  background: var(--glass-bg);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border-radius: 16px;
}
.chat-panel.is-dragover .coach-drop-overlay,
.article-chat-panel.is-dragover .coach-drop-overlay { display: flex; animation: ctxExpand 160ms ease-out both; }
.coach-drop-stamp {
  width: 72px; height: 72px; border-radius: 14px;
  border: 2.5px solid var(--accent); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  transform: rotate(-4deg);
  box-shadow: 0 0 0 4px color-mix(in oklch, var(--accent) 10%, transparent);
  animation: stampDrop 220ms cubic-bezier(0.2, 0, 0, 1) both;
}
.coach-drop-main { font-family: var(--font-zh-sans); font-size: 14px; font-weight: 500; letter-spacing: 0.03em; color: var(--text-primary); }
.coach-drop-sub  { font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-muted); margin-top: -8px; }
@keyframes stampDrop {
  from { transform: scale(1.25) rotate(-4deg); opacity: 0; }
  to   { transform: scale(1) rotate(0); opacity: 1; }
}
/* 拖放覆蓋層（.coach-drop-overlay，position:absolute; inset:0）的 containing block
   由面板的 `position: sticky` 提供（sticky 是定位值，一樣建立 containing block）——
   **不要**在此另加 `position: relative`：它會蓋掉 sticky，殘留的 top:80px 會變成向下位移
   80px，整個面板偏下。規格 §三.3「全面板」的覆蓋範圍靠 sticky 就成立。 */

/* Lightbox（點縮圖放大）。scrim = 允許的玻璃面。 */
.img-lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: none; align-items: center; justify-content: center;
  background: rgba(12, 10, 8, 0.62);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  cursor: zoom-out;
}
.img-lightbox.open { display: flex; animation: ctxExpand 200ms var(--ease-entry, ease-out) both; }
.img-lightbox img {
  max-width: 86%; max-height: 86%;
  border-radius: 12px; border: 1px solid var(--glass-border); box-shadow: var(--shadow-glass);
}
.img-lightbox__close {
  position: absolute; top: 22px; right: 26px;
  width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid var(--border-default); background: var(--bg-elevated);
  color: var(--text-secondary); cursor: pointer; font-size: 22px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.img-lightbox__close:hover { color: var(--text-primary); }

/* 超量 / 非圖檔提示（CTA 回饋，不靜默截斷） */
.toast {
  position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%) translateY(8px);
  z-index: 210; padding: 9px 16px; border-radius: var(--radius-md);
  background: var(--bg-elevated); border: 1px solid var(--border-default);
  color: var(--text-primary); font-family: var(--font-zh-sans); font-size: 13px;
  box-shadow: var(--shadow-card); opacity: 0; pointer-events: none;
  transition: opacity var(--duration-base), transform var(--duration-base);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* 復原鈕：toast 本體 pointer-events:none，子鈕設 auto 才收得到點擊（子可覆寫父）。 */
.toast { display: flex; align-items: center; gap: 14px; }
.toast__action {
  pointer-events: auto; background: none; border: none; padding: 0;
  color: var(--accent); font-family: var(--font-zh-sans); font-size: 13px;
  font-weight: 600; cursor: pointer; white-space: nowrap;
}
.toast__action:hover { text-decoration: underline; }

/* ── Article Body Two-Tab Layout ── */
.article-body--tabbed {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}
.article-body-tabs {
  display: flex;
  border-bottom: 2px solid var(--border-default);
  background: var(--bg-surface);
  flex-shrink: 0;
}
.article-body-tab {
  padding: 10px 20px;
  font-size: var(--text-sm);
  font-weight: 600;
  font-family: var(--font-jp-sans);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-secondary);
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: color var(--duration-fast), border-color var(--duration-fast);
  letter-spacing: 0.02em;
}
.article-body-tab:hover { color: var(--text-primary); }
.article-body-tab.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.article-body-panel {
  display: none;
  flex: 1;
  overflow-y: auto;
  padding: 22px 26px;
}
.article-body-panel.active { display: flex; flex-direction: column; }
#analysis-panel { padding: 36px 44px; }

/* ── Comprehension Card — isolated namespace (.comp-card) ── */
.comp-card {
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: 22px 24px 20px; /* intentional: matches exercise-card structural value */
  box-shadow: var(--shadow-card);
  position: relative;
  transition: background 240ms cubic-bezier(0.2,0,0,1), border-color 120ms var(--ease-out);
}
.comp-card:hover { border-color: var(--border-default); }
.comp-card.correct { background: var(--status-correct-bg); }
.comp-card.partial { background: var(--status-partial-bg); }
.comp-card.wrong   { background: var(--status-wrong-bg); }

/* Left status bar — absolute 4px, mirrors .exercise-card-bar */
.comp-card-bar {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 4px;
  background: transparent;
  border-top-left-radius: var(--radius-lg);
  border-bottom-left-radius: var(--radius-lg);
  pointer-events: none;
  transition: background 240ms cubic-bezier(0.2,0,0,1);
}
.comp-card.correct .comp-card-bar { background: var(--status-correct); }
.comp-card.partial .comp-card-bar { background: var(--status-partial); }
.comp-card.wrong   .comp-card-bar { background: var(--status-wrong); }

/* Status pill colour — scoped to comp-card states */
.comp-card.correct .card-status-pill { color: var(--status-correct); }
.comp-card.partial .card-status-pill { color: var(--status-partial); }
.comp-card.wrong   .card-status-pill { color: var(--status-wrong); }

/* Prompt text */
.comp-prompt {
  font-family: var(--font-zh-serif);
  /* intentional: 17px zh-serif prompt, matches prototype ComprehensionList D-prompt spec */
  font-size: 17px;
  line-height: 1.65;
  color: var(--text-primary);
  margin: 0 0 14px;
  letter-spacing: 0.005em;
  text-wrap: pretty;
}

/* Textarea + 批改 row */
.comp-input-row {
  display: flex;
  gap: 10px;
  align-items: flex-end;
}

/* Textarea — D-5: override global .answer-input via .comp-answer-input modifier */
.comp-answer-input {
  resize: vertical;
  min-height: 64px;
  max-height: 160px;
  font-family: var(--font-zh-serif);
  padding: 10px 14px;
  border-radius: var(--radius-md);
}
.comp-answer-input:disabled { background: transparent; opacity: 0.85; cursor: default; }

/* Pre-grade answer reveal panel — neutral colours (not amber) */
.comp-reveal-panel {
  margin-top: 14px;
  padding: 14px 16px;
  background: var(--bg-subtle);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  animation: compRevealIn 220ms var(--ease-entry) both;
}
.comp-reveal-panel[hidden] { display: none; }

/* Graded body */
.comp-graded-body {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--border-subtle);
}
.comp-graded-body[hidden] { display: none; }

/* Shared label above reference block */
.comp-reveal-label {
  display: block;
  font-family: var(--font-zh-sans);
  /* intentional: 11.5px compact label, pre-grade panel; 12px graded body — use same class, close enough */
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  color: var(--text-muted);
  font-weight: 500;
  margin: 0 0 8px;
}

/* ReferenceBlock — jp-serif + zh-serif two-line structure (D-4) */
.comp-ref-jp {
  font-family: var(--font-jp-serif);
  /* intentional: 16px jp-serif reference quote, prototype D-4 ReferenceBlock spec */
  font-size: 16px;
  line-height: 1.65;
  color: var(--text-primary);
  margin: 0 0 6px;
  letter-spacing: 0.02em;
}
.comp-ref-zh {
  font-family: var(--font-zh-serif);
  /* intentional: 13.5px inter-language bridge value between --text-sm and --text-body, prototype D-4 */
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-muted);
  margin: 0;
}

/* Feedback text — D-3: renamed from .feedback-box, no status bg */
.comp-feedback {
  font-family: var(--font-zh-serif);
  font-size: var(--text-body);
  line-height: 1.75;
  color: var(--text-secondary);
  margin: 14px 0 0;
  text-wrap: pretty;
}

/* compRevealIn animation */
@keyframes compRevealIn {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── Comprehension exercise list ── */
.comprehension-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* intentional: panel has padding 22px 26px; +4px aligns top breathing space with left/right */
  padding-top: 4px;
}

/* ── QA Note footer + button (ghost, low-weight) — per reply-actions-spec v1.0 ── */
.qa-note-footer {
  display: flex;
  gap: 6px;
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px dashed var(--border-subtle);
}
.qa-note-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  background: transparent;
  border: 0;
  border-radius: 6px;
  color: var(--text-secondary);
  font-family: var(--font-zh-sans);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  transition: all var(--duration-fast);
}
.qa-note-btn:hover { background: var(--bg-subtle); }
.qa-note-btn.saved { color: var(--text-muted); cursor: default; }

/* ── 記憶訣竅 角度切換 chips ── */
.mnemonic-chips {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border-subtle);
}
.mnemonic-chips__label {
  font-family: var(--font-zh-sans);
  font-size: 11.5px;
  color: var(--text-muted);
  flex-shrink: 0;
}
.chip {
  background: transparent;
  border: 1px solid var(--border-default);
  border-radius: 999px;
  padding: 3px 11px;
  font-family: var(--font-zh-sans);
  font-size: 12px;
  color: var(--text-secondary);
  cursor: pointer;
  transition: border-color var(--duration-fast), color var(--duration-fast), background var(--duration-fast);
  line-height: 1.5;
}
.chip:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-soft);
}

/* ══════════════════════════════════════════════════════════════
   Phase 2 Stage 2 — EmptyStates
   AnalysisEmpty / AnalysisStreaming / ChatEmpty + SiteCard
   ══════════════════════════════════════════════════════════════ */

/* ── Keyframes ── */
@keyframes pulseDot {
  0%, 100% { transform: scale(1);   opacity: 1;   }
  50%      { transform: scale(1.5); opacity: 0.4; }
}
@keyframes caretBlink {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}
@keyframes streamFadeIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes siteCardIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── AnalysisEmpty ── */
.analysis-empty {
  min-height: 720px;
  display: flex;
  flex-direction: column;
}
.analysis-empty__content {
  flex: 1;
  /* intentional: top/bottom padding 88px/96px — no token in scale for 88px;
     96px = var(--space-9). Bottom uses token; top is intentional structural value.
     See EmptyStates audit Section 5-A. */
  padding: 88px var(--space-8) 96px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
}
.analysis-empty__eyebrow {
  display: flex;
  align-items: center;
  /* intentional: gap 14px and marginBottom 28px — no tokens; structural proportional values.
     See EmptyStates audit Section 5-A. */
  gap: 14px;
  margin-bottom: 28px;
}
.analysis-empty__eyebrow-label {
  font-family: var(--font-zh-sans);
  /* intentional: 12.5px — between --text-xs (11) and --text-sm (13); deliberate lightweight
     editorial label size. See EmptyStates D-4 and audit Section 5-A. */
  font-size: 12.5px;
  /* intentional: letterSpacing 0.32em — editorial wide-tracking for eyebrow. No token. */
  letter-spacing: 0.32em;
  color: var(--accent);
  font-weight: 500;
}
.analysis-empty__eyebrow-line {
  flex: 1;
  height: 1px;
  /* intentional: width 80px and height 1px — decorative hairline, not a structural token. */
  width: 80px;
  background: var(--border-default);
}
.analysis-empty__h1 {
  font-family: var(--font-jp-serif);
  /* intentional: jp-serif applied to ZH content for editorial tone;
     matches prototype landing card design. See EmptyStates D-4-F1. */
  font-size: var(--text-h1);
  /* intentional: lineHeight 1.45 — wider than --lh-h1 (1.25) for jp-serif breathing room.
     Matches prototype spec. See EmptyStates audit Section 5-A. */
  line-height: 1.45;
  font-weight: 500;
  color: var(--text-primary);
  /* intentional: margin-bottom 18px — structural value between --space-4 (16) and --space-5 (24). */
  margin: 0 0 18px;
  /* intentional: letterSpacing 0.005em — subtle jp-serif tracking, no token. */
  letter-spacing: 0.005em;
  max-width: 540px;
  text-wrap: pretty;
}
.analysis-empty__p {
  font-family: var(--font-zh-serif);
  /* intentional: fontSize 16px — prototype spec; nearest token is --text-body-lg (17px).
     Intentional 1px undercut for slightly lighter weight. See EmptyStates audit Section 5-A. */
  font-size: 16px;
  /* intentional: lineHeight 1.75 — close to --lh-body-lg (1.70) + 0.05. Prototype spec.
     See EmptyStates audit Section 5-A. */
  line-height: 1.75;
  color: var(--text-secondary);
  margin: 0;
  max-width: 460px;
  text-wrap: pretty;
}
.analysis-empty__hint {
  display: flex;
  align-items: center;
  gap: 10px;
  /* intentional: marginTop 56px — between --space-7 (48) and --space-8 (64). Structural. */
  margin-top: 56px;
  font-family: var(--font-zh-sans);
  /* intentional: 12.5px — same editorial small size as eyebrow. See EmptyStates audit Section 5-A. */
  font-size: 12.5px;
  color: var(--text-muted);
}
.analysis-empty__arrow {
  /* intentional: ArrowUp inline SVG — one-shot decorative, not promoting to Primitives.jsx.
     See EmptyStates D-3 and audit Section 6. */
  color: var(--accent);
  flex-shrink: 0;
}

/* ── ComprehensionEmpty (initial state of comprehension-panel during article streaming) ── */
.comp-empty {
  display: flex;
  flex-direction: column;
  flex: 1;
}
.comp-empty__content {
  flex: 1;
  padding: 88px var(--space-8) 96px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
}
.comp-empty__eyebrow {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 28px;
}
.comp-empty__eyebrow-label {
  font-family: var(--font-zh-sans);
  /* intentional: 12.5px — same editorial eyebrow size as AnalysisEmpty. */
  font-size: 12.5px;
  letter-spacing: 0.32em;
  color: var(--accent);
  font-weight: 500;
}
.comp-empty__eyebrow-line {
  flex: 1;
  height: 1px;
  width: 80px;
  background: var(--border-default);
}
.comp-empty__h1 {
  font-family: var(--font-jp-serif);
  font-size: var(--text-h1);
  line-height: 1.45;
  font-weight: 500;
  color: var(--text-primary);
  margin: 0 0 18px;
  letter-spacing: 0.005em;
  max-width: 540px;
  text-wrap: pretty;
}
.comp-empty__p {
  font-family: var(--font-zh-serif);
  font-size: 16px;
  line-height: 1.75;
  color: var(--text-secondary);
  margin: 0;
  max-width: 460px;
  text-wrap: pretty;
}

/* ── ArticleNoRecord (S1: URL entered, no cached analysis) ── */
.article-no-record {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  text-align: center;
  padding: 32px 24px;
}
.article-no-record__icon {
  color: var(--border-default);
  flex-shrink: 0;
}
.article-no-record__title {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  margin: 0;
  letter-spacing: 0.02em;
}
.article-no-record__hint {
  font-family: var(--font-zh-sans);
  /* intentional: 12.5px — matches editorial small size used across empty states. */
  font-size: 12.5px;
  color: var(--text-muted);
  margin: 0;
  line-height: 1.65;
}
.article-no-record__hint strong {
  color: var(--text-secondary);
  font-weight: 500;
}

/* ── ArticleErrState (system errors in article-body, comprehension-panel, exercises-list) ── */
.article-err-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* 靠上錨定（非置中）：三情境（文章解析 / 理解題 / 每日練習）共用同一定位。
     center 依容器總高浮動；flex-start + 上邊距從頂端固定錨定，各情境一致、對高度差異穩健。
     padding-top 16vh → 內容落在容器上方約 16%（偏上）。 */
  justify-content: flex-start;
  gap: 12px;
  text-align: center;
  padding: 16vh 24px 32px;
}
.article-err-state__icon {
  color: var(--status-wrong);
  opacity: 0.7;
  flex-shrink: 0;
}
.article-err-state__title {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  margin: 0;
}
.article-err-state__hint {
  font-family: var(--font-zh-sans);
  /* intentional: 12.5px — matches editorial small size used across empty states. */
  font-size: 12.5px;
  color: var(--text-muted);
  margin: 0;
  line-height: 1.65;
}
/* 錯誤畫面的重試鈕（套共用 btn-secondary）：icon 與字間距 + 與提示的上距。 */
.err-retry-btn { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px; }
.err-retry-btn__icon { flex-shrink: 0; }

/* ── AnalysisStreaming (overlay inside #analysis-panel) ── */
/* intentional: .analysis-streaming as an overlay inside the analysis panel;
   the outer card chrome (bg-surface, border, radius-lg) lives on the article-body--tabbed
   container and will be addressed in Stage 2 Analysis 基底 round. See EmptyStates audit 3-B. */
.streaming-header {
  display: flex;
  align-items: center;
  gap: 12px;
  /* intentional: padding 18px 36px — structural proportional value; no exact token match. */
  padding: 18px 36px;
  border-bottom: 1px solid var(--border-subtle);
}
.streaming-header__label {
  font-family: var(--font-zh-sans);
  /* intentional: 12.5px — matches AnalysisEmpty eyebrow small editorial size. */
  font-size: 12.5px;
  /* intentional: letterSpacing 0.08em — subtle tracking for accent label. */
  letter-spacing: 0.08em;
  color: var(--accent);
  font-weight: 500;
}
.streaming-header__status {
  font-family: var(--font-zh-sans);
  font-size: 12.5px;
  color: var(--text-muted);
}
.analysis-streaming__chunks {
  /* intentional: padding 32px 44px 40px — structural proportional value; top = var(--space-6). */
  padding: 32px 44px 40px;
}
/* When article-analysis-header is injected, streaming-header is removed;
   header's margin-bottom owns the gap — cancel the duplicate top padding. */
.article-analysis-header + .analysis-streaming__chunks {
  padding-top: 0;
}

/* ── Analysis Header (injected by _injectAnalysisHeader) ── */
.article-analysis-header {
  margin-bottom: 0;
}
.article-analysis-header h1 {
  font-family: var(--font-jp-serif);
  font-size: 26px; /* intentional: editorial jp-serif article title; distinct from --text-h1 (32px). See Analysis 基底 D-1. */
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.005em;
  color: var(--text-primary);
  margin: 0 0 12px;
}
.article-title-hash {
  color: var(--accent);
  margin-right: 10px;
}
/* meta row 全列同一字級 var(--text-sm)。先前這裡是 12.5px 的 intentional literal
   （「介於 --text-xs 11 與 --text-sm 13 之間」），domain 又另外用 12px、icon 有 13 與 12
   兩種 —— 一行裡四種尺寸。統一為 13 之後正好落在既有 token 上，四個 literal 全部退場，
   回到專案規則「禁止 px font-size，一律用 token」。 */
.analysis-meta-row {
  display: flex;
  align-items: center;
  gap: 14px; /* intentional: prototype gap 14, between --space-3 (12) and --space-4 (16) */
  flex-wrap: wrap;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
/* 「文章等級 [N4]」——名詞與徽章是一組，必須有自己的容器。
   ⚠️ 上面的 `.analysis-meta-row` 是 `display:flex; gap:14px`，**gap 會套用在所有子項之間**：
      名詞與徽章若各自當一個子項，中間會被撐開 14px（本來只需要 6px）。
      這與 T-28（標題被 flex gap 撐開）是同一個坑 —— 往有 gap 的 flex 容器加東西，
      就要問「這幾個東西是不是一組」，是的話包起來自己給間距。
   🔒 tests/test_article_level_badge.py::test_label_and_badge_have_their_own_gap */
.analysis-meta-level {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.meta-date { font-family: var(--font-en-display); }
.meta-dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--text-muted);
  display: inline-block;
  flex-shrink: 0;
}
/* ══ Meta action ══════════════════════════════════════════════════════════
   analysis meta row 裡每一個可點元素的**唯一**樣式來源（重新生成 / 生成朗讀音檔 /
   前往文章來源，以及未來新增的任何一個）。

   **規則（新增按鈕時必須遵守，有測試把關）：**
     1. meta row 內任何 <button> / <a> 都要帶 .meta-action —— 語意 class
        （.analysis-regen-btn 等）只作辨識用，不承載樣式
     2. icon 一律 13px，且**禁止宣告 color** —— 繼承 currentColor 才會與文字連動
     3. 狀態差異只能加 .is-* 修飾詞，不得為單一按鈕另開一條完整規則

   為什麼要收斂：先前三個按鈕各自一條規則、11 條宣告一字不差地重複三遍。複製時多寫
   一條沒人會發現 —— 「生成朗讀音檔」的 icon 橘色（`svg { color: var(--accent) }`）
   就是這樣長出來的：靜止時 icon 是橘的、文字是灰的，同一顆按鈕兩個顏色。 */
.meta-action {
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--text-secondary);
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  text-decoration: none;   /* 「前往文章來源」是 <a>；<button> 上無作用 */
  transition: color var(--duration-base);
}
.meta-action:hover { color: var(--accent); }
/* 不宣告 color —— icon 必須恆等於文字色。宣告了就會脫鉤（見上方註解） */
.meta-action svg { flex-shrink: 0; }

/* login 態：重新生成是唯一出路，高亮成明確去向（handoff 05）。
   不需要 :hover 覆寫 —— base hover 已是 accent，與此處同色。 */
.meta-action.is-highlight {
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 3px 9px; /* intentional: prototype highlight padding */
  color: var(--accent);
}

/* 「前往文章來源」是 <a>，樣式全走 .meta-action —— domain 文字已移除，但 JS 的
   domain 變數仍保留（needs_login 態的 strip 要顯示「請先登入 <domain>」）。 */

/* ── 朗讀入口（meta row，僅 ungenerated 態）── */
/* slot 只是 JS 注入用的錨點，不該存在於佈局中。display:contents 讓它自身不生成 box，
   內部的 meta-dot 與按鈕直接成為 .analysis-meta-row 的 flex 子項 —— 這才是 prototype
   的結構（設計用 React Fragment 把 Dot 與 MetaAction 攤平成兄弟，不含包裹節點）。
   不這樣做的話：(1) 點與按鈕退回 inline 排版，只剩一個空白字元的間距、且靠基線對齊而非
   置中；(2) 入口不顯示時，空的 slot 仍佔一個 flex 子項，前後各吃一份 gap。 */
.analysis-meta-tts { display: contents; }
/* 「生成朗讀音檔」樣式全走 .meta-action。先前這裡另有一條
   `svg { color: var(--accent) }`，讓 icon 在靜止時是橘的、文字是灰的 —— 同一顆按鈕
   兩個顏色。已移除：icon 一律繼承 currentColor。 */

/* ── 朗讀播放器 strip（O-4）──
   「安靜的錄音筆」：accent 只出現在播放鈕填色與已播進度，全畫面紅覆蓋 < 5%。 */
.tts-strip {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  background: var(--bg-subtle);
  border: 1px solid var(--border-subtle);
  border-radius: 14px; /* intentional: prototype strip radius，介於 --radius-md/lg */
  padding: 13px 16px; /* intentional: prototype strip padding */
  /* 上 22px：prototype 的 metaRow→strip 間距。設計裡是靠 MetaRow 的 margin-bottom:22
     與 strip 的 margin-top:4 摺疊達成（max=22）；本專案 .analysis-meta-row 的
     margin-bottom 是 0，鄰居不供給，故由 strip 自帶。不可照抄 prototype 的 4px。
     下 0：下方緊接 .prose-lg，其 h2 自帶 1.6em(=35.2px) margin-top（.prose/.prose-sm/
     .prose-lg 共用的排版節奏，全站文章適用）。再給 margin-bottom 會疊加成 61px；
     此處交由既有 prose 節奏負責，不重複宣告、也不覆寫共用排版。 */
  margin: 22px 0 0;
}

/* 生成中 */
.tts-strip--generating {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  padding: var(--space-4) 18px; /* intentional: prototype */
}
.tts-gen__head { display: flex; align-items: center; gap: 10px; }
.tts-gen__dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--accent);
  flex: 0 0 auto;
}
.tts-gen__text {
  font-family: var(--font-zh-sans);
  font-size: 13.5px; /* intentional: prototype strip 內文 */
  color: var(--text-secondary);
  white-space: nowrap;
}
.tts-num { font-family: var(--font-en-mono); font-variant-numeric: tabular-nums; }
/* ══ 生成中：只畫拿得到的事實 ═══════════════════════════════════════════════
   真實可得的只有兩件事：**總共幾段**、**已完成幾段**。
   「這一段跑到哪」證實取不到（串流 TTFB = 總時長的 99.3%，Google 生完才吐）。

   ⚠️ 所以**畫面上不得有任何會長大的橫條或百分比**。舊版的 genFill（22 秒從 6% 爬到
   88%）就是用長度編造位置 —— 它 22 秒就爬滿，而一段實測要 95–167 秒，於是使用者
   看著「快好了」再乾等兩分鐘，信任直接歸零。整條已移除，不要復活。
   🔒 tests/test_tts_multi_segment_frontend.py

   段落刻度三種**離散**態，都沒有寬度動畫：
     已完成＝實心 accent（事實）／進行中＝空軌 + 循環 sweep／未開始＝空軌。 */
.tts-gen__tracks { display: flex; gap: 3px; } /* intentional: prototype 細軌間距 */
/* ⚠️ **有明確 display 就一定要補這條。** `[hidden]` 靠的是 UA stylesheet 的
   `display:none`，author 端任何 `display` 都會蓋掉它 —— 單段時這個容器仍佔位，
   column gap 照樣算進去，文字就被擠得偏上（2026-08-12 使用者實測回報：上 17px / 下 29px）。 */
.tts-gen__tracks[hidden] { display: none; }
.tts-gen__slot {
  position: relative;
  flex: 1;
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--border-subtle);
  overflow: hidden;
}
/* 只有**真的做完**的段落才填 accent —— 那是事實。 */
.tts-gen__slot--done { background: var(--accent); }
/* ⚠️ **進行中的格子不填色。**
   填了就會出現「文字說已完成 0 段、圖形卻有 1/3 是實心紅」——
   使用者讀圖不讀字，那就是 33%，等於把「長度＝進度」的謊言搬回來
   （2026-08-12 使用者實測回報：「loading 畫面還是舊的」，看的就是這個）。
   規格 §3 已經為待生成列寫過同一條警告；§4 的離散格子漏了，這裡一併適用。
   「正在跑」由**循環的 sweep** 承載：它永不累積，讀不成位置。 */
.tts-gen__slot--running { background: var(--border-subtle); }

/* 生成中動畫：慢而安靜，無高頻 spinner（handoff §6） */
@keyframes breatheDot { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
.breathe-dot { animation: breatheDot 2600ms ease-in-out infinite; }

/* 待生成段落的軌：**永遠是空的**，只有一小段亮色反覆掠過。
   ⚠️ 這條軌**絕對不能填 accent**。填滿的紅軌會被讀成「這段已完成／已播完」，
   等於把剛拿掉的「長度＝進度」謊言搬回來（靜態截圖與 reduced-motion 下尤其明顯），
   也會撐爆 5% 紅色預算。sweep 之所以安全：它**循環**、永不累積，讀不成位置。 */
@keyframes genSweep {
  0% { transform: translateX(-120%); }
  100% { transform: translateX(420%); }
}
.tts-seg__pending-track {
  position: relative;
  flex: 1;
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--border-subtle);
  overflow: hidden;
}
.gen-sweep {
  position: absolute;
  inset: 0 auto 0 0;
  width: 24%;
  border-radius: var(--radius-full);
  background: linear-gradient(90deg, transparent,
              color-mix(in oklab, var(--accent) 55%, transparent), transparent);
  animation: genSweep 2900ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .breathe-dot { animation: none; }
  /* sweep 是純裝飾；停住的 sweep 會被誤讀成「進度到這裡」，所以直接不顯示。 */
  .gen-sweep { display: none; }
  /* 認證落地的「紙頁上移」是純過場裝飾（800ms 整塊內容位移），reduced-motion 下直接定格。 */
  body.page-enter .tab-panel.active > main { animation: none; }
}

/* ── 待生成的段落列：與可播列同一列寬、同一節奏，但不可按 ────────────────────
   欄寬必須與 .tts-seg 逐項相同（34 圓框／52 標籤／34 時間／flex 軌／34 右欄），
   否則第 1 段解鎖的瞬間版面會抽動。 */
.tts-seg--pending { opacity: 0.72; }
.tts-seg--pending.is-running { opacity: 1; }
.tts-seg__pending-btn {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-full);
  border: 1px dashed var(--border-default);
  display: flex;
  align-items: center;
  justify-content: center;
}
.tts-seg__pending-dot {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--accent);
}
.tts-seg__pending-gap { flex: 0 0 auto; min-width: 34px; }
.tts-seg__pending-state {
  flex: 0 0 auto;
  min-width: 34px;
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.tts-seg--pending.is-running .tts-seg__pending-state { color: var(--accent); }

/* 就緒播放器 */
.tts-play-btn {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-full);
  border: 0;
  cursor: pointer;
  background: var(--accent);
  color: var(--accent-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: filter var(--duration-base);
}
.tts-play-btn:active { filter: brightness(0.97); }
.tts-play-btn svg { width: 20px; height: 20px; fill: currentColor; stroke: none; }
/* 播放/暫停是同一顆鈕的兩個狀態，由 .is-playing 單一真相源決定顯哪個 icon。
   刻意不用 [hidden] 屬性：SVGElement 沒有 hidden 的 IDL 反射（svg.hidden = x 碰不到
   content 屬性），且本專案無全域 [hidden] 規則 —— 兩者疊加會讓兩個 icon 同時出現。 */
.tts-play-btn .tts-icon-pause { display: none; }
.tts-play-btn.is-playing .tts-icon-play { display: none; }
.tts-play-btn.is-playing .tts-icon-pause { display: block; }
.tts-time {
  font-family: var(--font-en-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  font-size: var(--text-sm);
  flex: 0 0 auto;
  min-width: 34px; /* intentional: 容納 m:ss 不跳動 */
}
.tts-time--current { color: var(--text-primary); }
.tts-time--total { color: var(--text-muted); }
.tts-track {
  flex: 1;
  height: 20px;
  display: flex;
  align-items: center;
  cursor: pointer;
  touch-action: none;
}
.tts-track__rail {
  position: relative;
  width: 100%;
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--border-default);
}
.tts-track__played {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  background: var(--accent);
  border-radius: var(--radius-full);
}
/* thumb 只在「碰過這段」時出現：播放中、已有進度、或滑鼠停在軌道上。
   一律顯示的話，多段版會有 N 顆橘點並排停在 0 —— 看起來像 N 個待處理的東西，
   而且平白增加 accent 面積（SKILL.md 硬規則 ≤5%）。設計本來就是條件顯示的。 */
.tts-track__thumb {
  position: absolute;
  top: 50%;
  left: 0%;
  transform: translate(-50%, -50%);
  width: 11px;
  height: 11px;
  border-radius: var(--radius-full);
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--bg-subtle);
  opacity: 0;
  transition: width var(--duration-base) var(--ease-out),
              height var(--duration-base) var(--ease-out),
              opacity var(--duration-base) var(--ease-out);
}
.tts-track.is-touched .tts-track__thumb,
.tts-track:hover .tts-track__thumb { opacity: 1; }
.tts-track:hover .tts-track__thumb { width: 13px; height: 13px; }
.tts-divider {
  width: 1px;
  height: 22px;
  background: var(--border-subtle);
  flex: 0 0 auto;
}
.tts-speed {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  background: var(--bg-base);
  border: 1px solid var(--border-subtle);
  border-radius: 10px; /* intentional: prototype segmented 外框，介於 --radius-sm (6) 與 --radius-md (12) */
  flex: 0 0 auto;
}
.tts-speed__btn {
  font-family: var(--font-en-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px; /* intentional: prototype segmented 標籤，介於 --text-xs (11) 與 --text-sm (13) */
  line-height: 1;
  cursor: pointer;
  padding: 5px 9px; /* intentional: prototype segmented padding */
  border-radius: 8px; /* intentional: prototype segmented 選中格，介於 --radius-sm (6) 與 --radius-md (12) */
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-muted);
  transition: color var(--duration-base), background var(--duration-base);
}
.tts-speed__btn.is-active {
  background: var(--bg-surface);
  border-color: var(--border-default);
  color: var(--text-primary);
  font-weight: 600;
}
.tts-audio { display: none; }

/* ── 多段就緒：標頭列 + 每段一列 ──────────────────────────────────────────────
   與單段是**兩種版型**，不是同一個塞更多東西：單段一列 44px、直接就是播放器；
   多段有標頭列、每段 34px，速度控制移到標頭列、分隔線不出現。
   設計不自動接續播放 —— 段與段之間音色會變，讓使用者自己按下一段的播放鍵，
   「重新播一段新的音檔」這個動作本身就解釋了聲音的落差。 */
.tts-strip--multi {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  padding: 12px 16px 14px; /* intentional: prototype 多段版 padding */
}
.tts-multi__head {
  display: flex;
  align-items: center;
  gap: 10px; /* intentional: prototype */
  padding-bottom: 11px; /* intentional: prototype */
}
.tts-multi__icon { display: flex; color: var(--accent); flex: 0 0 auto; }
.tts-multi__icon svg {
  width: 15px; height: 15px; fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}
.tts-multi__count {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  flex: 0 0 auto;
}
.tts-multi__dot {
  width: 3px; height: 3px; border-radius: var(--radius-full);
  background: var(--border-default); flex: 0 0 auto;
}
.tts-multi__note {
  font-family: var(--font-zh-sans);
  font-size: 12.5px; /* intentional: prototype 次要說明 */
  color: var(--text-muted);
  line-height: 1.5;
}
.tts-multi__spacer { flex: 1; }
.tts-multi__list {
  display: flex;
  flex-direction: column;
  gap: 2px; /* intentional: prototype */
  border-top: 1px solid var(--border-subtle);
  padding-top: 6px; /* intentional: prototype */
}
.tts-seg {
  display: flex;
  align-items: center;
  gap: 13px; /* intentional: prototype 小版列間距（大版是 16） */
  padding: 5px 0; /* intentional: prototype */
}
.tts-seg[hidden] { display: none; }
.tts-seg__label {
  flex: 0 0 auto;
  min-width: 52px; /* intentional: 容納「第 N 段 ✓」不跳動 */
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-zh-sans);
  font-size: 12.5px; /* intentional: prototype */
  color: var(--text-secondary);
}
.tts-seg.is-playing .tts-seg__label { color: var(--text-primary); }
.tts-seg__done { color: var(--status-correct); display: flex; }
.tts-seg__done[hidden] { display: none; }
.tts-seg__done svg {
  width: 13px; height: 13px; fill: none; stroke: currentColor;
  stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round;
}
/* 小版播放鈕：未播放是**描邊透明底**（大版是實心橘）——
   多段時有 N 顆鈕，全做成實心橘會讓 accent 覆蓋率爆掉（SKILL.md 硬規則 ≤5%）。 */
.tts-play-btn--sm {
  width: 34px;
  height: 34px;
  border: 1px solid var(--border-default);
  background: transparent;
  color: var(--accent);
}
.tts-play-btn--sm:hover { border-color: var(--accent); }
.tts-play-btn--sm.is-playing {
  border-color: transparent;
  background: var(--accent);
  color: var(--accent-ink);
}
.tts-play-btn--sm svg { width: 15px; height: 15px; }
.tts-multi__toggle {
  align-self: flex-start;
  margin-left: 47px; /* intentional: prototype，對齊段落列的時間欄起點 */
  background: transparent;
  border: 0;
  padding: 3px 0; /* intentional: prototype */
  cursor: pointer;
  font-family: var(--font-zh-sans);
  font-size: 12.5px; /* intentional: prototype */
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  transition: color var(--duration-base);
}
.tts-multi__toggle:hover { color: var(--text-secondary); }
.tts-multi__toggle[hidden] { display: none; }
.tts-multi__toggle svg {
  width: 11px; height: 11px; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  transition: transform var(--duration-slow) var(--ease-out);
}
.tts-multi__toggle.is-expanded svg { transform: rotate(180deg); }

.tts-gen__hint[hidden] { display: none; }
.tts-multi__gendot[hidden], .tts-seg__pending-dot[hidden], .gen-sweep[hidden] { display: none; }
.tts-gen__hint {
  display: flex;
  align-items: center;
  gap: 10px;
}
/* 「比平常久一些」的提示。兩處（生成中 strip、多段標頭）共用同一組宣告 ——
   分開寫遲早只改到一邊，而那一邊不會有任何測試變紅。 */
/* 提示是**行內**的：接續在說明之後，只有它自己粗體。
   ⚠️ 別把它拉成 .tts-multi__head 的直接子元素 —— 那會讓它變成獨立的 flex item，
      與說明各自換行（2026-08-12 使用者實測回報畫面很怪）。 */
.tts-gen__hint-text,
.tts-multi__hint {
  font-family: var(--font-zh-sans);
  font-size: 12.5px; /* intentional: prototype 次要說明 */
  color: var(--text-secondary); /* 比一般說明重一階：它在提醒，不只是陳述 */
  font-weight: 600;
  line-height: 1.5;
}
.tts-multi__hint[hidden] { display: none; }
.tts-multi__hint { margin-left: 4px; } /* intentional: 與前一句之間的呼吸，不到一個全形空格 */
.tts-multi__icon svg[hidden] { display: none; }

/* ── 太長，不提供朗讀 ────────────────────────────────────────────────────────
   ⚠️ 這**不是錯誤狀態**：不用 --status-wrong、不給重試鍵。文章本來就沒壞，
   只是這次不做這件事。用紅色會讓使用者以為要去修什麼。 */
.tts-strip--too-long {
  align-items: flex-start;
  gap: 13px; /* intentional: prototype */
  padding: 15px 17px; /* intentional: prototype */
}
.tts-toolong__icon { color: var(--text-muted); flex: 0 0 auto; display: flex; margin-top: 1px; }
.tts-toolong__icon svg {
  width: 17px; height: 17px; fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}
.tts-toolong__text {
  margin: 0;
  flex: 1;
  font-family: var(--font-zh-sans);
  font-size: 13.5px; /* intentional: prototype strip 內文 */
  color: var(--text-secondary);
  line-height: 1.65;
}

/* 失敗（一般） */
.tts-fail__icon { color: var(--status-wrong); flex: 0 0 auto; display: flex; }
.tts-fail__icon svg {
  width: 17px; height: 17px; fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}
.tts-fail__text {
  flex: 1;
  font-family: var(--font-zh-sans);
  font-size: 13.5px; /* intentional: prototype strip 內文 */
  color: var(--text-secondary);
  line-height: 1.5;
}
.tts-retry svg { width: 14px; height: 14px; flex-shrink: 0; }

/* 失敗（需要登入）：無重試鈕，導向 meta row 的重新生成 */
.tts-strip--login { align-items: flex-start; gap: 13px; padding: 15px 17px; } /* intentional: prototype */
.tts-login__icon { color: var(--text-muted); flex: 0 0 auto; display: flex; margin-top: 1px; }
.tts-login__icon svg {
  width: 17px; height: 17px; fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}
.tts-login__text {
  margin: 0;
  flex: 1;
  font-family: var(--font-zh-sans);
  font-size: 13.5px;
  color: var(--text-secondary);
  line-height: 1.65;
}
.tts-login__domain {
  font-family: var(--font-en-mono);
  font-size: 12.5px;
  color: var(--text-secondary);
}
.tts-login__pointer {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--accent);
  font-weight: 500;
}
.tts-login__pointer svg {
  width: 12px; height: 12px; fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}

/* ── URL 列並行入口：視覺明顯次於主按鈕「解析文章」 ── */
.tts-parallel {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  flex: 0 0 auto;
  font-family: var(--font-zh-sans);
  font-size: 12.5px; /* intentional: prototype，明顯小於主按鈕 */
  color: var(--text-muted);
  transition: color var(--duration-base);
}
.tts-parallel:has(.tts-parallel__input:checked) { color: var(--text-secondary); }
.tts-parallel__input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.tts-parallel__box {
  width: 16px;
  height: 16px;
  border-radius: 5px; /* intentional: prototype checkbox radius */
  flex: 0 0 auto;
  border: 1.5px solid var(--border-default);
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--duration-fast) var(--ease-out);
}
.tts-parallel__box svg {
  width: 11px; height: 11px; fill: none; stroke: var(--accent-ink);
  stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
  opacity: 0;
}
.tts-parallel__input:checked + .tts-parallel__box {
  border-color: var(--accent);
  background: var(--accent);
}
.tts-parallel__input:checked + .tts-parallel__box svg { opacity: 1; }
.tts-parallel__input:focus-visible + .tts-parallel__box { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── PulseDot ── */
.pulse-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  flex: 0 0 auto;
  animation: pulseDot 1400ms ease-in-out infinite;
}

/* ── StreamingCaret ── */
/* intentional: named .streaming-caret to avoid conceptual collision with
   .tooltip-caret / .translate-caret / .romaji-caret (all arrow/triangle carets).
   This is a text cursor blink, different semantic. See EmptyStates audit Section 7-B. */
.streaming-caret {
  display: inline-block;
  /* intentional: width 0.5em — scales with font size so caret weight stays proportional at both prose-lg (17px) and ai-prose (14px). */
  width: 0.5em;
  height: 1.1em;
  background: var(--accent);
  vertical-align: text-bottom;
  /* intentional: marginLeft 2px — tight adhesion to text, no token for 2px gap. */
  margin-left: 2px;
  animation: caretBlink 900ms steps(2, start) infinite;
}

/* ── ChatEmpty ── */
.chat-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 32px 28px 24px;
}
.chat-empty__avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border-default);
  background: var(--bg-subtle);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  margin-bottom: 18px;
  flex-shrink: 0;
}
.chat-empty__title {
  font-family: var(--font-zh-serif);
  /* intentional: fontSize 20px — prototype spec; --text-h3 is 19px. +1px for CoachWelcome-like
     panel greeting weight. See EmptyStates audit Section 5-D. */
  font-size: 20px;
  font-weight: 500;
  color: var(--text-primary);
  margin: 0 0 12px;
  /* intentional: letterSpacing 0.005em — mirrors .coach-welcome h3 tracking. */
  letter-spacing: 0.005em;
}
.chat-empty__body {
  font-family: var(--font-zh-serif);
  /* intentional: fontSize 14.5px — prototype spec; --text-body is 15px. -0.5px for compact
     secondary copy. See EmptyStates audit Section 5-D. */
  font-size: 14.5px;
  /* intentional: lineHeight 1.85 — prototype spec; no token match. Generous for zh-serif. */
  line-height: 1.85;
  color: var(--text-secondary);
  margin: 0;
  text-wrap: pretty;
}
.chat-empty__section-header {
  margin-top: 28px;
  margin-bottom: 12px;
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.chat-empty__section-eyebrow {
  font-family: var(--font-en-display);
  /* intentional: en-display + uppercase + letterSpacing applied to ZH content
     for editorial eyebrow style; ZH chars fallback to serif gracefully.
     See EmptyStates D-4-F2. */
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
  font-weight: 500;
  white-space: nowrap;
}
.chat-empty__section-hairline {
  flex: 1;
  height: 1px;
  background: var(--border-subtle);
}
.chat-empty__section-level {
  font-family: var(--font-en-display);
  /* intentional: en-display applied to mixed ZH-EN content;
     matches prototype level label style. See EmptyStates D-4-F3. */
  /* intentional: fontSize 10.5px — prototype spec; slightly smaller than eyebrow (11px)
     for typographic layer distinction. See EmptyStates audit Section 5-D. */
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}
.chat-empty__site-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.chat-empty__expand-btn {
  align-self: flex-start;
  margin-top: 14px;
  padding: 4px;
  background: transparent;
  border: 0;
  cursor: pointer;
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: color var(--duration-fast) var(--ease-out);
}
.chat-empty__expand-btn:hover { color: var(--accent); }
.chat-empty__chevron {
  transition: transform var(--duration-base) var(--ease-out);
}
.chat-empty__hint-area {
  margin-top: auto;
  padding-top: 18px;
  border-top: 1px solid var(--border-subtle);
}
.chat-empty__hint {
  margin: 14px 0 0;
  font-family: var(--font-zh-sans);
  /* intentional: 12.5px — matches AnalysisEmpty hint / AnalysisStreaming header small size. */
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--text-muted);
  text-wrap: pretty;
}

/* ── ChatBusy ── */
.chat-busy {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 32px 28px 24px;
}
.chat-busy__avatar-wrap {
  position: relative;
  width: 44px;
  height: 44px;
  margin-bottom: 18px;
  flex-shrink: 0;
}
.chat-busy__avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border-default);
  background: var(--bg-subtle);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
}
.chat-busy__halo {
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  animation: coachHalo 1800ms ease-out infinite;
}
.chat-busy__title {
  font-family: var(--font-zh-serif);
  /* intentional: 20px — mirrors ChatEmpty title size. */
  font-size: 20px;
  font-weight: 500;
  color: var(--text-primary);
  margin: 0 0 10px;
  letter-spacing: 0.005em;
}
.chat-busy__body {
  font-family: var(--font-zh-serif);
  /* intentional: 14.5px / 1.85 — mirrors ChatEmpty body spec. */
  font-size: 14.5px;
  line-height: 1.85;
  color: var(--text-secondary);
  margin: 0;
  text-wrap: pretty;
}
.chat-busy__section-header {
  margin-top: 32px;
  margin-bottom: 12px;
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.chat-busy__section-eyebrow {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
  font-weight: 500;
  white-space: nowrap;
}
.chat-busy__section-hairline {
  flex: 1;
  height: 1px;
  background: var(--border-subtle);
}
.chat-busy__ability-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.chat-busy__ability-card {
  border: 1px solid var(--border-subtle);
  background: var(--bg-subtle);
  border-radius: 12px;
  padding: 12px 14px;
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: 12px;
  align-items: start;
  animation: abilityIn 360ms var(--ease-entry) both;
}
.chat-busy__ability-icon {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 1px;
  flex-shrink: 0;
}
.chat-busy__ability-title {
  font-family: var(--font-zh-sans);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: 4px;
  letter-spacing: 0.005em;
}
.chat-busy__ability-example {
  font-family: var(--font-zh-serif);
  /* intentional: 12.5px — matches ChatBusy prototype spec. */
  font-size: 12.5px;
  color: var(--text-muted);
  line-height: 1.6;
  text-wrap: pretty;
}
.chat-busy__ability-example[lang="ja"] {
  font-family: var(--font-jp-serif);
}
.chat-busy__ability-ex-label {
  font-family: var(--font-en-display);
  /* intentional: 10.5px — smaller than body for EX label hierarchy. */
  font-size: 10.5px;
  letter-spacing: 0.16em;
  color: var(--text-muted);
  margin-right: 6px;
  opacity: 0.7;
}
@keyframes coachHalo {
  0%   { opacity: 0.9; transform: scale(1);    }
  70%  { opacity: 0;   transform: scale(1.55); }
  100% { opacity: 0;   transform: scale(1.55); }
}
@keyframes abilityIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0);   }
}

/* ── SiteCard ── */
.site-card-wrap {
  list-style: none;
  animation: siteCardIn 320ms var(--ease-entry) both;
}
.site-card {
  display: block;
  /* intentional: padding 12px 14px 13px — compact card inset; no exact token match. */
  padding: 12px 14px 13px;
  background: var(--bg-subtle);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  text-decoration: none;
  color: inherit;
  transition: background var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out);
}
.site-card:hover {
  background: var(--bg-elevated);
  border-color: var(--border-default);
}
.site-card__header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 6px;
}
.site-card__name {
  font-family: var(--font-zh-serif);
  /* intentional: fontSize 14px — prototype spec; --text-sm is 13px. +1px for site name
     as content heading within card. */
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: 0.005em;
  line-height: 1.3;
}
.site-card__spacer { flex: 1; }
.site-card__external-arrow {
  color: var(--text-muted);
  flex-shrink: 0;
  transition: color var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}
.site-card:hover .site-card__external-arrow {
  color: var(--accent);
  transform: translate(1px, -1px);
}
.site-card__desc {
  margin: 0;
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  line-height: 1.55;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── LevelBadge ── */
.level-badge {
  display: inline-flex;
  align-items: center;
  /* 🔴 **視覺置中，不是盒子置中。** 補償量見 `--pill-cap-nudge`（單一來源）。
     `align-items: center` 對齊的是**行盒**，但徽章內容永遠是「N5」這種大寫＋數字 ——
     沒有下伸部，墨跡整團偏在行盒上半。實測（Playwright，10.5px / --font-en-display）：
     字體 ascent 8 / descent 3，墨跡只有 7.34 高，於是原本的
     `padding: 2px 7px; line-height: 1.4` 讓字**偏上 1.24px**（上 5.44 / 下 7.92），
     在 20.7px 高的徽章裡看得出來 —— 使用者 2026-08-16 回報「字應該要在 badge 正中心」。
     做法：`line-height: 1` 讓行盒貼住字級，再用上下不對稱的 padding 推回正中。
     盒高 20.7 → 20.5（差 0.2px，不造成版面位移）。
     **量法**（換字體/改字級時重跑）：文字節點的 Range rect 取行盒，
     用 canvas `fontBoundingBoxAscent/Descent` 求 baseline，
     再用 `actualBoundingBoxAscent/Descent` 求墨跡上下緣，比較上下留白。
     ⚠️ 量測時**不可**把探針 span 插進這個元素來取 baseline —— 它是 `display:flex`，
        探針會變成 flex item、`vertical-align` 失效，量到的會是盒子中心（踩過）。 */
  padding: calc(4px + var(--pill-cap-nudge)) 7px calc(4px - var(--pill-cap-nudge));
  border-radius: var(--radius-misc-1);
  font-family: var(--font-en-display);
  /* intentional: fontSize 10.5px — prototype spec; slightly below --text-xs (11px). */
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.04em;
  line-height: 1;
  border: 1px solid;
  /* default (unmatched): status-correct palette */
  background: var(--status-correct-bg);
  color: var(--status-correct);
  /* intentional: border-color rgba(79,184,146,0.22) — status-correct derived alpha;
     no token in scale for status-* border variants. See EmptyStates D-5. */
  border-color: rgba(79, 184, 146, 0.22);
}
/* 等級徽章的 accent 變體 —— **這個 class 描述長相，不描述語意**，由呼叫端決定何時套：
     · 推薦網站卡片：符合使用者等級時才加（見 app.js 的 `matched` 判斷）
     · 文章解析標頭：一律加（那裡陳述的是「文章的等級」，與你和它的關係無關）
   ⚠️ 2026-08-16 從 `--matched` 改名為 `--accent`。原名描述的是**推薦卡片的語意**，
      文章標頭套上去會讓 class 名說謊；而另外寫一份同色的規則就是「同一條規則存在
      N 份」—— 那正是這個專案吃過虧的形狀（分歧的那一份不會有測試變紅）。
   🔒 tests/test_article_level_badge.py::test_accent_variant_is_single_source（只准一份宣告） */
.level-badge--accent {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent-border);
}

/* ── CategoryTag ── */
.category-tag {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: var(--radius-misc-1);
  background: transparent;
  color: var(--text-secondary);
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  border: 1px solid var(--border-subtle);
  line-height: 1.4;
}


/* ════════════════════════════════════════════════════════════════════════════
   VocGroup — Section 03 字彙分組（Category Icons × Grammar Badges）
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Category Badge (CatBadge) ── */
.cat-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--accent);
  flex-shrink: 0;
}
.cat-badge--md { width: 28px; height: 28px; border-radius: 6px; }
.cat-badge--md svg { width: 18px; height: 18px; }
.cat-badge--sm { width: 18px; height: 18px; border-radius: 4px; }
.cat-badge--sm svg { width: 12px; height: 12px; }

/* ── Grammar Badge (GramBadge) ── */
.gram-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-jp-serif);
  font-weight: 600;
  line-height: 1;
  flex-shrink: 0;
}
.gram-badge--md { width: 28px; height: 28px; border-radius: 6px; font-size: 14px; padding-bottom: 1px; }
.gram-badge--sm { width: 18px; height: 18px; border-radius: 4px; font-size: 10.5px; }

/* ── VocGroup 容器 ── */
.voc-section { margin-top: 4px; }

.voc-section__intro {
  font-family: var(--font-zh-serif);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0 0 20px;
  line-height: var(--lh-body-lg);
}
.voc-section__intro-muted { color: var(--text-muted); }

/* ── 單一分組 ── */
.voc-group { margin-bottom: 28px; }
.voc-group:last-child { margin-bottom: 0; }

.voc-group__header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 15px;
}
.voc-group__label {
  font-family: var(--font-zh-serif);
  font-size: var(--text-body);
  font-weight: 500;
  color: var(--text-primary);
}
.voc-group__kicker {
  font-family: var(--font-en-display);
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.voc-group__sep {
  flex: 1;
  height: 1px;
  background: var(--border-subtle);
  margin: 0 6px;
  align-self: center;
}
.voc-group__count {
  font-family: var(--font-en-mono);
  font-size: 11px;
  color: var(--text-muted);
}

/* ── Column header（只在第一組出現）── */
.voc-col-header {
  display: grid;
  grid-template-columns: var(--voc-cols, 22% 24% 38% 16%);
  padding: 5px 12px 5px 38px;
  border-bottom: 1px solid var(--border-default);
}
.voc-col-header__cell {
  font-family: var(--font-zh-sans);
  font-size: 11.5px;
  color: var(--text-muted);
}

/* ── Word rows ── */
.voc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--border-subtle);
}
/* intentional: override .prose-lg ul li flex + ::before bullet for vocab grid rows */
.prose-lg .voc-list .voc-row,
.voc-list .voc-row {
  display: grid;
  grid-template-columns: var(--voc-cols, 22% 24% 38% 16%);
  align-items: baseline;
  padding: 8px 12px 8px 38px;
  border-bottom: 1px solid var(--border-subtle);
  gap: 0;
}
.prose-lg .voc-list .voc-row::before,
.voc-list .voc-row::before { display: none; }
.voc-list .voc-row:last-child { border-bottom: none; }
.voc-list .voc-row:nth-child(odd) { background: var(--bg-subtle); }

.voc-row__jp {
  font-family: var(--font-jp-serif);
  font-size: var(--text-body);
  color: var(--text-primary);
}
.voc-row__kana {
  font-family: var(--font-jp-serif);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.voc-row__zh {
  font-family: var(--font-zh-serif);
  font-size: var(--text-sm);
  color: var(--text-primary);
}
.voc-row__pos {
  font-family: var(--font-zh-sans);
  font-size: 11px;
  color: var(--text-primary);
}

/* ── Section 03 loading / error wrapper ── */
.voc-loading {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 20px 0 4px;
  color: var(--text-muted);
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
}
.voc-loading__spinner {
  width: 14px;
  height: 14px;
  border: 2px solid var(--border-subtle);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: voc-spin 0.8s linear infinite;
  flex-shrink: 0;
}
@keyframes voc-spin { to { transform: rotate(360deg); } }

/* ═══════════════════════════════════════════════════════════════════════════
   題庫（O-1 Stage 1）
   Row / preview-card / marker / generating. Dark + light via existing tokens.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The display rules below would otherwise beat the HTML hidden attribute, leaving
   every collapsed action strip / stamp / spinner on screen. Scoped to 題庫 so the
   rest of the app keeps its per-class [hidden] rules. */
#tab-bank [hidden] { display: none !important; }

/* 種類標籤（課程／文章／筆記）— 設計 Primitives.jsx → Tag：填色、無框、矮、安靜。
   刻意不掛在 .chip 底下：.chip 是可點擊的互動元件（cursor: pointer，hover 整個
   變 accent），這裡是永遠不能點的靜態標籤。繼承它會把兩者綁死，也會讓標籤跟同一
   列的 .qb-marker（外框 + accent 的狀態蓋章）撞成同一種視覺語言。
   Tag 填色 / Marker 外框 —— 這個對比是設計刻意的層次，不能抹平。
   （REFACTOR_AUDIT 2.2 把設計誤述為「方角邊框」，該項裁決已作廢） */
.qb-kind {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--bg-subtle);
  color: var(--text-secondary);
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  line-height: 1.5;
  white-space: nowrap;
}

/* The two existing tabs are 60/40 two-column grids; 題庫 is a single column.
   Without this it would be squeezed into the left 60% with dead space right. */
.tab-panel > main.main--single {
  /* ⚠️ 寬度與內距在 `.page-frame--single`（與我的點數共用同一份）。 */
  grid-template-columns: minmax(0, 1fr);
}
/* 區塊直向間距的**單一來源** = 繼承自 `.tab-panel > main` 的 `gap: 32px`（單欄時即 row-gap）。
   子區塊（header / 卡片 grid / syllabus）一律不加 margin-top/bottom —— 加了會疊在這個 gap
   上（實測踩過：header 的 margin-bottom 32 疊 gap 32 → 描述到卡片變 64，設計只要 32）。 */

/* ── Page header ── */
.qb-eyebrow {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  font-weight: 500;
  color: var(--text-muted);
  margin: 0 0 var(--space-2);
}
.qb-title {
  font-family: var(--font-zh-serif);
  font-size: var(--text-h1);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-primary);
  margin: 0;
}
.qb-lede {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--text-secondary);
  margin: var(--space-3) 0 0;
  /* ⚠️ **不要加 max-width。** 這段說明的右緣要對齊標題與預覽卡格線的右緣
     （＝容器寬）。先前是 560px，於是它在卡片格線一半的位置就折行，右邊空一大塊，
     看起來像沒對齊的孤兒。行長由容器負責，不在這裡另訂一個。 */
}

/* ── Preview cards ── */
.qb-preview-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
  align-items: stretch;
  /* 卡片→syllabus 的直向間距由 main--single 的 row-gap(32px) 單一負責，這裡不加
     margin-bottom（否則疊加：gap 32 + margin → 56）。gap 只作卡片彼此的水平間距。 */
}
.qb-preview {
  display: flex;
  flex-direction: column;
  position: relative;
  min-height: 260px;
  padding: var(--space-5);
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.qb-preview__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  animation: qbFade var(--duration-entry) var(--ease-entry) both;
}
.qb-preview__material {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}
/* 「本次主教材 / 下次主教材」—— 由原本的 eyebrow（本次／下次）與這一行合併而來，
   所以沿用 eyebrow 的字重與字距，讓這段資訊讀起來是連著的一句。
   ⚠️ 合併前是兩行（.qb-preview__eyebrow + 這行），兩者字重/字距/顏色都不同，
      視覺上會斷成兩段沒有關聯的標籤。 */
.qb-preview__material-label {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
}
/* 類別字「翻譯」—— 與本輪列的 .qb-round__kind 同一套降階（400 + muted）。 */
.qb-preview__material-kind {
  font-weight: 400;
  color: var(--text-muted);
}
.qb-preview__source {
    /* 內容是中文為主的課標題 —— 字體決定 、。 的位置（實測：中文字體置中 0.49、日文字體靠下 0.87）。
     ⚠️ 這裡與 .sources-summary 的方向**相反**，不可一律換成同一種。2026-08-07 */
  font-family: var(--font-zh-serif);
  font-size: var(--text-h3);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: var(--text-primary);
  margin-bottom: var(--space-4);
}
.qb-preview__source.is-notes { font-family: var(--font-zh-serif); }

.qb-preview__focus { margin-bottom: var(--space-4); }
.qb-preview__focus-label {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}
.qb-preview__focus-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.qb-focus-item {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}
.qb-focus-item__num {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  color: var(--accent);
  opacity: 0.7;
  font-feature-settings: "lnum" 1;
  flex: 0 0 auto;
}
.qb-focus-item__text {
  font-family: var(--font-jp-sans);
  font-size: var(--text-sm);
  color: var(--text-primary);
  line-height: 1.5;
}

.qb-preview__foot { margin-top: auto; }

/* ── 出題單字提示（「本次」卡；filler 天 / 尚未出題時整條 hidden）──
   高度影響是預期的：提示 1 行時卡片 +26px，長標題 2 行時 +43px；qb-preview-grid 是
   align-items:stretch，所以「下次」卡會被拉齊 —— 該不對稱本來就存在（「本次」有取消指定鈕）。 */
.qb-preview__vocab {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  line-height: 1.6;
  color: var(--text-muted);
  margin: 0 0 var(--space-2);
}
.qb-preview__vocab svg {
  width: 13px; height: 13px;
  flex: 0 0 auto;
  margin-top: 2px;
  /* icon 不宣告 color —— 繼承 currentColor 才與文字連動（比照 .meta-action 規則） */
}
/* 連結照全站慣例（見 .prose a）：accent 色、平常無底線、hover 才出現底線 */
.qb-preview__vocab-link {
  color: var(--accent);
  text-decoration: none;
  font-family: var(--font-jp-serif);
  cursor: pointer;
}
.qb-preview__vocab-link:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.qb-preview__vocab-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.qb-preview__review {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  line-height: 1.6;
  color: var(--text-muted);
  margin: 0;
}
.qb-preview__review-label {
  letter-spacing: 0.04em;
  margin-right: var(--space-2);
}
/* 複習內容是**日文文法名並列**（`〜すぎる（N4）、〜ほど…`）—— 那些 `、` 是日文標點。
   父層 .qb-preview__review 是中文黑體，會讓它們置中（實測 0.49，應為 0.83）。
   ⚠️ 這裡與同一張卡片的 .qb-preview__source（中文課標題，改用中文字體）方向**相反**。 */
.qb-preview__review-text {
  font-family: var(--font-jp-sans);
}
.qb-cancel-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: 0;
  background: transparent;
  border: 0;
  cursor: pointer;
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.02em;
  color: var(--text-secondary);
  transition: color var(--duration-fast) var(--ease-out);
}
.qb-cancel-btn:hover { color: var(--text-primary); }
.qb-cancel-btn svg { width: 13px; height: 13px; flex: 0 0 auto; }

/* ── Position marker ── */
/* ── 可點的教材標題（設計 `BankControls.jsx` → `SourceJumpTitle`）───────────
   hover 變 accent、點了捲到清單裡的那一列。**三個地方共用**：
   口說本次列、本次卡、下次卡。

   🔴 這裡只放**互動**；字體與字級留在各自的 class ——
      設計的 `SourceJumpTitle` 本來就帶 `size` 參數（列 16px／卡 19px），
      那是刻意的差異，不是該收斂掉的重複。
   ⚠️ 用 `span[role="button"]` **不是 `<button>`**（設計 §四明訂）——
      預覽卡與列標題本身就是可點區域，巢狀 button 是無效 HTML。
      代價是鍵盤要自己補（Enter／Space），那在 `qbBindJump` 裡。 */
.qb-jump {
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out);
}
.qb-jump:hover,
.qb-jump:focus-visible { color: var(--accent); }
/* 沒有教材可跳時（設計 `SourceJumpTitle` 的 `!source` 態）：
   **不是標題的樣子** —— 降成小字 muted，而且不可點。 */
.qb-jump.is-empty {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--text-muted);
  cursor: default;
}

.qb-marker {
  display: inline-flex;
  align-items: baseline;
  flex: 0 0 auto;
  padding: 3px var(--space-2);
  border: 1px solid var(--accent-soft);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--accent);
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.qb-marker--strong {
  border-color: var(--accent);
  background: var(--accent-soft);
}
/* 列的章容器：0～2 個。gap 比 .qb-row__right 的窄，讓兩個章讀起來是一組。 */
.qb-markers {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex: 0 0 auto;
}
.qb-markers:empty { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   O-5 影子跟讀 session（P4b）
   設計：ShadowingSession.jsx / Shadowing.jsx；實測值見 DESIGN_MEASURED.md

   🔴 **設計檔假設「一段獨白 + 一個音檔」，實際是「A/B 對話 + 逐輪音檔」**（P0 裁決）。
      版面照設計走（使用者裁決：13 列 + A/B 標示），但播放器是「N 個檔上的
      單一虛擬時間軸」—— 那部分的邏輯在 static/shadowing.js。
   ⚠️ 字級 12.5 / 13.5 / 11.5 / 19 等在本專案**無對應 token**（text-xs 11 /
      text-sm 13 / text-body 15 / text-h3 19）。依專案慣例用 literal + intentional，
      **禁止用「最接近的 token」頂替** —— 1–2px 的偏差會累積成可見的不協調。
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 今日練習兩格 tab ── */
.sh-ov {
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-bottom: 1px solid var(--border-subtle);
}
.sh-ov__cell {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 5px;
  padding: 13px 16px 12px;   /* intentional: 設計值，非 spacing scale */
  text-align: left;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  cursor: pointer;
}
/* 選中底線 —— 與 stepper 同一套語彙：**貼在容器 hairline 上**，中間不留縫。
   ⚠️ 設計檔記載這是修過的 bug（bottom:-1 → -2）。 */
.sh-ov__cell.is-active::after,
.sh-stage.is-active::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 2px;
  background: var(--accent);
  transform-origin: left;
  animation: shTabLine var(--duration-base) var(--ease-entry) both;
}
@keyframes shTabLine { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.sh-ov__head { display: flex; align-items: baseline; gap: var(--space-2); }
.sh-ov__zh {
  font-family: var(--font-zh-serif);
  font-size: 16px;           /* intentional: 設計 16px；text-body(15)/text-h3(19) 間無 token */
  color: var(--text-secondary);
}
/* 🔴 **課名與數字一起跟著 active 走**（設計 `Shadowing.jsx:288-292`：
      兩者都是 `on ? --text-primary : --text-secondary`）。
   ⚠️ 數字先前寫死 `--text-primary` —— 於是**沒被選的那一格數字仍然是白的**，
      而課名已經變灰：同一格裡兩個字級各講一套狀態，使用者一眼看出不對
      （2026-08-21 截圖回報「[10] 是白色」）。
   ⚠️ 「題」／「秒」**不跟著變**，永遠 `--text-secondary`（設計第三個 span）。 */
.sh-ov__cell.is-active .sh-ov__zh,
.sh-ov__cell.is-active .sh-ov__num { color: var(--text-primary); }
.sh-ov__num {
  font-family: var(--font-en-display); font-size: 19px; line-height: 1;
  color: var(--text-secondary);
}
.sh-ov__unit { font-family: var(--font-zh-sans); font-size: var(--text-xs); color: var(--text-secondary); }
.sh-ov__meta { font-family: var(--font-zh-sans); font-size: 11.5px; color: var(--text-muted); }
/* ── 內文小字裡的數字：**唯一一份** ──────────────────────────────────────
   設計系統有兩套數字，用途分明（`docs/design-handoff/o5/Shadowing.jsx:72-73`）：
     enNum   = --font-en-display + lnum   → 展示型大字（overview 的 10／60）
     monoNum = --font-en-mono + tabular   → **內文小字裡的數字**（這一條）
   ⚠️ 這條先前是**六份一模一樣的宣告**散在六處（.sh-ov__meta b / .sh-trail__hint b /
      .sh-repline__text b / .sh-fatigue__text b / .sh-slim__loop b / .sh-summary__verdict b）。
      2026-08-21 我正要加第七、第八份時收成一份 —— 留六份就是等其中一份走鐘，
      而走鐘的那份不會有人發現（沒人會把六個畫面並排讀）。
   ⇒ **新增位置＝在這個選擇器清單加一行**，不要再複製宣告。 */
.sh-ov__meta b,
.sh-trail__hint b,
.sh-repline__text b,
.sh-fatigue__text b,
.sh-slim__loop b,
.sh-summary__verdict b,
.practice-header__est b,        /* 「約 30 分鐘」的 30 */
.btn-secondary .num-mono        /* 「重新生成 10 題」的 10 */
{ font-family: var(--font-en-mono); font-variant-numeric: tabular-nums; font-weight: 400; }

/* ── session 外框 ── */
/* 🔴 **與上方的兩單位 tab 條要有間隙。**
   `#today-overview` 是一條帶 `border-bottom` 的 tab 條；session 直接接在它下面，
   沒有這個 padding 的話練習日軌跡列會**貼著頁籤**（2026-08-21 使用者截圖回報，
   實測間隙 **0px**）。
   ⚠️ 放在 `.sh-session` 而不是 `.sh-trail` —— 這樣**三種狀態**（載入／失敗／可練）
      都拿到同一個間隙。掛在軌跡列上的話，生成中與失敗畫面仍然貼著頁籤。
   ⚠️ 22px 是這個元件自己的節奏（設計 `ShadowingSession.jsx` 的
      `<header style={{marginBottom:22}}>` 與 `.sh-stepper` 的 `margin-bottom:22px`），
      不是最接近的 token —— 軌跡列因此坐在 22／26 的帶裡。
   ⚠️ 中翻日那側是 20px（來自 `.exercises-panel .panel-header` 的 padding，D-1A 原型）。
      兩側差 2px 是**兩份設計來源**造成的，不強行統一。 */
.sh-session {
  padding-top: var(--unit-panel-top);
  animation: shFadeUp var(--duration-entry) var(--ease-entry) both;
  /* 🔴 撐滿 `.exercises-panel` 的剩餘高度，讓底緣與右側教練面板齊
     （使用者裁決 2026-08-29：不照設計稿的內容高，改成與 coach panel 對齊）。
     ⚠️ **沿用隔壁 `.exercises-list` 既有的 `flex: 1 1 0%`**，不是寫死高度 ——
        寫死的話換一個視窗高度就對不齊（§15 那個「卡片被畫到畫面外」同一類錯）。
        `min-height: 0` 是 flex item 能真的縮小的必要條件（預設 auto 會被內容撐住）。 */
  flex: 1 1 0%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
@keyframes shFadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.sh-session[hidden], .sh-ready[hidden], .sh-state[hidden] { display: none; }

/* 🔴 **這裡不定義任何錯誤樣式。** 失敗與載入都走全站共用元件
   （`.article-err-state` / `.loading-placeholder`），這個容器只負責讓它們填滿。
   ⚠️ 原本這裡自刻了 `padding:28px`、置中文字、自己的動作列 ——
      於是影子跟讀的錯誤畫面與文章解析／理解題／出題失敗長得不一樣
      （位置、icon、字級全不同），使用者一眼認出（2026-08-21 回報）。
      `.article-err-state` 靠 `flex:1` 撐開，所以這裡必須是 flex column。 */
.sh-state {
  display: flex;
  flex-direction: column;
  min-height: 320px;
  flex: 1;   /* 承上：把 `.sh-session` 撐出來的高度傳下去給狀態畫面 */
}

/* ── 餘額不足的缺口數字：**型 A（popover）與型 B（閃卡）共用一份** ──────────
   🔴 **只有缺口上色，卡片其餘部分維持中性**（使用者裁決 2026-08-29）。
      刻意偏離設計交付 `CreditGate.jsx` —— 它把點數符號、金額、整句說明
      三處一起染成 `--status-wrong`。整張卡都是紅的時候，「你目前有 3 點」
      與「還差 2 點」份量相同，而使用者要找的只有後者。
   ⚠️ 用 `--status-wrong` 不用 `--accent`：`--accent` 已經給了同一張卡的
      「查看我的點數」，兩個橘色並排會分不出哪個可以點。
   ⚠️ `.cg-short` 本身**刻意沒有視覺樣式**，它的職責是結構（讓整句在
      `.cg-reveal__cost` 那個 inline-flex 容器裡永遠是單一 item，
      否則 `gap: 8px` 會插進句子中間）。說明見 `credit-gate.js` 的
      `creditShortfallHTML`。 */
.cg-short__gap { color: var(--status-wrong); }

/* ── R4 Step 7：影子跟讀第一次進分頁的閃卡（型 B `RevealGate`）──────────────
   設計交付 `CreditGate.jsx::RevealGate`。內容已經先生成好（成本內部吸收），
   但使用者看到它才扣點 —— 這種情境不排 popover，蓋住內容、寫清扣幾點，
   按下去才揭開。畫進 `#sh-state`（同 `.article-err-state`／載入態的既有容器，
   不另開 DOM 節點）。
   ⚠️ 進場動畫沿用既有的 `shFadeUp`（`translateY(8px)→0`）——與設計稿的
      `creditIn` keyframe 逐值相同，不必再寫一份（同 Step 5 popover 沿用
      `accountMenuPop` 的既有先例）。
   ⚠️ 勾選框沿用 `.cg-pop__skip` 那一組（自繪，不用原生 checkbox）——
      同一元件的規則只有一份，不為這裡另刻一次。 */
/* 🩸 **閃卡在兩個練習單位裡長得不一樣**（2026-08-30 使用者截圖回報，兩個症狀）。

   ① **寬高不一致**：影子跟讀側 710×630（`#sh-state` 無內距，卡片剛好填滿）；
      中翻日側 670×620 —— `.exercises-list` 的 `padding: 16px 20px` 是**題目卡片**
      需要的內距，閃卡被連帶內縮左右各 20、上下各 16。
   ② **上緣貼著分頁列**：我第一版把內距整條設成 0 來對齊寬度，卻連上方的呼吸
      也一起拿掉了 —— 而影子那側是靠 `.sh-session` 的 `padding-top` 撐開的。
      對齊寬度與對齊節奏是兩件事，一起拿掉就換成另一個症狀。

   ⇒ 閃卡蓋住整個列表時：**左右歸零**（卡片撐滿，與影子同寬）、
     **上方採用兩個單位共用的起始節奏**（`--unit-panel-top`，與 `.sh-session` 同源）。
     此時 `.panel-header` 一定是藏著的（見 `app.js::syncDrillHeader`），
     所以這個容器就直接接在分頁列下面 —— 與影子跟讀的位置逐像素相同。

   ⚠️ 用 `:has()` 而不是「掛一個 class 再記得拿掉」——後者是靠記憶的規則，
      而閃卡有三個消費者、兩條還原路徑，漏掉一條就會讓題目貼著邊緣。
   ⚠️ **底部不補內距**：閃卡是 `flex: 1` 撐滿容器的，補了會讓它比影子那側矮一截。 */
.exercises-list:has(> .cg-reveal) { padding: var(--unit-panel-top) 0 0; }

.cg-reveal {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-card);
  padding: 38px 34px 30px;   /* intentional: CreditGate.jsx RevealGate padding */
  text-align: center;
  animation: shFadeUp var(--duration-entry) var(--ease-entry) both;
  /* 🔴 卡片撐滿容器、內容垂直置中（使用者裁決 2026-08-29：底緣要與右側教練
     面板齊，不照設計稿的內容高）。撐高之後若不置中，內容會全部黏在上緣、
     下面留一大片空白。 */
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.cg-reveal__mark {
  display: inline-flex;
  color: var(--text-secondary);
  margin-bottom: 18px;   /* intentional: CreditGate.jsx RevealGate */
  /* 🩸 卡片改成 flex column（上面那條，為了撐滿高度）之後，這個 `inline-flex`
     的 span 變成 flex item，被 `align-items: normal`（＝stretch）拉成整行寬，
     圖示就靠到左邊去了（2026-08-29 使用者截圖回報「歪掉了」）。
     ⚠️ 只讓**這一個**置中，不改容器的 `align-items` —— 那會讓標題／說明文字
        全部縮成內容寬，斷行位置跟著變。其餘元素本來就有自己的置中方式
        （文字靠 `text-align: center`、勾選框靠 `margin: … auto`）。 */
  align-self: center;
}
.cg-reveal__eyebrow {
  /* ⚠️ 文案是中文（使用者裁決 2026-08-29：「不要用英文」）——設計交付寫的是
     `SHADOWING`，但專案的 eyebrow **既有先例全是中文**（`.modal-eyebrow` 的
     「設定」「更換教材」、選取脈絡卡的「記憶訣竅」「深入詢問」），設計稿的
     `SETTINGS` 當初也是照這條改成「設定」的。
     ⇒ 字體跟著換成 `--font-zh-sans`：留著英文顯示字體會讓中文用到不該用的字族。 */
  font-family: var(--font-zh-sans);
  font-size: 12px;   /* intentional: CreditGate.jsx RevealGate eyebrow */
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin-bottom: 10px;   /* intentional: CreditGate.jsx RevealGate */
}
.cg-reveal__title {
  margin: 0;
  font-family: var(--font-zh-serif);
  font-size: 21px;   /* intentional: CreditGate.jsx RevealGate title */
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: 0.01em;
}
.cg-reveal__lead {
  margin: 13px auto 0;   /* intentional: CreditGate.jsx RevealGate */
  max-width: 400px;
  font-family: var(--font-zh-sans);
  font-size: 13px;
  line-height: 1.85;
  color: var(--text-muted);
}
.cg-reveal__cost {
  margin: 20px auto 0;   /* intentional: CreditGate.jsx RevealGate */
  display: inline-flex;
  align-items: baseline;
  gap: 8px;   /* intentional: CreditGate.jsx RevealGate */
  font-family: var(--font-zh-sans);
  font-size: 13px;
  color: var(--text-secondary);
}
/* 🔴 **兩種狀態的數字用同一條規則**（使用者裁決 2026-08-30：框架一致，
   狀態只做些微變化）。改之前「餘額不足」那一版的數字完全沒有放大 ——
   充足是 16px Newsreader、不足是 13px Noto Sans TC，同一張卡的同一個位置
   兩種排版。`.cg-num` 是缺口句裡的數字（見 `credit-gate.js::creditShortfallHTML`），
   只在閃卡裡放大：型 A 的彈窗不套這條，渲染逐字元不變。
   ⚠️ 選擇器合寫成一條，不是複製一份 16px —— 兩份遲早有一份沒被調到。 */
.cg-reveal__cost-n,
.cg-reveal .cg-num { font-family: var(--font-en-display); font-size: 16px; color: var(--text-primary); }
/* 缺口那個數字**跟著 `.cg-short__gap` 的警示色**：排版由 `.cg-num` 管、顏色由
   `.cg-short__gap` 管，兩件事分開 —— 不必為了放大而複製一份上色規則
   （`test_the_gap_is_the_only_thing_the_shortage_colours` 釘住上色只有一份）。 */
.cg-reveal .cg-short__gap .cg-num { color: inherit; }
/* 🩸 **數字兩側的間距要與「餘額足夠」那一版相同**（使用者裁決 2026-08-30：
   「以點數充足為標準，改成一樣」）。實測差異的成因：足夠那版的
   `.cg-reveal__cost` 是 `inline-flex; gap: 8px`，**每個文字片段與數字各自是
   一個 flex item**，所以數字兩側各有 8px；不足那版整句是**單一 flex item**
   （刻意的 —— 拆開會讓 gap 插進句子中間、句號被推開），於是完全沒有 gap。
   ⇒ 用 margin 補回同樣的 8px。
   ⚠️ **不能改成讓 `.cg-short` 自己 `display:flex`** —— 那正是被禁止的做法：
      句號會變成獨立的 flex item 而被推開 8px。
   ⚠️ **只作用在閃卡**：型 A 彈窗的金額行是 `gap: 3px`（`.cg-pop__cost`，
      各自有設計來源），不套這條、渲染不變。 */
.cg-reveal .cg-num { margin: 0 8px; }   /* ＝ `.cg-reveal__cost` 的 gap */
/* 型 A 彈窗：缺口句落在 `.cg-pop__note`（不是 flex），同樣由 margin 決定 ——
   值取它自己金額行 `.cg-pop__cost` 的 `gap: 3px`。
   🔴 **規則只有一條：「數字兩側的間距 ＝ 該型金額行的 gap」**，兩個值都是
      既有的設計來源，不是各自手感調出來的。 */
.cg-pop__note .cg-num { margin: 0 3px; }
.cg-reveal__cost-after { font-family: var(--font-en-display); font-size: 15px; }

/* ⚠️ **餘額不足時主按鈕是 `<a>`**（連到點數頁），而 `.check-btn` 沒有 `display`
   規則 —— 套在連結上會退回 `inline`（高度與內距失效）並帶 UA 底線。
   補這兩行讓兩種狀態的按鈕**渲染完全相同**，而不是各用一套 class。 */
a.cg-reveal__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}
.cg-reveal__actions {
  margin-top: 22px;   /* intentional: CreditGate.jsx RevealGate */
  display: flex;
  justify-content: center;
}
.cg-reveal .cg-pop__skip { display: inline-flex; margin: 18px auto 0; }
.cg-reveal__rules {
  margin: 16px 0 0;   /* intentional: CreditGate.jsx RevealGate */
  font-family: var(--font-zh-sans);
  font-size: 11.5px;   /* intentional: 比照 .tts-parallel 的 12.5px 既有慣例 */
  color: var(--text-muted);
}
/* 🩸 **`<p>` 的 color 到不了裡面的 `<a>`**（2026-08-29 使用者截圖回報「怎麼會
   出現紫色」）：瀏覽器 UA stylesheet 對 `a[href]` 有自己的 color，**不繼承父層**
   —— 未點過是 `#0000EE` 藍、點過變 `#551A8B` 紫。只在容器上設 color 等於沒設。
   ⚠️ **沿用既有慣例，不另立一種**（使用者裁決 2026-08-29）：點數頁／扣點規則頁
      的連結都是 `.credits-sub a, .crules a`（accent 橘、無底線、hover 才加底線，
      源頭是 `.legal a`）。連規則頁**註腳位置**的 `.crules-foot a` 也只覆寫字級、
      顏色照樣吃 accent（瀏覽器實測 `rgb(217, 79, 48)`）——所以「在卡片底部」
      不是改用 muted 的理由。 */
.cg-reveal__rules a { color: var(--accent); text-decoration: none; }
.cg-reveal__rules a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* ── 標題列：練習日軌跡 + 兩個文字動作 ── */
/* 三段式：描述列 → 成本標 → 動作鈕，用 `space-between` 把成本標留在中間 ——
   與中翻日區段標題（`.exercises-panel .panel-header`）**同一個位置邏輯**
   （使用者裁決 2026-08-26：「影子跟讀就跟翻譯頁一樣，在同樣邏輯的位置處理」）。
   ⚠️ `.sh-trail` 因此不能再是 `flex: 1` —— 吃掉全部剩餘空間的話，成本標會被
      擠到緊貼動作鈕，那就跟中翻日長得不一樣了。 */
.sh-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 22px; }
.sh-trail { flex: 0 1 auto; display: flex; align-items: center; gap: var(--space-3); }
/* 🔴 方塊＝練習日、圓點＝遍數（.sh-dot）。**兩種形狀分工，不可混用**（設計 §一.2）。 */
.sh-trail__blocks { display: inline-flex; gap: 5px; flex: 0 0 auto; }
.sh-day {
  width: 10px; height: 10px;   /* intentional: 設計 10×10 方塊 */
  border-radius: 2px;
  box-sizing: border-box;
  background: transparent;
  border: 1px solid var(--border-default);
}
.sh-day.is-current { background: var(--accent-soft); border-color: var(--accent); }
.sh-day.is-done { background: var(--accent); border-color: var(--accent); }
.sh-trail__text { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; min-width: 0; }
.sh-trail__day { font-family: var(--font-zh-sans); font-size: 12.5px; color: var(--text-primary); white-space: nowrap; }
.sh-trail__hint { font-family: var(--font-zh-sans); font-size: 11.5px; color: var(--text-muted); }
.sh-trail__hint.is-done { color: var(--status-correct); }

.sh-header__actions { display: flex; align-items: center; gap: 20px; flex: 0 0 auto; }
.sh-textbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;                  /* intentional: 設計 icon 與文字間距 6，非 space-2(8) */
  padding: 2px 0;
  background: transparent;
  border: 0;
  cursor: pointer;
  font-family: var(--font-zh-sans);
  font-size: 12.5px;         /* intentional: 設計 12.5px */
  color: var(--text-secondary);
  transition: color var(--duration-fast) var(--ease-out);
}
.sh-textbtn:hover, .sh-textbtn[aria-expanded="true"] { color: var(--text-primary); }
.sh-header__divider { width: 1px; height: 14px; background: var(--border-subtle); flex: 0 0 auto; }
.sh-ic { width: 14px; height: 14px; flex: 0 0 auto; }
.sh-textbtn__caret { display: flex; transition: transform var(--duration-fast) var(--ease-out); }
.sh-textbtn[aria-expanded="true"] .sh-textbtn__caret { transform: rotate(90deg); }

/* ── 三天走完（§六.1：不用綠底，成就感由排印承擔）── */
.sh-done {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  margin-top: 14px; padding: 14px 16px;   /* intentional: 設計值 */
  border-radius: 10px;                    /* intentional: 設計 10；radius-sm(6)/md(12) 間無 token */
  background: var(--bg-subtle);
  border: 1px solid var(--border-subtle);
}
.sh-done[hidden], .sh-tips[hidden], .sh-gate[hidden], .sh-topic[hidden],
.sh-masknote[hidden], .sh-fatigue[hidden], .sh-summary[hidden] { display: none; }
.sh-done__text { flex: 1; font-family: var(--font-zh-sans); font-size: 12.5px; line-height: 1.7; color: var(--text-muted); text-wrap: pretty; }
.sh-done__title { display: block; font-family: var(--font-zh-serif); font-size: 15px; color: var(--text-primary); margin-bottom: 3px; }
.sh-done__cta { flex: 0 0 auto; }

/* ── 跟讀要訣 ── */
.sh-tips {
  margin-top: var(--space-4);
  padding: 14px 16px 16px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
}
/* 三欄，**無頂線分隔**（設計 §六.2：線太多）。 */
.sh-tips__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; margin-bottom: var(--space-4); }
.sh-tips__k { display: block; font-family: var(--font-zh-sans); font-size: var(--text-xs); color: var(--text-muted); margin-bottom: 3px; }
.sh-tips__n { display: flex; align-items: baseline; gap: 4px; margin-bottom: 5px; }
.sh-tips__n b { font-family: var(--font-en-display); font-size: 26px; color: var(--accent); line-height: 1; font-weight: 400; }
.sh-tips__n i { font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-secondary); font-style: normal; }
.sh-tips__col p { margin: 0; font-family: var(--font-zh-sans); font-size: 12px; line-height: 1.65; color: var(--text-muted); text-wrap: pretty; }
.sh-tips__method {
  margin: 0; padding: 10px 12px; border-radius: var(--radius-misc-3);
  background: var(--bg-subtle);
  font-family: var(--font-zh-sans); font-size: 12.5px; line-height: 1.7;
  color: var(--text-secondary); text-wrap: pretty;
}

/* ── 階段 stepper ── */
.sh-stepper {
  display: grid; grid-template-columns: repeat(3, 1fr);
  border-bottom: 1px solid var(--border-subtle);
  margin-bottom: 22px;
}
.sh-stage {
  position: relative;
  display: flex; flex-direction: column; gap: 7px;
  min-height: 44px;
  padding: 10px 12px 12px;
  text-align: left; background: transparent; border: 0;
  border-bottom: 2px solid transparent;
  cursor: pointer;
}
.sh-stage.is-active { cursor: default; }
.sh-stage__line { display: flex; align-items: baseline; gap: var(--space-2); width: 100%; }
.sh-stage__no { font-family: var(--font-en-display); font-size: 12px; letter-spacing: 0.06em; color: var(--text-muted); }
.sh-stage.is-active .sh-stage__no { color: var(--accent); }
.sh-stage__zh { font-family: var(--font-zh-serif); font-size: 15px; color: var(--text-secondary); transition: color var(--duration-fast) var(--ease-out); }
.sh-stage.is-active .sh-stage__zh, .sh-stage:hover .sh-stage__zh { color: var(--text-primary); }
.sh-stage__dots { margin-left: auto; display: flex; }

/* 遍數點 —— 已完成實心、配額內空心、配額外淡描邊。 */
.sh-dots { display: flex; gap: 5px; align-items: center; }
.sh-dot {
  width: 9px; height: 9px;    /* intentional: 設計 9px（播放卡那列 8px） */
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--border-default);
}
.sh-dot.is-bonus { border-color: var(--border-subtle); }
.sh-dot.is-filled { background: var(--accent); border-color: var(--accent); }
.sh-dots--sm .sh-dot { width: 8px; height: 8px; }

/* ── 播放卡 ── */
.sh-card {
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 20px 22px 22px;    /* intentional: 設計值 */
  margin-bottom: var(--space-4);
}
.sh-guide { display: flex; gap: var(--space-3); align-items: flex-start; margin-bottom: 18px; }
.sh-guide__icon { display: flex; align-items: center; height: 22.95px; padding-top: 3px; color: var(--accent); flex: 0 0 auto; }
.sh-guide__icon svg { width: 18px; height: 18px; }
.sh-guide__text { margin: 0; font-family: var(--font-zh-sans); font-size: 13.5px; line-height: 1.7; color: var(--text-primary); text-wrap: pretty; }
.sh-guide__zh { font-family: var(--font-zh-serif); font-size: 15px; }

.sh-player {
  display: flex; align-items: center; gap: var(--space-4);
  margin-bottom: 14px;
  background: var(--bg-subtle);
  border: 1px solid var(--border-subtle);
  border-radius: 14px;        /* intentional: 設計 14；radius-md(12)/lg(16) 間無 token */
  padding: 13px 16px;
}
.sh-play {
  width: 44px; height: 44px; border-radius: 50%; border: 0; flex: 0 0 auto;
  background: var(--accent); color: var(--accent-ink);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.sh-play svg { width: 18px; height: 18px; }
.sh-time { font-family: var(--font-en-mono); font-variant-numeric: tabular-nums; font-size: var(--text-sm); flex: 0 0 auto; min-width: 34px; }
.sh-time--cur { color: var(--text-primary); }
.sh-time--dur { color: var(--text-muted); }
.sh-player__divider { width: 1px; height: 22px; background: var(--border-subtle); flex: 0 0 auto; }

.sh-track { flex: 1; height: 20px; display: flex; align-items: center; cursor: pointer; touch-action: none; }
.sh-track__rail { position: relative; width: 100%; height: 4px; border-radius: var(--radius-full); background: var(--border-default); }
/* 循環時：範圍外調淡、範圍內加深 —— 說明 playhead 為何只在這一小段往返。 */
.sh-track.is-looping .sh-track__rail { background: var(--border-subtle); }
.sh-track__loop { position: absolute; top: 0; bottom: 0; background: var(--border-default); border-radius: var(--radius-full); }
.sh-track__loop[hidden] { display: none; }
.sh-track__fill { position: absolute; top: 0; bottom: 0; left: 0; width: 0; background: var(--accent); border-radius: var(--radius-full); }
.sh-track__knob {
  position: absolute; top: 50%; left: 0; transform: translate(-50%, -50%);
  width: 11px; height: 11px; border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 0 3px var(--bg-subtle);
  transition: width var(--duration-fast) var(--ease-out), height var(--duration-fast) var(--ease-out);
}
.sh-track:hover .sh-track__knob { width: 13px; height: 13px; }

.sh-speed {
  display: inline-flex; align-items: center; gap: 2px; padding: 2px; flex: 0 0 auto;
  background: var(--bg-base); border: 1px solid var(--border-subtle); border-radius: 10px;
}
.sh-speed__btn {
  font-family: var(--font-en-mono); font-variant-numeric: tabular-nums;
  font-size: 12px; line-height: 1; cursor: pointer;
  padding: 5px 9px; border-radius: var(--radius-misc-3);
  background: transparent; border: 1px solid transparent; color: var(--text-muted);
  transition: color var(--duration-fast), background var(--duration-fast);
}
.sh-speed__btn.is-active { background: var(--bg-surface); border-color: var(--border-default); color: var(--text-primary); font-weight: 600; }

.sh-repeat {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 28px; height: 28px; border-radius: var(--radius-sm); cursor: pointer;
  background: transparent; border: 1px solid var(--border-subtle); color: var(--text-muted);
  transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out),
              background var(--duration-fast) var(--ease-out);
}
.sh-repeat.is-on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

.sh-repline { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-4); flex-wrap: wrap; margin-bottom: 6px; }
/* ⚠️ 「此階段 / N / 遍」包成**單一 span**（設計 §六.4 記載的已修 bug）——
   三個文字節點各自當 flex item 會被 gap 撐開。 */
.sh-repline__text { font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-secondary); }

/* ── 自檢閘門（左緣 accent，與教練面板的引導高亮同一套語彙）── */
.sh-gate {
  margin-bottom: var(--space-4);
  border: 1px solid var(--border-default);
  border-left: 3px solid var(--accent);
  background: var(--bg-subtle);
  border-radius: var(--radius-md);
  padding: 16px 18px;
  animation: shFadeUp 240ms var(--ease-entry) both;
}
.sh-gate__row { display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.sh-gate__q { margin: 0; font-family: var(--font-zh-serif); font-size: 16px; color: var(--text-primary); }
.sh-gate__sub { margin: 6px 0 0; font-family: var(--font-zh-sans); font-size: 12.5px; color: var(--text-muted); }
.sh-gate__advice { margin: 0; font-family: var(--font-zh-sans); font-size: var(--text-sm); line-height: 1.7; color: var(--text-secondary); max-width: 460px; text-wrap: pretty; }
.sh-gate__actions { display: flex; gap: var(--space-2); }

/* ── 句群卡 ── */
.sh-subs-card {
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: 10px 12px 14px;
}
.sh-masknote { margin: 6px 6px 4px; font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-muted); }
.sh-topic {
  margin: 6px 0 12px; padding: 14px 16px; border-radius: 10px;
  background: var(--bg-subtle);
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
}
.sh-topic__main { flex: 1; min-width: 240px; }
.sh-topic__label { font-family: var(--font-zh-sans); font-size: 11.5px; letter-spacing: 0.04em; color: var(--text-muted); margin-bottom: 4px; }
.sh-topic__value { margin: 0; font-family: var(--font-zh-serif); font-size: 20px; line-height: 1.45; font-weight: 600; color: var(--text-primary); text-wrap: pretty; }
.sh-topic__scope { font-family: var(--font-zh-sans); font-size: 11.5px; color: var(--text-muted); white-space: nowrap; }

.sh-subs { display: flex; flex-direction: column; gap: var(--space-3); }
.sh-row {
  padding: 14px 16px 12px; border-radius: 10px;
  background: transparent; border: 1px solid transparent;
  transition: background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.sh-row.is-looping { background: var(--bg-base); border-color: var(--border-default); }
.sh-row__head { display: flex; align-items: baseline; gap: 10px; margin-bottom: var(--space-2); }
.sh-row__no { font-family: var(--font-en-display); font-size: var(--text-xs); letter-spacing: 0.08em; color: var(--text-muted); }
/* 說話者姓名 —— mock 討論三輪後定案「**乙 · 姓名加冒號**」（2026-08-21）。
   與句子**同字體、同大小、同顏色**，兩人**不分色**。

   🔴 **不用 accent**（使用者當場否決）：`design-system/README.md:127`
      「Red is the only chromatic accent. Coverage budget: **5% of any given screen, max**」
      —— 13 輪裡 6–7 個名字都上 accent 遠超預算，會讓橘色從「重點」變成「UI 色」。
   ⚠️ **已知代價**：名字長度不一時（`佐々木` 三個字）**日文左緣會參差**。
      另一個候選「甲 · 固定寬度欄」會對齊，使用者比較後仍選了乙 —— 這是取捨不是疏漏。
   ⚠️ 姓名與句子是**兩欄**（`.sh-row__line` 的 flex），不是同一段文字流 ——
      同一流的話換行的第二行會跑到姓名底下（mock 討論時實測抓到）。 */
.sh-row__line { display: flex; align-items: baseline; gap: 0; }
/* 🔴 姓名與句子的排版是**同一條規則** —— 定案要求兩者同字體、同字級、同色。
   ⚠️ 分兩份寫的話，改了句子而沒改姓名**不會有任何東西變紅**（本輪突變驗證實際抓到：
      兩處各有一份完全相同的宣告）。 */
.sh-row__who,
.sh-row__jp {
  font-family: var(--font-jp-serif); font-size: 19px; line-height: 2.5;
  color: var(--text-primary);
}
.sh-row__who { flex: 0 0 auto; }
.sh-row__jp { flex: 1; min-width: 0; }
.sh-row__spacer { flex: 1; }
.sh-row__loopctl { display: inline-flex; align-items: center; gap: var(--space-2); align-self: center; }
.sh-playseg {
  cursor: pointer; border-radius: var(--radius-misc-3); padding: 6px 12px; flex: 0 0 auto; align-self: center;
  /* intentional: 設計 gap 6（雙邊量測抓到我原本用了 --space-2=8 → 鈕寬 82 vs 設計 80） */
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--bg-base); border: 1px solid var(--border-default); color: var(--text-secondary);
  font-family: var(--font-zh-sans); font-size: 12px; font-weight: 500;
  transition: border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.sh-playseg:hover { border-color: var(--text-muted); color: var(--text-primary); }
.sh-row.is-looping .sh-playseg { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.sh-playseg svg { width: 12px; height: 12px; }

.sh-row__jp {
  /* 排版（字體／字級／行高／顏色）在上面與 `.sh-row__who` 共用那條，這裡只放句子自己的。 */
  margin: 0; text-wrap: pretty;
  transition: filter 180ms ease-out, opacity 180ms ease-out;
}
/* 影子階段：遮蔽。⚠️ `user-select:none` 不是裝飾 —— 不遮的話反白就能看到答案。 */
.sh-row__jp.is-masked { filter: blur(7px); opacity: 0.45; user-select: none; line-height: 1.75; }
/* 注音（振り仮名）樣式：**全站共用**（2026-08-24 從 `.sh-row__jp rt` 解綁）。
 * 影子跟讀、出題參考答案、活用卡……凡是 <ruby><rt> 都吃這條，宣告內容與解綁前一字不差，
 * 所以影子跟讀渲染結果不變（由 test_shadow_* 反向護欄確認）。
 * 全部用 em → 字級不同的脈絡自動縮放；某脈絡若需微調，在該脈絡加 scoped override，
 * 不要動這條 base。 */
rt {
  font-family: var(--font-jp-sans); font-size: 0.56em; font-weight: 500;
  color: var(--text-secondary); letter-spacing: 0.02em;
  /* intentional: 設計實測 translateY(1.9152px) ＝ 0.18em @10.64px。
     ⚠️ 用 em 才會隨字級縮放；寫死 px 在別的字級上就偏了。
     這是把注音壓近漢字 —— 少了它，注音與本行之間會多出一條可見的空隙。 */
  transform: translateY(0.18em);
  /* 🔴 反白時不選到讀音（視覺 + Selection.toString()）。這是輔助防線，
     不是唯一防線——正確性的權威來源是 ruby.js 的 rubyCleanText()，
     它連 CSS 沒生效也擋得住（實測 Range.toString()／textContent 不受
     user-select 影響，只有 Selection.toString() 受影響）。見 CLAUDE.md
     〈反白取字不含注音〉。 */
  user-select: none;
}

.sh-row__disc { margin-top: 10px; }
.sh-row__chips { display: flex; align-items: center; gap: var(--space-2); }
.sh-chip {
  padding: 4px 10px; background: transparent; border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm); cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--font-zh-sans); font-size: 12.5px; line-height: 1.4; color: var(--text-muted);
  transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.sh-chip:hover { color: var(--text-primary); border-color: var(--border-default); }
.sh-chip.is-open { border-color: var(--border-default); color: var(--text-primary); }
.sh-chip svg { width: 11px; height: 11px; display: block; flex: 0 0 auto; }

/* 🔴 chip 是**按鈕**，展開的區塊是它的**兄弟**（DESIGN_MEASURED 記載：
   用 <details> 把兩者包成一體，展開時 chip 會被撐寬成整條）。 */
.sh-grammar { margin-top: var(--space-2); padding: 10px 12px; border-radius: var(--radius-misc-3); background: var(--bg-subtle); }
.sh-grammar__list { display: flex; flex-direction: column; gap: 6px; }
.sh-grammar__item { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.sh-grammar__jp { font-family: var(--font-jp-sans); font-size: var(--text-sm); color: var(--text-primary); min-width: 92px; }
.sh-grammar__zh { flex: 1; min-width: 180px; font-family: var(--font-zh-sans); font-size: 12.5px; line-height: 1.7; color: var(--text-secondary); text-wrap: pretty; }
.sh-grammar__foot { margin: 10px 0 0; font-family: var(--font-zh-sans); font-size: 11.5px; line-height: 1.6; color: var(--text-muted); }
.sh-zh { margin: var(--space-2) 0 0; font-family: var(--font-zh-serif); font-size: 14px; line-height: 1.75; color: var(--text-secondary); text-wrap: pretty; }

/* ── 疲勞提醒 ── */
.sh-fatigue {
  margin-top: var(--space-4); padding: 12px 14px; border-radius: 10px;
  background: var(--bg-subtle); border: 1px solid var(--border-subtle);
  display: flex; align-items: center; gap: 14px;
}
.sh-fatigue__text { flex: 1; font-family: var(--font-zh-sans); font-size: 12.5px; line-height: 1.7; color: var(--text-secondary); text-wrap: pretty; }

/* ── 「重新生成」的教練面板訊息（P4c，設計 §二.5）── */
.shadow-regen__now { margin: 0 0 var(--space-3); }
/* 換個主題：label ＋ chips（設計 `CoachPanel.jsx::ShadowRegenPrompt`）。 */
.shadow-regen__topics {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2); margin-bottom: var(--space-3);
}
.shadow-regen__topics-label {
  font-family: var(--font-zh-sans); font-size: 12px;
  letter-spacing: 0.04em; color: var(--text-muted);
  margin-right: var(--space-1);
}
.shadow-regen__chip {
  background: transparent; border: 1px solid var(--border-default);
  border-radius: 999px; cursor: pointer;
  /* intentional: 設計 `4px 12px` / 12.5px —— 都沒有對應 token。 */
  padding: 4px 12px;
  font-family: var(--font-zh-sans); font-size: 12.5px;
  color: var(--text-secondary);
  transition: border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              background var(--duration-fast) var(--ease-out);
}
.shadow-regen__chip:hover:not(.is-on) {
  border-color: var(--text-muted); color: var(--text-primary);
}
/* 選中：與 `.sh-repeat.is-on`、`.qb-marker` 同一套語彙 —— 不另造一種強調。 */
.shadow-regen__chip.is-on {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
}
/* 「其他」才展開的輸入框（設計 §二.5：點了才出現）。 */
.shadow-regen__custom {
  width: 100%; box-sizing: border-box; margin-bottom: var(--space-3);
  background: var(--bg-base); border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  /* intentional: 設計 `9px 12px` / 13px。 */
  padding: 9px 12px;
  font-family: var(--font-zh-sans); font-size: 13px;
  color: var(--text-primary); outline: none;
}
.shadow-regen__custom:focus { border-color: var(--accent); }
.shadow-regen__act {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.shadow-regen__note {
  font-family: var(--font-zh-sans); font-size: 12px; line-height: 1.7;
  color: var(--text-muted); text-wrap: pretty;
}
/* 虛線分隔 —— 「這底下是另一件事」。 */
.shadow-regen__foot {
  margin: var(--space-4) 0 0; padding-top: var(--space-3);
  border-top: 1px dashed var(--border-subtle);
  font-family: var(--font-zh-sans); font-size: 12px; line-height: 1.7; color: var(--text-muted);
}
.shadow-regen__link {
  background: transparent; border: 0; padding: 0 2px; cursor: pointer;
  font-family: inherit; font-size: inherit; color: var(--accent);
}
.shadow-regen__link:hover { text-decoration: underline; }

/* 引導高亮：3px accent 左緣 + accent-soft 底 + 10px 圓角，1.6 秒後以 640ms 淡出。
   ⚠️ 左緣的用法與自檢閘門（.sh-gate）同一套語彙 —— 不要為它另造一種強調。 */
/* 🔴 **幾何兩態相同，只有上色會變。**
   設計 `CoachPanel.jsx::ShadowRegenPrompt`：`margin: 0 -12px; padding: 12px 12px 2px` ——
   那兩個值在設計裡是**無條件**的，只有 border-color／background／radius 隨亮滅切換。
   ⚠️ 只加 padding 不加負 margin → 高亮出現時文字右移 12px、淡出再移回來。
   ⚠️ 只加在 `.is-guided` 不加在 `.is-fading` → 淡出那一刻跳一次。
   ⚠️ 過場**不含** padding／margin —— 動幾何會讓整串對話跟著抖。
   ⚠️ 少了這一組，橘線會**直接貼著字**（使用者 2026-08-21 截圖回報，實測間隙 0px）。
   🔴 **選擇器必須帶 `.msg.assistant`** —— `.msg.assistant .bubble { padding: 0 }` 的
      特異性 (0,3,0) 高於 `.bubble.is-guided` (0,2,0)。只寫後者的話 padding **完全沒有作用**
      （我第一版就這樣寫，實測 margin 生效了、padding 仍是 0px、橘線照樣貼著字）。 */
.msg.assistant .bubble.is-guided,
.msg.assistant .bubble.is-fading {
  /* 🔴 **不向左外撐。** 設計是 `margin: 0 -12px`，但那是照 `CoachPanel.jsx` 的
     訊息列 `gap: 14` 訂的；本專案的 `.msg` 是 `gap: 10`（連同 `.avatar` 28px 都標著
     `intentional: D-6B prototype` —— **兩份設計的數字對不起來**）。
     12 > 10 ⇒ 高亮框會**壓在頭像上**（實測重疊 2px，使用者截圖回報）。
     ⚠️ 代價：這張卡的文字比鄰近訊息內縮 12px（設計靠外撐讓它對齊）。
        三個條件（橘線→文字 12px／文字對齊／離頭像有間隙）在 gap 10 下無法同時成立，
        捨掉的是最不明顯的那個。 */
  margin: 0;
  /* intentional: 設計 `12px 12px 2px` —— 底部 2 沒有對應 token（space-1 是 4）。 */
  padding: var(--space-3) var(--space-3) 2px;
}
.bubble.is-guided {
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 10px;
}
.bubble.is-fading {
  border-left: 3px solid transparent;
  background: transparent;
  border-radius: 10px;
  transition: background 640ms var(--ease-entry), border-left-color 640ms var(--ease-entry);
}

/* ── sticky slim bar（掛 body，避開祖先 transform 的參考系問題）── */
.sh-slim { position: fixed; z-index: 4; }
.sh-slim__box {
  display: flex; align-items: center; gap: var(--space-3); padding: 8px 12px;
  background: var(--bg-surface); border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md); box-shadow: var(--shadow-card);
  animation: shFadeUp var(--duration-base) var(--ease-entry) both;
}
.sh-slim .sh-play { width: 28px; height: 28px; }
.sh-slim .sh-play svg { width: 12px; height: 12px; }
.sh-slim .sh-time { font-size: 12px; min-width: 0; }
.sh-slim__loop { display: inline-flex; align-items: center; gap: 4px; font-family: var(--font-zh-sans); font-size: 11.5px; color: var(--accent); flex: 0 0 auto; }
.sh-slim__loop svg { width: 11px; height: 11px; }

/* ── 收尾頁 ── */
.sh-summary {
  background: var(--bg-surface); border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-card);
  padding: 28px 30px 30px;
  animation: shFadeUp 240ms var(--ease-entry) both;
}
.sh-summary__title { margin: 0; font-family: var(--font-zh-serif); font-size: 26px; font-weight: 500; color: var(--text-primary); }
.sh-summary__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); margin: 24px 0 26px; }
.sh-summary__stat { border-top: 1px solid var(--border-subtle); padding-top: var(--space-3); }
.sh-summary__stat p { margin: 0; font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-muted); }
.sh-summary__stat span { display: flex; align-items: baseline; gap: 4px; margin-top: 6px; }
.sh-summary__stat b { font-family: var(--font-en-display); font-size: 30px; color: var(--text-primary); line-height: 1; font-weight: 400; }
.sh-summary__stat i { font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-secondary); font-style: normal; }
.sh-summary__verdict {
  margin: 0 0 20px; padding: 14px 16px; border-radius: 10px;
  background: var(--bg-subtle); border: 1px solid var(--border-subtle);
  font-family: var(--font-zh-sans); font-size: var(--text-sm); line-height: 1.75;
  color: var(--text-secondary); text-wrap: pretty;
}
.sh-summary__trail { margin-bottom: 22px; }
.sh-summary__note { margin: 0 0 22px; font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-muted); }
.sh-summary__actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }

/* ── 口說本輪列（`#qb-round`）───────────────────────────────────────────────
   設計：BankControls.jsx → ShadowRoundBar；實測值見 docs/design-handoff/o5/DESIGN_MEASURED.md
   ⚠️ 這一條是 P4a **唯一新刻的元件** —— 其餘（印章、動作鈕、確認框）全部沿用既有的。 */
.qb-round {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
  padding: 18px var(--space-5);   /* intentional: 設計 18px 縱向內距；space-3(12)/space-4(16) 間無對應 token */
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.qb-round[hidden] { display: none; }

/* label「口說　本輪」——「口說」降一階，時間單位才是主詞。
   ⚠️ 設計的 min-width:108 是要與預覽卡的 eyebrow 對齊成一條垂直讀線，
      但本專案的 eyebrow 已併進「本次主教材」那一行（見 .qb-preview__material-label），
      沒有對齊的對象 —— **刻意不設 min-width**，不為了它把 eyebrow 拆回來。 */
.qb-round__label {
  flex: 0 0 auto;
  font-family: var(--font-zh-sans);
  font-size: 12.5px;              /* intentional: 設計 12.5px；text-xs(11)/text-sm(13) 間無對應 token */
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
}
.qb-round__kind {
  font-weight: 400;
  color: var(--text-muted);
}

.qb-round__body {
  flex: 1;
  min-width: 260px;               /* intentional: 設計 minWidth 260，避免主題與標題在窄欄擠成一直行 */
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
}
/* 教材標題兼「捲到清單」錨點 —— 不加下線、不加 icon，沿用既有連結色語彙。 */
/* 只留排版；互動在共用的 `.qb-jump`（含 `.is-empty` 的降級樣子）。 */
.qb-round__source {
  font-family: var(--font-jp-serif);
  font-size: 16px;                /* intentional: 設計 SourceJumpTitle 的 size=16 */
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: var(--text-primary);
}
.qb-round__topic {
  font-family: var(--font-zh-sans);
  font-size: 12.5px;              /* intentional: 同 label，設計 12.5px */
  color: var(--text-muted);
}

.qb-round__days {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;                      /* intentional: 設計 gap 10；space-2(8)/space-3(12) 間無對應 token */
}
.qb-round__days[hidden] { display: none; }
.qb-round__dots {
  display: inline-flex;
  gap: 5px;                       /* intentional: 設計 gap 5，圓點是 7px 的密集序列 */
}
/* ⚠️ 圓點三態全部用 --text-secondary，**差別只在填不填**。
   規格 §二 寫「已完成＝實心 --accent」，與設計檔實作不符 ——
   以實測為準（DESIGN_MEASURED §本輪列）。accent 有 5% 覆蓋預算，
   而這裡一次會出現三顆。 */
.qb-round__dot {
  width: 7px;                     /* intentional: 設計 7×7 圓點，無對應 spacing token */
  height: 7px;
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--border-default);
}
.qb-round__dot.is-current { border-color: var(--text-secondary); }
.qb-round__dot.is-done {
  border-color: var(--text-secondary);
  background: var(--text-secondary);
}
.qb-round__day-text {
  font-family: var(--font-zh-sans);
  font-size: 12px;                /* intentional: 設計 12px；text-xs(11)/text-sm(13) 間無對應 token */
  color: var(--text-muted);
}
.qb-round__day-num { font-family: var(--font-en-display); }

/* 從卡片標題捲過來時的落地提示（設計 §四）：捲動走完 65% 才亮，約 1.2 秒後退回。 */
.qb-row.is-flash { border-color: var(--accent); }

/* ── Syllabus ── */
.qb-level { margin-bottom: var(--space-8); }   /* 設計 64px = --space-8 */
.qb-level__header {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-5);
  border-bottom: 1px solid var(--border-default);
}
.qb-level__name {
  font-family: var(--font-en-display);
  font-size: var(--text-h1);
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1;
  color: var(--text-primary);
}
.qb-level__tag {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.qb-subsection { margin-bottom: var(--space-6); }
.qb-subsection__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  padding-left: 2px;
}
.qb-subsection__label {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--text-secondary);
}
.qb-subsection__note {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.qb-subsection__rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
/* 此等級 0 筆（但別級有）時的空狀態卡片：沿用 .qb-row 卡面，非按鈕（不可點）。
   標題白色大字用 .qb-row__title；第二行小字說明歸類規則與動作；不顯示曾練習列與 chevron。 */
.qb-row--empty .qb-row__head { cursor: default; }
.qb-row__empty-hint {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-zh-sans);
  font-size: 12px;                       /* intentional: 同跨等級提示註腳字級，text-xs(11)/text-sm(13) 間無對應 token */
  color: var(--text-muted);
  line-height: 1.75;
  letter-spacing: 0.01em;
  text-wrap: pretty;
}
/* 備註列 = 頁尾註腳（非卡片/提示框，不可看起來能點）。滿欄寬分隔線 + muted 小字。見 spec §四 */
.qb-subsection__hint {
  margin-top: var(--space-3);            /* 12px */
  padding-top: 10px;                     /* intentional: spec §四 線下距，space-2(8)/space-3(12) 間無對應 token */
  border-top: 1px solid var(--border-subtle);
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);                   /* 8px */
}
.qb-subsection__hint-text {
  font-family: var(--font-zh-sans);
  font-size: 12px;                       /* intentional: spec §四 註腳字級，text-xs(11)/text-sm(13) 間無對應 token */
  color: var(--text-muted);
  line-height: 1.75;
  letter-spacing: 0.01em;
  text-wrap: pretty;
}

/* ── Row ── */
.qb-row {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  overflow: hidden;
  transition: border-color var(--duration-fast) var(--ease-out);
}
.qb-row:hover,
.qb-row.is-expanded { border-color: var(--border-default); }

.qb-row__head {
  width: 100%;
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-4);
  text-align: left;
  background: transparent;
  border: 0;
  cursor: pointer;
}
.qb-row__glyph {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  margin-top: 3px;
  color: var(--text-muted);
}
.qb-row__main {
  flex: 1;
  min-width: 0;
  display: block;
}
.qb-row__title-line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.qb-row__title {
    /* 內容是中文為主的課標題 —— 字體決定 、。 的位置（實測：中文字體置中 0.49、日文字體靠下 0.87）。
     ⚠️ 這裡與 .sources-summary 的方向**相反**，不可一律換成同一種。2026-08-07 */
  font-family: var(--font-zh-serif);
  font-size: var(--text-body-lg);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: var(--text-primary);
}
/* pdf 課程序號「第 N 課」：灰色小字，低調。
   刻意 muted（非 accent）—— 上方本次/下次小卡的文法重點用 accent「01 02 03」，
   課次若也用同色同格式會混淆兩者語意。muted + 中文「第 N 課」雙重區隔。 */
.qb-row__num {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-muted);
  align-self: center;
  white-space: nowrap;
  flex: 0 0 auto;
}
.qb-row__count {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.qb-row__src {
  font-family: var(--font-en-display);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.qb-row__topics {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin-top: var(--space-2);
}
.qb-row__topics:empty { display: none; }
.qb-topic {
  font-family: var(--font-jp-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.01em;
  color: var(--text-secondary);
}
.qb-row__last {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  letter-spacing: 0.02em;
  color: var(--text-muted);
}
.qb-row__right {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
  padding-top: 1px;
}
.qb-row__chevron {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  color: var(--text-muted);
  transition: transform var(--duration-base) var(--ease-out);
}
.qb-row.is-expanded .qb-row__chevron { transform: rotate(180deg); }

/* ── Inline action strip ── */
.qb-row__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border-subtle);
  animation: qbExpand var(--duration-base) var(--ease-entry) both;
}
/* 題庫動作列的按鈕直接用共用系統：現在就練/排到下次 → .btn-secondary，
   移出題庫 → .btn-ghost。原本的 .qb-action-btn / --subtle 已收斂掉。 */
/* 設計的 ActionBtn 有 0.02em 字距 —— 但每日練習的 followup/reveal/retry 都沒有，
   所以字距只屬於題庫動作列，加在這裡的 scope，不污染共用的 .btn-secondary。 */
.qb-row__actions .qb-action-btn { letter-spacing: 0.02em; }
.qb-action-hint {
  font-family: var(--font-zh-sans);
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.qb-action-divider {
  width: 1px;
  height: 18px;
  margin: 0 2px;
  background: var(--border-subtle);
}

/* ⚠️ `.modal--compact` 的內距規則**全部收在一處**（見上方「確認型對話框」區塊）。
   這裡原本另有一條 `.modal--compact .modal-body { gap }` —— 同一個變體的定義散在
   兩個地方，改的人只會看到其中一份。2026-08-15 併過去了。 */
.qb-modal-note {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--text-muted);
  margin: 0;
}
.qb-modal-lede {
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  line-height: 1.75;
  color: var(--text-secondary);
  /* 段間距交給 .modal-body 的 flex gap；自己不帶 margin，否則會與 gap 疊加。 */
  margin: 0;
}
.qb-modal-lede .qb-modal-source {
  font-family: var(--font-jp-serif);
  color: var(--text-primary);
}

/* ── Empty state ── */
.qb-empty {
  padding: var(--space-7) 0;
  text-align: center;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

@keyframes qbFade {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes qbExpand {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
  .qb-preview-grid { grid-template-columns: 1fr; }
}

/* ══ AI 回覆失敗：錯誤區塊 ══════════════════════════════════════════════════
   設計來源：Claude Design `RetryAction.jsx` 的 AiErrorNotice（唯一視覺規格）。
   幾何值取自設計檔；專案無對應 token 者用 literal + intentional 註記（見 CLAUDE.md），
   不以「最接近的 token」頂替 —— 1–2px 的偏差會在同一元件內累積成可見的不協調。
   紅色預算：用 --status-wrong（磚紅）**不是** --accent（珊瑚），兩者不搶。 */
.ai-error {
  flex: 1;
  min-width: 0;
  border-left: 2px solid var(--status-wrong);
  background: var(--status-wrong-bg);
  border-radius: 4px 12px 12px 4px;   /* intentional: RetryAction.jsx，左側切齊實線邊 */
  padding: 12px 16px 14px;            /* intentional: RetryAction.jsx */
  animation: hibiRetryIn 240ms var(--ease-entry) both;
}
.ai-error__title {
  margin: 0 0 6px;                    /* intentional: RetryAction.jsx */
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--status-wrong);
}
.ai-error__title svg { flex: 0 0 auto; }
.ai-error__detail {
  margin: 0 0 12px;                   /* intentional: RetryAction.jsx */
  font-family: var(--font-zh-serif);
  font-size: 13.5px;                  /* intentional: RetryAction.jsx，介於 --text-sm 與 --text-base */
  line-height: 1.75;
  color: var(--text-secondary);
  text-wrap: pretty;
}
.ai-error__actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.ai-error__retry {
  height: 32px;
  padding: 0 14px;                    /* intentional: RetryAction.jsx */
  border-radius: 12px;
  background: transparent;
  border: 1px solid var(--status-wrong);
  color: var(--status-wrong);
  font-family: var(--font-zh-sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;                           /* intentional: RetryAction.jsx */
  transition: transform 120ms var(--ease-out), border-color 120ms var(--ease-out);
}
.ai-error__retry:hover { transform: translateY(-1px); }
/* icon 不宣告 color —— 繼承 currentColor 才會與文字連動（比照 .meta-action 的規則） */

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

/* ══ 小螢幕阻擋層（Viewport Guard）══════════════════════════════════════════
   登入後的 app 是「練習＋教練」並排版面，視窗一窄兩欄就互相擠壓（規格實測
   1180px 起溢出）。<1200px 一律遮住整個 app，請使用者換寬螢幕。

   ⚠️ **這段 CSS 被 4 個 template 共用（index/landing/login/onboarding），
      但 `#vp-guard` 的 DOM 只寫在 index.html** —— 其餘頁面沒有那個節點，
      這段規則對它們完全沒有作用。行銷頁與登入頁**結構上不可能被誤擋**，
      不需要 URL 白名單（白名單會隨頁面增加而腐化）。

   ⚠️ 顯示與捲動鎖**全部交給 media query**，不經 JS 加 class ——
      少一個會與 CSS 不同步的狀態。JS 只負責「目前寬度」數字與 inert。

   ⚠️ 斷點 1199 = 文案裡的最低需求 1200 減 1。CSS 吃不到 JS 變數，這兩個數字
      只能各寫一份；由 tests/test_viewport_guard.py 斷言兩者永遠相差 1。 */
#vp-guard {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  background: var(--bg-base);          /* 不透明、非 glass：glass 僅限 tooltip/FAB/banner/dropdown/scrim */
  align-items: center;
  justify-content: center;
  padding: 32px;
  text-align: center;
}
#vp-guard .vg-box {
  max-width: 440px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
#vp-guard .vg-mark {
  width: 26px;
  height: 26px;
  color: var(--accent);
  margin-bottom: 24px;
}
#vp-guard .vg-mark svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
#vp-guard h1 {
  margin: 0;
  font-family: var(--font-zh-serif);
  font-size: 24px;
  font-weight: 500;
  line-height: 1.6;
  letter-spacing: .01em;
  color: var(--text-primary);
}
#vp-guard p {
  margin: 16px 0 0;
  font-family: var(--font-zh-sans);
  font-size: 14px;
  line-height: 1.9;
  color: var(--text-secondary);
  text-wrap: pretty;
}
#vp-guard .vg-spec {
  margin-top: 28px;
  padding-top: 16px;
  border-top: 1px solid var(--border-subtle);
  width: 100%;
  display: flex;
  justify-content: center;
  gap: 28px;
  font-family: var(--font-en-mono);
  font-size: 12px;
  color: var(--text-muted);
  font-feature-settings: "tnum" 1;
}
#vp-guard .vg-spec b {
  font-weight: 400;
  color: var(--text-primary);
}

@media (max-width: 1199px) {
  #vp-guard { display: flex; }
  html, body { overflow: hidden; }
}

/* ══ [hidden] 護欄 ═══════════════════════════════════════════════════════════
   `[hidden]` 靠的是 UA stylesheet 的 `display: none`，**author 端任何 `display`
   都會蓋掉它**。所以「有明確 display 的 class」只要會被 `hidden` 控制，就一定要
   在這裡補一條，否則它**該藏的時候藏不住**。

   ⚠️ 這不是理論：2026-08-12 使用者回報「生成中文案偏上」，查出來是
   `.tts-gen__tracks` 明明 `hidden` 卻仍佔位（column gap 照算，上 17px / 下 29px）。
   窮舉之後發現**同一形狀有 6 個**，其餘 5 個在題庫與 chat（實測 computed display
   都是 `flex`，也就是 template 裡寫了 `hidden` 卻藏不住）。

   ⚠️ **2026-08-21 又發生一次**，而且測試是綠的 —— 因為掃描器有**第三個盲點**：
   `createElement` 建出來、再設 `className` 與 `hidden` 的元素它認不出來。
   後果：`.shadow-regen__act` 的 `hidden` 完全沒作用，畫面上出現一顆**沒有文字的
   圓形按鈕**（使用者截圖回報「這裡按鈕怎麼長這樣」）。掃描器已補上那一種，
   並加了**自證測試**（`test_the_scanner_sees_dynamically_built_elements`）。

   ⚠️ 新增「用 hidden 控制、且 CSS 有 display」的元素時，掃描指令：
   見 tests/test_hidden_attribute_guard.py（那條測試會直接把漏掉的抓出來）。 */
.chat-empty__expand-btn[hidden] { display: none; }
.qb-cancel-btn[hidden] { display: none; }
.qb-marker[hidden] { display: none; }
.qb-preview__vocab[hidden] { display: none; }
.qb-row__actions[hidden] { display: none; }
.shadow-regen__act[hidden] { display: none; }

/* ══════════════════════════════════════════════════════════════════════════════
   我的點數（R1）
   設計交付（唯一視覺規格）：CreditsApp.jsx / CreditsBalance.jsx / CreditsLedger.jsx
   逐項對照表：`暫存檔/DESIGN_AUDIT_R1.md`（實作後雙邊量測）

   ⚠️ 有 token 的用 token；設計有明確數值而專案無對應 token 時，用 literal ＋
      `intentional:` 註明出處（既有慣例）。**禁止用「最接近的 token」頂替** ——
      1–2px 的偏差會累積成可見的不協調（專案踩過：同一元件內三個值各差 1–2px）。
   ══════════════════════════════════════════════════════════════════════════════ */

.credits-body { background: var(--bg-base); color: var(--text-primary); }

/* 我的點數：寬度與內距走共用的 `.page-frame--single`（與題庫同一份）。
   ⚠️ **刻意與設計稿不同**（設計 `CreditsApp.jsx` 是 1040 / 48 32 120）——
      使用者裁決：「應一致」。同為單欄頁，對照組是**題庫**不是設計稿。 */
.credits { }

.credits-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 20px; margin-bottom: 28px;   /* intentional: CreditsApp.jsx（--space-5 是 24，不是這裡要的 20）*/
}
.credits-head h1 {
  margin: 0; font-family: var(--font-zh-serif);
  font-size: 30px;            /* intentional: CreditsApp.jsx（介於 h2 24 與 h1 32 之間）*/
  font-weight: 500; line-height: 1.2; letter-spacing: 0.01em;
  color: var(--text-primary);
}
.credits-sub a, .crules a { color: var(--accent); text-decoration: none; }  /* intentional: .legal a 同一慣例 */
.credits-sub a:hover, .crules a:hover { text-decoration: underline; text-underline-offset: 3px; }
.credits-sub {
  margin: 13px 0 0;           /* intentional: CreditsApp.jsx */
  font-family: var(--font-zh-sans); font-size: var(--text-sm);
  line-height: 1.6; color: var(--text-muted);
}

/* 購買動線 —— 第二階段才接，先做佔位。
   ⚠️ **不用 `cursor: not-allowed`**：專案既有慣例（全站 disabled 已移除它）。 */
.credits-buy { display: inline-flex; align-items: center; gap: 10px; }  /* intentional: CreditsBalance.jsx */
.credits-buy__btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: 40px; padding: 0 20px; border-radius: var(--radius-md);   /* intentional: BuyButton size=md */
  border: 1px solid var(--border-subtle); background: transparent;
  color: var(--text-muted); font-family: var(--font-zh-sans);
  font-size: 14px;            /* intentional: CreditsBalance.jsx BuyButton size=md */
  line-height: 1;
}
.credits-buy__mk { display: inline-flex; }
.credits-buy__soon {
  font-family: var(--font-zh-sans);
  font-size: 11.5px;          /* intentional: CreditsBalance.jsx */
  letter-spacing: 0.04em; color: var(--text-muted); white-space: nowrap;
}

/* ── 餘額面板 ──────────────────────────────────────────────────────────────── */
/* 中間那條分隔線是一個 **grid 欄**（1px），不是 border —— 設計如此，
   而且用 border 會讓兩欄的 padding 算法不同。 */
.cbal {
  display: grid; grid-template-columns: minmax(0, 44fr) 1px minmax(0, 56fr);
  background: var(--bg-surface); border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-card);
  margin-bottom: 48px;        /* intentional: CreditsApp.jsx 面板與紀錄的距離 */
}
/* 🔴 **有提示帶時，它上下的間隙必須相等**（使用者裁決 2026-08-28）——
   讓提示帶看起來浮在餘額面板與使用紀錄的正中間。
   ⚠️ **與設計交付不同**：`CreditsApp.jsx` 把餘額面板與提示帶放進同一個
      `flex column` 且 `gap: 18`、整組再與使用紀錄隔 `48`（＝上緊下鬆，
      讓提示帶「黏著」它在講的那張面板）。使用者看過兩種之後選了置中。
      見 `docs/ROADMAP.md`〈與設計交付的偏離〉。
   ⚠️ **這一組兩個值必須一起改**：`.cbal:has(+ .cnotice)` 的下方間距與
      `.cnotice` 的下方間距是**同一條規則的兩半**（上間隙／下間隙），
      只改一邊就是「相等」這個不變式被破壞，而畫面上只差幾 px、
      不會有任何測試因為「值變了」而紅 ——
      🔒 `tests/test_credit_notice_spacing.py` 釘的正是「兩者相等」。
   ⚠️ 用 32（`--space-6`）不是 36：36 不在間距級距上（4/8/12/16/24/32/48/64/96），
      而規則 12 禁止硬寫間距數值。32 是級距上最接近原本 48／24 中間值的一階。 */
.cbal:has(+ .cnotice) { margin-bottom: var(--space-6); }
.cbal__rule { background: var(--border-subtle); }
.cbal__total { padding: 30px 34px 28px; }   /* intentional: CreditsBalance.jsx */
.cbal__label {
  font-family: var(--font-zh-sans); font-size: var(--text-sm);
  color: var(--text-muted); letter-spacing: 0.04em;
}
.cbal__big { display: flex; align-items: baseline; gap: 10px; margin: 10px 0 0; }  /* intentional: CreditsBalance.jsx */
.cbal__num {
  font-family: var(--font-en-display);
  font-size: 64px;            /* intentional: CreditsBalance.jsx 大數字 */
  line-height: 1; font-weight: 400; letter-spacing: -0.015em;
  color: var(--text-primary); font-feature-settings: "lnum" 1;
}
.cbal__unit {
  font-family: var(--font-zh-sans);
  font-size: 15px;            /* intentional: CreditsBalance.jsx（= --text-body）*/
  color: var(--text-secondary);
}
.cbal__order {
  margin: 22px 0 0; max-width: 300px;   /* intentional: CreditsBalance.jsx */
  font-family: var(--font-zh-sans);
  font-size: 12.5px;          /* intentional: CreditsBalance.jsx */
  line-height: 1.85; color: var(--text-muted);
}

.cbal__rows {
  padding: 26px 34px 24px;    /* intentional: CreditsBalance.jsx */
  display: flex; flex-direction: column; justify-content: center;
}
.cbal__row { display: flex; align-items: center; gap: var(--space-3); }
/* ⚠️ 永久列有底線、期限列沒有 —— 設計如此（期限列底下還會接批次展開）。 */
.cbal__row--perm { padding: 13px 0; border-bottom: 1px solid var(--border-subtle); }
.cbal__row--exp  { padding: 13px 0 0; }
.cbal__mk { display: flex; color: var(--text-primary); }
.cbal__name { font-family: var(--font-zh-sans); font-size: 14px; color: var(--text-primary); }
.cbal__amt {
  margin-left: auto; font-family: var(--font-en-display);
  font-size: 20px;            /* intentional: CreditAmount size=20 */
  letter-spacing: 0.01em; font-feature-settings: "lnum" 1; color: var(--text-primary);
}
.cbal__amt.is-zero { color: var(--text-muted); }
.cbal__none {
  margin: 8px 0 0 32px;       /* intentional: 32 ＝ 符號 20 ＋ gap 12，對齊文字欄 */
  font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-muted);
}

/* ── 期限點數批次（R3）──────────────────────────────────────────────────────
   ⚠️ **一律中性色**（`--text-muted` / `--text-secondary`），即使快到期也不變紅
   ——使用者裁決 2026-08-27（方案 B）：警示只由 `.cnotice--expiring` 提示帶
   承擔，這裡跟設計原案（7 天內走 `--status-wrong`）刻意不同，
   見 docs/ROADMAP.md〈與設計交付的偏離〉。 */
.cbal__soon {
  font-family: var(--font-zh-sans);
  font-size: 11.5px;          /* intentional: CreditsBalance.jsx soonest 那段 */
  color: var(--text-muted);
}
.cbal__toggle {
  display: block; background: transparent; border: 0; cursor: pointer;
  padding: 8px 0 0; margin-left: 32px;   /* intentional: 32 ＝ 符號 20 ＋ gap 12，對齊文字欄 */
  font-family: var(--font-zh-sans);
  font-size: 11.5px;          /* intentional: CreditsBalance.jsx */
  color: var(--text-muted); letter-spacing: 0.04em;
  text-decoration: underline; text-underline-offset: 3px;
}
.cbal__batches { margin: 10px 0 0 32px; }
.cbal__batch {
  display: flex; align-items: baseline; gap: 10px; padding: 7px 0;
  font-family: var(--font-zh-sans);
  font-size: 12.5px;          /* intentional: CreditsBalance.jsx */
  color: var(--text-secondary);
}
.cbal__batch + .cbal__batch { border-top: 1px solid var(--border-subtle); }
.cbal__batch-amt {
  font-family: var(--font-en-display);
  font-size: 15px;            /* intentional: CreditsBalance.jsx */
  font-weight: 400; color: inherit;
}
.cbal__batch-date {
  margin-left: auto; font-family: var(--font-en-display); letter-spacing: 0.04em;
}
.cbal__batch-left { min-width: 56px; text-align: right; }

/* ── 使用紀錄 ──────────────────────────────────────────────────────────────── */
.cledger__head { display: flex; align-items: flex-end; gap: 26px; margin-bottom: 22px; }
.cledger__head h2 {
  margin: 0; font-family: var(--font-zh-serif);
  font-size: 21px;            /* intentional: CreditsLedger.jsx */
  font-weight: 500; color: var(--text-primary);
}
.cledger__tabs { display: flex; gap: 20px; align-items: center; margin-bottom: 2px; }  /* intentional: CreditsLedger.jsx */
.cledger__tab {
  position: relative; background: transparent; border: 0; cursor: pointer;
  padding: var(--space-1) 0; font-family: var(--font-zh-sans);
  font-size: var(--text-sm); letter-spacing: 0.04em; line-height: 1;
  color: var(--text-secondary); font-weight: 400;
}
.cledger__tab.is-on { color: var(--text-primary); font-weight: 500; }
.cledger__tab.is-on::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -5px;
  height: 2px; background: var(--accent);
}
.cledger__count {
  margin-left: auto; font-family: var(--font-zh-sans);
  font-size: 12px; color: var(--text-muted);
}
.cledger__n { font-family: var(--font-en-display); font-size: var(--text-sm); }

.clday { margin-bottom: 30px; }
.clday__head { display: flex; align-items: center; gap: var(--space-3); padding: 0 18px 10px; }
.clday__date {
  font-family: var(--font-en-display);
  font-size: 12.5px;          /* intentional: CreditsLedger.jsx */
  letter-spacing: 0.08em; color: var(--text-secondary);
}
.clday__wd { font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-muted); }
.clday__wd.is-today { color: var(--text-secondary); }
.clday__rule { flex: 1; height: 1px; background: var(--border-subtle); }
.clday__sum { font-family: var(--font-zh-sans); font-size: 12px; color: var(--text-muted); }
.clday__n { font-family: var(--font-en-display); font-size: var(--text-sm); }
.clday__n.is-plus { color: var(--status-correct); }
.clday__card {
  border: 1px solid var(--border-subtle); border-radius: var(--radius-lg);
  background: var(--bg-surface); padding: 6px; box-shadow: var(--shadow-card);
}

.clrow {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 15px 18px;         /* intentional: CreditsLedger.jsx */
  border-radius: 10px;        /* intentional: CreditsLedger.jsx（介於 sm 6 與 md 12）*/
  background: transparent; transition: background var(--duration-fast) var(--ease-out);
}
.clrow:hover { background: var(--bg-subtle); }
.clrow + .clrow { border-top: 1px solid var(--border-subtle); }
.clrow__mk { display: flex; gap: var(--space-1); padding-top: 1px; flex: none; color: var(--text-primary); }
.clrow__body { min-width: 0; flex: 1; }
.clrow__name {
  display: block; font-family: var(--font-zh-sans); font-size: 14px;
  line-height: 1.4; color: var(--text-primary);
}
.clrow__meta {
  display: block; margin-top: 5px; font-family: var(--font-zh-sans);
  font-size: 12px; line-height: 1.6; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.clrow__time { font-family: var(--font-en-display); letter-spacing: 0.04em; }
.clrow__dot { color: var(--border-default); padding: 0 7px; }
.clrow__after { font-family: var(--font-en-display); font-size: 12.5px; }
.clrow__amt {
  flex: none; display: inline-flex; align-items: baseline; gap: var(--space-1);
  padding-top: 1px; color: var(--text-primary);
}
.clrow__amt.is-plus { color: var(--status-correct); }
.clrow__n {
  font-family: var(--font-en-display);
  font-size: 19px;            /* intentional: CreditsLedger.jsx 金額 */
  letter-spacing: 0.01em; font-feature-settings: "lnum" 1;
}
.clrow__unit { font-family: var(--font-zh-sans); font-size: 11.5px; }
/* 到期那一筆：劃線 ＋ muted（設計 §08）。R3 已上線，`credits.expire_batch()` 真的會寫這筆。 */
.clrow.is-expired .clrow__mk,
.clrow.is-expired .clrow__amt { color: var(--text-muted); }
.clrow.is-expired .clrow__amt { text-decoration: line-through; }
.clrow.is-expired .clrow__name { color: var(--text-secondary); }

/* 空狀態 —— **三種文案各自對應一個篩選**，不共用一個通用容器
   （專案硬規則：每個空狀態只對應一個語意元件）。 */
.clempty {
  border: 1px solid var(--border-subtle); border-radius: var(--radius-lg);
  background: var(--bg-surface); padding: 54px 34px; text-align: center;
}
.clempty__mk { display: inline-flex; color: var(--text-muted); margin-bottom: 18px; }
.clempty__main {
  margin: 0; font-family: var(--font-zh-serif);
  font-size: var(--text-body-lg); color: var(--text-secondary);
}
.clempty__sub {
  margin: 10px 0 0; font-family: var(--font-zh-sans); font-size: var(--text-sm);
  line-height: 1.8; color: var(--text-muted);
}

/* R2：「載入更早的紀錄」—— 只在後端說 `has_more` 時渲染，空字串＝零筆時
   什麼都不畫，容器本身不佔位（不用固定高度撐版面）。 */
.cledger__more { display: flex; justify-content: center; padding-top: 6px; }  /* intentional: CreditsLedger.jsx */
.cledger__more-btn {
  background: transparent; border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md); padding: 0 20px; height: 40px; cursor: pointer;  /* intentional: CreditsLedger.jsx */
  font-family: var(--font-zh-sans); font-size: var(--text-sm); color: var(--text-secondary);
}
.cledger__more-btn:hover { border-color: var(--border-default); }
.cledger__more-btn:disabled { opacity: 0.5; cursor: default; }

/* 導覽列的餘額 chip —— 走**墨色不走紅**（紅留給 CTA 與品牌標，維持 navbar 安靜）。 */
/* 餘額 chip —— 外框走 `.nav-pill`（與等級同一份），這裡只放它自己的內容。
   ⚠️ 字級與符號尺寸**比設計小**（設計 14／16）：藥丸只有 23px 高，
      塞 16px 的符號會把外框撐開、也會比旁邊的等級重一階。使用者裁決 2026-08-25：
      「我怕字和 icon 會太大，這部分也做調整」。 */
.nav-credits {
  display: inline-flex;
  align-items: center;
  gap: 5px;                    /* intentional: 23px 藥丸內的節奏（設計是 7）*/
  padding: 0 9px;              /* intentional: 比等級的 11 窄，因為多了一個符號 */
  transition: border-color var(--duration-fast) var(--ease-out);
}
/* ⚠️ hover **只深一階邊框，不做 transform**（設計明訂）。
   ⚠️ 靜止已經是 `--border-default`（跟等級一樣）⇒ hover 往**文字色**再深一階，
      而不是回到更淺的 subtle（那會變成「hover 反而更不明顯」）。 */
.nav-credits:hover { border-color: var(--text-muted); }
.nav-credits__n {
  font-family: var(--font-en-display);
  font-size: 12px;             /* intentional: 設計 14；藥丸 23px 高，14 會太重 */
  letter-spacing: 0.02em;
  color: var(--text-primary);
  font-feature-settings: "lnum" 1;
  /* 視覺置中：數字**沒有下伸部**，但字體的 descent 仍佔位 → 墨跡整團偏上
     （實測 −1.13px，與 `.nav-level` 未校正前的 1.30px 同一個病）。
     🔴 **補償量必須是單一來源**（`--pill-cap-nudge`）—— 專案踩過：
        只修了 `.level-badge`、導覽列的 `.nav-level` 仍偏上，同一條規則存在兩份。
     ⚠️ 這裡用 `translateY` 不是「上下 padding 一加一減」—— 它是 **flex item**，
        改 padding 會動到藥丸的內距而不是把字推下去。
     ⚠️ 用 `em` 才會隨字級縮放（改字級不必重算）。
     ⚠️ 符號**不補**：它是幾何圖形、沒有 baseline 問題（實測偏移 0）。 */
  transform: translateY(var(--pill-cap-nudge));
}

/* 成本標（動作前，`CreditCost` 設計）／已扣回執（動作後，`CreditSpent` 設計）——
   R2：兩者都是**唯讀顯示**，數字來自 `/api/credits/rates`／回應的 `_credits`，
   前端不自己算。 */
.credit-tag {
  display: inline-flex; align-items: center; gap: 5px;
  color: var(--text-muted); white-space: nowrap;
}
.credit-tag__n {
  font-family: var(--font-en-display); font-size: 13px;  /* intentional: CreditGate.jsx size+1 */
  letter-spacing: 0.02em; line-height: 1;
  transform: translateY(var(--credit-num-nudge));  /* 正值＝往下，見下方說明 */
}
.credit-tag__u { font-family: var(--font-zh-sans); font-size: 12px; line-height: 1; }

.credit-receipt {
  display: inline-flex; align-items: center; gap: 5px;  /* intentional: CreditGate.jsx */
  margin: var(--space-2) 0 0; color: var(--text-muted);
  font-family: var(--font-zh-sans); font-size: 12px; line-height: 1;
}
.credit-receipt b {
  font-family: var(--font-en-display); font-size: 13px; font-weight: 500;
  color: var(--text-secondary);
  transform: translateY(var(--credit-num-nudge));  /* 正值＝往下，見下方說明 */
}
/* 🩸 **icon 跟文字沒對齊，前三輪都只對了一半**（2026-08-26 使用者四輪實測回報，
   附截圖）。

   **第一輪**假設「baseline 對齊本身沒問題，問題只在 SVG 沒有基線可用」，
   只把 icon 改 `align-self: center`——對照實驗推翻：「1」（`--font-en-display`）
   與「點」（`--font-zh-sans`）兩個文字用不同字體，baseline 對齊本身就讓
   兩者的視覺墨跡中心對不齊，只修 icon 只會偏向其中一邊。

   **第二輪**改成整排 `align-items: center`（line-box 置中）——中文字與符號對齊了
   （實測墨跡中心差 0.65px），但數字仍偏上 1.90px，使用者回報「數字還是歪的很離譜」。

   **第三輪**方向對了（校正數字本身），但**符號寫反**：我把 line-box 裡剩下的空白
   （13 − 9.04 − 0.13 = 3.83px）整團算到墨跡**上方**，於是得出「墨跡偏下、要往上推」
   —— 實際上 baseline 在 line-box 頂端下方 10px 處，空白是**上 0.96／下 2.87** 分配的，
   墨跡偏**上**。套 −0.14em 等於再往上推 1.82px，把 1.90px 的落差擴大成 3.72px，
   使用者第四次回報「你沒修好喔」。
   🔴 **教訓：字體度量不可以用減法推，要把 baseline 的位置算出來。**
      `fontBoundingBoxAscent/Descent` 給的是 baseline 到 line-box 上下緣的距離，
      `actualBoundingBoxAscent/Descent` 給的是 baseline 到墨跡上下緣的距離 ——
      兩者**共用 baseline 這個原點**，相減才是各自的留白。少了這一步，
      算出來的方向會剛好相反，而**數字看起來仍然很合理**。
   🔴 **而且我第三輪的「驗證」也沒抓到**：那次只比對元素的 `getBoundingClientRect()`
      中心（＝line-box 中心），而 line-box 中心正是這個 bug 裡**不能用**的那個基準
      —— 量的東西本身就是病灶，怎麼量都會顯示「已對齊」。
      這一輪改成**像素級量測**（10 倍放大截圖 → 逐列掃描找墨跡上下緣），
      那是唯一與「眼睛看到的」同一個東西的證據。

   **第四輪（現行）**：像素量測三者的墨跡中心，數字往**下** 0.146em。
   實測（10 倍放大截圖逐列掃描）：
     修正前 數字 vs 點 −3.95px、數字 vs 符號 −3.30px
     修正後 數字 vs 點 −0.25px、數字 vs 符號 +0.40px
   三種變體（成本標 13/12px、小工具 12.5/11.5px、已扣回執 13/12px）都驗過。
   ⚠️ **只補數字不補「點」**：`.credit-receipt` 的中文是純文字節點（`已扣 <b>5</b> 點`），
      沒有元素可以掛 transform，所以基準只能選「中文墨跡」而不是「行的中心」。
      代價是符號與數字仍差 0.40px（低於半個像素，實測看不出來）。
   ⚠️ 補償量必須是**單一來源**（`--credit-num-nudge`）——同一條規則存在兩份時，
      修一份、另一份沒有任何東西會提醒（`--pill-cap-nudge` 已經踩過）。 */

/* ══════════════════════════════════════════
   點數符號的動畫（R2，設計交付：CreditMark.jsx:41–52 的 `cm-motion` 區塊）
   ⚠️ **數值逐一對照設計**（700/900/420/520ms、scale .94、opacity .62、
      translateY -2px、dasharray 56）；只有排版展開成多行，值不得改。

   🔴 **只放「這一刀真的有觸發條件」的那幾個。** 設計那段還有 `cm-lift`
      （餘額面板某一列的符號輕抬），但**我們的「我的點數」頁是伺服器渲染、
      數字在頁面存活期間不會變** —— 設計那邊是 React 即時元件才有得抬。
      沒有觸發條件的動畫放進來就是「存在但從沒被執行到」，這個專案吃過那個虧
      （`source_fp`、`_shadow_replenish_plan` 的去重都是）。
      ⚠️ 延後決策｜「我的點數」頁改成會即時更新餘額時：`cm-lift` 要一起補，
         見 `docs/ROADMAP.md` §💠 點數系統〈與設計交付的偏離〉。

   🔴 **設計明訂的禁止清單仍然全部有效**：旋轉／鏡像／彈跳／閃爍／陰影／漸層／
      **常駐循環**。這裡每一個都是 `both` 或 `forwards` 的**單次**播放。
   🔴 **`prefers-reduced-motion` 一律要處理**（設計 CreditMark.jsx:49–52）——
      跟著動畫一起加，不留到最後補（補的時候一定會漏掉其中一個）。
══════════════════════════════════════════ */
@keyframes cm-tick {
  0%, 100% { transform: scale(1);   opacity: 1; }
  38%      { transform: scale(.94); opacity: .62; }
}
@keyframes cm-draw   { to { stroke-dashoffset: 0; } }
@keyframes cm-charge { to { opacity: 1; } }

/* 任何點數變化：内紋縮 6% 淡到 62%，播一次。
   ⚠️ `transform-box: fill-box` 不可省 —— SVG 內的 `transform-origin: center`
      預設是對**整個 viewBox**算，少了它内紋會從別的點縮放（看起來像在飄）。 */
.cm-tick .cm-flower {
  animation: cm-tick 700ms var(--ease-entry) both;
  transform-box: fill-box; transform-origin: center;
}

/* 規則頁 hero：六角描邊 900ms，内紋在 520ms 後補進來（420ms）。一 session 一次。 */
.cm-draw .cm-hex {
  stroke-dasharray: 56; stroke-dashoffset: 56;
  animation: cm-draw 900ms var(--ease-entry) forwards;
}
.cm-draw .cm-flower {
  opacity: 0;
  animation: cm-charge 420ms var(--ease-entry) 520ms forwards;
}

@media (prefers-reduced-motion: reduce) {
  .cm-tick .cm-flower { animation: none; }
  /* ⚠️ 描邊要**還原成完整的圖**，不是只把動畫關掉 ——
     只寫 `animation: none` 的話 `stroke-dashoffset: 56` 還在，六角會整個消失。 */
  .cm-draw .cm-hex    { stroke-dasharray: none; stroke-dashoffset: 0; animation: none; }
  .cm-draw .cm-flower { opacity: 1; animation: none; }
}

/* ══════════════════════════════════════════
   扣點規則頁（R2，設計交付：CreditRules.jsx）

   🩸 **這裡原本寫著 `.crules { max-width: 780px; padding: … }`，那是錯的。**
      理由當時寫成「設計本來就是 780，文字長需要窄一點」—— 而使用者 2026-08-26
      回報「左右撐滿頁面寬度和其他頁不同，應該要一樣」。
      這與 2026-08-25 的〈`.page-frame` 由來〉**是同一個回報，隔一天再犯一次**：
      新頁面自己宣告寬度 ⇒ 必然與既有的分歧。
   🔴 **寬度與內距一律走 `.page-frame--single`**（題庫／我的點數同一份），
      這裡只放這一頁自己的東西。🔒 `tests/test_page_frame.py`
══════════════════════════════════════════ */
.crules-head { margin-bottom: 44px; }  /* intentional: CreditRules.jsx header */
.crules-head__mk { display: flex; color: var(--accent); margin-bottom: var(--space-5); }
.crules-head h1 {
  margin: 0; font-family: var(--font-zh-serif); font-size: 30px;  /* intentional: CreditRules.jsx h1 */
  font-weight: 500; line-height: 1.2; letter-spacing: 0.01em; color: var(--text-primary);
}
.crules-head__sub {
  margin: 14px 0 0; font-family: var(--font-zh-sans); font-size: 13.5px;
  line-height: 1.8; color: var(--text-muted);
}

.crules-group { margin-bottom: var(--space-6); }  /* intentional: CreditRules.jsx section (40) */
.crules-group--last { margin-bottom: 0; }
.crules-group__head {
  display: flex; align-items: baseline; gap: var(--space-3);
  margin-bottom: 14px; padding: 0 2px;  /* intentional: CreditRules.jsx */
}
.crules-group__n {
  font-family: var(--font-en-display); font-size: var(--text-xs);
  letter-spacing: 0.1em; color: var(--accent);
}
.crules-group__head h2 {
  margin: 0; font-family: var(--font-zh-serif); font-size: var(--text-h3);
  font-weight: 500; color: var(--text-primary);
}

.crules-card {
  border: 1px solid var(--border-subtle); border-radius: var(--radius-lg);
  background: var(--bg-surface); box-shadow: var(--shadow-card); overflow: hidden;
}

.crules-row {
  display: flex; align-items: flex-start; gap: var(--space-5);
  padding: 17px 22px;  /* intentional: CreditRules.jsx row */
}
.crules-row + .crules-row { border-top: 1px solid var(--border-subtle); }
.crules-row__body { min-width: 0; flex: 1; }
.crules-row__name {
  display: block; font-family: var(--font-zh-sans); font-size: 14.5px;
  line-height: 1.4; color: var(--text-primary);
}
.crules-row__note {
  display: block; margin-top: var(--space-2); font-family: var(--font-zh-sans);
  font-size: 12.5px; line-height: 1.75; color: var(--text-muted);
}
.crules-row__price {
  flex: none; display: inline-flex; align-items: baseline; gap: var(--space-1);
  padding-top: 1px;  /* intentional: CreditRules.jsx */
}
.crules-row__unit { font-family: var(--font-zh-sans); font-size: 11.5px; color: var(--text-muted); margin-right: var(--space-1); }
.crules-row__n {
  font-family: var(--font-en-display); font-size: var(--text-h2);
  color: var(--text-primary); font-feature-settings: "lnum" 1;
}
.crules-row__u { font-family: var(--font-zh-sans); font-size: 11.5px; color: var(--text-secondary); }

.crules-free { padding: var(--space-5) 22px; display: flex; flex-direction: column; gap: 11px; }  /* intentional: CreditRules.jsx */
.crules-free__item {
  display: flex; align-items: center; gap: var(--space-3);
  font-family: var(--font-zh-sans); font-size: 13.5px; color: var(--text-secondary);
}
.crules-free__check { flex: none; color: var(--status-correct); }

.crules-how { display: flex; flex-direction: column; gap: var(--space-5); padding: var(--space-1) 2px; }  /* intentional: CreditRules.jsx (26) */
.crules-how h3 {
  margin: 0; font-family: var(--font-zh-sans); font-size: 13.5px;
  font-weight: 500; color: var(--text-primary); letter-spacing: 0.03em;
}
.crules-how p {
  margin: var(--space-2) 0 0; font-family: var(--font-zh-sans); font-size: var(--text-sm);
  line-height: 1.9; color: var(--text-secondary);
}

.crules-foot {
  margin: var(--space-7) 0 0; padding-top: var(--space-5);
  border-top: 1px solid var(--border-subtle);
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-5);
  font-family: var(--font-zh-sans); font-size: var(--text-xs); line-height: 1.8; color: var(--text-muted);
}
.crules-foot a { font-size: 12.5px; white-space: nowrap; }  /* intentional: CreditRules.jsx */

/* 透支提示帶（R2 裁決，設計未涵蓋——CreditsBalance.jsx 的示範資料全四位數，
   沒有任何負餘額狀態）。沿用設計已有的 `CreditNotice` 容器規格
   （PLAN §1.9：「沿用設計已有的位置，不新建元件」）。 */
.cnotice {
  display: flex; align-items: center; gap: var(--space-3); padding: 14px var(--space-5);  /* intentional: CreditsBalance.jsx noticeStyle */
  border: 1px solid var(--status-wrong); border-radius: var(--radius-md);
  background: var(--status-wrong-bg);
  /* ⚠️ 這個值必須與 `.cbal:has(+ .cnotice)` 的 `margin-bottom` 相同
     ——見那條規則上方的完整說明（提示帶上下等距，使用者裁決 2026-08-28）。 */
  margin-bottom: var(--space-6);
}
.cnotice__mk { display: flex; color: var(--status-wrong); }
/* 🔴 **提示帶的文字整句一個顏色**（使用者裁決 2026-08-28）。
   設計 `CreditNotice` 的 soon 分支把尾句染 `--text-muted`（曾實作為
   `.cnotice__sub`），使用者看過之後裁決不分色、到期日改成括號附註併進同一句。
   ⇒ `.cnotice__sub` 已隨那個 span 一起移除（**沒有消費者的樣式一律清掉**，
   留著會讓下一個人以為提示帶還有第二種顏色可用）。 */
.cnotice__text { font-family: var(--font-zh-sans); font-size: 13.5px; color: var(--text-primary); }

/* ══════════════════════════════════════════
   同意層（R4，設計交付：CreditGate.jsx）

   扣點前的同意小卡。使用者裁決（2026-08-28）：
   **背景用實心，不用 glass**——設計稿的註解寫「glass popover」，但實際
   `style` 是 `background: var(--bg-elevated)`，不是玻璃。同意層是要人讀清楚
   數字再做決定的地方，透明底會讓底下內容干擾判讀；且 code 比註解可信。
   ⚠️ 看到這裡沒用 `--glass-bg` 不是漏做，是裁決——不要「修」回玻璃。

   進場動畫沿用既有的 `accountMenuPop`（scale .96→1），不另寫一份。

   🔴 **定位是 `position:fixed` ＋ JS 算座標，不是純 CSS**（R4 §15，2026-08-29）：
      原本抄 `.nav-account-menu`（導覽列選單，永遠在畫面最上方，往下開一定
      放得下）做成 `position:absolute` ＋ 四個方向 class，但同意層的觸發鈕
      散落整個頁面，很多在畫面下半部——卡片因此會被畫到畫面外，使用者連
      按鈕都點不到（2026-08-29 使用者連續三輪回報「按了沒反應」）。
      改用 `static/float-position.js` 的 `_positionFloat()`——**專案既有**、
      讀音卡／翻譯卡／反白工具列本來就在用的那套：以畫面為基準、優先往上、
      放不下自動翻下、水平置中並夾在畫面內。方向不再由呼叫端猜，JS 執行
      時用 `trigger.getBoundingClientRect()` 自動判斷。
      ⇒ `.cg-trigger`（DOM 包裝層）與四個方向 class 已整支刪除。

   🔒 元件對照與硬規則見 `docs/design-decisions/`（若未來新增）／
      `docs/design/credits-r4-consent-gate.md` §11／§15（R4 定稿計畫）。
══════════════════════════════════════════ */
.cg-pop {
  position: fixed; z-index: 80;
  width: 296px;  /* intentional: CreditGate.jsx GatePopover 固定寬 */
  border-radius: var(--radius-lg);
  padding: 18px 18px 14px;  /* intentional: CreditGate.jsx GatePopover padding */
  text-align: left;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  box-shadow: var(--shadow-elevated);
  animation: accountMenuPop var(--duration-base) var(--ease-entry);
  transform-origin: top center;  /* R4 §15：方向 class 拿掉了，補一個固定預設值 */
}

@media (prefers-reduced-motion: reduce) {
  .cg-pop { animation: none; }
}

.cg-pop__head {
  display: flex; align-items: baseline; gap: 10px;  /* intentional: CreditGate.jsx gap:10，不在間距級距上 */
  margin-bottom: 10px;  /* intentional: CreditGate.jsx marginBottom:10，不在間距級距上 */
}
.cg-pop__mk { display: flex; transform: translateY(3px); color: var(--text-primary); }
.cg-pop__name {
  flex: 1; font-family: var(--font-zh-serif); font-size: 16px;  /* intentional: CreditGate.jsx */
  line-height: 1.45; color: var(--text-primary);
}
.cg-pop__cost {
  display: inline-flex; align-items: baseline; gap: 3px;  /* intentional: CreditGate.jsx */
  color: var(--text-primary);
}
.cg-pop__cost-n {
  font-family: var(--font-en-display); font-size: 19px;  /* intentional: CreditGate.jsx */
  transform: translateY(var(--credit-num-nudge));
}
.cg-pop__cost-u { font-family: var(--font-zh-sans); font-size: 11.5px; }  /* intentional: CreditGate.jsx */

.cg-pop__note {
  margin: 0; font-family: var(--font-zh-sans); font-size: 12.5px;  /* intentional: CreditGate.jsx */
  line-height: 1.8; color: var(--text-muted);
  text-wrap: pretty;
}
.cg-pop__note b {
  font-family: var(--font-en-display); font-size: 13.5px; font-weight: 400;  /* intentional: CreditGate.jsx */
  color: var(--text-secondary);
}

/* 「以後不再問，直接執行」——**不可用瀏覽器原生 checkbox**（設計稿的
   `accentColor` 樣式在深色底會露出原生方框，同後台 `.cr-in` 踩過的坑）。
   沿用專案既有的自繪勾選框（`.tts-parallel` 那一組結構），這裡另立
   class 名以貼合這個元件的語境，但**視覺規格逐一對齊那一組**。 */
.cg-pop__skip {
  display: flex; align-items: center; gap: 9px;  /* intentional: CreditGate.jsx */
  margin: 15px 0 0; cursor: pointer;  /* intentional: CreditGate.jsx */
  font-family: var(--font-zh-sans); font-size: 12.5px; color: var(--text-secondary);
}
.cg-pop__skip-input { position: absolute; opacity: 0; width: 0; height: 0; }
.cg-pop__skip-box {
  width: 15px; height: 15px;  /* intentional: CreditGate.jsx checkbox size */
  border-radius: 5px;  /* intentional: 同 .tts-parallel__box 既有慣例 */
  flex: 0 0 auto;
  border: 1.5px solid var(--border-default);
  background: transparent;
  display: flex; align-items: center; justify-content: center;
  transition: all var(--duration-fast) var(--ease-out);
}
.cg-pop__skip-box svg {
  width: 10px; height: 10px; fill: none; stroke: var(--accent-ink);
  stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
  opacity: 0;
}
.cg-pop__skip-input:checked + .cg-pop__skip-box {
  border-color: var(--accent); background: var(--accent);
}
.cg-pop__skip-input:checked + .cg-pop__skip-box svg { opacity: 1; }
.cg-pop__skip-input:focus-visible + .cg-pop__skip-box {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

.cg-pop__actions {
  display: flex; align-items: center; gap: 10px;  /* intentional: CreditGate.jsx GatePopover actions gap:10 */
  margin-top: 14px; padding-top: 13px;  /* intentional: CreditGate.jsx */
  border-top: 1px solid var(--border-subtle);
}
/* 🩸 **「關閉」／「取消」是純文字，不是按鈕**（2026-08-29 使用者回報：
   「mouse over 過去時，按鈕黑底形狀怪怪的」）。設計交付的 `ghostBtn` 是
   `background: transparent; border: 0`，**沒有底色、沒有邊框、沒有固定高度**。
   ⚠️ 改之前只覆寫了 `padding`，`.btn-ghost` 的 `height: 32px` 與 hover 底色
      （`--bg-subtle`）都還在 ⇒ 兩個字的按鈕被撐成 **32×32 的正方形**，
      hover 時就冒出一塊方形色塊。
   ⇒ 這裡把 `.btn-ghost` 的按鈕外觀整組關掉，只留文字。兩個變體（餘額不足的
     「關閉」、正常的「取消」）是**同一顆**，改這一條兩邊都好。 */
.cg-pop__actions .btn-ghost {
  margin-left: auto;
  padding: 6px 2px;   /* intentional: CreditGate.jsx ghostBtn */
  height: auto;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--text-muted);   /* intentional: CreditGate.jsx ghostBtn */
}
.cg-pop__actions .btn-ghost:hover:not(:disabled) {
  background: transparent;    /* 不要方形色塊——用字色變化表示 hover */
  color: var(--text-primary);
}
.cg-pop__link {
  font-family: var(--font-zh-sans); font-size: 13px; color: var(--accent);
  text-decoration: none;
}
.cg-pop__link:hover { text-decoration: underline; }
