/* ==========================================================================
   Korean Dictation Hub — giao diện tối giản
   Light: tone be, nền #F6EFE0 · Dark: nâu than ấm, nền #17130E
   Đổi mode bằng công tắc trong header (thuần CSS, không cần JS)

   ĐƠN VỊ: toàn bộ kích thước dùng rem (1rem = cỡ chữ gốc của trình duyệt,
   mặc định 16px). Nhờ vậy khi người dùng zoom hoặc đổi cỡ chữ mặc định thì
   mọi thứ — chữ, khoảng cách, chiều rộng, breakpoint — giãn nở cùng tỉ lệ,
   bố cục không đổi. Chỉ giữ px cho đường viền hairline 1px.
   ========================================================================== */

/* MẶC ĐỊNH LÀ DARK MODE. Bảng màu light nằm ở khối
   :root[data-theme="light"] phía dưới.

   Thuộc tính `data-theme` do đoạn script nội tuyến ở <head> đặt — nó đọc lựa
   chọn đã lưu TRƯỚC lần vẽ đầu tiên, nếu không thì mỗi lần đổi trang người
   dùng thấy loé một nhịp dark rồi mới sang light. `shell.js` giữ nó đồng bộ với
   công tắc trong header. Trước đây chọn bằng `:has(#theme-toggle:checked)`, tức
   trạng thái nằm trong một cái checkbox bị dựng lại mỗi lần vẽ header — nên đổi
   trang là mất. */
/* Bù phần chữ Việt mà bản dynamic-subset của Pretendard bỏ sót — xem
   assets/fonts/README.md và tools/font-subset.py. Cắt từ chính Pretendard nên
   cùng một mặt chữ, người đọc không nhận ra ranh giới; đổi tên họ vì OFL cấm
   dùng Reserved Font Name cho bản đã sửa đổi.

   `unicode-range` KHÔNG được thừa: khai rộng hơn thực tế thì trình duyệt lấy
   file này cho cả ký tự nó không có, và chữ đó biến mất thay vì rơi xuống
   Pretendard. Dải dưới đây do tools/font-subset.py in ra — sửa font thì chạy
   lại script rồi dán lại, đừng gõ tay. */
@font-face {
  font-family: "KDH Sans VN";
  font-weight: 45 920;
  font-style: normal;
  font-display: swap;
  src: url("../fonts/kdh-sans-vn.woff2") format("woff2-variations");
  unicode-range: U+00A6,U+00AF,U+00B5,U+00C1,U+00C3-00C5,U+00CC-00CD,U+00D1-00D3,U+00D5-00D6,U+00DA,U+00DD,U+00E1,U+00E3-00E5,U+00EC-00ED,U+00F1-00F3,U+00F5-00F6,U+00FA,U+00FD,U+0100-0110,U+0112-0125,U+0128-0130,U+0134-0137,U+0139-013E,U+0143-0148,U+014C-0151,U+0154-0165,U+0168-01C3,U+01C5-024F,U+1EA0-1EFF;
}

:root {
  color-scheme: dark;

  /* MỘT mặt chữ duy nhất trên mọi máy VÀ trên mọi bảng chữ: Pretendard lo cả
     Latin, tiếng Việt có dấu và 한글 trong cùng một họ, nên không còn cảnh
     ghép hai họ rồi lệch chiều cao chữ thường giữa dòng Việt và dòng Hàn.
     CỐ Ý KHÔNG dùng font hệ thống (-apple-system, "Segoe UI"…): stack đó để mỗi
     hệ điều hành tự chọn nên Mac vẽ bằng San Francisco còn Windows vẽ bằng
     Segoe UI + Malgun Gothic — khác chiều cao chữ thường, khác độ đậm, khác cả
     bề rộng, thành ra bố cục canh trên Mac sang Windows là lệch.
     "Pretendard Variable" là tên họ của bản variable; "Pretendard" đứng sau cho
     ai đã cài sẵn bản tĩnh trong máy. Font nạp bằng thẻ <link> ở <head> MỌI
     trang; sửa danh sách này thì sửa kèm ở đó.
     `sans-serif` cuối hàng chỉ là lưới an toàn cho lúc mạng chết. */
  --font: "KDH Sans VN", "Pretendard Variable", Pretendard, sans-serif;

  /* Nền & bề mặt — thứ bậc: nền → thanh play & footer → panel (nổi nhất) */
  --bg: #17130e;
  --surface: #241e17;
  --surface-2: #1e1912;
  /* Nền các ô nhập / nút phụ */
  --field: #1b1610;

  /* Viền */
  --line: #372e23;
  --line-dark: #4b4031;

  /* Chữ — trắng ngà ấm */
  --ink: #f2e8d9;
  --ink-2: #c2b3a0;
  --ink-3: #8d806f;

  /* Nhấn — be trầm. --on-accent là màu chữ đặt trên nền --accent, tách riêng
     khỏi --ink để không bị chữ sáng trên nền sáng khi đổi mode. */
  --accent: #d8b681;
  --accent-dark: #e6c795;
  --accent-soft: #2f2618;
  --on-accent: #221c14;
  /* CHỮ màu vàng đặt trên nền TRANG (--bg), tách khỏi --accent/--accent-dark vì
     hai màu kia sinh ra để TÔ NỀN, không để viết chữ: ở light mode
     `--accent-dark` (#e4bd85) nằm trên nền #f6efe0 chỉ được ~1.9:1, đọc không
     ra. Cặp này đo được 11.8:1 ở dark và 4.4:1 ở light. Dùng cho nhãn nhỏ và
     câu trích — thứ vẫn phải đọc được ở CẢ HAI mode. */
  --accent-ink: #e6c795;

  /* Kết quả */
  --ok: #86c08c;
  --ok-soft: #1d2a1e;
  --err: #e59175;
  --err-soft: #33201a;
  --warn: #d9b23c;
  --warn-soft: #332c10;
  --notify: #e88732;
  /* --ok/--err trầm để hợp tông nâu than cả site, nhưng lúc CHẤM đúng/sai (bài
     tập đục lỗ ở My Grammar › Luyện tập) thì đúng lúc người học cần thấy "tôi
     đúng rồi" / "tôi sai rồi" ngay lập tức — trầm quá thì hai màu nhìn gần
     giống nhau. Cặp "vivid" này chỉ dành riêng cho lúc chấm, không đụng --ok/--err
     ở các chỗ khác (tag, trái tim, trạng thái admin…). */
  --ok-vivid: #4ade80;
  --err-vivid: #f87171;

  /* Cỡ chữ */
  --fs-2xs: 0.71875rem;  /* 11.5px */
  --fs-xs: 0.75rem;      /* 12px   */
  --fs-sm: 0.78125rem;   /* 12.5px */
  --fs-md: 0.8125rem;    /* 13px   */
  --fs-lg: 0.84375rem;   /* 13.5px */
  --fs-xl: 0.875rem;     /* 14px   */
  --fs-body: 0.9375rem;  /* 15px   */
  --fs-2xl: 1rem;        /* 16px   */
  --fs-3xl: 1.0625rem;   /* 17px   */

  /* Phần NỘI DUNG (câu tiếng Hàn, bài chép, bản dịch, từ mới, ngữ pháp) to hơn
     phần khung (tiêu đề panel, nhãn, chú thích) đúng 5px ở cỡ chữ gốc 16px.
     Để dạng rem chứ không phải px cứng, để vẫn co giãn theo màn hình. */
  --fs-bump: 0.3125rem;  /* 5px */

  /* Nét ✓ dùng làm mặt nạ cho ô tick tự vẽ. Để ở :root vì đây là hình dùng
     chung, và vì chuỗi data-URI dài — nhét thẳng vào rule thì rule đó không
     còn đọc được. Nét nằm CHÍNH GIỮA khung 24×24, đừng sửa path mà quên kiểm
     lại điều đó. */
  --tick-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 13 4 4 10-10'/%3E%3C/svg%3E");
  --radius: 0.375rem;    /* 6px */
  /* Bo góc TO, chỉ cho thẻ nội dung lớn (thẻ ngữ pháp ở My Grammar). Khung
     chung của site giữ --radius 6px; đừng đổi hàng loạt sang cỡ này. */
  --radius-lg: 0.75rem;  /* 12px */
  /* Chiều cao chung của ba nút công cụ trên header — xem .header__tools */
  --ctl-h: 2.25rem;      /* 36px */
  --header-h: 3.75rem;   /* chiều cao header dính, dùng để chừa chỗ cho cột sticky */

  /* Bề ngang vùng nội dung — tính theo % màn hình, không neo pixel */
  --page-w: 92%;
  --col-gap: 2%;
  /* Khe giữa MỌI section của trang chi tiết Dictation — hai cột, các panel
     trong một cột, và các khối xếp dọc dưới .work. Một con số để mọi khe bằng
     nhau; đơn vị rem chứ không phải % vì % chỉ bằng nhau ở đúng một bề ngang. */
  --gap-section: 1.375rem;
}

/* ==========================================================================
   ĐỔI TAB DICTATION ↔ FULL TRANSCRIPT KHÔNG ĐƯỢC NHÁY

   Hai tab vẫn là hai file HTML thật (để máy tìm kiếm thấy đủ hai URL — docs/new-page.md),
   nhưng khi đã có JS thì bấm đổi tab KHÔNG tải lại trang: `setupTabSwitch()`
   trong dictation-details.js chỉ thay đúng `.work` và `#guide`. Header, breadcrumb, tên bài
   và chính thanh tab giữ nguyên DOM nên đứng im tuyệt đối — không chớp trắng,
   không nhảy, và bài đang gõ dở cũng không mất.

   Vì thế ở đây KHÔNG dùng @view-transition: nó dành cho trường hợp thật sự tải
   lại trang, và chính nó gây ra cảnh trắng màn hình một nhịp.
   ========================================================================== */

/* Chữ tĩnh trong HTML là bản tiếng Việt. Nếu người dùng đang chọn ngôn ngữ
   khác thì giấu nó cho tới khi i18n.js thay xong, để trang không loé lên chữ
   tiếng Việt rồi mới đổi — chính là cảnh "nút nháy ra ngôn ngữ khác".
   Giấu bằng visibility chứ không phải display: chỗ vẫn được giữ, bố cục không
   xô. Cờ này do đoạn script nội tuyến ở <head> đặt và dictation-details.js gỡ. */
[data-i18n-pending] [data-i18n],
[data-i18n-pending] [data-i18n-html] { visibility: hidden; }

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


/* font-size: 100% → tôn trọng cỡ chữ mặc định người dùng đặt trong trình duyệt.
   overflow-y/scrollbar-gutter → luôn chừa chỗ cho thanh cuộn, đổi tab giữa
   Dictation và Full Transcript không bị nhảy ngang. */
/* Cỡ chữ gốc co giãn theo bề ngang màn hình (≈15px ở mobile → 18px ở màn lớn).
   Mọi kích thước rem bên dưới bám theo nó, nên cả giao diện phóng to / thu nhỏ
   theo kích thước màn hình chứ không đứng yên ở một cỡ pixel cố định. */
html {
  font-size: clamp(93.75%, 0.8125rem + 0.35vw, 112.5%);
  -webkit-text-size-adjust: 100%;
  overflow-y: scroll;
}
@supports (scrollbar-gutter: stable) {
  html {
    overflow-y: auto;
    scrollbar-gutter: stable;
  }
}

body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

button, input, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }
a { color: var(--ink-2); text-decoration: none; }
a:hover { text-decoration: underline; }
/* Đổi màu chữ khi hover KHÔNG áp cho link mang class `.btn`.

   Link chạy chữ thì sáng lên khi rê chuột là đúng. Nhưng `<a class="btn">` là
   một cái NÚT: nút đã tự đặt màu chữ theo nền của nó, mà `a:hover` (0,1,1) đè
   được lên `.btn--primary` (0,1,0) — nên chữ gần đen `#221c14` trên nền be hoá
   trắng ngà `#f2e8d9` ngay lúc người ta rê vào để bấm, đọc gần như không ra.
   `.btn--ghost` thoát vì `.btn--ghost:hover` (0,2,0) tự đặt màu; `.btn--primary`
   thì rule hover của nó chỉ đổi nền nên không có gì chắn.

   Gạch chân vẫn giữ cho MỌI link, kể cả nút — đó là dấu hiệu bấm được. */
a:not(.btn):hover { color: var(--ink); }
ul { list-style: none; padding: 0; }

/* Ép `hidden` thắng MỌI rule display của site.

   Đã đo lại (2026-08): Chrome HIỆN NAY tự đặt `!important` cho [hidden] trong
   stylesheet mặc định, nên `.col{display:flex}` không đè được nữa — tức dòng
   này không còn sửa lỗi nào đang xảy ra trên Chrome. Giữ nguyên vì nó vẫn là
   lưới an toàn thật: sàn hỗ trợ của site có Safari 16+ và Firefox 121+
   (docs/ui.md), và ba màn hình giờ đổi trạng thái hoàn toàn bằng `hidden`
   (.exam, .exam-bar, .filters, hai tab của My Vocab) — thiếu !important ở một
   trình duyệt nào đó là cả màn bày ra chồng lên nhau. Đúng cái bẫy
   docs/quality.md cảnh báo: `element.hidden === true` vẫn đúng trong khi màn
   hình thì hiện đủ.

   ⚠ Vì có !important, đừng dùng `hidden` cho phần tử cần ẩn/hiện bằng animation. */
[hidden] { display: none !important; }
:focus-visible { outline: 2px solid var(--accent-dark); outline-offset: 2px; }

/* Bề ngang theo % màn hình — nội dung luôn lấp đầy màn hình theo cùng tỉ lệ,
   không bị khoá cứng ở một con số pixel rồi thừa lề hai bên khi zoom ra. */
.container {
  width: var(--page-w);
  margin-inline: auto;
}

/* ----------------------------------------------------------------- Header */
/* Dính trên cùng khi cuộn — nền đục nên nội dung không lộ ra phía sau */
.header {
  position: sticky;
  top: 0;
  z-index: 100;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.header__inner {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  min-height: var(--header-h);
}

/* Logo + tên site là cấp cao nhất của header — cố ý to và đậm hơn hẳn menu.

   flex-shrink: 0 + white-space: nowrap — BẮT BUỘC, đừng bỏ.
   Menu mỗi ngôn ngữ một độ dài (bản tiếng Việt/Anh dài hơn bản tiếng Hàn ~60px).
   Không khoá thì menu dài sẽ bóp logo lại, tên site vỡ 2–3 dòng và header cao
   lên — đổi sang tiếng Hàn là header tụt xuống thấp hơn, cả trang giật.
   Đo được: 84.2px (vi/en) so với 62.1px (ko) ở bề ngang 960px.
   Chỗ để menu xuống hàng riêng là breakpoint 75em ở cuối file. */
.logo {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  flex-shrink: 0;
  white-space: nowrap;
  color: var(--ink);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -.022em;
}
.logo:hover { text-decoration: none; color: var(--ink); }
/* Ô logo mang sẵn nền be và bốn góc bo TRONG chính hình vẽ (ICON.logo ở
   assets/js/layout.js, cùng một bản với favicon.svg) — nên ở đây KHÔNG tô nền
   accent và KHÔNG đặt màu chữ: tô nền là bốn góc bo của hình bị lấp phẳng,
   còn `color` thì hình không dùng tới vì màu viết cứng.
   Hình chiếm trọn ô (100%), không co lại như hồi nét monochrome nằm giữa ô. */
.logo__mark {
  width: 2.125rem;
  height: 2.125rem;
  flex-shrink: 0;
  display: grid;
  place-items: center;
}
.logo__mark svg { width: 100%; height: 100%; display: block; }

/* Menu là cấp phụ: nhỏ hơn, nhạt hơn, không đậm bằng logo */
.nav {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  margin-left: 1.75rem;
  font-size: var(--fs-lg);
}
.nav > a,
.navdrop__btn {
  white-space: nowrap;
  color: var(--ink-2);
  font-weight: 400;
}
.nav > a:hover { color: var(--ink); }
.nav .is-active { color: var(--ink); font-weight: 500; }

/* Dropdown trong menu — mở khi rê chuột vào, đóng khi rời khỏi vùng.
   padding-block khiến vùng hover của mục cha chạm sát mép trên của submenu,
   nhờ vậy di chuột từ mục cha xuống submenu không bị đứt quãng làm menu tắt. */
.navdrop {
  position: relative;
  padding-block: 0.4375rem;
}
.navdrop__btn {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}
.navdrop__btn:hover { color: var(--ink); }
.navdrop:hover > .navdrop__btn,
.navdrop:has(:focus-visible) > .navdrop__btn { color: var(--ink); }

/* Mở khi rê chuột, hoặc khi đi bằng bàn phím.
   Dùng :has(:focus-visible) chứ KHÔNG dùng :focus-within — vì bấm chuột cũng
   tạo focus, nên :focus-within sẽ giữ submenu mở mãi sau khi click, rời chuột
   cũng không tắt, rồi hover menu khác là hai submenu chồng lên nhau.
   :focus-visible chỉ khớp khi focus đến từ bàn phím, nên vẫn giữ được khả năng
   điều hướng bằng Tab mà không dính lỗi trên. */
.navdrop__menu { display: none; }
.navdrop:hover > .navdrop__menu,
.navdrop:has(:focus-visible) > .navdrop__menu,
/* `.is-open` do shell.js bật, và CHỈ trên máy không có chuột (điện thoại,
   tablet): ở đó không có sự kiện hover nào cả, nên nếu mục cha vừa không điều
   hướng vừa không mở được bằng chạm thì Listening Test và My Vocab thành hai
   màn không có đường vào. Máy có chuột thì bấm mục cha vẫn không làm gì. */
.navdrop.is-open > .navdrop__menu { display: block; }

/* Mục cha là <button> nên phải gỡ nền/viền mặc định và ép cỡ chữ theo menu —
   nút của trình duyệt mặc định có cỡ chữ riêng, không kế thừa từ .nav. */
.navdrop__btn--parent {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
}

/* Admin — tô khác một chút để thấy ngay đây là khu vực đặc quyền */
.navdrop--admin .navdrop__btn { gap: 0.3125rem; color: var(--accent-dark); }
.navdrop--admin .navdrop__btn:hover,
.navdrop--admin:hover > .navdrop__btn { color: var(--accent); }
.navdrop--admin .navdrop__menu { border-color: var(--accent-dark); }

/* ------------------------------------------- Nhóm điều khiển bên phải */
/* Ba nút ở góc phải header (ngôn ngữ · tài khoản/đăng nhập · sáng-tối) phải
   CAO BẰNG NHAU. Trước đây mỗi nút tự cao theo padding + cỡ chữ của nó nên ra
   ba con số lệch nhau (đo được 34,85 · 35,54 · 36,84px) — đủ để nhìn thấy là
   hàng nút không thẳng. Khoá chiều cao ở một chỗ, đừng chỉnh padding từng nút:
   padding còn đổi theo cỡ chữ gốc mỗi khi bề ngang màn hình đổi. */
.header__tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
}
.header__tools > .lang > .lang__btn,
.header__tools .acct__btn,
.header__tools .acct__login,
.header__tools #acct-slot > .btn,
.header__tools .theme-btn {
  /* `inline-flex` là BẮT BUỘC ở đây, không phải cho đẹp: nút "Đăng nhập" là thẻ
     <a class="btn">, mà `.btn` không khai display nên nó là inline — mà phần tử
     inline thì `height` KHÔNG có tác dụng. Thiếu dòng này nút cao 18px trong khi
     hai nút bên cạnh cao 36px. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--ctl-h);
  padding-block: 0;
}
/* Nút sáng-tối là nút vuông chỉ có icon — cao lên thì phải rộng theo, nếu
   không nó thành hình chữ nhật lệch hẳn khỏi hai nút bên cạnh. */
.header__tools .theme-btn { width: var(--ctl-h); }
/* Chữ chỉ dành cho trình đọc màn hình */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.icon { width: 1.05rem; height: 1.05rem; display: block; flex-shrink: 0; }
.icon--sm { width: 0.85rem; height: 0.85rem; }

/* Dropdown đa ngữ — dùng <details> nên mở/đóng được không cần JS.
   Cùng khuôn này là dropdown lọc trạng thái ở Dictation List (`.multi__*`, phần
   riêng nằm ở mục DICTATION LIST): khai chung selector ở đây để cả site chỉ có
   MỘT kiểu nút dropdown và MỘT kiểu panel — sửa một chỗ là hai nơi theo. */
.lang,
.multi { position: relative; }
.lang__btn,
.multi__btn {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.3125rem 0.625rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  font-size: var(--fs-md);
  color: var(--ink-2);
  cursor: pointer;
  white-space: nowrap;
  list-style: none;
}
.lang__btn::-webkit-details-marker,
.multi__btn::-webkit-details-marker { display: none; }
.lang__btn:hover,
.multi__btn:hover { border-color: var(--accent-dark); color: var(--ink); }
.lang[open] .lang__btn,
.multi[open] .multi__btn { border-color: var(--accent-dark); color: var(--ink); }
.lang[open] .lang__caret,
.multi[open] .multi__caret { transform: rotate(180deg); }
.lang__caret,
.multi__caret { transition: transform .15s; }

/* Panel bật ra — dùng chung cho nút đa ngữ, menu Practice và dropdown lọc
   trạng thái của Dictation List */
.lang__menu,
.navdrop__menu,
.multi__menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  z-index: 60;
  min-width: 9.5rem;
  padding: 0.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / .5);
}
.lang__menu { right: 0; }
/* Sát mép dưới mục cha để đường di chuột không bị hở */
.navdrop__menu { left: 0; top: 100%; }

.lang__menu a,
.navdrop__menu a {
  display: block;
  padding: 0.4375rem 0.625rem;
  border-radius: var(--radius);
  font-size: var(--fs-lg);
  font-weight: 400;
  color: var(--ink-2);
  white-space: nowrap;
}
.navdrop__menu a:has(.nav-unread-dot) {
  display: flex;
  align-items: center;
}
.nav-unread-dot {
  width: 0.5rem;
  height: 0.5rem;
  margin-inline-start: 0.5rem;
  border-radius: 50%;
  background: var(--notify);
  box-shadow: 0 0 0 0.125rem var(--surface);
  flex: 0 0 auto;
}
.lang__menu a:hover,
.navdrop__menu a:hover { background: var(--accent-soft); color: var(--ink); text-decoration: none; }
.lang__menu a.is-active,
.navdrop__menu a.is-active { background: var(--accent-soft); color: var(--ink); font-weight: 600; }


/* ---------------------------------------------------------- Tài khoản
   Menu tài khoản dùng lại đúng bộ khung của nút đa ngữ (`<details>` + panel
   `.acct__menu`): cùng một kiểu bật/mở thì người dùng không phải học lại, và
   `<details>` cho sẵn điều hướng bàn phím mà không cần một dòng JS nào. */
.acct { position: relative; }
.acct__login { list-style: none; }            /* summary đang mang class .btn */
.acct__login::-webkit-details-marker { display: none; }

.acct__btn {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.25rem 0.5rem 0.25rem 0.25rem;
  border: 1px solid var(--line-dark);
  border-radius: 999px;                        /* tròn để phân biệt với nút vuông */
  background: var(--field);
  font-size: var(--fs-md);
  color: var(--ink-2);
  cursor: pointer;
  white-space: nowrap;
  list-style: none;
}
.acct__btn::-webkit-details-marker { display: none; }
.acct__btn:hover { border-color: var(--accent-dark); color: var(--ink); }
.acct[open] .acct__btn { border-color: var(--accent-dark); color: var(--ink); }
.acct[open] .lang__caret { transform: rotate(180deg); }

.acct__avatar {
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
}
/* Đường lùi khi không có ảnh: chữ cái đầu trên nền nhạt */
.acct__avatar--text {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--ink);
  font-size: var(--fs-md);
  font-weight: 600;
}

/* Tên dài thì cắt bằng ba chấm, KHÔNG cho nó đẩy header rộng ra: tên do người
   dùng đặt ở Google nên có thể dài bất kỳ, mà header thì đã chật ở 320px. */
.acct__name {
  max-width: 7rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.acct__menu {
  position: absolute;
  right: 0;
  top: calc(100% + 0.5rem);
  z-index: 60;
  min-width: 10rem;
  padding: 0.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / .5);
}
/* `a` và `button` cùng một dáng — người dùng không cần biết cái nào điều hướng,
   cái nào gọi API. `width: 100%` để vùng bấm của nút trải hết dòng. */
.acct__menu a,
.acct__menu button {
  display: block;
  width: 100%;
  padding: 0.4375rem 0.625rem;
  border: 0;
  border-radius: var(--radius);
  background: none;
  font-family: inherit;
  font-size: var(--fs-lg);
  font-weight: 400;
  color: var(--ink-2);
  text-align: start;
  white-space: nowrap;
  cursor: pointer;
}
.acct__menu a:hover,
.acct__menu button:hover {
  background: var(--accent-soft);
  color: var(--ink);
  text-decoration: none;
}

/* Màn hẹp: giấu tên, chỉ còn ảnh. Header ở 320px đã có logo + menu + ngôn ngữ +
   nút sáng/tối, thêm một cái tên nữa là tràn. Menu bật ra vẫn có đủ mọi mục. */
@media (max-width: 30em) {
  .acct__name { display: none; }
}

/* Công tắc Light / Dark — checkbox ẩn, nhãn là nút bấm */
.theme-input { position: absolute; opacity: 0; pointer-events: none; }
.theme-btn {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  color: var(--ink-2);
  cursor: pointer;
}
.theme-btn:hover { border-color: var(--accent-dark); color: var(--ink); }
.theme-input:focus-visible + .theme-btn { outline: 2px solid var(--accent-dark); outline-offset: 2px; }
/* Mặc định dark → hiện mặt trăng, ẩn mặt trời */
.theme-btn .icon-sun { display: none; }

/* ------------------------------------------------------------------- Main */
.main { flex: 1; padding-block: 1.625rem 2.875rem; }

/* -------------------------------------------------- Breadcrumb màn hình */
.crumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-bottom: 0.625rem;
  font-size: var(--fs-md);
  color: var(--ink-3);
}
.crumbs a { color: var(--ink-2); }
.crumbs a:hover { color: var(--ink); }
.crumbs__sep { color: var(--ink-3); }
/* Cấp cuối = trang đang mở, không bấm được */
.crumbs__current {
  padding: 0.125rem 0.5rem;
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--ink);
  font-weight: 600;
}

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding-bottom: 0.875rem;
  border-bottom: 1px solid var(--line);
}
.page-head h1 {
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -.015em;
  overflow-wrap: break-word;
}
.page-head .subtitle {
  margin-top: 0.125rem;
  font-size: var(--fs-lg);
  color: var(--ink-3);
}
.file-count { font-size: var(--fs-lg); color: var(--ink-3); }

/* ------------------------------------------- Tiến độ bài (Dictation Details)
   Nằm trong `.page-head`, KHÔNG nằm trong `.work` — xem ghi chú trong HTML.
   Một hàng: thanh mảnh + con số, để nó không đẩy `h1` và `.tabs` xuống.

   Bề ngang thanh cố định theo `rem` chứ không co theo chữ: con số bên cạnh đổi
   độ dài liên tục khi học ("Đã làm 9/11 câu" ngắn hơn "Đã làm 10/11 câu"), mà
   thanh nhảy ngang mỗi lần chép đúng một câu thì rất nhiễu mắt. */
.lesson-prog {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.375rem;
  flex-wrap: wrap;
}
.lesson-prog[hidden] { display: none; }   /* flex đè mất `hidden` — docs/quality.md */

.lesson-prog__bar {
  flex: 0 0 auto;
  width: 7.5rem;
  max-width: 100%;
  height: 4px;
  border-radius: 2px;
  background: var(--line-dark);
  overflow: hidden;
}
.lesson-prog__fill {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: inherit;
  transition: width .2s ease;
}
.lesson-prog.is-done .lesson-prog__fill { background: var(--ok); }

.lesson-prog__text {
  font-size: var(--fs-lg);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/* Tổng kết cuối bài — cùng dòng, tách bằng dấu chấm giữa do CSS sinh ra để
   không phải nhét dấu vào bảng chữ i18n. */
.lesson-prog__sum { color: var(--ink-2); }
.lesson-prog__sum::before { content: " · "; color: var(--ink-3); }
.lesson-prog__sum[hidden] { display: none; }
.page-head__right {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* ------------------------------------------------------------------ Tabs */
.tabs {
  display: flex;
  gap: 0.375rem;
  margin-top: 0.875rem;
}
.detail-tabs-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
}
.lesson-nav { display: flex; gap: 0.375rem; }
.lesson-nav__button {
  width: 7.5rem;
  min-height: 2rem;
  padding: 0.3125rem 0.6875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-3);
  font: inherit;
  font-size: var(--fs-md);
  cursor: pointer;
}
.lesson-nav__button:hover:not(:disabled) {
  border-color: var(--ink-3);
  background: var(--surface-2);
  color: var(--ink-2);
}
.lesson-nav__button:disabled { opacity: 0.42; cursor: default; }
.tab {
  padding: 0.4375rem 1rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--field);
  font-size: var(--fs-lg);
  font-weight: 500;
  color: var(--ink-2);
  text-align: center;
}
.tab:hover { background: var(--accent-soft); color: var(--ink); text-decoration: none; }
.tab.is-active {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

/* --------------------------------------------------------- Bố cục 2 cột */
/* Cột trái (player + ô chép + kết quả) và cột phải (bản dịch) bằng nhau */
.work {
  display: grid;
  /* Hai cột chia đôi bề ngang, khoảng cách cũng tính theo % */
  grid-template-columns: 1fr 1fr;
  /* MỘT con số cho cả hai chiều, và cũng là con số dùng cho mọi khe giữa các
     section của trang này (`--gap-section`): trái↔phải, trên↔dưới, giữa các
     panel trong một cột, và giữa .work · #credit · #guide. Mắt bắt ngay khi
     một khe rộng hơn khe kia vài px.

     ⚠ KHÔNG dùng `var(--col-gap)` (2%) ở đây, dù docs/ui.md nói bề ngang bố
     cục tính theo %: `%` co giãn theo bề ngang khung nên khe ngang và khe dọc
     KHÔNG BAO GIỜ bằng nhau (đo ở 1280: 23.5px so với 24px; ở 1600: 29.4px so
     với 24px). Muốn hai chiều bằng nhau thì phải cùng đơn vị.
     Và `%` cho row-gap còn ra 0px — % của row-gap tính theo chiều cao khung,
     mà chiều cao ở đây không xác định. */
  gap: var(--gap-section);
  align-items: start;
  margin-top: var(--gap-section);
  /* Giữ chiều cao tối thiểu bằng nhau giữa 2 tab để phần dưới không bị hụt lên.
     Cả hai vế đều chỉ phụ thuộc khung nhìn (không phụ thuộc nội dung) nên hai
     tab luôn ra cùng một giá trị.
     Đo lại 2026-08-18 sau khi đổi bố cục (kết quả sang cột phải, bản dịch/từ
     mới/ngữ pháp xuống khối Learn more): hàng trên cao nhất 444px, khối Learn
     more lúc khoá 206px → cả luồng trước khi mở khoá gói trong ~674px = 38.5rem
     ở cỡ chữ gốc lúc đo. Sàn cũ 49rem là tính cho cột phải dài, giờ để nguyên
     thì dưới ô chép hở một khoảng trống 220px.
     Thêm/bớt panel trong tab Dictation thì phải đo và chỉnh lại con số này. */
  min-height: max(38.5rem, 46vh);
}
.col { display: flex; flex-direction: column; gap: var(--gap-section); }

.panel {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
/* flex-wrap: đầu panel chứa tiêu đề + cụm nút; tiêu đề mỗi ngôn ngữ một độ dài
   nên khi chật phải cho xuống dòng. Trước đây để nowrap thì bản tiếng Việt
   (tiêu đề dài nhất) nén cụm nút lại và cắt mất chữ trong nút. */
.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding: 0.625rem 0.875rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink-2);
}
.panel__head .muted { font-weight: 400; color: var(--ink-3); }
.panel__body { padding: 0.875rem; }

/* -------------------------------------------------------- Thanh play nghe */
.player {
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.player__row {
  display: flex;
  align-items: center;
  gap: 0.625rem;
}

/* Nút chuyển file: chỉ ký hiệu < >, không viền */
.step-btn {
  flex-shrink: 0;
  width: 1.625rem;
  height: 2rem;
  display: grid;
  place-items: center;
  font-size: 1.125rem;
  line-height: 1;
  color: var(--ink-3);
}
.step-btn:hover { color: var(--ink); }

.play-btn {
  width: 2.25rem;
  height: 2.25rem;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--fs-xs);
}
.play-btn:hover { background: var(--accent-dark); }

.seek {
  flex: 1;
  min-width: 3.75rem;
  height: 0.25rem;
  border-radius: 0.125rem;
  background: var(--line-dark);
  position: relative;
}
.seek.is-seekable { cursor: pointer; }
/* Thanh chỉ dày 4px, ngón tay lẫn chuột đều khó bấm trúng. Nới vùng bấm thêm
   8px trên và dưới bằng một lớp phủ trong suốt: pseudo-element không phải đích
   của sự kiện nên click vẫn rơi vào chính `.seek`, còn bố cục không đổi vì lớp
   này nằm ngoài luồng. Chỉ nới theo chiều dọc — nới ngang sẽ chồng lên nút. */
.seek.is-seekable::after {
  content: '';
  position: absolute;
  inset: -0.5rem 0;
}
.seek__fill {
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: inherit;
  background: var(--ink-2);
}
.seek__thumb {
  position: absolute;
  top: 50%;
  width: 0.625rem;
  height: 0.625rem;
  margin-left: -0.3125rem;
  transform: translateY(-50%);
  border-radius: 50%;
  background: var(--ink-2);
}

.time {
  font-size: var(--fs-sm);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Ô chọn tốc độ: bỏ mũi tên mặc định của trình duyệt (nó bị dí sát viền phải)
   và tự vẽ mũi tên bằng pseudo-element, để lề trái của chữ và lề phải của mũi
   tên bằng nhau đúng 0.5rem. */
.speed-wrap {
  position: relative;
  display: inline-flex;
  color: var(--ink-3);
}
.speed-wrap::after {
  content: "";
  position: absolute;
  right: 0.5rem;
  top: 50%;
  width: 0.36rem;
  height: 0.36rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
}
.speed {
  appearance: none;
  -webkit-appearance: none;
  padding: 0.25rem 1.35rem 0.25rem 0.5rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--field);
  font-size: var(--fs-sm);
  color: var(--ink-2);
}

.player__foot {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.625rem;
  padding-top: 0.625rem;
  border-top: 1px solid var(--line);
}

.loop {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  margin-left: auto;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  cursor: pointer;
}
.loop input { accent-color: var(--accent-dark); }

/* ---------------------------------------------------------------- Buttons */
/* `text-align: center` là BẮT BUỘC, không phải trang trí: `lockWidth()`
   (docs/i18n.md) đặt `min-width` bằng nhãn RỘNG NHẤT trong ba ngôn ngữ, nên ở
   hai ngôn ngữ còn lại hộp nút rộng hơn chữ. Không có dòng này thì chữ dán vào
   mép trái của cái hộp đã nới — thấy rõ nhất ở "Bắt đầu chép chính tả",
   "Tiếp tục" và "Học bài này" trên trang chủ. Nút không bị khoá sàn thì hộp co
   sát chữ, dòng này là vô hại. */
.btn {
  padding: 0.4375rem 1rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-size: var(--fs-lg);
  font-weight: 500;
  white-space: nowrap;
  text-align: center;
}
.btn--primary { background: var(--accent); color: var(--on-accent); }
.btn--primary:hover { background: var(--accent-dark); }
.btn--ghost {
  background: var(--field);
  border-color: var(--accent);
  color: var(--ink-2);
}
.btn--ghost:hover { background: var(--accent-soft); color: var(--ink); }
.btn--sm { padding: 0.3125rem 0.6875rem; font-size: var(--fs-sm); }

/* Nút chỉ có icon (reset) */
.btn--icon {
  width: 2.0625rem;
  height: 2.0625rem;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--field);
  font-size: var(--fs-3xl);
  line-height: 1;
  color: var(--ink-2);
}
/* Nút reset đựng SVG, ba nút đóng (×) vẫn đựng chữ — nên `font-size` ở trên
   phải giữ, còn kích thước icon khai riêng ở đây. 0.875rem là để khớp bề rộng
   glyph ↺ cũ (≈15px); lấy 1.125rem như nút micro thì icon to hơn bản cũ. */
.btn--icon svg { width: 0.875rem; height: 0.875rem; display: block; }
.btn--icon:hover { background: var(--accent-soft); color: var(--ink); }

/* --------------------------------------------------------- Ô chép chính tả */
.sentence-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.625rem;
  font-size: var(--fs-md);
  color: var(--ink-2);
}
.sentence-bar .track {
  flex: 1;
  height: 0.1875rem;
  border-radius: 0.125rem;
  background: var(--line);
  overflow: hidden;
}
.sentence-bar .track span {
  display: block;
  height: 100%;
  background: var(--accent-dark);
}

.type-box {
  width: 100%;
  /* `display: block` chứ không để mặc định inline-block: chữ inline chừa thêm
     một khoảng cho phần đuôi chữ, làm khung `.type-wrap` cao hơn ô đúng ~6px và
     nút micro neo theo khung sẽ tụt xuống sát mép đáy (đo được: cách đáy 1.5px
     thay vì 8px). */
  display: block;
  min-height: 3.375rem;
  padding: 0.625rem 0.75rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  font-size: calc(var(--fs-2xl) + var(--fs-bump));
  line-height: 1.7;
  resize: vertical;
}
.type-box::placeholder { color: var(--ink-3); font-size: calc(var(--fs-xl) + var(--fs-bump)); }
.type-box:focus { outline: none; border-color: var(--accent-dark); }

/* Máy CÓ nút micro thì góc dưới phải của ô thuộc về nút đó: chừa chỗ để chữ
   không chui xuống dưới nút, và bỏ tay kéo giãn vì nó nằm đúng chỗ ấy — hai
   thứ chồng lên nhau thì bấm micro lại thành kéo ô. Máy không có bộ nhận dạng
   (Firefox) thì không có lớp `has-speech`, ô vẫn kéo giãn được như cũ. */
:root.has-speech .type-wrap .type-box {
  padding-right: 3.25rem;
  resize: none;
}

.actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.625rem;
}
.actions .spacer { margin-left: auto; }

/* ------------------------------------------------------------ Kết quả */
.result__status {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.875rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-md);
}
.result__status--ok { color: var(--ok); background: var(--ok-soft); }
.result__status--err { color: var(--err); background: var(--err-soft); }

.result__label {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-3);
  margin-bottom: 0.25rem;
}
.result__line {
  font-size: calc(var(--fs-3xl) + var(--fs-bump));
  line-height: 1.85;
  word-break: keep-all;
  overflow-wrap: break-word;
}
.result__line + .result__label { margin-top: 0.875rem; }

/* Tô lỗi */
.w-ok { color: var(--ink); }
.w-bad {
  color: var(--err);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
.w-fix {
  color: var(--ok);
  background: var(--ok-soft);
  border-radius: 0.1875rem;
  padding: 0 0.125rem;
}
.w-miss {
  color: var(--ok);
  background: var(--ok-soft);
  border-bottom: 1px solid var(--ok);
  border-radius: 0.1875rem 0.1875rem 0 0;
  padding: 0 0.125rem;
}
/* Từ sai đầu tiên — chỗ người học dừng lại */
.w-hit {
  color: var(--err);
  background: var(--err-soft);
  border-radius: 0.1875rem;
  padding: 0 0.25rem;
  font-weight: 600;
}
/* Các từ sau vị trí sai — che bằng dấu *, số dấu = số ký tự của từ */
.w-mask {
  color: var(--ink-3);
  user-select: none;
  /* Siết các dấu * trong cùng một từ lại và nới khoảng cách giữa các từ,
     để nhìn ra được câu còn mấy từ và mỗi từ mấy âm tiết */
  letter-spacing: -0.14em;
  margin-inline: 0.2em;
}

.legend {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  margin-top: 0.875rem;
  padding-top: 0.625rem;
  border-top: 1px solid var(--line);
  font-size: var(--fs-xs);
  color: var(--ink-3);
}
.legend span { display: inline-flex; align-items: center; gap: 0.375rem; }
.legend i,
.legend-say i {
  width: 0.5625rem;
  height: 0.5625rem;
  border-radius: 0.125rem;
  display: inline-block;
  flex-shrink: 0;
}
.legend .i-ok { background: var(--ink); }
.legend .i-bad { background: var(--err); }
.legend .i-fix { background: var(--ok); }
.legend .i-say { background: var(--accent); }
.legend .i-mask { background: var(--ink-3); }

/* ------------------------------------------------- Nhập bằng giọng nói */
/* Nút micro nằm TRONG ô nhập, góc dưới phải — chỗ tay đang gõ nhìn thấy ngay,
   không phải đi tìm trong hàng nút phía dưới. */
.type-wrap { position: relative; }

/* Ẩn/hiện bằng CSS chứ KHÔNG bằng JS: lớp `has-speech` được đoạn script ở
   <head> gắn lên <html> trước khung hình đầu tiên, nên nút có mặt ngay lúc
   trang vẽ lần đầu. Trước đây JS bật nút sau khi tải xong dữ liệu bài, và nút
   nhảy vào ô nhập muộn hơn cả trang — thấy rõ mỗi lần F5. */
.mic-btn,
.mic-privacy { display: none; }
:root.has-speech .mic-privacy { display: block; }

:root.has-speech .mic-btn {
  position: absolute;
  right: 0.5rem;
  bottom: 0.5rem;
  width: 2.25rem;
  height: 2.25rem;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line-dark);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink-3);
  cursor: pointer;
}
:root.has-speech .mic-btn svg { width: 1.125rem; height: 1.125rem; }
:root.has-speech .mic-btn:hover { color: var(--ink); border-color: var(--accent); background: var(--accent-soft); }
:root.has-speech .mic-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Đang ghi âm: đổi hẳn màu chứ không chỉ nhấp nháy — người tắt hiệu ứng chuyển
   động (prefers-reduced-motion) vẫn phải nhìn ra là micro đang bật. */
:root.has-speech .mic-btn.is-recording {
  color: var(--err);
  border-color: var(--err);
  background: var(--err-soft);
  animation: mic-pulse 1.4s ease-in-out infinite;
}
@keyframes mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  50% { box-shadow: 0 0 0 0.25rem var(--err-soft); }
}
@media (prefers-reduced-motion: reduce) {
  :root.has-speech .mic-btn.is-recording { animation: none; }
}

.mic-note {
  margin-top: 0.5rem;
  font-size: var(--fs-xs);
  color: var(--ink-2);
}
.mic-note--err { color: var(--err); }

.mic-privacy {
  margin-top: 0.5rem;
  font-size: var(--fs-2xs);
  color: var(--ink-3);
}

/* Nhãn "câu này nhập bằng giọng nói" — đứng ngay dưới dòng trạng thái, cùng
   kiểu dải ngang để đọc như một phần của kết quả chứ không phải một lời chú. */
.result__voiced {
  padding: 0.4375rem 0.875rem;
  border-bottom: 1px solid var(--line);
  background: var(--accent-soft);
  color: var(--ink-2);
  font-size: var(--fs-xs);
}

/* -------------------------------------------------------- Nghe thực tế */
/* Dòng phát âm dùng lại .result__line nên cùng cỡ chữ, cùng cách xuống dòng với
   dòng đáp án ngay trên nó — hai dòng phải đọc được theo cặp. */
.result__line--say { color: var(--ink-2); }
/* Từ có âm bị đổi so với chữ viết — chính là chỗ hay nghe nhầm */
.w-say {
  color: var(--accent-dark);
  border-bottom: 1px dashed var(--accent);
}

.sound-list {
  list-style: none;
  margin-top: 0.625rem;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}
.sound__item {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.375rem;
  font-size: var(--fs-md);
  color: var(--ink-2);
}
.sound__item b {
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
}
.sound__arrow { color: var(--ink-3); }
.sound__out { color: var(--accent-dark) !important; }
/* Tên luật đẩy hẳn sang phải để mắt quét được cột "viết → đọc" thành một cột */
.sound__rules {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--ink-3);
  text-align: right;
}

.sound-none,
#sound-none {
  margin-top: 0.625rem;
  font-size: var(--fs-xs);
  color: var(--ink-3);
}

/* ------------------------------------------------------- Cột dịch (phải) */
/* Cột phải đứng yên, luôn ngang hàng với cột trái — không trôi theo khi cuộn */
.side { position: static; }

/* ------------------------------------ Learn more about this sentence ----
   Bản dịch · từ mới · ngữ pháp. Đây là phần MỞ RỘNG chứ không phải việc chính
   của màn hình. Trước đây ba khối này đứng ở cột phải, cùng kiểu `.panel` và
   gần cùng cỡ chữ với ô chép, nên mắt không biết đâu là phần chính — đó là lý
   do đổi bố cục (chốt 2026-08-18). Cách tách: gom vào MỘT khung trải hết bề
   ngang nằm dưới, nền chìm hơn, và mọi chữ bên trong hạ một bậc.

   `grid-column: 1 / -1` — khối này là con thứ ba của `.work` (lưới 2 cột). Nó
   PHẢI nằm trong `.work` vì đổi tab chỉ thay `.work` và `#guide` (docs/ui.md). */
.learn {
  grid-column: 1 / -1;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
}
.learn__head {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.6875rem 0.875rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink-2);
}
.learn__head-icon {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-dark);
}
.learn__head-icon svg { width: 1.125rem; height: 1.125rem; }
.learn__head-copy { display: flex; flex-direction: column; gap: 0.125rem; }
.learn__intro {
  font-size: var(--fs-xs);
  font-weight: 400;
  line-height: 1.45;
  color: var(--ink-3);
}

/* Ngữ pháp và từ vựng chia đôi hàng trên; cách nghe thực tế trải hết hàng dưới.
   Hai thẻ kiến thức có hình dạng đủ rõ để người học nhận ra, nhưng dùng nền
   chìm + viền mảnh nên vẫn thấp hơn khối Dictation chính ở phía trên. */
.learn__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Chỉ khe NGANG; khe dọc do padding/margin của thẻ trải hết bề ngang lo, để
     đường kẻ ngang nằm đúng giữa hai hàng. */
  gap: 0 var(--gap-section);
  padding: 0.875rem;
}
.learn__card { min-width: 0; }
/* Kẻ ngang là MỘT đường liền trải hết bề ngang, đặt ở đầu thẻ dưới — không
   phải hai đoạn border-top rời của hai thẻ dưới. Hai đoạn rời thì đoạn bên
   phải nối liền với kẻ dọc thành hình ⌐, còn đoạn bên trái đứng trơ ra: nhìn
   như kẻ dọc bị dính vào một bên. Khoảng hở trên và dưới đường kẻ bằng nhau. */
.learn__card--full {
  grid-column: 1 / -1;
  order: 3;
  padding-top: 0.875rem;
  margin-top: 0.875rem;
  border-top: 1px solid var(--line);
}
.learn__card--grammar,
.learn__card--vocab {
  align-self: start;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--line-dark);
  border-inline-start: 0.1875rem solid var(--accent);
  border-radius: var(--radius);
  background: var(--field);
}
.learn__card--grammar { order: 1; }
.learn__card--vocab { order: 2; }
.learn__card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-2);
}
.learn__card-title { display: inline-flex; align-items: center; gap: 0.4375rem; }
.learn__card-title svg {
  width: 1rem;
  height: 1rem;
  color: var(--accent-dark);
}
.learn__card-head .muted {
  padding: 0.125rem 0.4375rem;
  border-radius: 999rem;
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-size: var(--fs-2xs);
  font-weight: 500;
}
/* Card rỗng vẫn hiện rõ là một khu vực kiến thức, nhưng lùi nhẹ để card có nội
   dung trở thành điểm vào tự nhiên hơn. */
.learn__card--grammar:has(#grammar-empty:not([hidden])),
.learn__card--vocab:has(#vocab-empty:not([hidden])) { border-inline-start-color: var(--line-dark); }

/* Hạ một bậc so với ô Kết quả: dòng "Nghe thực tế" 22px → 18px, từ mới và ngữ
   pháp 18.5px → 15px. Đây chính là chỗ tạo ra thứ bậc chính/phụ mà mắt nhìn ra
   được; sửa mấy dòng này là mất luôn tác dụng của cả khối. */
.learn .result__line { font-size: calc(var(--fs-2xl) + 0.125rem); line-height: 1.7; }
.learn .vocab li,
.learn .grammar li { font-size: var(--fs-body); }

/* Ô vuông "Âm bị đổi" đứng ngay đầu thẻ Nghe thực tế — nói luôn cái gạch chân
   nét đứt dưới kia nghĩa là gì, khỏi phải bắc cầu sang chú giải ở ô Kết quả. */
.learn__card-head .legend-say { display: inline-flex; align-items: center; gap: 0.375rem; }
.learn__card-head .legend-say i { background: var(--accent); }

/* Nghĩa của câu, đứng ngay dưới dòng đáp án trong ô Kết quả — không có nhãn,
   nên phải tự chừa khoảng cách với dòng trên. */
.translate__vi {
  margin-top: 0.75rem;
  font-size: calc(var(--fs-body) + var(--fs-bump));
  line-height: 1.75;
  color: var(--ink-2);
}

.vocab { margin-top: 0.75rem; padding-top: 0.625rem; border-top: 1px solid var(--line); }
.vocab li {
  display: flex;
  gap: 0.625rem;
  padding: 0.25rem 0;
  font-size: calc(var(--fs-lg) + var(--fs-bump));
  color: var(--ink-2);
}
.vocab b { color: var(--ink); font-weight: 600; min-width: 4.5rem; }
/* Khi "Từ mới" đứng thành section riêng thì không cần đường kẻ phân cách trên */
.vocab:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* ------------------------------------------------------- Ngữ pháp sử dụng */
.grammar li {
  padding: 0.625rem 0;
  border-top: 1px solid var(--line);
  font-size: calc(var(--fs-lg) + var(--fs-bump));
  line-height: 1.7;
  color: var(--ink-2);
}
.grammar li:first-child { padding-top: 0; border-top: 0; }
.grammar li b {
  display: block;
  color: var(--ink);
  font-weight: 600;
  margin-bottom: 0.125rem;
}
/* Chữ Hàn nhấn giữa dòng giải thích thì giữ nguyên trên cùng một dòng */
.grammar li b[lang="ko"]:not(:first-child) { display: inline; margin: 0; }
.grammar .ex {
  display: block;
  margin-top: 0.25rem;
  color: var(--ink-3);
  word-break: keep-all;
}

/* ------------------------------------------------- Từ mới → My Pocket */
.vocab__item {
  display: flex;
  align-items: baseline;
  gap: 0.5625rem;
  width: 100%;
  cursor: pointer;
}
.vocab__item:hover { color: var(--ink); }
.vocab__item input {
  flex-shrink: 0;
  position: relative;
  top: 0.125rem;
  margin: 0;
  accent-color: var(--accent-dark);
  cursor: pointer;
}

.pocket {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  flex-wrap: wrap;
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px dashed var(--line-dark);
}
.pocket__hint { font-size: var(--fs-xs); color: var(--ink-3); }
.pocket__msg { font-size: var(--fs-xs); color: var(--ok); }

/* Nhắc phím tắt nghe lại, thay cho nút −5s / Replay đã bỏ.
   min-height tính đúng bằng chiều cao của .btn--sm (line-height × cỡ chữ +
   padding trên dưới + viền) để thanh play ở 2 tab cao bằng nhau, dù tab này
   dùng chữ còn tab kia dùng nút. */
.kbd-hint {
  font-size: var(--fs-xs);
  color: var(--ink-3);
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  min-height: calc(var(--fs-sm) * 1.6 + 0.625rem + 2px);
}
/* Kiểu phím bấm dùng chung cho mọi chỗ có <kbd> (thanh play, hướng dẫn…) —
   đừng chép lại theo từng khối, docs/code-structure.md. */
kbd {
  display: inline-block;
  padding: 0.125rem 0.375rem;
  border: 1px solid var(--line-dark);
  border-bottom-width: 2px;
  border-radius: 0.25rem;
  background: var(--field);
  font: inherit;
  font-size: 0.85em;
  line-height: 1.3;
  color: var(--ink-2);
}
.kbd-hint kbd { font-size: var(--fs-2xs); }

/* Khối "Learn more" khi chưa mở khoá: một icon mờ và một câu nói rõ phải làm gì
   mới mở được. Cột kết quả thì KHÔNG có khối tương tự — chưa chấm là để trống. */
.side-locked {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.625rem;
  padding-block: 1.75rem;
  text-align: center;
  color: var(--ink-3);
}
.side-locked svg { width: 1.75rem; height: 1.75rem; opacity: .65; }
.side-locked p { font-size: var(--fs-lg); max-width: 26ch; }
.side-locked b { color: var(--ink-2); }
/* Trong khung Learn more không có .panel__body nên phải tự chừa lề hai bên */
.learn .side-locked { padding-block: 1.375rem; padding-inline: 0.875rem; }

/* Báo khi thiếu file audio — không chặn các chức năng còn lại */
.audio-notice {
  margin-top: 0.625rem;
  padding: 0.5rem 0.625rem;
  border-radius: var(--radius);
  background: var(--err-soft);
  color: var(--err);
  font-size: var(--fs-xs);
}

/* Trạng thái kết quả khi chép đúng */
.result__status--ok { color: var(--ok); background: var(--ok-soft); }

/* Nút bị vô hiệu (đầu/cuối danh sách câu) */
.step-btn:disabled,
.btn:disabled,
.play-btn:disabled { opacity: .4; cursor: not-allowed; }

/* Mặc định: chưa tick từ nào → nút bị khoá */
.pocket__btn {
  opacity: .45;
  cursor: not-allowed;
  pointer-events: none;
}
/* Có ít nhất 1 từ được tick → mở khoá nút */
.vocab-block:has(.vocab input:checked) .pocket__btn {
  opacity: 1;
  cursor: pointer;
  pointer-events: auto;
}
.vocab-block:has(.vocab input:checked) .pocket__hint { display: none; }

/* ------------------------------------------------- Full Script (transcript) */
/* Chỉ hiển thị đúng câu đang phát, không đổ cả bài ra màn hình */
.now {
  padding: 1.875rem 1.375rem 1.5rem;
  text-align: center;
}
.now__meta {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  font-size: var(--fs-2xs);
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.now__meta i {
  width: 0.4375rem;
  height: 0.4375rem;
  border-radius: 50%;
  background: var(--ok);
  display: inline-block;
}
/* Cỡ chữ co theo bề ngang màn hình, tối đa bằng cỡ hiện tại (25px) */
.now__ko {
  margin-top: 0.875rem;
  font-size: clamp(1.25rem, 1rem + 1.6vw, 1.5625rem);
  line-height: 1.75;
  letter-spacing: -.012em;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* Từ đang được đọc */
.now__ko .cue {
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 0.1875rem;
  padding: 0 0.1875rem;
}
.now__time {
  margin-top: 0.75rem;
  font-size: var(--fs-sm);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* Thanh vị trí câu trong bài — biết đang ở đâu mà không cần đọc trước.
   Vạch chỉ dày 4px nên rất khó bấm; dùng content-box + padding để nới vùng
   bấm lên ~20px, còn background-clip giữ phần nhìn thấy vẫn mảnh như cũ. */
.seg-track {
  display: flex;
  align-items: center;
  gap: 0.1875rem;
  padding: 0 1.375rem 0.5rem;
}
.seg-track span {
  flex: 1;
  box-sizing: content-box;
  height: 0.25rem;
  padding-block: 0.5rem;
  background-clip: content-box;
  border-radius: 0.125rem;
  background-color: var(--line);
  cursor: pointer;
}
.seg-track span:hover { background-color: var(--ink-3); }
/* Dùng background-color chứ không phải shorthand background,
   vì shorthand sẽ reset background-clip khiến vùng đệm bấm bị tô màu theo */
.seg-track span.is-done { background-color: var(--accent); }
.seg-track span.is-now { background-color: var(--ink-2); }

/* Toàn văn tiếng Hàn */
.script { display: flex; flex-direction: column; gap: 0.125rem; }
.script li {
  display: grid;
  grid-template-columns: 1.375rem 1fr auto;
  gap: 0.625rem;
  padding: 0.4375rem 0.5rem;
  border-radius: var(--radius);
  font-size: calc(var(--fs-2xl) + var(--fs-bump));
  line-height: 1.75;
  color: var(--ink-2);
  word-break: keep-all;
  overflow-wrap: break-word;
  cursor: pointer;
  transition: background-color .12s, color .12s;
}
.script li:hover { background: var(--surface-2); color: var(--ink); }
.script li.is-now { background: var(--accent-soft); color: var(--ink); }
.script .num,
.script .time {
  font-size: var(--fs-xs);
  color: var(--ink-3);
  padding-top: 0.3125rem;
  font-variant-numeric: tabular-nums;
}
.script li.is-now .num,
.script li.is-now .time { color: var(--ink-2); }

/* Nút chuyển kiểu xem toàn văn, đặt trong đầu panel */
.script-view { display: inline-flex; align-items: center; gap: 0.625rem; flex-shrink: 0; }
/* flex-shrink: 0 — nút đã khoá bề ngang theo nhãn dài nhất của 3 ngôn ngữ
   (lockButtonWidths ở i18n.js); để nó co lại thì `overflow: hidden` sẽ cắt chữ. */
.seg-btns {
  display: inline-flex;
  flex-shrink: 0;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  overflow: hidden;
}
.seg-btns button {
  padding: 0.1875rem 0.5rem;
  background: var(--field);
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--ink-2);
  white-space: nowrap;
}
.seg-btns button + button { border-left: 1px solid var(--line-dark); }
.seg-btns button:hover { background: var(--accent-soft); color: var(--ink); }
.seg-btns button.is-active { background: var(--accent); color: var(--on-accent); }

/* Kiểu xem "Đoạn văn": vẫn cùng DOM, chỉ đổi cách bày, nên tô sáng câu đang
   phát và bấm để nhảy câu vẫn hoạt động y như kiểu xem theo câu. */
.script.is-paragraph {
  display: block;
  line-height: 2;
  word-break: keep-all;
}
.script.is-paragraph li {
  display: inline;
  padding: 0.0625rem 0.1875rem;
}
.script.is-paragraph .num,
.script.is-paragraph .time { display: none; }

.now-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.625rem;
  flex-wrap: wrap;
  padding: 0.625rem 0.875rem;
  border-top: 1px solid var(--line);
  font-size: var(--fs-sm);
  color: var(--ink-3);
}

/* Khối dịch bên phải */
.trans + .trans {
  margin-top: 0.875rem;
  padding-top: 0.75rem;
  border-top: 1px dashed var(--line-dark);
}
.trans p {
  font-size: calc(var(--fs-body) + var(--fs-bump));
  line-height: 1.75;
  color: var(--ink-2);
  overflow-wrap: break-word;
}
.trans__ko p {
  font-size: calc(var(--fs-2xl) + var(--fs-bump));
  color: var(--ink);
  word-break: keep-all;
}

/* ==========================================================================
   HOME — index.html

   Sáu khối xếp dọc, MỨC ĐỘ NỔI GIẢM DẦN theo đúng thứ tự đó:

     1. hero      — tiêu đề lớn nhất + nút chính của cả site
     2. resume    — dải "đang học dở": viền accent, nền accent-soft
     3. starter   — bài vào cửa + bản xem trước ngắn của một lượt học
     4. steps     — ba bước Dictation: tiêu đề có gạch chân, thẻ nền --surface
     5. eco       — hai mảng còn lại: tiêu đề nhỏ, không gạch chân, thẻ chữ nhỏ
     6. ending    — câu kết: không khung, không viền, nhiều khoảng trắng

   Thứ bậc đó làm bằng CỠ CHỮ · KHOẢNG CÁCH · ĐỘ ĐẬM VIỀN, KHÔNG thêm màu mới:
   cả trang vẫn chỉ có bảng nâu/be trong `:root` (docs/ui.md mục 4). Muốn một
   khối nổi hơn thì cho nó chữ to hơn hoặc nhiều khoảng trắng hơn, đừng cấp cho
   nó một màu riêng.

   Không dùng `.panel` cho mấy thẻ ở đây: panel có thanh đầu `.panel__head`
   riêng, còn thẻ trên trang chủ chỉ là một khung viền có tiêu đề nằm trong
   ruột. Vẫn giữ đúng ngôn ngữ thị giác chung — nền phẳng, viền 1px, bo 6px,
   không đổ bóng (docs/ui.md mục 5).
   ========================================================================== */

/* ------------------------------------------------------------------ Hero */

.hero { padding-block: 1.75rem 0.5rem; }

.hero__eyebrow {
  margin-top: 1rem;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--accent-dark);
  letter-spacing: .02em;
}

/* Bề ngang chữ có TRẦN: dòng dài quá 70–75 ký tự là mắt bắt đầu lạc dòng. Trần
   của tiêu đề hẹp hơn trần của câu dẫn vì chữ to hơn — cùng một con số rem thì
   tiêu đề đã dài gấp rưỡi về mặt thị giác. */
.hero__title {
  margin-top: 0.375rem;
  font-size: 2.125rem;
  font-weight: 700;
  line-height: 1.24;
  letter-spacing: -.02em;
  /* Tiêu đề dài, tiếng Hàn phải ngắt theo từ chứ không cắt giữa chữ. */
  word-break: keep-all;
  overflow-wrap: break-word;
  max-width: 30rem;
}
/* Cột phải của hero: lời chào chạy thử + đoạn mô tả phương pháp Dictation.
   HIỆN Ở MỌI CỠ MÀN (chủ dự án chốt 2026-08-24). Bản trước `display:none` dưới
   56.25em — tức khách điện thoại, phần đông người vào từ Google, không đọc được
   câu nào ở đây; mà đây lại là đoạn chữ mô tả phương pháp cho cả người đọc lẫn
   máy tìm kiếm. Dưới mốc 2 cột nó tự rơi xuống dưới cụm CTA theo thứ tự DOM.

   Grid chứ không phải float/absolute: chiều cao cột trái do chính nội dung của
   nó quyết định, cột phải dài hơn cũng không kéo giãn hay đẩy CTA xuống. */
@media (min-width: 56.25em) {
  .hero__top {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 24rem;
    gap: 2rem;
    align-items: start;
  }
}

/* Lời chào "đang chạy thử". Trước 2026-08-24 đây là <dialog> mở bằng
   `showModal()`: đo ở 375×812 thì nó che 48% màn hình cộng backdrop phủ kín
   phần còn lại, bung trên cả 18 trang. Cờ "đã xem" nằm ở localStorage nên máy
   tìm kiếm và Lighthouse lần nào cũng là khách lần đầu — tức lần render nào
   cũng gặp tấm chắn đó, đúng mô tả "intrusive interstitial" của Google (miễn
   trừ chỉ dành cho thông báo pháp lý, lời chào không thuộc nhóm này).
   Nay nó đứng yên trong luồng trang, chỉ ở Home, không che gì. */
.hero-notice {
  margin-top: 2.25rem;
  padding: 0.875rem 1rem;
  border: 1px solid var(--line-dark);
  border-left: 3px solid var(--accent-dark);
  border-radius: var(--radius);
  background: var(--field);
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.hero-notice__title {
  display: block;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink);
}
.hero-notice__text {
  margin-top: 0.25rem;
  color: var(--ink-2);
  word-break: keep-all;
  overflow-wrap: break-word;
}
.hero-notice__link { color: var(--accent-dark); text-decoration: underline; }
.hero-notice__link:hover { color: var(--accent); }

/* Đoạn mô tả phương pháp Dictation. Bề ngang có TRẦN như `.hero__title`: dưới
   mốc 2 cột nó trải hết bề ngang màn desktop thì chữ nghiêng nhỏ rất dễ lạc dòng. */
.hero__intro {
  position: relative;
  max-width: 38rem;
  margin-top: 1.25rem;
  padding: 1.25rem 1.25rem;
  font-size: var(--fs-md);
  font-style: italic;
  color: var(--ink-2);
  line-height: 1.55;
}
.hero__intro p + p { margin-top: 0.625rem; }

/* Khung góc trang trí: chỉ vẽ 2 góc chéo nhau (trên-trái, dưới-phải) bằng
   border, không viền kín cả khối — mỗi góc là nửa bề ngang + nửa bề cao. */
.hero__intro::before,
.hero__intro::after {
  content: "";
  position: absolute;
  width: 50%;
  height: 50%;
  border: 1px solid var(--line-dark);
  pointer-events: none;
}
.hero__intro::before {
  top: 0;
  left: 0;
  border-right: none;
  border-bottom: none;
}
.hero__intro::after {
  bottom: 0;
  right: 0;
  border-left: none;
  border-top: none;
}

.hero__actions { margin-top: 1.75rem; }

/* Nút chính của cả màn: to hơn nút thường bằng padding và cỡ chữ, KHÔNG bằng
   `flex: 1` — nút có data-i18n mà bị flex kéo giãn thì `lockWidth()` đo trúng
   bề ngang đã giãn rồi khoá sàn bằng con số phình to ấy (docs/i18n.md). */
.btn--hero {
  display: inline-block;
  padding: 0.75rem 1.75rem;
  font-size: var(--fs-3xl);
  font-weight: 600;
}
/* `--ink-2` chứ không phải `--ink-3`: đây là câu chữ cần đọc được, mà --ink-3
   trên nền trang ở light mode chỉ đạt tỉ lệ tương phản 2.6:1. Cùng lý do cho
   `.resume__head` bên dưới. */
.hero__hint { margin-top: 0.75rem; font-size: var(--fs-md); color: var(--ink-2); }

/* Lối tắt theo cấp độ: nhẹ hơn CTA chính nhưng vẫn đủ giống một thứ bấm được. */
.home-levels {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1rem;
}
.home-levels__label {
  margin-inline-end: 0.125rem;
  font-size: var(--fs-md);
  color: var(--ink-2);
}
.home-level {
  padding: 0.375rem 0.75rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink);
}
.home-level:hover { text-decoration: none; }

/* ------------------------------------------------ Accent sóng âm → 한글

   Trang trí, KHÔNG được to tiếng hơn tiêu đề: vạch mảnh, màu accent, opacity
   thấp. Các vạch là <span> tô nền — hình thuần, không phải ký tự ▁▃▆ (mỗi font
   vẽ một kiểu, chân vạch so le). Mũi tên cũng dựng bằng viền, cùng cách với
   mũi tên nối ba bước bên dưới. */
.wave {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  opacity: .7;
}
.wave__bars {
  display: flex;
  align-items: flex-end;
  gap: 0.1875rem;
  height: 1.375rem;
}
.wave__bar {
  width: 0.1875rem;
  border-radius: 1px;
  background: var(--accent-dark);
}
/* ▁ ▃ ▆ ▂ ▇ ▅ — đúng dáng chủ dự án mô tả, dựng bằng chiều cao chứ không bằng chữ */
.wave__bar:nth-child(1) { height: 22%; }
.wave__bar:nth-child(2) { height: 55%; }
.wave__bar:nth-child(3) { height: 88%; }
.wave__bar:nth-child(4) { height: 34%; }
.wave__bar:nth-child(5) { height: 100%; }
.wave__bar:nth-child(6) { height: 68%; }
/* Bản dùng ở khối kết: chỉ còn vạch, mờ hơn nữa, nằm giữa — nền chứ không phải
   nội dung. */
.wave--ghost {
  justify-content: center;
  opacity: .3;
}
.wave--ghost .wave__bars { height: 1.75rem; }

/* ------------------------------------------------- Dải "bạn đang học dở"

   Nổi hơn thẻ thường (viền + nền accent) nhưng vẫn dưới hero: chữ nhỏ hơn tiêu
   đề hero, nút là `.btn--sm` chứ không phải `.btn--hero`.

   Lưới 2 cột: chữ | nút, rồi thanh tiến độ trải hết bề ngang ở hàng dưới. Nút
   `justify-self: start` chứ không stretch — nút có data-i18n không được để lưới
   kéo giãn (docs/i18n.md). */
/* ----------------------------------------- Nhịp học: chuỗi ngày + mục tiêu
   Khối trên trang chủ (2026-09-11; dựng lại thứ bậc 2026-09-12; làm gọn lại
   2026-09-13). Nằm TRÊN dải "đang học dở" và dùng cùng ngôn ngữ hình khối với
   nó — viền mảnh, nền `--surface`.

   Thứ tự mắt đọc, từ trên xuống:
     1 trạng thái chuỗi ngày   2 tiến độ hôm nay (con số TO NHẤT khối)
     3 nút học                 4 bảy ngày của tuần     5 dòng mục tiêu
   Thứ bậc dựng bằng CỠ CHỮ, ĐỘ ĐẬM và KHOẢNG CÁCH, với đúng MỘT khung ngoài.
   Bản 2026-09-12 có thêm hộp lời nhắn (nền chìm + gạch vàng) và hộp viền quanh
   lịch tuần: hai hộp con cùng tranh mắt với con số chính, và cả khối cao 545px
   ở 1280 (651px ở 375). Đừng lồng thêm hộp vào đây nữa.

   Màu vàng chỉ dành cho năm thứ: phần thanh đã đi · nút · ngày đạt mục tiêu ·
   vòng "hôm nay" · ngọn lửa. Mọi thứ phụ (dải ngày, "Thay đổi", "Về tuần này")
   là nâu xám — một liên kết chữ tô vàng là thêm một thứ tranh mắt với nút.

   `hidden` cho tới khi dữ liệu về (kể cả với khách, dù localStorage gần như
   tức thì): vẽ "0/5" rồi nhảy sang "3/5" là một cú giật vô cớ ngay đầu trang. */
.daily {
  margin-top: 2.25rem;
  padding: 1.25rem 1.375rem 1.125rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
}
.daily[hidden] { display: none; }      /* đừng tin `hidden` một mình — docs/quality.md */

/* BẬC 1 — trạng thái chuỗi ngày, cũng là tiêu đề khối. Cỡ chữ đi theo token
   (+0.375rem) chứ không khai cứng rem, để khối "Cỡ chữ mobile" cuối file tự nâng.
   `tabular-nums` để 9 → 10 → 100 ngày không làm dòng nhảy qua nhảy lại. CỐ Ý
   KHÔNG `text-transform: uppercase`: 한글 không có chữ hoa. */
.daily__streak {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: calc(var(--fs-2xl) + 0.375rem);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* Ngọn lửa chỉ hiện khi CÓ chuỗi — lửa cạnh "Bắt đầu chuỗi hôm nay" là hình vô
   nghĩa. `--accent-ink` chứ không `--accent`: `--accent` sinh ra để TÔ NỀN, ở
   light mode nó gần như biến mất trên nền trắng (ghi chú ở :root). */
.daily__flame { display: none; width: 1.25rem; height: 1.25rem; color: var(--accent-ink); }
.daily__streak.is-on .daily__flame { display: block; }
/* `--ink-2` chứ không `--ink-3`: dòng này là lời mời hành động ("Hoàn thành mục
   tiêu hôm nay để tiếp tục chuỗi"), không phải chú thích — phải đọc rõ. */
.daily__sub {
  margin-top: 0.1875rem;
  font-size: var(--fs-md);
  color: var(--ink-2);
}

/* BẬC 2 + 3 — tiến độ hôm nay và nút. Dưới 48em xếp dọc; từ 48em nút đứng CẠNH
   khối tiến độ, canh giữa theo chiều dọc — cùng kiểu với dải "đang học dở" ngay
   bên dưới (chữ trái, nút phải), và bớt hẳn một tầng chiều cao.

   Cột tiến độ chặn ở 34rem: thẻ trải hết bề ngang trang, để thanh 6px kéo dài
   gần 1000px ở màn 1280 thì nó thành một đường kẻ chứ không còn là thanh tiến độ. */
.daily__today { margin-top: 1.125rem; }
.daily__act { margin-top: 0.875rem; }
@media (min-width: 48em) {
  .daily__today {
    display: grid;
    grid-template-columns: minmax(0, 34rem) auto;
    justify-content: space-between;
    align-items: center;
    column-gap: 2rem;
  }
  .daily__act { margin-top: 0; }
}

/* "10 / 20  câu hôm nay" trên CÙNG một dòng, canh theo đường chân chữ: nhãn đứng
   ngay sau con số thay vì xuống dòng riêng là tiết kiệm một tầng. Chật thì
   `flex-wrap` cho nhãn tự xuống dưới. */
.daily__tally {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.5rem;
}
/* Con số TO NHẤT khối (~36px ở 1280, ~33px ở mobile — theo token nên tự nâng). */
.daily__count {
  font-size: calc(var(--fs-3xl) + 1rem);
  font-weight: 650;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* "/ 20" nhỏ hơn và nhạt hơn một nấc: mẫu số là bối cảnh, tử số mới là tin.
   Khoảng trắng quanh dấu gạch nằm ở ĐÂY chứ không ở trong chữ, nên đọc lên và
   `textContent` vẫn là "10/20" gọn gàng. */
.daily__count-of {
  margin-left: 0.3125rem;
  font-size: calc(var(--fs-2xl) + 0.375rem);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--ink-2);
}
.daily__count-slash { margin-right: 0.25rem; }
.daily__count-label { font-size: var(--fs-xl); color: var(--ink-2); }

.daily__bar {
  margin-top: 0.625rem;
  height: 0.375rem;
  border-radius: 999px;
  background: var(--line-dark);
  overflow: hidden;
}
.daily__fill {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: inherit;
  transition: width .45s cubic-bezier(.2, .7, .2, 1);
}
/* Đủ mục tiêu thì thanh SÁNG lên chứ không đổi sang xanh: cả khối này chỉ đi
   một dải be–vàng, thêm một màu trạng thái nữa là thành bảng đèn báo. Việc
   "đã xong" đã được nói bằng chữ, bằng thanh đầy và bằng dấu tích ở lịch tuần. */
.daily.is-done .daily__fill { background: var(--accent-dark); }
/* Light mode: phần thanh đã đi phải là vàng ĐẬM (`--accent-ink`). `--accent`
   #eecfa1 trên rãnh #d9c9a9 chỉ được 1.09:1 — đo 2026-09-13, ảnh chụp 17/20
   gần như không phân biệt được đâu là phần đã đi. `--accent-ink` được 3.08:1,
   qua ngưỡng 3:1 của hình đồ hoạ. Dark mode giữ `--accent` (5.26:1). Áp cả khi
   đã xong: `.is-done` ở trên đặc hiệu cao hơn, không nhắc lại là nó đè mất. */
:root[data-theme="light"] .daily__fill,
:root[data-theme="light"] .daily.is-done .daily__fill { background: var(--accent-ink); }
/* Mừng một nhịp khi VỪA chạm mốc trong phiên này — một quầng sáng tắt dần, hết.
   Không pháo hoa, không nảy: khối này là chỗ xem tiến độ, không phải trò chơi. */
.daily.is-cheer .daily__bar { animation: daily-cheer 1.1s ease-out 1; }
@keyframes daily-cheer {
  0%   { box-shadow: 0 0 0 0 rgba(216, 182, 129, 0); }
  30%  { box-shadow: 0 0 0 4px rgba(216, 182, 129, .28); }
  100% { box-shadow: 0 0 0 8px rgba(216, 182, 129, 0); }
}

/* Lời động viên. KHÔNG `display: flex`: câu ghép từ nhiều mảnh (hình · mảnh đậm
   · mảnh nhạt), flex biến mỗi mảnh thành một ô riêng và câu hết xuống dòng được
   như một câu bình thường. Khe giữa các mảnh là khoảng trắng có sẵn trong HTML;
   mảnh nào rỗng thì khoảng trắng thừa tự gộp lại. */
.daily__lead {
  margin-top: 0.625rem;
  font-size: var(--fs-xl);
  line-height: 1.45;
  color: var(--ink-2);
}
.daily__lead-head { font-weight: 600; color: var(--ink); }
/* Hình theo giọng: ✦ khi đang học (khởi động · nửa đường · gần xong), dấu tích
   khi xong, không hình khi chưa bắt đầu — chưa có gì để lấp lánh. Mặc định ẩn
   cả hai: thiếu luật nào thì câu vẫn đọc được, chỉ là không có hình. */
.daily__lead-spark {
  display: none;
  width: 0.75rem;
  height: 0.75rem;
  margin-bottom: 0.1875rem;
  vertical-align: middle;
  color: var(--accent-ink);
}
.daily__lead[data-tone="start"] .daily__lead-spark,
.daily__lead[data-tone="half"] .daily__lead-spark,
.daily__lead[data-tone="near"] .daily__lead-spark { display: inline-block; }
/* Dấu tích: HÌNH, không phải chữ — vẽ bằng hai cạnh viền xoay 45° chứ không dùng
   `content: '✓'` (docs/i18n.md cấm chữ hiển thị nằm trong CSS). */
.daily__lead-tick {
  display: none;
  width: 0.3125rem;
  height: 0.5625rem;
  margin: 0 0.1875rem 0.25rem 0.125rem;
  border: solid var(--accent-ink);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  vertical-align: middle;
}
.daily__lead[data-tone="done"] .daily__lead-tick { display: inline-block; }

/* `<a class="btn">` là phần tử inline: không có dòng này thì padding dọc của nút
   tràn lên dòng trên khi khối xếp dọc. */
.daily__cta { display: inline-block; }

/* BẬC 4 — bảy ngày của tuần. Ngăn với phần trên bằng MỘT đường kẻ mảnh trải hết
   bề ngang thẻ, không phải một hộp viền riêng. Nội dung bên trong chặn ở 28rem:
   bảy chấm dàn hết 1100px thì mỗi chấm cách nhau 160px — không còn đọc thành
   "một tuần" nữa, mà thành bảy thứ rời rạc. */
.daily__weeksec {
  margin-top: 1.125rem;
  padding-top: 0.875rem;
  border-top: 1px solid var(--line);
}
.daily__weekhead,
.daily__weekrow { max-width: 28rem; }

/* Đầu đề: "Tuần này" (hoặc lối "Về tuần này") · dải ngày căn theo chữ "Chủ
   nhật" · ‹ › ngay sau dải (chủ dự án chốt 2026-09-13).

   Đầu đề chia ĐÚNG bảy cột như hàng ngày bên dưới (cùng `max-width`, cùng
   `gap`), nên cột 7 của nó nằm thẳng trên cột Chủ nhật. `.daily__weekanchor`
   đứng giữa cột ấy và rộng bằng nhãn "Chủ nhật" — thước ẩn `::before`, cùng cỡ
   chữ, cùng độ đậm, chữ lấy từ i18n qua `data-ruler` — tức mép trái của nó
   CHÍNH LÀ mép trái chữ "Chủ nhật". Dải ngày + mũi tên đặt tuyệt đối từ mép đó,
   tràn sang phần trống bên phải thẻ. Căn bằng CSS chứ không đo bằng JS: nhãn
   đổi theo ngôn ngữ, font tải muộn, độ đậm đổi khi bấm — JS phải bám cả ba.

   `min-height` = cao mũi tên: mũi tên đã rời luồng, không ai giữ chiều cao hộ. */
.daily__weekhead {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  align-items: center;
  column-gap: 0.25rem;
  min-height: 1.75rem;
  margin-bottom: 0.5rem;
}
.daily__weektitle,
.daily__weekhead > .daily__textlink { grid-area: 1 / 1 / 2 / 7; justify-self: start; }
.daily__weektitle { font-size: var(--fs-md); font-weight: 600; color: var(--ink-2); }
.daily__weekanchor {
  grid-area: 1 / 7;
  justify-self: center;
  position: relative;
}
.daily__weekanchor::before {
  content: attr(data-ruler);
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  white-space: nowrap;
  font-size: var(--fs-2xs);
}
.daily__weekanchor.is-strong::before { font-weight: 600; }
.daily__weekctl {
  position: absolute;
  top: 50%;
  left: 0;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  transform: translateY(-50%);
}
.daily__weekrange {
  display: flex;
  flex-direction: column;
  font-size: var(--fs-md);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Sàn bề ngang = bản rộng nhất của dải (home.js ghi vào `data-widest`): mũi tên
   đứng ngay sau dải, mà "7/9/2026" với "12/10/2026" dài ngắn khác nhau — thiếu
   sàn này thì đổi tuần là mũi tên xê dịch dưới con trỏ. */
.daily__weekrange::after {
  content: attr(data-widest);
  height: 0;
  overflow: hidden;
  visibility: hidden;
}
/* Màn hẹp — cùng mốc nhãn rút còn "CN": cột CN chỉ ~36px, dải ngày ~150px căn
   theo nó là lòi khỏi thẻ, mà ở 320px chữ này cũng không vừa chung một dòng
   với "Tuần này" và hai mũi tên. Dòng 1 "Tuần này … ‹ ›", dòng 2 dải ngày căn
   trái. Hai lớp bọc thành `contents` để ba thứ bên trong vào thẳng lưới. */
@media (max-width: 56.25em) {
  .daily__weekhead {
    grid-template-columns: minmax(0, 1fr) auto auto;
    row-gap: 0.125rem;
  }
  .daily__weektitle,
  .daily__weekhead > .daily__textlink { grid-area: 1 / 1; }
  .daily__weekanchor,
  .daily__weekctl { display: contents; }
  .daily__weekanchor::before,
  .daily__weekrange::after { content: none; }
  .daily__weekrange { grid-area: 2 / 1 / 3 / 4; }
  .daily__weekctl > .daily__weeknav { grid-row: 1; }
}
/* Mũi tên lùi/tiến. Không bấm được thì GIẤU bằng `visibility` — không đi đâu
   được thì mũi tên chỉ là nhiễu — nhưng vẫn GIỮ CHỖ: nút rời khỏi luồng thì mũi
   tên còn lại nhảy ngang mỗi lần bấm. `visibility: hidden` cũng rút nút khỏi thứ
   tự Tab. Hai đầu chặn nằm ở `renderWeekNav()` trong home.js. */
.daily__weeknav {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  flex-shrink: 0;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--ink-3);
}
.daily__weeknav:hover:not(:disabled) { border-color: var(--line-dark); color: var(--ink); }
.daily__weeknav:disabled { visibility: hidden; }
.daily__weeknav .icon { width: 0.9375rem; height: 0.9375rem; }

/* Liên kết chữ dùng chung cho "Về tuần này" và "Thay đổi": nâu xám, gạch chân
   mảnh, KHÔNG tô vàng. `text-align: left` vì lockButtonWidths() đặt sàn bề ngang
   theo nhãn dài nhất trong ba ngôn ngữ — canh giữa thì nhãn ngắn trôi vào giữa
   khoảng sàn, cách dấu "·" đứng trước một khe lạ. */
.daily__textlink {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--fs-md);
  color: var(--ink-2);
  text-align: left;
  text-decoration: underline;
  text-decoration-color: var(--line-dark);
  text-underline-offset: 0.1875rem;
}
.daily__textlink:hover { color: var(--ink); text-decoration-color: currentColor; }

/* Hàng bảy ngày + dải chữ bên dưới. `padding-bottom` chừa SẴN chỗ cho dải ấy ở
   mọi trạng thái: nhãn "Hôm nay" và dòng chi tiết đều đặt tuyệt đối vào đây,
   nên sang ngày khác hay bấm một ngày thì hàng không nhảy. */
.daily__weekrow {
  position: relative;
  padding-bottom: 1.375rem;
}
/* `padding: 0` là bắt buộc: `<ol>` mang sẵn `padding-left` ~40px của trình
   duyệt, và thiếu dòng này thì cả hàng lệch phải — ô CN đẩy chữ "Hôm nay" lòi
   ra khỏi mép thẻ ở màn hẹp. */
.daily__week {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.daily__day {
  position: relative;
  font-size: var(--fs-2xs);
  color: var(--ink-3);
}
/* Cả ô là MỘT cái nút thật (Tab tới được, Enter bấm được, trình đọc màn hình gọi
   đúng tên "nút"): nhãn thứ ở trên, chấm ở dưới, trọn bề ngang cột để vùng bấm
   rộng nhất có thể. */
.daily__daybtn {
  display: grid;
  justify-items: center;
  row-gap: 0.3125rem;
  width: 100%;
  padding: 0.125rem 0;
  border: 0;
  border-radius: var(--radius);
  background: none;
  font: inherit;
  color: inherit;
}
.daily__daybtn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 1px; }
.daily__wd { line-height: 1.25; white-space: nowrap; }
/* Nhãn ĐỦ ("Thứ 2 … Chủ nhật") khi có chỗ, nhãn NGẮN ("T2 … CN") dưới 56.25em.
   Mốc này trùng mốc cỡ chữ mobile (+2px): dưới đó "Chủ nhật" cần ~58px mà một
   cột chỉ còn 36–60px. Cả hai bản đều nằm sẵn trong ô và CSS chọn, nên đổi bề
   ngang cửa sổ không phải chờ JS vẽ lại. Tiếng Anh (Mon…Sun) và tiếng Hàn (월…일)
   hai bản trùng nhau. Luôn MỘT dòng ⇒ bảy chấm luôn thẳng hàng. */
.daily__wd-short { display: none; }
@media (max-width: 56.25em) {
  .daily__wd-long { display: none; }
  .daily__wd-short { display: inline; }
}
/* Nền `--field` chứ không `--surface-2`: ở light mode `--surface-2` (#ebe1cc)
   ĐẬM hơn `--accent-soft` (#f8ecd8) của ngày học dở, nên chấm "chưa học" trông
   nặng hơn chấm "có học" — đọc ngược nghĩa. `--field` là trắng ở light, tối ở
   dark: ba mức mới đậm dần đúng thứ tự tiến bộ ở cả hai mode. */
.daily__dot {
  position: relative;
  width: 1.375rem;
  height: 1.375rem;
  border: 1px solid var(--line-dark);
  border-radius: 50%;
  background: var(--field);
}
/* Ba mức, đọc được cả khi không phân biệt màu: chấm trống · viền vàng nền mờ · tô
   đầy kèm DẤU TÍCH. Ngày có học mà chưa đạt KHÔNG bị coi như ngày trắng — công
   học dở dang vẫn là công học (luật 2026-09-11). Viền `--accent-dark` chứ không
   `--accent`: trên nền trắng light mode `--accent` chỉ được ~1.5:1. */
.daily__day.is-active .daily__dot { border-color: var(--accent-dark); background: var(--accent-soft); }
.daily__day.is-done .daily__dot { border-color: var(--accent); background: var(--accent); }
/* Dấu tích vẽ bằng hai cạnh viền xoay 45°, KHÔNG dùng `content: '✓'`. */
.daily__day.is-done .daily__dot::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0.3125rem;
  height: 0.5625rem;
  margin: -0.375rem 0 0 -0.15625rem;
  border: solid var(--on-accent);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* Ngày chưa tới: chỉ còn một vòng viền mờ, không nền. */
.daily__day.is-future .daily__dot { border-color: var(--line); background: none; }
.daily__day.is-future .daily__wd { opacity: .6; }
/* Hôm nay: viền vàng + một quầng mờ bao ngoài — nhận ra ngay mà không nổi hơn con
   số tiến độ. KHÔNG nhấp nháy, KHÔNG phập phồng. */
.daily__day.is-today .daily__dot {
  border-color: var(--accent-ink);
  box-shadow: 0 0 0 0.1875rem color-mix(in srgb, var(--accent-ink) 18%, transparent);
}
/* Ngày đang mở chi tiết: nhãn sáng lên. KHÔNG tô thêm màu vào chấm — chấm đã mang
   ba mức trạng thái rồi, thêm mức thứ tư vào đó là không ai đọc ra nữa. */
.daily__day.is-today .daily__wd,
.daily__day.is-picked .daily__wd { color: var(--ink); font-weight: 600; }

/* Dải chữ dưới hàng ngày, hai thứ thay nhau dùng. Nhãn "Hôm nay" treo dưới đúng
   cột của nó, đặt tuyệt đối và `nowrap`: ở 320px một cột chỉ ~36px, để trong
   dòng thì nó vỡ thành "Hôm / nay"; đặt tuyệt đối thì nó tràn nhẹ sang hai bên
   mà vẫn nằm gọn trong lề thẻ. Dòng chi tiết canh giữa, CÙNG đường với nhãn. */
.daily__now {
  position: absolute;
  top: calc(100% + 0.3125rem);
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: var(--fs-2xs);
  font-weight: 600;
  line-height: 1.25;
  color: var(--accent-ink);
}
.daily__daydetail {
  position: absolute;
  top: calc(100% - 1.0625rem);
  left: 0;
  right: 0;
  text-align: center;
  font-size: var(--fs-2xs);
  line-height: 1.25;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* BẬC 5 — dòng mục tiêu: chú thích nhỏ ở chân khối. Bảng chọn (chip) và ô tự
   nhập mở ra BÊN DƯỚI dòng này khi bấm "Thay đổi". */
.daily__goal { margin-top: 0.5rem; }
.daily__goal-line { font-size: var(--fs-md); color: var(--ink-3); }
.daily__goal-dot { margin-inline: 0.3125rem; }
.daily__goals { margin-top: 0.625rem; }
/* `.chips` là `display: flex` — mà `display` đè được thuộc tính `hidden`, nên
   phải tắt bằng CSS chứ không tin mỗi `hidden` (docs/quality.md). */
.daily__goals[hidden] { display: none; }

/* Ô "Tuỳ chỉnh": hàng riêng dưới các chip. `flex-wrap` để ở màn hẹp nhãn và ô
   nhập tự xuống dòng thay vì bóp ô nhập còn ba chữ số không đọc được. */
.daily__custom {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.625rem;
}
.daily__custom[hidden] { display: none; }
.daily__custom-label { font-size: var(--fs-md); color: var(--ink-3); }
.daily__custom-input {
  width: 4.5rem;
  padding: 0.3125rem 0.5rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
}
.daily__custom-input:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 1px; }
/* Lời báo lỗi chiếm trọn hàng để không đẩy nút Lưu đi chỗ khác. */
.daily__custom-err {
  flex-basis: 100%;
  font-size: var(--fs-md);
  color: var(--err);
}
.daily__custom-err[hidden] { display: none; }

.daily__hint { margin-top: 0.375rem; font-size: var(--fs-md); color: var(--ink-3); }
.daily__hint[hidden] { display: none; }

/* Màn hẹp: nút trải hết bề ngang (ngón cái bấm được ở bất kỳ đâu), chấm và khe
   giữa các ngày co lại để bảy ô vẫn nằm trên MỘT hàng ở 320px — hàng này KHÔNG
   BAO GIỜ được cuộn ngang. */
@media (max-width: 30em) {
  .daily { padding: 1.125rem 1rem 1rem; }
  .daily__cta { display: block; width: 100%; }
  .daily__week { gap: 0.125rem; }
  .daily__dot { width: 1.25rem; height: 1.25rem; }
}

/* Cảm ứng: mũi tên 1.75rem (~28px) quá nhỏ cho ngón tay — docs/ui.md mục 6. */
@media (hover: none) {
  .daily__weeknav { width: 2.5rem; height: 2.5rem; }
  .daily__weekhead { min-height: 2.5rem; }
}

/* Người đã tắt hiệu ứng chuyển động thì đừng trượt thanh, cũng đừng loé quầng. */
@media (prefers-reduced-motion: reduce) {
  .daily__fill { transition: none; }
  .daily.is-cheer .daily__bar { animation: none; }
}

.resume {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0.875rem 1rem;
  margin-top: 2.25rem;
  padding: 1.125rem 1.25rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent-soft);
}
.resume__text { min-width: 0; }
.resume .btn { justify-self: start; }
.resume__head { font-size: var(--fs-sm); color: var(--ink-2); }
.resume__title {
  margin-top: 0.125rem;
  font-size: calc(var(--fs-2xl) + 0.125rem);
  font-weight: 600;
  word-break: keep-all;
  overflow-wrap: break-word;
}
.resume__meta { margin-top: 0.1875rem; font-size: var(--fs-md); color: var(--ink-2); }

/* Thanh tiến độ trải hết hai cột, nằm dưới cùng của dải. */
.resume__prog {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 0.625rem;
}
.resume__bar {
  flex: 1;
  min-width: 0;
  height: 0.4375rem;
  border-radius: 999px;
  background: var(--field);
  overflow: hidden;
}
/* Bề ngang do home.js đặt theo tiến độ thật — số duy nhất trên trang này phải
   tính từ dữ liệu, không viết cứng được. */
.resume__fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
}
.resume__count {
  flex-shrink: 0;
  font-size: var(--fs-md);
  color: var(--ink-2);
  white-space: nowrap;
}

/* --------------------------------------------------- Section của trang chủ

   Khe 3rem giữa các section, RỘNG HƠN `--gap-section` (1.375rem) của trang chi
   tiết. Ở đó các panel là những phần của cùng một việc nên phải chụm; ở đây mỗi
   khối là một chuyện khác nhau, chụm lại thì đọc thành một mảng chữ liền. */
.home-sec { margin-top: 3rem; }
.home-sec__head {
  padding-bottom: 0.625rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-3xl);
  font-weight: 600;
}
/* Khối "ba mảng còn lại" CỐ Ý nhẹ hơn khối ba bước: tiêu đề nhỏ hơn một nấc và
   bỏ hẳn gạch chân. Đây là chỗ tạo thứ bậc 3 → 4 mà không cần thêm màu. */
.home-sec--eco .home-sec__head {
  padding-bottom: 0;
  border-bottom: 0;
  font-size: var(--fs-2xl);
}
.home-sec__sub {
  margin-top: 0.375rem;
  font-size: var(--fs-lg);
  color: var(--ink-2);
}

/* ------------------------------------------ Bài vào cửa + xem trước Dictation */
.starter-grid {
  display: grid;
  gap: 0.75rem;
  margin-top: 1.125rem;
}
.starter-card,
.dictation-peek {
  min-width: 0;
  padding: 1.125rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.starter-card {
  display: flex;
  flex-direction: column;
  align-items: start;
}
.starter-card__kicker,
.dictation-peek__eyebrow {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--accent-dark);
}
.starter-card__title {
  margin-top: 0.375rem;
  font-size: calc(var(--fs-2xl) + 0.125rem);
  font-weight: 650;
  word-break: keep-all;
  overflow-wrap: break-word;
}
.starter-card__summary {
  margin-top: 0.375rem;
  font-size: var(--fs-lg);
  line-height: 1.55;
  color: var(--ink-2);
  overflow-wrap: break-word;
}
.starter-card__meta {
  margin-top: 0.75rem;
  font-size: var(--fs-md);
  color: var(--ink-2);
}
.starter-card .btn { margin-top: 1rem; }

.dictation-peek__head {
  margin-top: 0.25rem;
  font-size: var(--fs-xl);
  font-weight: 600;
}
.dictation-peek__audio {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.875rem;
  padding: 0.625rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field);
}
.dictation-peek__play {
  position: relative;
  border: 0;
  padding: 0;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
}
/* Vòng tròn cố ý nhỏ cho khớp chiều cao dải sóng bên cạnh, nhưng chừng đó thì
   ngón cái phải nhắm. Lớp phủ trong suốt này nới VÙNG BẤM lên 44px mà không
   đụng gì tới hình: đặt absolute nên nó nằm ngoài dòng chảy, grid không sinh
   thêm hàng.

   `px` chứ KHÔNG phải `rem` — chỗ duy nhất trong file này cố ý làm vậy. Cỡ chữ
   gốc là clamp() co theo bề ngang màn (dòng 119), nên 2.75rem ra 48px trên
   desktop nhưng chỉ 41px trên điện thoại — hụt ngưỡng đúng ở nơi cần nhất.
   44px là kích thước ngón tay, không phải kích thước chữ, nên không được co
   theo chữ. */
.dictation-peek__play::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.dictation-peek__play > * { grid-area: 1 / 1; }
.dictation-peek__wave {
  display: flex;
  align-items: center;
  gap: 0.1875rem;
  height: 1.5rem;
  overflow: hidden;
}
.dictation-peek__wave i {
  flex: 1;
  max-width: 0.25rem;
  border-radius: 1px;
  background: var(--accent-dark);
}
.dictation-peek__wave i:nth-child(1) { height: 30%; }
.dictation-peek__wave i:nth-child(2) { height: 65%; }
.dictation-peek__wave i:nth-child(3) { height: 100%; }
.dictation-peek__wave i:nth-child(4) { height: 48%; }
.dictation-peek__wave i:nth-child(5) { height: 82%; }
.dictation-peek__wave i:nth-child(6) { height: 38%; }
.dictation-peek__wave i:nth-child(7) { height: 70%; }
.dictation-peek__wave i:nth-child(8) { height: 24%; }
.dictation-peek__time { font-size: var(--fs-sm); color: var(--ink-2); }
.dictation-peek__answer,
.dictation-peek__result {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.625rem;
  padding: 0.625rem 0.75rem;
  border-radius: var(--radius);
  font-size: var(--fs-md);
  color: var(--ink-2);
}
.dictation-peek__answer { background: var(--field); }
.dictation-peek__result { background: var(--accent-soft); }
/* `font-weight: 400` đè mặc định của <strong>: dòng kết quả dùng lại .w-hit của
   ô Kết quả thật, mà .w-hit chỉ đậm 600. Để <strong> ở 700 thì từ sai hoá ra
   MẢNH hơn phần xung quanh — nhấn mạnh ngược hẳn với app, nơi .result__line là
   <p>. Cỡ chữ --fs-xl đã đủ tách hai dòng này khỏi nhãn bên trái rồi. */
.dictation-peek__answer strong,
.dictation-peek__result strong {
  flex-shrink: 0;
  font-size: var(--fs-xl);
  font-weight: 400;
  color: var(--ink);
  word-break: keep-all;
}
/* Ba bước hướng dẫn. Vẫn là <ol> để trình đọc màn hình biết đây là các bước có
   thứ tự. Thẻ cố ý THẤP: mỗi bước chỉ một dòng tiêu đề + một dòng chữ, cao hơn
   nữa là ba ô rỗng giữa trang. */
.steps {
  /* Khe phải rộng hơn chính mũi tên để hai phía còn khoảng thở; ở màn Retina
     13-inch, khe 0.75rem chỉ dư khoảng 0.15rem mỗi bên nên nhìn như dính thẻ. */
  --steps-gap: 1.25rem;
  --step-arrow-size: 0.4375rem;
  /* Chevron là hai cạnh kề của hình vuông xoay 45°. Tâm phần NÉT nhìn thấy lệch
     khỏi tâm hộp size / (2√2); bù lại để căn tâm thị giác, không chỉ tâm hộp. */
  --step-arrow-optical-offset: 0.1547rem;
  display: grid;
  gap: var(--steps-gap);
  margin-top: 1.125rem;
  /* <ol> có sẵn `padding-inline-start: 40px` của trình duyệt. Bỏ list-style mà
     quên bỏ padding thì cả hàng ba bước bị thụt vào 40px so với tiêu đề section
     và so với ba thẻ bên dưới — đo được 2026-08-19 ở 1280: thẻ bước bắt đầu ở
     x=115 trong khi mọi thứ khác bắt đầu ở x=63. */
  padding: 0;
  list-style: none;
}
.step {
  position: relative;                  /* mốc cho mũi tên nối ở ::before */
  padding: 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.step__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
/* Số thứ tự. Vòng tròn nền accent để mắt bắt ngay đây là bước mấy — nhưng chữ
   số nằm trong HTML, không phải `content` của CSS (docs/i18n.md). */
.step__num {
  display: grid;
  place-items: center;
  width: 1.625rem;
  height: 1.625rem;
  border-radius: 50%;
  background: var(--accent);
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--on-accent);
}
.step__icon { color: var(--accent-dark); }

/* Mũi tên nối hai bước: một góc vuông xoay 45°, dựng bằng VIỀN chứ không phải
   ký tự — CSS không được chứa chữ hiển thị. Mặc định (một cột) mũi tên chỉ
   XUỐNG và nằm trong khe giữa hai thẻ. */
.step + .step::before {
  content: '';
  position: absolute;
  left: 50%;
  top: calc(var(--steps-gap) / -2 - var(--step-arrow-optical-offset));
  width: var(--step-arrow-size);
  height: var(--step-arrow-size);
  border-right: 1px solid var(--line-dark);
  border-bottom: 1px solid var(--line-dark);
  transform: translate(-50%, -50%) rotate(45deg);
}
.step__head { font-size: var(--fs-lg); font-weight: 600; }
.step__body {
  margin-top: 0.25rem;
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--ink-2);
  overflow-wrap: break-word;
}

/* ------------------------------------------- Ba mảng còn lại của site

   Thẻ ở đây NHỎ HƠN thẻ ba bước: padding ít hơn, tiêu đề nhỏ hơn, chỉ một dòng
   mô tả. Chúng không được tranh chỗ với Dictation. */
/* `margin-top` tách hàng thẻ khỏi câu phụ đề — nhẹ thôi, chỉ đủ để câu đó
   thuộc về tiêu đề chứ không dính vào thẻ đầu tiên (chủ dự án 2026-08-19).

   Số cột do `auto-fit` quyết theo SỐ THẺ và chỗ trống thật, không đếm tay: thẻ
   "Từ vựng" vừa được gỡ ra và sẽ quay lại, mà `repeat(3, 1fr)` viết cứng thì
   hai thẻ để lại một ô trống toang ở cột ba. */
.home-cards {
  display: grid;
  gap: 0.75rem;
  margin-top: 1.125rem;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}
.home-card {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.375rem;
  padding: 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
}
.home-card:hover { text-decoration: none; }
/* Icon nằm CÙNG HÀNG với tiêu đề, không phải một dòng riêng: cho icon một dòng
   riêng là thẻ cao thêm ~24px, thành ra thẻ "mảng phụ" lại cao hơn thẻ ba bước
   của Dictation — đúng thứ bậc bị đảo ngược (đo 2026-08-19: 164px so với
   142.7px, ở 1280). */
.home-card__top {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
}
.home-card__icon { color: var(--accent-dark); }
.home-card__head { font-size: var(--fs-lg); font-weight: 600; }
.home-card__body {
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--ink-2);
  overflow-wrap: break-word;
}
/* Đẩy dòng CTA xuống đáy để ba thẻ có mô tả dài ngắn khác nhau vẫn thẳng hàng
   nhau ở dưới. */
/* `--ink` chứ không phải `--accent-dark`: đây là CHỮ, mà ở light mode
   `--accent-dark` trên nền thẻ trắng chỉ đạt 1.86:1 (đo 2026-08-19). Mũi tên →
   và cú hover đổi màu viền đã đủ nói đây là chỗ bấm được. */
.home-card__go {
  margin-top: auto;
  padding-top: 0.125rem;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink);
}

/* ------------------------------------------------------------ Câu kết

   Không khung, không viền, chỉ khoảng trắng — chỗ nghỉ mắt sau một trang toàn
   thẻ. Khe trên rộng gấp rưỡi khe giữa các section để tách hẳn khỏi phần trước. */
.ending {
  margin-top: 4.5rem;
  padding-block: 2rem 1rem;
  text-align: center;
}
.ending .wave { margin-bottom: 1.25rem; }
.ending__line {
  max-width: 34rem;
  margin-inline: auto;
  font-size: var(--fs-2xl);
  line-height: 1.7;
  color: var(--ink-2);
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* Câu thứ hai mới là câu cần đọng lại — to hơn, đậm hơn, màu chữ chính. */
.ending__line--strong {
  margin-top: 0.375rem;
  font-size: calc(var(--fs-3xl) + 0.1875rem);
  font-weight: 600;
  color: var(--ink);
}
.ending__actions { margin-top: 1.75rem; }
.btn--end {
  padding: 0.625rem 1.25rem;
  font-size: var(--fs-lg);
  font-weight: 600;
}

/* Ba BƯỚC chỉ xếp ngang từ 56.25em (900px): dưới mốc đó mỗi cột còn ~200px, câu
   tiếng Hàn ngắt tan nát. Cùng mốc với chỗ hai cột của trang chi tiết xếp dọc
   (docs/ui.md mục 3). Hàng thẻ bên dưới KHÔNG cần mốc nào — `auto-fit` ở trên
   đã tự lo. */
@media (min-width: 56.25em) {
  .steps { grid-template-columns: repeat(3, 1fr); }
  .starter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Ba bước nằm ngang thì mũi tên phải chỉ SANG PHẢI, và đứng ngang tâm vòng
     tròn số (padding 0.875rem + nửa vòng tròn 0.8125rem). Offset quang học bù
     phần nét chevron lệch khỏi tâm hộp về phía đầu mũi tên. */
  .step + .step::before {
    left: calc(var(--steps-gap) / -2 - var(--step-arrow-optical-offset));
    top: 1.6875rem;
    border-right: 1px solid var(--line-dark);
    border-bottom: none;
    border-top: 1px solid var(--line-dark);
    transform: translate(-50%, -50%) rotate(45deg);
  }
}

/* Hover chỉ trên máy CÓ CHUỘT. Rất nhẹ — đổi mỗi màu viền, không dịch chuyển,
   không đổ bóng: thẻ nhảy một nhịp khi rê chuột là thứ duy nhất người ta nhớ. */
@media (hover: hover) {
  .step,
  .home-card,
  .home-level { transition: border-color .15s ease; }
  .step:hover,
  .home-card:hover,
  .home-level:hover { border-color: var(--accent); }
}

@media (max-width: 38.75em) {
  .hero { padding-block: 1.25rem 0.25rem; }
  .hero__title { font-size: 1.625rem; }
  /* Nút chính chiếm gần hết bề ngang trên điện thoại — ngón cái không phải nhắm.
     `width: 100%` chứ không phải `flex: 1`: `.hero__actions`/`.ending__actions`
     là <p> thường nên `lockWidth()` vẫn đo được bề ngang thật của nút, và ba
     ngôn ngữ cùng ra một con số (bề ngang khối cha) nên sàn không lệch. */
  .btn--hero {
    width: 100%;
    text-align: center;
  }

  .home-levels__label { flex-basis: 100%; }
  .home-level { flex: 1; text-align: center; }

  .dictation-peek__answer,
  .dictation-peek__result {
    align-items: start;
    flex-direction: column;
    gap: 0.25rem;
  }

  /* Dải "đang học dở" xếp dọc: nút xuống dưới, không bóp cột chữ còn 40%. */
  .resume { grid-template-columns: 1fr; }

  .ending { margin-top: 2.75rem; padding-block: 1.25rem 0.5rem; }
  .ending__line--strong { font-size: var(--fs-3xl); }
  .ending__actions { margin-top: 1.25rem; }
  .btn--end { width: 100%; text-align: center; }
}

/* ==========================================================================
   DICTATION LIST — dictation-list.html

   Bố cục một cột: MỘT hàng lọc trên cùng (ô tìm + chip cấp độ), rồi mỗi chủ đề
   một thẻ đóng/mở chứa danh sách bài.
   Bốn màn danh sách sau (Listening / Reading / Speaking Test, My Report) dùng
   lại đúng những class này — đặt tên theo chức năng, không theo nội dung
   Dictation (docs/i18n.md).
   ========================================================================== */

/* Không còn khung panel bọc ngoài: ô tìm và chip đã tự có viền, bọc thêm một
   lớp nữa là ba khung lồng nhau. */
.filters {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.625rem 0.875rem;
  margin-bottom: 1.125rem;
}

/* flex-basis 16rem: đủ rộng thì ô tìm và chip nằm cùng hàng, chật thì chip tự
   rơi xuống hàng dưới chứ ô tìm không bị bóp thành khe hẹp. */
.filters__search {
  flex: 1 1 16rem;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  color: var(--ink-3);
}
.filters__search:focus-within { border-color: var(--accent); color: var(--ink-2); }
.filters__search .icon { width: 1.0625rem; height: 1.0625rem; flex-shrink: 0; }

/* Ô nhập tự bỏ viền/nền: khung đã do .filters__search vẽ rồi */
.filters__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--fs-body);
  color: var(--ink);
}
.filters__input:focus { outline: none; }
.filters__input::placeholder { color: var(--ink-3); }
/* Bỏ nút "x" mặc định của input[type=search] trên WebKit — thay bằng
   .filters__clear để đủ vùng bấm và đúng tông màu */
.filters__input::-webkit-search-cancel-button { -webkit-appearance: none; }

.filters__clear {
  display: flex;
  flex-shrink: 0;
  padding: 0.125rem;
  color: var(--ink-3);
}
.filters__clear:hover { color: var(--ink); }

.chips { display: flex; flex-wrap: wrap; gap: 0.375rem; }
.chip {
  padding: 0.25rem 0.6875rem;
  border: 1px solid var(--line-dark);
  border-radius: 1rem;
  background: var(--field);
  font-size: var(--fs-md);
  color: var(--ink-2);
  white-space: nowrap;
}
.chip:hover { border-color: var(--accent); color: var(--ink); }
.chip.is-active {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

/* ---------------------------------------- Dropdown chọn nhiều: lọc trạng thái
   Thay bốn chip trạng thái học (2026-09-13). Nút + panel mượn khuôn nút đa ngữ
   ở header — selector khai chung ở đầu file, cạnh `.lang__btn` / `.lang__menu` —
   để cả site chỉ có MỘT kiểu dropdown. Phần riêng ở đây: vị trí panel, ba ô
   tích và viền "đang lọc". */
.multi__caret { width: 0.875rem; height: 0.875rem; }
/* Đệm ngang hẹp hơn nút đa ngữ một nấc — hàng này chật ở 375px (xem ghi chú ở
   `.list-view--filters`). */
.multi__btn { padding-inline: 0.5rem; }
/* Đang lọc (chưa chọn đủ ba mức) thì viền nút vàng: dropdown đóng rồi, nhìn lướt
   vẫn biết danh sách đang bị lọc. Cùng màu với lúc hover/mở — không thêm màu. */
.multi.is-filtering .multi__btn { border-color: var(--accent-dark); color: var(--ink); }
/* Panel mở từ mép TRÁI nút sang phải: nút nằm sát lề trái trang, mở về bên trái
   như panel đa ngữ (`right: 0`) là tràn ra ngoài màn hình. `<fieldset>` mang sẵn
   margin của trình duyệt nên phải tự đặt lại. */
.multi__menu {
  left: 0;
  top: calc(100% + 0.375rem);
  min-width: max(100%, 11rem);
  margin: 0;
}
/* Cả hàng là <label>: bấm vào chữ cũng tích được ô, vùng bấm rộng bằng panel. */
.multi__opt {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4375rem 0.625rem;
  border-radius: var(--radius);
  font-size: var(--fs-lg);
  color: var(--ink-2);
  white-space: nowrap;
  cursor: pointer;
}
.multi__opt:hover { background: var(--accent-soft); color: var(--ink); }
.multi__opt:has(input:checked) { color: var(--ink); }
/* Ô cuối cùng còn chọn bị khoá bằng `aria-disabled` (xem renderStatusFilter) — con
   trỏ thôi báo "bấm được", nhưng ô vẫn tích VÀNG: không dùng `disabled` vì trình
   duyệt tô nó xám. */
.multi__opt:has(input[aria-disabled="true"]) { cursor: default; }
.multi__opt input { margin: 0; flex-shrink: 0; accent-color: var(--accent-dark); }

/* ------------------------------------------ Thẻ chủ đề đóng/mở (accordion) */
/* Mặc định mọi thẻ đều đóng: người dùng lướt qua danh sách chủ đề trước, mở cái
   nào cần cái đó. Mở được nhiều thẻ cùng lúc — cuộn xuống mở tiếp thẻ khác mà
   thẻ đang xem không tự đóng lại.

   Hiệu ứng trượt làm bằng grid-template-rows 0fr ↔ 1fr chứ không phải max-height
   (không phải đoán một con số cao thừa) và cũng không phải <details> (đóng lại
   là display:none ngay, không kịp trượt). */

/* Một hàng hai thẻ từ 64em (1024px) trở lên. Không hạ thấp mốc này: mỗi cột khi
   đó rộng ~46% khung nhìn, dưới 1024px là cột hẹp hơn 424px — mốc mà thẻ bài
   phải xuống hai hàng — nhưng media query của .lesson__card lại tính theo BỀ
   NGANG KHUNG NHÌN nên nó không kịp bật, thẻ bài sẽ bị bóp chứ không xuống hàng.

   align-items: start là bắt buộc: thiếu nó thì thẻ đóng bị kéo cao bằng thẻ mở
   cùng hàng, thành một ô rỗng to đùng. */
.topics { display: grid; gap: 0.5rem; }
@media (min-width: 64em) {
  .topics {
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem var(--col-gap);
    align-items: start;
  }

}

.topic { margin-bottom: 0; }

.topic__head {
  position: relative;            /* mốc cho .prog bám mép dưới */
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.75rem 0.875rem;
  border-radius: var(--radius);
  font-size: calc(var(--fs-lg) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  text-align: start;
}
.topic__head:hover { background: var(--surface-2); }
.topic__head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.topic.is-open .topic__head {
  border-bottom: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
}

.topic__name { min-width: 0; overflow-wrap: break-word; }
.topic__count {
  margin-inline-start: auto;
  font-size: var(--fs-md);
  font-weight: 400;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.topic__chevron { color: var(--ink-3); transition: transform .24s ease; }
.topic.is-open .topic__chevron { transform: rotate(180deg); }

.topic__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .24s ease;
}
.topic.is-open .topic__panel { grid-template-rows: 1fr; }

/* visibility: đóng rồi thì nội dung phải biến mất khỏi trình đọc màn hình và
   khỏi thứ tự Tab, nhưng chỉ sau khi trượt xong nên hoãn bằng transition-delay.
   overflow + min-height: 0 là bắt buộc, thiếu là hàng grid không co xuống 0. */
.topic__inner {
  overflow: hidden;
  min-height: 0;
  visibility: hidden;
  transition: visibility 0s .24s;
}
.topic.is-open .topic__inner { visibility: visible; transition-delay: 0s; }

.topic__desc {
  padding: 0.75rem 0.875rem 0;
  font-size: var(--fs-lg);
  color: var(--ink-3);
}

/* container-type: thẻ bài phải tự xuống 2 hàng theo bề ngang CỦA CỘT chứ không
   theo bề ngang khung nhìn — từ khi danh sách chia 2 cột thì hai con số đó khác
   hẳn nhau (khung nhìn 1024px nhưng cột chỉ ~460px). Xem @container bên dưới. */
.lessons {
  container: lessons / inline-size;
  list-style: none;
  margin: 0;
  padding: 0.5rem 0.5rem 0.625rem;
}

/* Thẻ bài — HAI tầng ở MỌI bề ngang (làm gọn lại 2026-09-13):
     tầng 1: số thứ tự · tên bài + tóm tắt · › · trái tim
     tầng 2: hàng tiến độ "5/27 câu  ━━━────  19%  [Sơ cấp]", thẳng mép tên bài
   Trước đó thẻ còn một chế độ MỘT hàng khi cột rộng hơn 52rem (hàng meta chen
   ngang tên bài) kèm thanh tiến độ dài chạy sát mép dưới thẻ. Chế độ một hàng
   chỉ còn sống ở dải khung nhìn ~950–1023px nên bỏ hẳn: một kiểu thẻ cho mọi cỡ
   màn, và tên bài không còn phải chia bề ngang với hàng meta — nên cũng không
   còn mốc `@container` nào phải đo lại mỗi khi thêm chữ vào hàng meta. */
.lesson__card {
  display: grid;
  /* Cột cuối là nút trái tim. Thêm nút vào mà quên nới lưới thì nó rơi xuống
     hàng dưới và cả thẻ cao thêm một tầng. */
  grid-template-columns: 1.75rem minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 0.375rem 0.75rem;
  padding: 0.625rem 0.75rem;
  border-radius: var(--radius);
  color: inherit;
  transition: background-color .15s ease;
}
/* Hover: nền sáng hơn một nấc. KHÔNG thêm viền vàng, KHÔNG làm thanh tiến độ sáng
   lên — thanh là thông tin, không phải hiệu ứng. */
a.lesson__card:hover { background: var(--surface-2); text-decoration: none; }
a.lesson__card:hover .lesson__title { color: var(--accent-dark); }
a.lesson__card:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.lesson__action { grid-column: 3; grid-row: 1; align-self: center; }
.lesson__card > .heart { grid-column: 4; grid-row: 1; }

/* Ngăn giữa hai bài: MỘT đường mảnh màu viền nhạt nhất của bảng (`--line`),
   trung tính, KHÔNG vàng — thay cho thanh tiến độ dài từng kiêm luôn việc chia
   bài. Đủ tách hai bài mà không thành một bậc thang kẻ ngang. */
.lesson + .lesson { border-top: 1px solid var(--line); }

.lesson__no {
  font-size: var(--fs-sm);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  text-align: end;
}

/* Ô chữ của thẻ bài: tên bài + dòng tóm tắt. `min-width: 0` phải nằm Ở ĐÂY,
   trên chính con của cột `minmax(0, 1fr)` — để ở `.lesson__title` bên trong thì
   ô lưới vẫn nở theo chữ dài nhất và thẻ đẩy nhau lệch cột. Khe giữa hai dòng
   rất nhỏ để tên + tóm tắt đọc thành MỘT cụm. */
.lesson__text {
  min-width: 0;
  display: grid;
  gap: 0.0625rem;
}

.lesson__title {
  min-width: 0;
  font-size: calc(var(--fs-lg) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}

/* Dòng tóm tắt. Số dòng bị KHOÁ CỨNG chứ không để chữ quyết định: bài dài bài
   ngắn khác nhau thì thẻ cao thấp so le. `min-height` giữ sàn khi chữ ngắn,
   `line-clamp` chặn trần khi chữ dài, nên mọi thẻ cao bằng nhau ở cả hai chiều.
   Bản đầy đủ của tóm tắt vẫn có ở màn chi tiết, đây chỉ là chỗ nhận ra bài. */
.lesson__summary {
  /* Hai dòng ở MỌI bề ngang. Một dòng thì ở cột rộng đã cắt mất 15/57 tóm tắt
     bản tiếng Anh, mà ở 320px còn tệ hơn — một dòng chỉ chứa ~30 ký tự. Hai
     dòng thì không bài nào bị cắt ở bất kỳ mốc nào, đo 2026-08-24. */
  --sum-lines: 2;
  min-width: 0;
  font-size: var(--fs-sm);
  line-height: 1.4;
  color: var(--ink-3);
  min-height: calc(var(--sum-lines) * 1.4em);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--sum-lines);
  overflow: hidden;
}

/* Tầng 2 — hàng tiến độ: "5/27 câu  ━━━────  19%  [Sơ cấp]", dưới ô chữ, thẳng mép
   tên bài (cột 2 → hết).

   Ba món đầu có bề ngang CỐ ĐỊNH để thanh và phần trăm của các bài thẳng hàng:
   · số câu — rộng đúng bằng nhãn DÀI NHẤT có thể có (kèm dấu tích). `--count-w`
     do dictation-list.js#syncMetaWidths đo theo ngôn ngữ đang chọn: "31/31
     sentences" dài gần gấp đôi "31/31문장", đoán một con số trong CSS là sai;
   · thanh — 6.5rem, 4rem ở cột hẹp. KHÔNG co giãn theo chỗ trống: nhãn cấp độ
     dài ngắn khác nhau ("Sơ cấp" / "Trung cấp"), thanh co giãn thì mỗi thẻ co
     một khác và kéo lệch cả cột phần trăm — đo được 2026-09-13 ở 375px: thanh
     75px cạnh 69px, mép phần trăm 265px cạnh 289px;
   · phần trăm — ô rộng bằng "100%" (`--pct-w`, cùng hàm đo), canh phải.
   Chỉ nhãn cấp độ được xuống dòng, và chỉ khi thật sự hết chỗ (`flex-wrap`). */
.lesson__meta {
  grid-column: 2 / -1;
  grid-row: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem 0.75rem;
  min-width: 0;
}
.lesson__stat {
  font-size: var(--fs-xs);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.lesson__count { flex: 0 0 var(--count-w, auto); }
/* Phần trăm: cùng cỡ, cùng màu nhạt với số câu — KHÔNG vàng. */
.lesson__pct { flex: 0 0 var(--pct-w, 4.5ch); text-align: end; }
/* Bài HOÀN THÀNH (chép đúng hết — luật 2026-08-17): một dấu tích nhỏ trước số
   câu, thay cho nhãn "Hoàn thành". Bài bỏ qua hết mà chưa chép đúng hết cũng
   100% nhưng KHÔNG có dấu này — hai con số "đi tới đâu" và "chép đúng hết" vẫn
   không bị gộp. Vẽ bằng viền, không dùng ký tự '✓' (docs/i18n.md). */
.lesson__tick {
  display: inline-block;
  width: 0.25rem;
  height: 0.5rem;
  margin: 0 0.4375rem 0.1875rem 0.125rem;
  border: solid var(--ok);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  vertical-align: middle;
}

.lesson__go { font-size: var(--fs-2xl); color: var(--ink-3); }
a.lesson__card:hover .lesson__go { color: var(--accent-dark); }

/* Nhãn cấp độ. Mỗi cấp một tông, nhưng vẫn trong bảng màu chung — không thêm
   hex rời, chỉ dùng biến ở :root (docs/ui.md). */
.tag {
  padding: 0.125rem 0.5rem;
  border-radius: 1rem;
  border: 1px solid var(--line-dark);
  font-size: var(--fs-2xs);
  font-weight: 600;
  white-space: nowrap;
}
.tag--beginner { border-color: var(--ok); color: var(--ok); }
.tag--intermediate { border-color: var(--accent); color: var(--accent); }
.tag--advanced { border-color: var(--err); color: var(--err); }
/* Cờ báo mode ở màn Listening Test Details — chỉ hiện từ lúc bấm Bắt đầu tới
   hết màn Kết quả (assets/js/topik-listening-test-details.js#renderModeFlag). */
.tag--practice { border-color: var(--ok); color: var(--ok); }
.tag--exam { border-color: var(--warn); color: var(--warn); }

/* --------------------------------------------------- Thanh tiến độ mảnh
   Dùng chung cho đầu chủ đề và thẻ bài ở Dictation List, HAI dáng:

   · Đầu chủ đề — `position: absolute` sát mép dưới: đầu chủ đề là một hàng flex
     chật, thanh nằm ngoài dòng chảy nên đầu thẻ không cao thêm pixel nào. Chưa
     xong bài nào thì JS không dựng thanh (dictation-list.js#progressBar).
   · Thẻ bài — `.prog--inline`: thanh MINI nằm TRONG hàng tiến độ, chỉ có ở bài
     đã xử lý câu nào đó. Bài chưa học chỉ hiện "26 câu  [Sơ cấp]". */
.prog {
  position: absolute;
  inset-inline: 0.75rem;
  bottom: 0.25rem;
  height: 2px;
  border-radius: 1px;
  background: var(--line-dark);
  overflow: hidden;
  pointer-events: none;          /* thẻ là một <a>: đừng ăn mất cú bấm */
}
.prog__fill {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: inherit;
}
.prog--done .prog__fill { background: var(--ok); }

/* Đầu chủ đề có padding rộng hơn thẻ bài, canh thanh theo đúng mép chữ của nó */
.topic__head .prog { inset-inline: 0.875rem; bottom: 0.375rem; }

/* Thanh mini của thẻ bài: bề ngang CỐ ĐỊNH 6.5rem (~113px ở 1280), 4rem (~60px)
   ở cột hẹp — xem lý do ở `.lesson__meta`. Không bao giờ kéo dài theo bề ngang
   thẻ: nó là chỉ báo nhỏ, không phải vạch chia bài. Không glow, không bóng. */
.prog--inline {
  position: static;
  flex: 0 0 6.5rem;
  height: 0.1875rem;
  border-radius: 999px;
}
/* Light mode: `--accent` #eecfa1 trên rãnh `--line-dark` #d9c9a9 chỉ được 1.09:1 —
   thanh mini gần như tàng hình. `--accent-ink` được 3.08:1 (đo 2026-09-13, cùng
   phép đo với khối Nhịp học ở trang chủ). Bài hoàn thành giữ màu `--ok`. */
:root[data-theme="light"] .prog--inline .prog__fill { background: var(--accent-ink); }
:root[data-theme="light"] .prog--inline.prog--done .prog__fill { background: var(--ok); }

/* Cột hẹp (điện thoại): khe khép lại và thanh xuống 4rem để cả hàng — kể cả nhãn
   dài nhất "Trung cấp" — nằm trên MỘT dòng ở điện thoại 375px bản tiếng Việt.
   Ngân sách đo 2026-09-13 (ca dài nhất, dấu tích + "31/31 câu" + 100%): số câu
   ~78 + thanh 60 + phần trăm ~38 + "Trung cấp" ~76 + ba khe 5.6px = ~269px;
   hàng có ~275px trên điện thoại. ⚠ Trình duyệt DESKTOP thu cửa sổ về 375px có
   thanh cuộn cổ điển ~15px nên chỉ còn ~260px — ở đó "Trung cấp" rớt dòng.
   Mốc 28rem chứ không 24rem: ở 414px content box là ~24.4rem, để cỡ rộng thì
   "Trung cấp" rớt dòng; từ 1024px (hai cột, ~28.7rem) mới đủ chỗ cho cỡ rộng.
   Nhãn cấp độ ĐƯỢC PHÉP xuống dòng (chủ dự án chốt) ở 320px, và ở bản tiếng Anh
   tới 414px: "31/31 sentences" ~113px + "Intermediate" ~92px cần ~317px trong
   hàng 312px. Thu thanh dưới 4rem để vừa là trái khoảng 60–80px chủ dự án đặt.
   `@container` đo content box của `.lessons`. */
@container lessons (max-width: 28rem) {
  .lesson__meta { column-gap: 0.375rem; }
  .prog--inline { flex-basis: 4rem; }
}

.list-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.875rem;
  padding: 2.5rem 1rem;
  text-align: center;
  font-size: var(--fs-lg);
  color: var(--ink-3);
}

/* ---------------------------------------------------------------- Guide */
/* Cùng một khe với mọi section khác của trang — xem `--gap-section` */
.credit { margin-top: var(--gap-section); }
.guide { margin-top: var(--gap-section); }
.guide p { color: var(--ink-2); font-size: var(--fs-xl); }

/* Hướng dẫn dạng các bước. Dùng padding-inline-start (không phải padding-left)
   để sau này thêm ngôn ngữ RTL không phải viết lại — docs/i18n.md. */
.guide__steps {
  margin: 0;
  padding-inline-start: 1.375rem;
  display: grid;
  gap: 0.5rem;
  color: var(--ink-2);
  font-size: var(--fs-xl);
  line-height: 1.75;
}
.guide__steps li { padding-inline-start: 0.125rem; }
.guide__steps li::marker { color: var(--ink-3); }
.guide__steps b { color: var(--ink); font-weight: 600; }

/* ----------------------------------------------------------------- Footer */
.footer {
  border-top: 1px solid var(--line);
  background: var(--surface-2);
  padding-block: 1.25rem;
  font-size: var(--fs-md);
  color: var(--ink-3);
}
.footer__inner {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
}
.footer__contact { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.footer__contact span { overflow-wrap: anywhere; }
.privacy-copy { max-width: 52rem; }
.privacy-copy h2 { margin-block: 1.5rem 0.45rem; font-size: var(--fs-xl); }
.privacy-copy h2:first-child { margin-top: 0; }
.privacy-copy p { color: var(--ink-2); line-height: 1.75; }
.btn--danger { border-color: var(--bad); color: var(--bad); }
.btn--danger:hover:not(:disabled) { background: color-mix(in srgb, var(--bad) 12%, transparent); }

/* ==========================================================================
   LISTENING TEST LIST — topik-listening-test-list.html

   Danh sách PHẲNG (không accordion như trang Dictation): hàng lọc ở trên, rồi
   mỗi đề một thẻ. Dùng lại .filters / .chips / .tag của khối trên — đặt tên
   theo chức năng nên không phải chép lại (docs/i18n.md).
   ========================================================================== */

/* container-type: thẻ đề phải xuống hàng theo bề ngang CỦA CỘT, không theo bề
   ngang khung nhìn — cùng lý do với @container lessons ở trên. */
.tests {
  container: tests / inline-size;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}
@media (min-width: 64em) {
  .tests {
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem var(--col-gap);
    align-items: start;
  }
}

.test__card {
  display: grid;
  /* Cột cuối là nút trái tim — xem ghi chú ở .lesson__card. */
  grid-template-columns: 1.75rem minmax(0, 1fr) auto auto;
  align-items: start;
  gap: 0.875rem;
  height: 100%;
  padding: 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
}
a.test__card:hover { border-color: var(--accent); text-decoration: none; }
a.test__card:hover .test__title { color: var(--accent-dark); }
a.test__card:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.test__no {
  padding-top: 0.125rem;
  font-size: var(--fs-sm);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  text-align: end;
}

.test__body { min-width: 0; }

.test__title {
  font-size: calc(var(--fs-lg) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}

.test__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.375rem 0.625rem;
  margin-top: 0.625rem;
}
.test__stat {
  font-size: var(--fs-xs);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.test__action { align-self: center; }
.test__go { font-size: var(--fs-2xl); color: var(--ink-3); }
a.test__card:hover .test__go { color: var(--accent-dark); }

/* Nhãn cấp độ TOPIK — vẫn trong bảng màu chung, không thêm hex rời */
.tag--topik1 { border-color: var(--ok); color: var(--ok); }
.tag--topik2 { border-color: var(--accent); color: var(--accent); }


/* Cột hẹp: mũi tên xuống hàng riêng thay vì bóp tên đề lại.
   ⚠ @container đo theo CONTENT BOX của .tests (padding 0) — con số này là bề
   ngang cột thật. */
@container tests (max-width: 24rem) {
  .test__card { grid-template-columns: 1.75rem minmax(0, 1fr) auto; }
  .test__action { grid-column: 2; justify-self: start; margin-top: 0.5rem; }
  .test__card > .heart { grid-column: 3; grid-row: 1; align-self: start; }
}

/* ==========================================================================
   LISTENING TEST DETAILS — listening-test.html

   Ba giai đoạn trên cùng một trang: chọn chế độ → làm bài → kết quả. Chuyển
   giai đoạn bằng thuộc tính `hidden` trong JS, dựa hẳn vào rule
   `[hidden] { display: none !important }` ở phần Reset.
   ========================================================================== */

/* ------------------------------------------------ Giai đoạn 1: trước khi làm */
.start { margin-top: 1.375rem; }

.start__meta {
  list-style: none;
  margin: 0 0 1.125rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 0.5rem;
}
.start__meta li {
  padding: 0.1875rem 0.625rem;
  border: 1px solid var(--line-dark);
  border-radius: 1rem;
  font-size: var(--fs-md);
  color: var(--ink-2);
  white-space: nowrap;
}

#official-time-note {
  margin: 0 0 1.125rem;
  font-size: var(--fs-sm);
}
#official-time-note-exam {
  margin: 0.875rem 0;
  font-size: var(--fs-sm);
}

.modes {
  display: grid;
  gap: 0.5rem;
  margin: 0 0 1.125rem;
  padding: 0;
  border: 0;
}
.modes__legend {
  padding: 0 0 0.5rem;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink-2);
}

/* Cả ô là vùng bấm, không chỉ cái nút tròn bé xíu */
.mode {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  cursor: pointer;
}
.mode:hover { border-color: var(--accent); }
.mode:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.mode:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.mode input { margin-top: 0.25rem; accent-color: var(--accent-dark); flex-shrink: 0; }

.mode__body { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.mode__name { font-size: var(--fs-xl); font-weight: 600; color: var(--ink); }
.mode__hint { font-size: var(--fs-lg); color: var(--ink-3); }

/* ------------------------------------------------- Giai đoạn 2: thanh trạng thái */
/* Dính ngay dưới header để đồng hồ và nút Nộp bài luôn trong tầm mắt, dù danh
   sách câu hỏi dài tới đâu. z-index thấp hơn header (100) để không đè lên nó. */
/* Cụm dính: thanh thời gian + trình phát. Nền phải ĐỤC, không thì câu hỏi chạy
   xuyên qua khi cuộn. Padding dưới để câu hỏi không dán sát mép cụm.

   Chỉ khối này dính, hai thanh con thì không: dính lồng nhau thì thanh dưới
   phải tự biết chiều cao thanh trên, mà chiều cao đó đổi theo ngôn ngữ và theo
   bề ngang (cả hai thanh đều wrap ở màn hẹp). */
.exam-sticky {
  position: sticky;
  top: var(--header-real-h, var(--header-h));
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  margin-top: 1.375rem;
  padding-bottom: 0.625rem;
  background: var(--bg);

  /* ⚠ ĐỪNG BỎ `will-change`. Nó KHÔNG phải tối ưu tốc độ, nó chữa một lỗi vẽ:
     lúc đang phát audio, `timeupdate` bắn ~4 lần/giây và bắt Chrome vẽ lại cụm
     dính này liên tục; thỉnh thoảng Chrome vẽ nhầm phần nền ở mép dưới (dải
     `padding-bottom` 0.625rem) bằng ảnh cũ của nội dung đang cuộn phía sau, tạo
     ra một vệt chữ mờ lóe lên rồi tắt (chủ dự án báo 2026-08-23, Chrome).
     Ép cụm ra một lớp vẽ riêng thì Chrome không còn trộn nó với nội dung sau
     lưng nữa. Đã đo: bố cục đứng im tuyệt đối trong 900 khung hình liên tiếp
     (chiều cao cụm, thẻ player, dòng ghi chú, `--sticky-h` đều một giá trị duy
     nhất), nên đây chắc chắn không phải lỗi bố cục. */
  will-change: transform;
}

.exam-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.625rem 0.875rem;
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.exam-bar__clock { display: inline-flex; align-items: baseline; gap: 0.5rem; }
.exam-bar__label { font-size: var(--fs-md); color: var(--ink-3); }
.exam-bar__time {
  font-size: calc(var(--fs-2xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* Còn dưới một phút — đổi màu chứ không nhấp nháy, tránh làm hoảng người đang thi */
.exam-bar.is-urgent .exam-bar__time { color: var(--err); }
.exam-bar__answered {
  margin-inline-start: auto;
  font-size: var(--fs-md);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------- Giai đoạn 2: bố cục */
/* KHÔNG dùng .work: class đó mang min-height neo tay để hai tab của trang
   Dictation bằng chiều cao nhau (docs/ui.md mục 2) — ở đây chỉ tạo khoảng
   trống thừa dưới đề ngắn. */
.exam {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 14rem;
  /* ⚠ KHOẢNG CÁCH DÒNG PHẢI LÀ rem, KHÔNG được dùng `var(--col-gap)` (2%).
     Gap theo % ở trục dọc tính trên CHIỀU CAO của chính lưới này. Màn hẹp thì
     lưới xếp dọc, đề 50 câu cao hơn 21 000px → 2% thành hơn 400px, mà chiều cao
     auto lại được tính khi gap còn bằng 0, nên phiếu trả lời tụt xuống quá đáy
     .exam và ĐÈ LÊN mục Bản quyền + Cách làm bài. Đề ngắn không lộ ra vì 2% của
     vài trăm px chỉ là vài px. Trục ngang giữ % theo docs/ui.md. */
  gap: 1.25rem var(--col-gap);
  align-items: start;
  margin-top: 1rem;
}
.exam__main { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }

/* Phiếu trả lời đi theo khi cuộn. Chừa chỗ cho header + cụm dính.

   `--sticky-h` là chiều cao THẬT của .exam-sticky, do topik-listening-test-details.js
   đo bằng ResizeObserver — không neo tay được vì cụm đó xuống hàng theo bề
   ngang và theo ngôn ngữ. Giá trị dự phòng chỉ dùng trong nháy mắt trước khi JS
   đo xong.

   ⚠ Đề 50 câu: lưới số cao hơn khung nhìn, không chặn trần thì phần cuối phiếu
   nằm ngoài màn hình và không tài nào cuộn tới (đúng chỗ docs/screens.md ghi là
   "chưa chạy tốt"). Chặn trần rồi cho CUỘN TRONG phiếu. */
.exam__side {
  position: sticky;
  top: calc(var(--header-real-h, var(--header-h)) + var(--sticky-h, 4.5rem) + 0.75rem);
  max-height: calc(100vh - var(--header-real-h, var(--header-h)) - var(--sticky-h, 4.5rem) - 1.5rem);
  max-height: calc(100dvh - var(--header-real-h, var(--header-h)) - var(--sticky-h, 4.5rem) - 1.5rem);
  display: flex;
}
.exam__side > .panel { display: flex; flex-direction: column; width: 100%; min-height: 0; }
/* min-height:0 ở cả hai cấp — thiếu nó thì flex item không chịu co, `overflow`
   không có gì để cuộn và phiếu vẫn tràn ra ngoài trần vừa đặt. */
.exam__side .panel__body { overflow-y: auto; min-height: 0; }

.sheet__hint {
  margin-bottom: 0.75rem;
  font-size: var(--fs-sm);
  color: var(--ink-3);
}
.sheet__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(2.25rem, 1fr));
  gap: 0.375rem;
}
.sheet__cell {
  height: 2.25rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  font-size: var(--fs-md);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.sheet__cell:hover { border-color: var(--accent); color: var(--ink); }
.sheet__cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Đã trả lời = ô tô đậm. Nhìn lưới là biết còn bỏ trống câu nào. */
.sheet__cell.is-answered {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}
/* Sau khi nộp, phiếu đổi thành bản đồ đúng/sai */
.sheet__cell.is-right {
  border-color: var(--ok);
  background: var(--ok-soft);
  color: var(--ok);
}
.sheet__cell.is-wrong {
  border-color: var(--err);
  background: var(--err-soft);
  color: var(--err);
}
/* Bỏ trống: viền đứt, không tô đỏ. Cùng 0 điểm với câu sai nhưng là hai
   chuyện khác nhau — tô đỏ hết thì mất luôn thông tin "câu này chưa kịp làm".
   Số trong ô vẫn phải đọc rõ nên dùng --ink-2, riêng chuyện "chưa làm" đã có
   viền đứt nói hộ rồi. */
.sheet__cell.is-blank {
  border-style: dashed;
  border-color: var(--line-dark);
  background: none;
  color: var(--ink-2);
}

/* ------------------------------------------------- Giai đoạn 2: câu hỏi */
.qlist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
/* Bấm số trên phiếu trả lời là cuộn tới câu — chừa chỗ cho header + thanh
   trạng thái, không thì câu vừa nhảy tới bị hai thanh đó che mất. */
.q { scroll-margin-top: calc(var(--header-real-h, var(--header-h)) + var(--sticky-h, 5rem) + 0.75rem); }
.q__no { color: var(--ink); }
.q__score { font-variant-numeric: tabular-nums; }

/* Nút nghe riêng đoạn của câu — chỉ có ở chế độ Luyện tập */
.q__play {
  width: 1.875rem;
  height: 1.875rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--fs-2xs);
}
.q__play:hover { background: var(--accent-dark); }

.q__prompt {
  font-size: calc(var(--fs-lg) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* Bản dịch câu lệnh đề và bản dịch đáp án là NỘI DUNG ĐỂ ĐỌC, không phải chú
   thích phụ — nên dùng --ink-2 chứ không phải --ink-3. Đo được: --ink-3 ở light
   mode chỉ đạt tỉ lệ tương phản 2.99 trên nền panel, --ink-2 đạt 6.46. */
.q__prompt-tr {
  margin-top: 0.1875rem;
  font-size: var(--fs-lg);
  color: var(--ink-2);
}

/* Câu chọn tranh / biểu đồ: bốn phương án nằm trong MỘT ảnh cắt từ đề gốc.
   Ảnh co theo bề ngang panel, không bao giờ đẩy trang rộng ra (luật 5 ở
   CLAUDE.md); nền trắng cố định vì bản quét là nét đen trên trắng — để nó
   trong suốt thì ở dark mode chữ đen nằm trên nền tối, không đọc được. */
.q__figure {
  margin-top: 0.875rem;
}
.q__image {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: #fff;
}
/* Màn đề đọc: ảnh giữ bề ngang gốc, chỉ co khi rộng hơn khung. Ảnh câu 40–42
   TOPIK I có khi rất hẹp (hộp sữa kỳ 83 rộng 364px) — `width: 100%` kéo nó to
   gấp đôi, cao hơn cả màn hình và nhoè. Ảnh rộng vẫn chạm `max-width: 100%`. */
.q__image--natural {
  width: auto;
}

.q__options {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-top: 0.875rem;
}

/* Phương án của câu chọn tranh chỉ có số ①②③④ — xếp ngang cho đỡ tốn bốn dòng
   trống, và chừa vùng bấm đủ rộng để bấm trúng trên điện thoại. */
.q__options:has(.opt--label-only) {
  flex-direction: row;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* Bốn ô ①②③④ chia đều một hàng, kể cả ở 320px: để `min-width` cứng thì ô thứ
   tư rơi xuống dòng riêng, nhìn như một phương án bị lạc. */
.opt--label-only { flex: 1 1 3.25rem; justify-content: center; }

/* Cả dòng là vùng bấm, không chỉ nút tròn */
.opt {
  display: flex;
  align-items: baseline;
  gap: 0.625rem;
  padding: 0.5rem 0.625rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
}
.opt:hover { background: var(--surface-2); }
.opt:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.opt:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }
.opt:has(input:disabled) { cursor: default; }
.opt:has(input:disabled):hover { background: none; }
.opt:has(input:disabled):has(input:checked) { background: var(--accent-soft); }
.opt input { accent-color: var(--accent-dark); flex-shrink: 0; align-self: center; }

.opt__label {
  min-width: 1rem;
  color: var(--ink-3);
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
}
.opt__text {
  min-width: 0;
  font-size: calc(var(--fs-md) + var(--fs-bump));
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}

/* Sau khi nộp bài: tô đáp án đúng và ô mình chọn sai */
.opt.is-right { border-color: var(--ok); background: var(--ok-soft); }
.opt.is-right .opt__text { color: var(--ok); font-weight: 600; }
.opt.is-wrong { border-color: var(--err); background: var(--err-soft); }
.opt.is-wrong .opt__text { color: var(--err); text-decoration: line-through; }

.q__feedback {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-top: 0.875rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
  font-size: var(--fs-xl);
  color: var(--ink-2);
}
.q__answer, .q__why { overflow-wrap: break-word; }
.q__answer b, .q__why b { color: var(--ink); font-weight: 600; }
.q__answer-tr { color: var(--ink-2); }

.tag--right { border-color: var(--ok); color: var(--ok); }
.tag--wrong { border-color: var(--err); color: var(--err); }
.tag--blank { color: var(--ink-3); }

/* ------------------------------------------- Reading Test: nhóm câu + đoạn văn
   topik-reading-test-details.html. Mỗi nhóm ([31~33], [49~50]…) là một
   <section class="qset">: đề bài + đoạn văn dùng chung, rồi <ol class="qlist">
   các câu. Khe giữa hai NHÓM rộng hơn khe giữa hai câu trong nhóm để mắt nhận
   ra đâu là chỗ đổi đề bài. Khe dọc viết bằng rem, cùng lý do ghi ở .exam. */
.qsets {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}
.qset {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}
.qset__head {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  min-width: 0;
}
.qset__title {
  font-size: calc(var(--fs-lg) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* Bản dịch đề bài: nội dung để đọc nên --ink-2, lý do ở .q__prompt-tr. */
.qset__title-tr {
  margin-top: -0.375rem;
  font-size: var(--fs-lg);
  color: var(--ink-2);
}

/* Khung đoạn văn tiếng Hàn — viền như đề giấy. `pre-line` giữ xuống dòng của
   các câu (가)(나)(다)(라) mà vẫn cho dòng dài tự xuống hàng theo bề ngang. */
.passage {
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  min-width: 0;
}
.q .passage { margin-top: 0.75rem; }
.q .q__prompt + .q__prompt-tr + .passage,
.q .q__prompt + .passage { margin-top: 0.625rem; }
.panel__body > .passage:first-child { margin-top: 0; }
.passage__text {
  font-size: calc(var(--fs-md) + var(--fs-bump));
  line-height: 1.8;
  color: var(--ink);
  white-space: pre-line;
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* Ngoặc trống "(      )" / "(  ㉠  )" — dựng ở setTextWithBlanks. Khe rộng như
   sách đề: đo trên bản scan kỳ 102 (câu 34–37, 49), phần trống giữa hai ngoặc
   ≈ 4.5 chữ Hàn, ô có ㉠ cũng rộng đúng chừng đó với ㉠ ở giữa. Tính bằng `em`
   để theo cỡ chữ của dòng chứa nó (đề bài to hơn đoạn văn). `nowrap` giữ "("
   và ")" trên cùng một dòng. */
.blank { white-space: nowrap; }
.blank__gap {
  display: inline-block;
  min-width: 4.5em;
  text-align: center;
}
/* Ô 〈보기〉: nền nhạt hơn khung đoạn văn thật để không bị nhầm là câu phải làm. */
.passage--example { background: var(--surface-2); }
.passage__label {
  margin-bottom: 0.25rem;
  font-size: var(--fs-md);
  color: var(--ink-3);
}
.example__opts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.5rem;
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-md);
  color: var(--ink-2);
}
.example__opt.is-answer { color: var(--ink); font-weight: 600; }
.qset > .q__figure,
.qset__head > .q__figure { margin-top: 0; }

/* --------------------------------------------------- Giai đoạn 3: kết quả */
.result { margin-top: 1.375rem; }

.result__score {
  font-size: calc(var(--fs-3xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.result__right {
  margin-top: 0.1875rem;
  font-size: var(--fs-lg);
  color: var(--ink-3);
}
.result__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1.125rem;
}

/* ==========================================================================
   MY POCKET — my-pocket.html

   Hai tab trên cùng một trang. Tab danh sách dùng lại .filters / .chips / .tag
   của các màn trên; phần thẻ từ và phần thẻ lật là mới.
   ========================================================================== */

/* Hàng lọc nằm NGAY DƯỚI thanh tab ở màn này (các màn danh sách khác thì nó
   nằm dưới .page-head, vốn đã có padding-bottom riêng). Đo được khoảng cách
   tabs → filters là 0px, dính liền — nên cho .filters một lề trên ở đúng màn
   này thay vì sửa .filters dùng chung. */
#view-list .filters { margin-top: 1.125rem; }

/* ------------------------------------------------------ Trạng thái rỗng */
/* Dùng chung cho "túi từ còn trống" và "chưa có từ nào để học". Khác
   .list-empty ở chỗ đây là màn hình rỗng CÓ HƯỚNG DẪN LÀM GÌ TIẾP, không phải
   một dòng báo "không khớp bộ lọc". */
.blank-slate { margin-top: 1.375rem; }
.blank-slate .panel__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding-block: 2.5rem;
  text-align: center;
}
.blank-slate__head {
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
}
.blank-slate__hint {
  max-width: 34rem;
  font-size: var(--fs-xl);
  color: var(--ink-2);
}
.blank-slate .btn, .blank-slate .result__actions { margin-top: 0.75rem; }

/* ------------------------------------------------- Thanh Hoàn tác bỏ lưu */
.undo-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.625rem 0.875rem;
  margin-bottom: 0.875rem;
  padding: 0.5rem 0.875rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: var(--fs-lg);
  color: var(--ink-2);
}
.undo-bar .btn { margin-inline-start: auto; }

/* ------------------------------------------------------------ Thẻ từ */
.wordlist {
  list-style: none;
  margin: 1.125rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}
@media (min-width: 64em) {
  .wordlist {
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem var(--col-gap);
    align-items: start;
  }
}

.word-card { padding: 0.875rem; }
/* Từ đã thuộc: viền xanh mảnh, KHÔNG làm mờ chữ. Mờ đi là đúng lúc muốn ôn lại
   thì không đọc nổi — trạng thái "đã thuộc" không có nghĩa là bớt quan trọng. */
.word-card.is-known { border-color: var(--ok); }

/* Đầu thẻ là <button> trải hết bề ngang: bấm đâu trên hàng đó cũng mở được thẻ,
   không phải nhắm vào mỗi cái mũi tên. Trong <button> KHÔNG được có <a>, nên
   đường sang trang chi tiết từ nằm ở `.word-card__detail` trong phần mở ra. */
.word-card__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.375rem 0.625rem;
  width: 100%;
  /* padding: 0 — <button> mang sẵn padding của trình duyệt (Chrome: 1px 6px),
     đủ để lệch mép chữ so với phần thân thẻ bên dưới. */
  padding: 0;
  text-align: start;
}
/* Chữ xuống dòng bên trong bọc này, mũi tên ở ngoài nên không bao giờ bị đẩy
   xuống một dòng của riêng nó — xem ghi chú ở renderWord() trong my-vocab.js. */
.word-card__titles {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.375rem 0.625rem;
  flex: 1;
  min-width: 0;
}
.word-card__head:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.word-card__head:hover .word-card__term { color: var(--accent-dark); }

/* KHÔNG dùng `margin-inline-start: auto` để đẩy sang phải: margin auto nuốt hết
   khoảng trống còn lại TRƯỚC khi flex-grow được chia, nên `.word-card__titles`
   bị bó ở đúng bề ngang chữ (đo được 173px trong chỗ trống 218px) và chữ xuống
   dòng sớm. `flex: 1` của bọc chữ đã đẩy mũi tên sang phải rồi. */
.word-card__chevron {
  align-self: center;
  color: var(--ink-3);
  transition: transform .24s ease;
}
.word-card.is-open .word-card__chevron { transform: rotate(180deg); }

/* Gập/mở dùng đúng cơ chế của .topic — xem ghi chú ở .topic__panel. */
.word-card__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .24s ease;
}
.word-card.is-open .word-card__panel { grid-template-rows: 1fr; }
.word-card__inner {
  overflow: hidden;
  min-height: 0;
  visibility: hidden;
  transition: visibility 0s .24s;
}
.word-card.is-open .word-card__inner { visibility: visible; transition-delay: 0s; }

/* Hàng trên cùng của thẻ: đầu thẻ (nút mở/đóng) + nút loa, HAI nút anh em.

   Dùng flex chứ KHÔNG định vị tuyệt đối nút loa: đặt tuyệt đối thì phải đoán
   `top` cho khớp tâm hàng chữ, mà tâm đó đổi theo cỡ chữ và padding của từng
   kiểu xem — đo được lệch lên 7px ở kiểu 4 cột. `align-items: center` thì hai
   nút tự canh giữa theo nhau, không có con số nào phải neo tay. */
.word-card__topline {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}
.word-card__topline .word-card__head { flex: 1; min-width: 0; }

.word-card__say {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--radius);
  color: var(--ink-3);
}
.word-card__say:hover { background: var(--surface-2); color: var(--accent-dark); }
.word-card__say:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Tên nguồn trong section Bản quyền. Là <a> khi bài có `audio.creditUrl`, là
   <span> khi không — cùng một class để câu văn nhìn không đổi kiểu giữa hai bài. */
.credit__source { font-weight: 600; color: var(--ink); }
a.credit__source { color: var(--accent-dark); }
a.credit__source:hover { text-decoration: underline; }

/* Hàng chọn kiểu xem danh sách, nằm trên danh sách từ. Khoảng cách dưới do
   `.list-view` giữ, nên danh sách phẳng bỏ margin-top mặc định của .wordlist —
   không thì hai khoảng cách cộng lại thành một vệt trống to. */
.list-view {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem 0.875rem;
  margin: 1.125rem 0 0.625rem;
}
/* Mở/đóng hàng loạt: chữ trơn, không viền — thao tác phụ, đứng về phía cuối
   hàng để không tranh chỗ với nút chọn kiểu xem. */
.list-view__bulk {
  display: inline-flex;
  gap: 0.875rem;
  margin-inline-start: auto;
}
/* Dictation List: hàng ngay dưới ô tìm — dropdown trạng thái bên TRÁI, cặp
   mở/đóng bên PHẢI (nhờ `margin-inline-start: auto` của .list-view__bulk, nên
   KHÔNG đặt `justify-content: flex-end` ở đây: lúc danh sách rỗng cặp nút bị ẩn,
   flex-end sẽ đẩy dropdown còn lại dạt sang phải). Bộ lọc ngay trên đã giữ
   margin-bottom 1.125rem, cộng thêm margin-top của .list-view nữa là một vệt
   trống to gấp đôi chỗ khác. */
.list-view--filters { margin-top: 0; column-gap: 0.5rem; }
/* Khe quanh dấu "/" khép lại CHỈ ở hàng này (My Vocab dùng chung `.list-view__bulk`
   thì giữ nguyên). Ở 375px hàng có 338px mà dropdown 152 + cặp Mở/Đóng 173 + khe
   13 = 337: dư đúng 1px — trên máy Mac vừa khít, lên CI Linux chữ rộng hơn một
   chút là cặp nút rớt dòng (CI đỏ 2026-09-13). Khép khe hàng + khe "/" + đệm
   ngang nút dropdown thì dư ~20px. Test `progress-display.spec.js` đòi dư ≥12px. */
.list-view--filters .list-view__bulk { gap: 0.5rem; }
/* `display: inline-flex` ở trên đè thuộc tính `hidden` — docs/quality.md. */
.list-view__bulk[hidden] { display: none; }
/* Gạch phân cách giữa hai nút. aria-hidden trong HTML: máy đọc màn hình đọc
   "Mở tất cả gạch chéo Đóng tất cả" là thừa, hai nút đã tự nói rõ việc của nó. */
.list-view__sep { color: var(--ink-3); user-select: none; }
.list-view__bulk button {
  padding: 0.125rem 0;
  font-size: var(--fs-md);
  color: var(--ink-2);
}
.list-view__bulk button:hover { color: var(--ink); text-decoration: underline; }
.list-view__bulk button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.wordlist--flat { margin-top: 0; }

/* Kiểu xem "Tất cả": 4 thẻ một hàng trên màn rộng.
   CHỈ danh sách phẳng — kiểu "Theo chủ đề" giữ nguyên 2 cột, vì ở đó thẻ từ đã
   nằm lồng trong thẻ chủ đề (2 cột lồng 2 cột) nên 4 cột nữa là hết chỗ.

   Thẻ ở đây đóng sẵn nên phần lớn thời gian chỉ có một hàng chữ — thu gọn được
   mà vẫn đọc tốt. Ba mốc thay vì ép 4 cột từ 64em: ở 1024px thì 4 cột chỉ còn
   ~230px một thẻ, tên từ tiếng Hàn kèm nhãn "Đã thuộc" vỡ thành 3 dòng. */
@media (min-width: 64em) {
  .wordlist--flat { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 75em) {
  .wordlist--flat { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.wordlist--flat .word-card { padding: 0.75rem; }
.wordlist--flat .word-card__term { font-size: calc(var(--fs-xl) + var(--fs-bump)); }
.wordlist--flat .word-card__head,
.wordlist--flat .word-card__titles { gap: 0.25rem 0.5rem; }
.word-card__term {
  font-size: calc(var(--fs-2xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* --ink-2 chứ không phải --ink-3: đây là nhãn người ta có đọc, mà --ink-3 ở
   light mode chỉ đạt tương phản 2.99 trong khi mọi chữ khác trong thẻ đều
   ≥ 6.46 — để lệch một mình nó thì nhìn như bị mờ lỗi. */
/* Ở My Vocab từ loại là một dòng riêng trong phần mở ra; ở kho từ vựng nó vẫn
   nằm cạnh từ trên cùng hàng. Cùng một class, hai chỗ dùng khác nhau — cỡ chữ
   và màu thì chung. */
.word-card__pos { font-size: var(--fs-md); color: var(--ink-2); }
.word-card__body .word-card__pos { margin-bottom: 0.125rem; }
.tag--known { border-color: var(--ok); color: var(--ok); }

.word-card__body { margin-top: 0.5rem; }
.word-card__meaning {
  font-size: calc(var(--fs-md) + var(--fs-bump));
  color: var(--ink);
  overflow-wrap: break-word;
}
.word-card__example {
  margin-top: 0.375rem;
  padding-inline-start: 0.625rem;
  border-inline-start: 2px solid var(--line-dark);
  font-size: var(--fs-xl);
  color: var(--ink-2);
  word-break: keep-all;
  overflow-wrap: break-word;
}
.word-card__example-tr {
  padding-inline-start: 0.625rem;
  border-inline-start: 2px solid var(--line-dark);
  font-size: var(--fs-lg);
  color: var(--ink-2);
  overflow-wrap: break-word;
}

.word-card__foot,
.grammar-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.625rem;
  margin-top: 0.875rem;
  padding-top: 0.625rem;
  border-top: 1px solid var(--line);
}
.word-card__meta,
.grammar-card__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.875rem;
  min-width: 0;
}
/* Chân thẻ của My Vocab và My Grammar dùng CHUNG bộ rule này: hai màn là hai
   nửa của cùng một túi My Pocket, sửa bảng màu hay khoảng cách một lần thì cả
   hai đổi theo, không có đường nào để chúng lệch nhau. */
.word-card__stat,
.grammar-card__stat { font-size: var(--fs-sm); color: var(--ink-2); }
.word-card__actions,
.grammar-card__actions { display: flex; flex-wrap: wrap; gap: 0.375rem; flex-shrink: 0; }
.word-card__remove:hover,
.grammar-card__remove:hover { border-color: var(--err); color: var(--err); }

/* Thẻ hẹp: cụm nút xuống hàng riêng, không bóp phần nguồn và ngày lưu.

   ⚠ Container phải là CHÍNH THẺ, không phải danh sách bao ngoài. Bản đầu đặt
   `container` trên `.wordlist` rồi query `max-width: 26rem` — đo được:
   `.wordlist` rộng 568px trong khi mỗi `.word-card` chỉ 285px, nên rule không
   bao giờ bật dù thẻ đã chật. Từ khi danh sách nằm trong thẻ chủ đề (2 cột lồng
   2 cột) thì thẻ hẹp đi một nửa nữa, và cụm nút 252px trong thẻ 285px chỉ còn
   thừa 16px. */
.word-card { container: wordcard / inline-size; }

@container wordcard (max-width: 22rem) {
  .word-card__foot { align-items: flex-start; flex-direction: column; }
  /* Nút rộng đúng bằng chữ của nó, KHÔNG kéo giãn.
     Bản trước để `flex: 1` cho hai nút chia đôi bề ngang thẻ — hồi đó thẻ lúc
     nào cũng mở sẵn nên cụm nút là thứ duy nhất ở cuối thẻ. Từ khi thẻ gập/mở,
     cụm nút nằm trong phần vừa bung ra và hai nút to bằng nửa thẻ trông như
     vỡ layout. Chật quá thì cho xuống hàng, đừng phình ra. */
  .word-card__actions { flex-wrap: wrap; }
}

/* --------------------------------------------------------- Thẻ lật */
.fc {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.875rem;
  /* Lề trên nhỏ hơn vì hàng chip chọn chủ đề đã nằm ngay trên */
  margin-top: 0.875rem;
}

/* Hàng chip chọn bộ thẻ. Căn giữa cho khớp trục của thẻ lật bên dưới, và giới
   hạn bề ngang bằng chính thẻ để nhiều chủ đề thì xuống hàng chứ không kéo dài
   thành một dải chạy hết màn hình. */
.fc__topics {
  justify-content: center;
  max-width: 34rem;
  margin: 1.375rem auto 0;
}

.fc__bar {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.375rem 0.875rem;
  text-align: center;
}
.fc__progress {
  font-size: var(--fs-xl);
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.fc__order { font-size: var(--fs-md); color: var(--ink-3); }

/* Hàng chứa thẻ: nút "từ trước" — thẻ — nút "từ sau". Ba nút anh em, không cái
   nào lồng trong cái nào. */
.fc__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  width: 100%;
  /* Chiều sâu cho cú lật dọc của thẻ. Đặt ở CHA vì `perspective` áp lên con,
     không áp lên chính phần tử đang xoay. Thiếu dòng này thì thẻ chỉ dẹt dần
     rồi phình ra, không ra dáng lật. */
  perspective: 62.5rem;
}
.fc__nav {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 1px solid var(--line-dark);
  border-radius: 50%;
  color: var(--ink-2);
}
.fc__nav:hover:not(:disabled) { border-color: var(--accent); color: var(--ink); }
.fc__nav:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Hết đầu / hết đuôi bộ thẻ: mờ đi và không bắt chuột, nhưng vẫn nằm nguyên chỗ
   cũ — nút biến mất là thẻ nhảy ngang một đoạn mỗi lần tới đầu hoặc cuối. */
.fc__nav:disabled { opacity: .35; cursor: default; }

/* Trượt khi sang từ khác: thẻ mới bay vào từ phía mình đang đi tới. Thời lượng
   ngắn — đây là thao tác lặp đi lặp lại hàng chục lần một lượt học.
   Khối @media (prefers-reduced-motion) ở cuối file tự rút còn 0. */
@keyframes fc-slide-next {
  from { opacity: 0; transform: translateX(1.75rem); }
  to   { opacity: 1; transform: none; }
}
@keyframes fc-slide-prev {
  from { opacity: 0; transform: translateX(-1.75rem); }
  to   { opacity: 1; transform: none; }
}
.fc__card.is-slide-next { animation: fc-slide-next .22s ease-out; }
.fc__card.is-slide-prev { animation: fc-slide-prev .22s ease-out; }

/* Lật mặt trước ↔ mặt sau: quay quanh trục ngang, mép dưới hất lên.

   Hai nửa rời nhau vì ruột thẻ được thay ở GIỮA chừng (flashcard.js) — mặt sau
   chỉ tồn tại trong DOM sau khi đã lật. `backface-visibility` không giúp được
   gì ở đây: không có hai mặt cùng lúc để mà giấu một mặt.
   ⚠ .13s phải khớp FLIP_HALF_MS trong flashcard.js. */
@keyframes fc-flip-out {
  from { transform: rotateX(0); }
  to   { transform: rotateX(-90deg); }
}
@keyframes fc-flip-in {
  from { transform: rotateX(90deg); }
  to   { transform: rotateX(0); }
}
.fc__card.is-flip-out { animation: fc-flip-out .13s ease-in; }
.fc__card.is-flip-in { animation: fc-flip-in .13s ease-out; }

/* Cả thẻ là một nút: bấm chỗ nào cũng lật. min-height cố định để lật qua lật
   lại không làm cả trang co giãn — mặt sau bao giờ cũng nhiều chữ hơn mặt trước. */
.fc__card {
  width: 100%;
  max-width: 34rem;
  min-height: 15rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.625rem;
  padding: 1.75rem 1.25rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  text-align: center;
}
.fc__card:hover { border-color: var(--accent); }
.fc__card.is-flipped { background: var(--surface-2); }

.fc__term {
  font-size: calc(var(--fs-3xl) + var(--fs-bump) * 3);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.3;
  word-break: keep-all;
  overflow-wrap: break-word;
}
.fc__side-hint { font-size: var(--fs-md); color: var(--ink-3); }
.fc__meaning {
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  color: var(--ink);
  overflow-wrap: break-word;
}
.fc__description {
  font-size: var(--fs-md);
  color: var(--ink-3);
  overflow-wrap: break-word;
}
.fc__example {
  font-size: var(--fs-xl);
  color: var(--ink-2);
  word-break: keep-all;
  overflow-wrap: break-word;
}
.fc__example-tr { font-size: var(--fs-lg); color: var(--ink-2); }

.fc__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  max-width: 34rem;
}
/* ⚠ KHÔNG cho hai nút này `flex: 1` để giãn đầy hàng.
   lockWidth() (i18n.js) khoá min-width bằng bề ngang ĐO ĐƯỢC của nút; nút đang
   bị flex kéo giãn thì nó đo trúng bề ngang đã giãn chứ không phải bề ngang của
   chữ. Đo được: min-width bị khoá ở 292.78px cho nhãn "Đã nhớ", hai nút cộng
   lại vượt khung 592.5px nên tự xuống hai hàng. Để nút tự co theo nội dung rồi
   nới bằng padding thì lockWidth đo đúng cái cần đo. */
.fc__actions .btn { padding-inline: 2.5rem; }
/* `display: block` đè `inline-flex` của `.kbd-hint`. Trong hàng flex, chữ và
   từng <kbd> thành từng mục flex riêng, không mục nào xuống dòng được — câu
   nhắc có sáu phím nên ở 320px nó dài hơn khung 2px, cả trang tràn ngang (đo
   2026-09-15, lỗi có sẵn từ trước khi tách flashcard.js). Khối thường thì chữ
   tự xuống dòng quanh các <kbd> inline-block. */
.fc__keys { display: block; text-align: center; }

/* ==========================================================================
   MẠNG LIÊN KẾT NỘI DUNG — nút trái tim, kho từ vựng, chi tiết một từ

   Cả site nối thành vòng khép kín: bài → từ vựng → bài khác → My Pocket.
   Mấy class dưới đây dùng lại ở kho ngữ pháp và trang chi tiết ngữ pháp khi
   dựng giai đoạn 2 — đặt tên theo chức năng, không theo "vocab".
   ========================================================================== */

/* ---------------------------------------------------------- Nút trái tim */
/* Nút này nằm LỒNG TRONG thẻ <a> của thẻ bài/thẻ từ, nên phải nổi lên trên và
   tự bắt chuột. Việc chặn điều hướng là ở JS (favorite.js), CSS chỉ lo chỗ đứng. */
.heart {
  position: relative;
  z-index: 1;
  flex-shrink: 0;
  width: 2.0625rem;
  height: 2.0625rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: var(--ink-3);
}
.heart .icon { width: 1.25rem; height: 1.25rem; fill: none; }
.heart:hover { background: var(--surface-2); color: var(--err); }
.heart:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Đã lưu = tim đặc. Cùng một path, chỉ đổi fill — hai hình khác nhau thì lúc
   đổi trạng thái nét sẽ nhảy. */
.heart.is-saved { color: var(--err); }
.heart.is-saved .icon { fill: currentColor; }

/* Danh sách từ nằm BÊN TRONG thẻ chủ đề: bỏ lề trên (thẻ đã có padding) và
   giữ 2 cột ở màn rộng. Không đặt lại `container` — .wordlist đã khai rồi, khai
   chồng là container query đo nhầm khung. */
.wordlist--in-topic {
  margin: 0;
  padding: 0.5rem 0.875rem 0.875rem;
}
/* Trong thẻ chủ đề, cột đã hẹp sẵn (2 cột lồng trong 2 cột) nên đừng chia đôi
   thêm lần nữa dưới 1400px — thẻ từ sẽ chỉ còn ~285px, không đủ cho cụm nút. */
@media (max-width: 87.5em) {
  .wordlist--in-topic { grid-template-columns: 1fr; }
}

/* Số đếm trong chip lọc của My Pocket. Nằm trong <span> riêng để lockWidth()
   đo được phần CHỮ thôi — nhãn đổi theo ngôn ngữ, số đổi theo dữ liệu. */
.chip__count {
  margin-inline-start: 0.375rem;
  opacity: .65;
  font-variant-numeric: tabular-nums;
}

/* Từ mới / ngữ pháp trong cột phải màn Dictation: phần nội dung co giãn, nút
   trái tim đứng yên bên phải. */
.vocab__link {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 0.625rem;
  flex-wrap: wrap;
  color: inherit;
}
a.vocab__link:hover { text-decoration: none; }
a.vocab__link:hover b { color: var(--accent-dark); }
a.vocab__link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Thẻ bài / thẻ đề: chừa một cột cuối cho nút trái tim */
.lesson__card, .test__card { position: relative; }
.lesson__card > .heart, .test__card > .heart { align-self: center; }

/* Chip bấm được, dùng cho danh sách "từ xuất hiện cùng câu" */
.chip--link { display: inline-block; }
a.chip--link:hover { text-decoration: none; border-color: var(--accent); color: var(--ink); }

/* ------------------------------------------------- Thẻ từ ở trang kho */
/* .wordlist / .word-card đã khai ở khối MY POCKET bên trên — ở đây chỉ thêm
   phần thẻ-là-link, vì thẻ trong kho bấm được còn thẻ trong túi thì không. */
.word-card__link {
  display: block;
  color: inherit;
  border-radius: var(--radius);
}
.word-card__link:hover { text-decoration: none; }
.word-card__link:hover .word-card__term { color: var(--accent-dark); }
.word-card__link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Thẻ trong kho: tim nằm góc trên phải, không đẩy nội dung */
.wordlist .word-card { position: relative; }
.wordlist .word-card > .heart {
  position: absolute;
  top: 0.5rem;
  inset-inline-end: 0.5rem;
}
/* Chừa chỗ cho nút tim để tên từ dài không chui xuống dưới nó.

   ⚠ CHỈ ở thẻ thật sự có tim. Bản trước nhắm mọi `.wordlist .word-card__head`,
   mà My Vocab cũng dùng `.wordlist` nhưng KHÔNG có tim — thẻ ở đó chịu oan
   2.25rem padding phải. Từ khi đầu thẻ My Vocab thành <button> chứa bọc chữ
   co giãn thì chỗ trống ma đó bóp bọc chữ còn 173px trong 218px và tên từ
   xuống dòng sớm. `:has()` nằm trong sàn hỗ trợ của site (docs/ui.md). */
.wordlist .word-card:has(> .heart) .word-card__head { padding-inline-end: 2.25rem; }

/* --------------------------------------------- Trang chi tiết một từ */
.entry__title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
}
.entry__title h1 { font-size: 1.75rem; word-break: keep-all; overflow-wrap: break-word; }
.entry__actions { display: flex; gap: 0.5rem; flex-shrink: 0; }

/* Bố cục 2 cột: trái là nghĩa và câu thật, phải là các đường đi sang phần khác
   của site. KHÔNG dùng .work — class đó mang min-height neo tay của trang
   Dictation (docs/ui.md mục 2). */
.entry {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  gap: var(--col-gap);
  align-items: start;
  margin-top: 1.375rem;
}
.entry__main, .entry__side {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}

.entry__meaning {
  font-size: calc(var(--fs-2xl) + var(--fs-bump));
  color: var(--ink);
  overflow-wrap: break-word;
}
.entry__meaning-ko {
  margin-top: 0.25rem;
  font-size: var(--fs-xl);
  color: var(--ink-2);
  word-break: keep-all;
}
.entry__sub {
  margin-top: 1.125rem;
  margin-bottom: 0.375rem;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink-2);
}
.entry__empty { font-size: var(--fs-lg); color: var(--ink-2); }

/* ---------------------------------- Danh sách link sang phần khác của site */
.linklist { display: flex; flex-direction: column; gap: 0.25rem; }
.linklist__item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.linklist__link {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  padding: 0.5rem 0.625rem;
  border-radius: var(--radius);
  color: inherit;
}
a.linklist__link:hover { background: var(--surface-2); text-decoration: none; }
a.linklist__link:hover .linklist__title { color: var(--accent-dark); }
a.linklist__link:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Bài chưa có nội dung: dựng <span> chứ không phải link chết (giống trang danh sách) */
.linklist__link[aria-disabled] { cursor: default; }
.linklist__link[aria-disabled] .linklist__title { color: var(--ink-3); }

.linklist__title {
  font-size: calc(var(--fs-md) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}
.linklist__note { font-size: var(--fs-sm); color: var(--ink-2); overflow-wrap: break-word; }

/* ------------------------------------------- Câu thật trích từ file bài */
.sentlist { display: flex; flex-direction: column; gap: 0.875rem; }
.sentlist__item {
  padding-inline-start: 0.75rem;
  border-inline-start: 2px solid var(--line-dark);
}
.sentlist__from {
  display: block;
  font-size: var(--fs-sm);
  color: var(--ink-2);
}
.sentlist__text {
  margin-top: 0.1875rem;
  font-size: calc(var(--fs-lg) + var(--fs-bump));
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* Đúng từ đang tra, tô nổi lên trong câu */
.sentlist__hit {
  padding: 0 0.125rem;
  border-radius: 0.1875rem;
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-weight: 700;
}
.sentlist__tr {
  margin-top: 0.125rem;
  font-size: var(--fs-lg);
  color: var(--ink-2);
}

/* ------------------------------------------- Popup mời đăng nhập
   assets/js/login-prompt.js — dùng chung cho My Vocab, nút trái tim, màn làm đề. */
/* CHÍNH GIỮA màn hình theo cả hai chiều. `<dialog>` mặc định chỉ căn giữa theo
   chiều ngang rồi dính lên đỉnh — phải tự đặt `inset: 0` + `margin: auto` mới
   ra giữa thật. `max-height` để hộp không tràn khỏi màn hình thấp (điện thoại
   nằm ngang), phần thừa thì cuộn trong hộp chứ không đẩy trang. */
/* Bề ngang tách làm HAI dòng `width` + `max-width` thay vì một `min()`:
   trên Safari iPhone bản `min(92vw, 26rem)` không ăn, `<dialog>` rơi về bề ngang
   mặc định của trình duyệt và trải kín màn hình — đo trên ảnh chụp máy thật
   (2026-08-29): hộp rộng đúng bằng viewport, hai viền trái/phải nằm ngoài mép.
   Hai dòng rời không cần hàm `min()` nên chạy ở mọi trình duyệt, và dù `width`
   có bị bỏ qua thì `max-width` vẫn chừa lại lề.
   Dùng `%` chứ không phải `vw`: hộp `position: fixed` lấy viewport làm khối
   chứa, nên `%` ra đúng bề ngang dùng được, còn `vw` thì tính cả thanh cuộn. */
.lp {
  position: fixed;
  inset: 0;
  margin: auto;
  width: 26rem;
  max-width: calc(100% - 2rem);
  max-height: 90vh;
  padding: 0;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  overflow: auto;
}
.lp::backdrop { background: rgb(0 0 0 / .55); }
.lp__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.625rem;
  padding: 1.75rem 1.25rem 1.25rem;
  text-align: center;
}
.lp__head {
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
}
.lp__hint { font-size: var(--fs-xl); color: var(--ink-2); }
.lp__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
/* "Để sau" cố ý nhạt và không viền: đây là lời mời, không phải câu hỏi có hai
   lựa chọn ngang nhau — nhưng vẫn phải đủ to để bấm trúng trên điện thoại. */
.lp__later {
  padding: 0.5rem 0.75rem;
  font-size: var(--fs-lg);
  color: var(--ink-3);
}
.lp__later:hover { color: var(--ink-2); text-decoration: underline; }

/* ------------------------------------------- Hộp nhập mã xác minh admin
   assets/js/admin-api.js — cùng hình khối với popup đăng nhập ở trên.
   Đây là `<dialog>` của trang chứ không phải `window.prompt()`: WebKit chặn
   hộp thoại của trình duyệt khi không có thao tác người dùng đứng sau, mà lượt
   hỏi mã đầu tiên luôn rơi đúng vào lúc trang vừa tải. */
.otp {
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(92vw, 22rem);
  max-height: 90vh;
  padding: 0;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  overflow: auto;
}
.otp::backdrop { background: rgb(0 0 0 / .55); }
.otp__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.625rem;
  padding: 1.75rem 1.25rem 1.25rem;
  text-align: center;
}
.otp__head {
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
}
.otp__hint { font-size: var(--fs-lg); color: var(--ink-2); }
/* Mã đổi mỗi 30 giây nên người ta gõ vội: ô to, chữ số cách đều và giãn rộng
   để soi lại con số vừa gõ mà không phải nheo mắt. `min-height` 44px là ngưỡng
   chạm của iOS — dưới mức đó là bấm trượt trên điện thoại. */
.otp__input {
  width: 9rem;
  max-width: 100%;
  min-height: 44px;
  margin-top: 0.25rem;
  text-align: center;
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.28em;
  text-indent: 0.28em;  /* bù khoảng cách thừa bên phải do letter-spacing */
}
.otp__problem { font-size: var(--fs-lg); color: var(--err); }
.otp__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.otp__later {
  padding: 0.5rem 0.75rem;
  font-size: var(--fs-lg);
  color: var(--ink-3);
}
.otp__later:hover { color: var(--ink-2); text-decoration: underline; }

/* -------------------------------------------- Nút nổi góp ý (mọi trang)
   assets/js/report-button.js. Chủ dự án chốt 2026-08-17: hiện ở MỌI màn hình.
   Vì vậy chỗ đứng phải tránh vùng thao tác — góc phải dưới, cùng phía với
   thanh cuộn và xa ô nhập ở giữa trang (ARCHITECTURE.md mục 1.1). */
.fab {
  position: fixed;
  right: 1rem;
  bottom: 4.3rem;
  z-index: 90;              /* dưới header (100), trên nội dung */
  display: grid;
  place-items: center;
  width: 2.55rem;
  height: 2.55rem;
  border: 1px solid var(--accent-dark);
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: 0 0.375rem 1rem rgb(0 0 0 / .35);
}
.fab:hover { background: var(--accent-dark); }
.fab .icon { width: 1.169rem; height: 1.169rem; }

/* Tooltip dùng chung cho hai nút nổi. Đặt bên trái để không vướng mép màn hình;
   hiện khi hover hoặc focus bàn phím, còn aria-label giữ tên đầy đủ cho screen reader. */
.fab[data-tooltip]::after,
.fab--donate[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  top: 50%;
  right: calc(100% + 0.625rem);
  width: max-content;
  max-width: calc(100vw - 5rem);
  padding: 0.375rem 0.625rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--ink);
  color: var(--surface);
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-50%) translateX(0.25rem);
  transition: opacity 120ms ease, transform 120ms ease;
}
.fab[data-tooltip]:hover::after,
.fab[data-tooltip]:focus-visible::after,
.fab--donate[data-tooltip]:hover::after,
.fab--donate[data-tooltip]:focus-visible::after {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
}
/* Nút Notes TẠM ẨN (chủ dự án chốt 2026-08-28): `shell.js` không gọi
   `mountNotesButton()` nữa nên phần tử này không còn được dựng. Giữ nguyên toạ
   độ ở đây để bật lại là khớp ngay — khi đó phải đẩy `.fab--ai` về 10.9rem. */
.fab--notes { right: 1rem; bottom: 7.6rem; }
/* AI dồn xuống chỗ Notes vừa bỏ trống: 4.3rem đáy Report + 2.55rem cao FAB +
   0.75rem khe. Không dùng chung toạ độ với nút khác vì phần tử vẽ sau sẽ che
   kín phần tử trước dù cả hai đều có mặt trong DOM. */
.fab--ai { right: 1rem; bottom: 7.6rem; }

/* Không phải box nữa (đỡ tốn diện tích cửa sổ vốn đã nhỏ) — một dòng chữ
   nghiêng nhạt, đứng đầu trang, đủ để nhắc mà không chiếm không gian. */
.nq-guest-note {
  margin: 0;
  font-size: var(--fs-sm);
  font-style: italic;
  line-height: 1.4;
  color: var(--ink-3);
}

/* Cửa sổ Ghi chú nhanh — assets/js/notes-button.js mở bằng dialog.show() (KHÔNG
   phải showModal()): không có ::backdrop, không chặn thao tác phần còn lại của
   trang, để vừa nghe dictation vừa ghi chú song song. `.lp`/`.rb__*` là khung
   dùng chung với popup đăng nhập/report — KHÔNG sửa hai class đó, mọi định vị
   riêng của cửa sổ này đè bằng `.notes-quick[open]` (2 lớp chọn, thắng `.lp`
   một lớp mà không cần !important).
   Desktop: widget góc phải dưới kiểu chat. Mobile (<56.25em, khớp breakpoint
   .notes-app đổi bố cục ở dưới): bottom sheet kéo được bằng thanh handle. */
/* right:4.75rem (không phải 1rem): chừa đúng cột 3 nút nổi (donate/report/notes,
   xem .fab ở trên) đứng right:1rem rộng 3rem + khe 0.75rem — mở cửa sổ ghi chú
   không được làm mất 3 nút đó, chủ dự án chốt việc này rõ ràng. */
.notes-quick[open] {
  display: flex;
  flex-direction: column;
  position: fixed;
  inset: auto;
  right: 4.75rem;
  bottom: 1rem;
  margin: 0;
  width: min(360px, calc(100vw - 5.75rem));
  max-width: min(90vw, 34rem);
  min-width: 19rem;
  max-height: 90vh;
  min-height: 23rem;
  padding: 0;
  border-radius: var(--radius);
  overflow: hidden;
  z-index: 92;
}
.notes-quick::backdrop { display: none; }
.notes-quick .rb__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.notes-quick__handle { display: none; }

/* Kéo di chuyển cửa sổ (desktop only, JS bindWindowMove trong notes-button.js):
   nắm bất cứ đâu trên thanh tiêu đề trừ nút đóng. */
.nq-head { cursor: move; user-select: none; }

/* Núm resize góc phải-dưới (desktop only, JS bindWindowResize) — đặt lọt trong
   padding 1.25rem của .rb__body nên không đè lên nút Lưu ở góc đó. */
.notes-quick__resize {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 1rem;
  height: 1rem;
  cursor: nwse-resize;
  touch-action: none;
  z-index: 6;
}
.notes-quick__resize::before {
  content: '';
  position: absolute;
  right: 0.1875rem;
  bottom: 0.1875rem;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid var(--line-dark);
  border-bottom: 2px solid var(--line-dark);
  border-radius: 0 0 0.125rem 0;
  opacity: 0.55;
}
.notes-quick__resize:hover::before { opacity: 1; }

@media (max-width: 56.25em) {
  /* Bottom sheet: dán đáy, chiều cao điều khiển qua biến --notes-quick-h mà JS
     set khi kéo thanh handle (mặc định 40vh lúc mở). right:4rem (không phải 0):
     chừa cột nút nổi ở mobile (right:0.625rem, rộng 2.75rem + khe) — lý do như
     bản desktop ở trên. */
  .notes-quick[open] {
    right: 4rem;
    bottom: 0;
    left: 0;
    width: auto;
    max-width: none;
    min-width: 0;
    max-height: none;
    min-height: 0;
    height: var(--notes-quick-h, 40vh);
    border-radius: var(--radius) var(--radius) 0 0;
  }
  /* Kéo-di-chuyển-tự-do + resize là tính năng desktop (chốt 2026-08-21) — mobile
     giữ nguyên bottom sheet dán đáy, tắt hẳn núm resize và con trỏ move. */
  .nq-head { cursor: default; }
  .notes-quick__resize { display: none; }
  .notes-quick__handle {
    display: flex;
    flex: none;
    justify-content: center;
    padding: 0.625rem 0;
    cursor: grab;
    touch-action: none;
  }
  .notes-quick__handle::before {
    content: '';
    display: block;
    width: 2.5rem;
    height: 0.25rem;
    border-radius: 999rem;
    background: var(--line-dark);
  }
}

/* Đầu cửa sổ ghi chú nhanh: tiêu đề + nút đóng. Dropdown chọn ghi chú có sẵn
   nằm ngay dưới (thẻ <select id="nq-select"> trong notes-button.js) — value
   rỗng là option "Tạo ghi chú mới" (mặc định), chọn một ghi chú có sẵn thì nạp
   vào form để sửa trực tiếp. Dùng <select> gốc trình duyệt thay vì tự vẽ danh
   sách: khỏi lo cuộn/tràn khi nhiều ghi chú, và tránh đúng loại lỗi flexbox
   "overflow khiến min-height tự động = 0" từng gặp với danh sách tự vẽ. */
.nq-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.nq-head .lp__head { text-align: left; }
/* Nút icon nhỏ hơn .btn--icon mặc định (2.0625rem) — vừa hàng tiêu đề, không
   lấn chữ "Ghi chú nhanh" ở màn hẹp. */
.nq-head #nq-close-btn { flex: none; width: 1.875rem; height: 1.875rem; font-size: var(--fs-2xl); }

/* Dropdown tự vẽ (không dùng <select> gốc): trình duyệt tự vẽ popup của
   <select> theo OS, không style được, không tự chọn bung trên/dưới theo ý,
   không giới hạn được chiều cao — ba lý do đúng ba yêu cầu chủ dự án chốt.
   .nq-dd là mốc định vị (position:relative), menu bung tuyệt đối NGAY DƯỚI
   trigger (top:100%), không đè lên giá trị đang chọn. */
.nq-dd { position: relative; }
.nq-dd__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  max-width: 100%; /* .pf-input tự chặn 20rem, nghĩ cho ô nhập ngắn — đè lại */
  cursor: pointer;
  text-align: left;
}
#nq-dd-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nq-dd__trigger:focus-visible { outline: none; border-color: var(--accent); }
.nq-dd__arrow { flex: none; width: 1.125rem; height: 1.125rem; color: var(--ink-3); transition: transform 0.15s ease; }
.nq-dd__trigger[aria-expanded="true"] .nq-dd__arrow { transform: rotate(180deg); }
/* z-index chỉ cần thắng nội dung form trong CÙNG cửa sổ (stacking context đã
   nằm trong <dialog> notes-quick rồi) — không cần so với z-index toàn trang. */
.nq-dd__menu {
  position: absolute;
  inset-block-start: calc(100% + 0.25rem);
  inset-inline: 0;
  z-index: 5;
  max-height: 12rem;
  overflow-y: auto;
  margin: 0;
  padding: 0.25rem;
  list-style: none;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 0.375rem 1rem rgb(0 0 0 / .35);
}
.nq-dd__menu[hidden] { display: none; }
.nq-dd__option {
  display: block;
  padding: 0.4375rem 0.5rem;
  border-radius: var(--radius);
  color: var(--ink);
  font-size: var(--fs-sm);
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nq-dd__option:hover { background: var(--accent-soft); }
.nq-dd__option.is-active { background: var(--surface-2); color: var(--accent-dark); font-weight: 600; }

.notes-toolbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-block: 0.75rem 1rem;
  color: var(--ink-3);
}
.notes-toolbar .result__status { margin: 0; }
.notes-app {
  display: grid;
  grid-template-columns: minmax(12rem, 35%) minmax(0, 1fr);
  gap: var(--gap-section);
  align-items: start;
}
.notes-app[hidden] { display: none; }
.notes-list-col { display: flex; flex-direction: column; gap: 0.75rem; min-width: 0; }
/* .filters__search vốn cho hàng NGANG (flex:1 1 16rem đặt CHIỀU CAO 16rem khi
   cha là cột dọc) — trung hoà lại để ô tìm chỉ cao vừa nội dung. */
.notes-search { flex: 0 0 auto; width: 100%; }
/* position:relative để nút mũi tên "cuộn xuống" định vị NỔI đúng trong khung
   danh sách, không lệch ra cả trang. */
.notes-list-wrap { position: relative; min-width: 0; }
/* Cố định chiều cao rồi cuộn riêng bên trong — 20 ghi chú không còn kéo dài cả
   trang. min(28rem, 65vh): đủ thấy nhiều thẻ trên màn lớn, không tràn khỏi màn
   hình thấp. */
.notes-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
  max-height: min(28rem, 65vh);
  overflow-y: auto;
}
.notes-list__placeholder { padding: 1.25rem 0.75rem; }
/* Mũi tên "cuộn xuống": nổi nhẹ ở đáy khung danh sách, tông trung tính — chỉ
   hiện khi notes-list còn nội dung bị che phía dưới (JS đo
   scrollHeight/clientHeight). Cố tình không nổi bật: đây là gợi ý phụ, không
   phải hành động chính của màn hình. */
.notes-list__more {
  position: absolute;
  left: 50%;
  bottom: 0.5rem;
  transform: translateX(-50%);
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border: 1px solid var(--line-dark);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink-2);
  box-shadow: 0 0.125rem 0.375rem rgb(0 0 0 / .25);
  cursor: pointer;
}
.notes-list__more:hover { border-color: var(--accent); color: var(--ink); }
.notes-list__more[hidden] { display: none; }
.notes-list__more .icon { width: 1.125rem; height: 1.125rem; }
.notes-editor-empty .panel__body {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 12rem;
  text-align: center;
  color: var(--ink-3);
}
.notes-card {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  width: 100%;
  min-width: 0;
  padding: 0.75rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  text-align: left;
}
.notes-card:hover, .notes-card.is-active { border-color: var(--accent); background: var(--surface-2); }
.notes-card strong, .notes-card span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.notes-card span { color: var(--ink-3); font-size: var(--fs-sm); }
.notes-editor .panel__body { display: flex; flex-direction: column; gap: 0.5rem; }
.notes-editor .pf-input { width: 100%; max-width: 100%; }
.notes-editor textarea { min-height: 15rem; resize: vertical; line-height: 1.55; }
/* flex-end + margin-right:auto trên nút Xoá: nút Lưu luôn dính phải kể cả khi
   nút Xoá bị ẩn (đang tạo ghi chú mới) — space-between cũ đẩy Lưu về trái lúc
   nó là con flex duy nhất. */
.notes-editor__actions { display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 0.5rem; }
.notes-editor__actions #note-delete { margin-right: auto; }

/* -------------------------------------------- Cửa sổ Trợ lý AI (mọi trang)
   assets/js/ai-chat.js — cùng khuôn dialog.show() không-chặn-trang như
   notes-quick ở trên. right:4.75rem giống notes-quick: chừa đúng cột 4 nút nổi
   (donate/report/notes/AI) đứng right:1rem rộng 3rem + khe 0.75rem. */
.ai-chat[open] {
  display: flex;
  flex-direction: column;
  position: fixed;
  inset: auto;
  right: 4.75rem;
  bottom: 1rem;
  margin: 0;
  width: min(380px, calc(100vw - 5.75rem));
  max-width: min(90vw, 34rem);
  min-width: 19rem;
  max-height: 90vh;
  /* `height` cố định (không phải `min-height`) — chat dài không được đẩy cửa
     sổ cao thêm, chỉ có `.ai-chat__log` cuộn bên trong. Kéo núm resize (JS)
     ghi đè bằng inline style nên vẫn tự do nới rộng khi người dùng chủ động. */
  height: calc(26rem + 30px);
  padding: 0;
  border-radius: var(--radius);
  overflow: hidden;
  z-index: 92;
}
.ai-chat::backdrop { display: none; }
/* 14px cho cả khối — tin nhắn, ô nhập VÀ placeholder của nó ("font: inherit"
   ở đầu file khiến input/textarea + ::placeholder ăn theo cỡ chữ cha). */
.ai-chat__body { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; padding: 0.75rem 1.25rem 1.25rem; gap: 0.625rem; font-size: var(--fs-xl); }
.ai-chat__handle { display: none; }
.ai-chat__head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; cursor: move; user-select: none; }
.ai-chat__head .lp__head { text-align: left; font-size: var(--fs-3xl); }
.ai-chat__head #ac-close-btn { flex: none; width: 1.875rem; height: 1.875rem; font-size: var(--fs-2xl); cursor: pointer; }
.ai-chat__disclaimer {
  margin: 0;
  font-size: var(--fs-sm);
  font-style: italic;
  line-height: 1.4;
  color: var(--ink-3);
}

/* Khung hội thoại — cột duy nhất co giãn khi resize cửa sổ. */
.ai-chat__log {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  overflow-y: auto;
  padding-right: 0.25rem;
  /* Vẫn cuộn được, chỉ ẩn thanh cuộn — Firefox + Chrome/Safari cần 2 cú pháp
     riêng, không cái nào thay được cái kia. */
  scrollbar-width: none;
}
.ai-chat__log::-webkit-scrollbar { display: none; }
.ai-chat__msg {
  margin: 0;
  max-width: 88%;
  /* Trên dưới khớp .pf-input (ô nhập) — 0.4375rem, chỉ đổi trục dọc. */
  padding: 0.4375rem 0.75rem;
  border-radius: var(--radius);
  font-size: var(--fs-md);
  line-height: 1.45;
  overflow-wrap: break-word;
  white-space: pre-wrap;
}
.ai-chat__msg--user {
  align-self: flex-end;
  background: var(--accent);
  color: var(--on-accent);
}
.ai-chat__msg--bot {
  align-self: flex-start;
  background: var(--field);
  border: 1px solid var(--line);
  color: var(--ink);
}
.ai-chat__msg--thinking { color: var(--ink-3); font-style: italic; }

.ai-chat__form { display: flex; align-items: flex-end; gap: 0.5rem; }
.ai-chat__input {
  flex: 1 1 auto;
  max-width: 100%;
  min-height: 2.55rem;
  max-height: 7rem;
  resize: none;
  line-height: 1.4;
  /* `.pf-input` (dùng chung mọi ô nhập, khai SAU trong file) tự đặt font-size
     riêng nên đè mất giá trị kế thừa từ .ai-chat__body — phải ghi đè ngược lại
     ở đây bằng selector 2 lớp (.ai-chat__body .ai-chat__input) mới thắng nổi
     .pf-input (chỉ 1 lớp) theo độ ưu tiên CSS. */
}
/* min-height giảm theo cỡ nút Gửi mới (xem .ai-chat__send) — khớp bằng nhau để
   giữ 2 ô ngang hàng, đồng thời gần khít chữ 1 dòng hơn để placeholder trông
   như đứng giữa hộp thay vì lệch lên trên do dư khoảng trống bên dưới. */
.ai-chat__body .ai-chat__input { font-size: var(--fs-md); min-height: 2.1rem; padding-block: 0.4375rem; }
/* Khớp min-height với .ai-chat__input ở trên — không có dòng này, nút "Gửi"
   thấp hơn ô nhập, `align-items: flex-end` chỉ ghim đáy nên đỉnh hai ô lệch
   nhau rõ. Thu nhỏ + chữ 13px + tự canh giữa cả hai chiều. */
.ai-chat__send {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2.1rem;
  padding: 0.3125rem 0.75rem;
  font-size: var(--fs-md);
}

/* Núm resize góc phải-dưới — chỉ desktop, cùng cơ chế notes-quick__resize. */
.ai-chat__resize {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 1rem;
  height: 1rem;
  cursor: nwse-resize;
  touch-action: none;
  z-index: 6;
}
.ai-chat__resize::before {
  content: '';
  position: absolute;
  right: 0.1875rem;
  bottom: 0.1875rem;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid var(--line-dark);
  border-bottom: 2px solid var(--line-dark);
  border-radius: 0 0 0.125rem 0;
  opacity: 0.55;
}
.ai-chat__resize:hover::before { opacity: 1; }

@media (max-width: 56.25em) {
  /* Bottom sheet: dán đáy, chiều cao điều khiển qua --ai-chat-h — cùng công
     thức notes-quick[open] ở mobile. */
  .ai-chat[open] {
    right: 4rem;
    bottom: 0;
    left: 0;
    width: auto;
    max-width: none;
    min-width: 0;
    max-height: none;
    min-height: 0;
    height: var(--ai-chat-h, 45vh);
    border-radius: var(--radius) var(--radius) 0 0;
  }
  .ai-chat__head { cursor: default; }
  .ai-chat__resize { display: none; }
  .ai-chat__handle {
    display: flex;
    flex: none;
    justify-content: center;
    padding: 0.625rem 0;
    cursor: grab;
    touch-action: none;
  }
  .ai-chat__handle::before {
    content: '';
    display: block;
    width: 2.5rem;
    height: 0.25rem;
    border-radius: 999rem;
    background: var(--line-dark);
  }
}

/* Nút góp ý đứng riêng ngay trên nút ủng hộ: 1rem đáy + 2.55rem FAB (đã thu
   85%) + 0.75rem khe. */
.fab--donate-wrap {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 90;
}
.fab--donate {
  position: relative;
  display: grid;
  place-items: center;
  width: 2.55rem;
  height: 2.55rem;
  padding: 0;
  border: 1px solid var(--accent-dark);
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-dark);
  box-shadow: 0 0.375rem 1rem rgb(0 0 0 / .35);
}
.fab--donate:hover { background: var(--field); }
.fab--donate__icon {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
}
.fab--donate__icon .icon { width: 1.063rem; height: 1.063rem; }

.donate__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  padding: 1.5rem 1.25rem 1.25rem;
  text-align: center;
}
.donate__body .lp__head { font-size: var(--fs-xl); }
/* Nút "Đóng" thu nhỏ lại + chữ 13px, canh giữa cả hai chiều cho chắc (không
   dựa vào canh giữa mặc định của <button>, vì đó chỉ đúng khi không icon). */
.donate__body .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.3125rem 0.75rem;
  font-size: var(--fs-md);
}
.donate__qr {
  display: block;
  width: min(100%, 16rem);
  height: auto;
  border-radius: var(--radius);
}
.donate__qr[hidden] { display: none; }
.donate__qr-pending {
  display: grid;
  place-items: center;
  width: min(100%, 16rem);
  min-height: 10rem;
  padding: 1rem;
  border: 1px dashed var(--line-dark);
  border-radius: var(--radius);
  color: var(--ink-2);
  font-size: var(--fs-lg);
}
.donate__qr-pending[hidden] { display: none; }
.donate__text { color: var(--ink-2); font-size: var(--fs-xl); }

/* Hộp gửi nhanh — dùng lại khung `.lp` của popup đăng nhập, chỉ khác phần ruột:
   ở đây là form nên căn TRÁI, không căn giữa như một lời mời. */
.rb__body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem 1.25rem 1.25rem;
  text-align: left;
  /* Cả form (nhãn, ô nhập, placeholder — input/textarea "font: inherit" ở đầu
     file) đồng loạt 14px, trừ tiêu đề bị .lp__head ghi đè riêng ngay dưới. */
  font-size: var(--fs-xl);
}
.rb__body .lp__head { text-align: left; font-size: var(--fs-3xl); }
/* Cùng khuôn với .nq-head (nút đóng ghi chú nhanh) — hàng tiêu đề + nút X. */
.rb__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.rb__head #rb-close { flex: none; width: 1.875rem; height: 1.875rem; font-size: var(--fs-2xl); }
/* Chế độ khách: nói trước khi người ta gõ rằng sẽ không có thư trả lời.
   Tông --warn chứ không phải --err: đây không phải lỗi, chỉ là một điều kiện
   cần biết — nhuộm đỏ thì trông như form đang hỏng và người ta bỏ đi.
   Xem assets/js/report-button.js. */
.rb__guest {
  margin: 0;
  padding: 0.5rem 0.625rem;
  border-left: 3px solid var(--warn);
  border-radius: 0.25rem;
  background: var(--warn-soft);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.rb__kinds { align-self: flex-start; margin-bottom: 0.25rem; }
.rb__label {
  font-size: var(--fs-xl);
  color: var(--ink-3);
}
/* Chạm đều hai mép hộp. `.pf-input` chặn `max-width: 20rem` và nó nằm SAU
   trong file, nên chọn theo tên `.rb__*` không thôi là thua độ ưu tiên: ô nhập
   hụt 3.5rem bên phải, form trông như bị đẩy lệch sang trái. Thêm `.rb__body`
   cho đủ nặng. */
/* Cùng lý do ghi đè `.pf-input` như .ai-chat__input ở trên — 2 lớp mới thắng. */
.rb__body .rb__input, .rb__body .rb__area { max-width: 100%; width: 100%; font-size: var(--fs-md); }
.rb__area { resize: vertical; min-height: 6rem; line-height: 1.5; }
.rb__actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.25rem;
}
/* Gửi/Huỷ thu nhỏ lại + chữ 13px, tự canh giữa cả hai chiều. */
.rb__actions .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.3125rem 0.75rem;
  font-size: var(--fs-md);
}

/* Báo đã gửi. Nổi ở góc, tự biến mất — không chặn thao tác như alert(). */
.toast {
  position: fixed;
  right: 1rem;
  bottom: 4.75rem;          /* ngay trên nút nổi, không đè lên nó */
  z-index: 95;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  max-width: min(92vw, 22rem);
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 0.375rem 1rem rgb(0 0 0 / .35);
  font-size: var(--fs-lg);
  color: var(--ink);
}
.toast a { color: var(--accent-dark); text-decoration: underline; white-space: nowrap; }

/* --------------------------------------------------- My Report / Inbox */
.rp-list { display: flex; flex-direction: column; gap: 0.625rem; }

/* Hàng chip lọc của Report Inbox đứng một mình giữa tiêu đề và danh sách —
   `.chips` không tự có lề, mà ở mấy màn khác nó nằm trong `.filters` nên không
   ai thấy thiếu. Ở đây thiếu là ba khối dính thành một mảng. */
.rp-filters { margin-block: 0.875rem 1.125rem; }

.rp-card {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
}
.rp-card:hover { border-color: var(--accent); text-decoration: none; }
.rp-card__top {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.rp-card__title {
  font-size: var(--fs-body);
  font-weight: 600;
  overflow-wrap: anywhere;
}
.rp-card__meta { font-size: var(--fs-sm); color: var(--ink-3); overflow-wrap: anywhere; }
.rp-card__new {
  padding: 0.0625rem 0.375rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--fs-2xs);
  font-weight: 700;
}

/* Trạng thái: hai màu, mỗi màu một nghĩa. Chữ luôn đi kèm — người phân biệt màu
   kém vẫn phải đọc được trạng thái (docs/ui.md). */
.rp-status {
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--line-dark);
  border-radius: 999px;
  font-size: var(--fs-sm);
  white-space: nowrap;
  color: var(--ink-2);
}
.rp-status--new { border-color: var(--accent); color: var(--accent-dark); }
.rp-status--answered { border-color: var(--ok); color: var(--ok); }

/* Huy hiệu "Khách" — luồng do người chưa đăng nhập gửi, CHỈ hiện ở hộp thư
   admin. Cùng khuôn viên với .rp-status để hai chip đứng cạnh nhau không lệch
   nhau, nhưng nền đặc để nó không bị đọc nhầm là một trạng thái thứ ba: nó nói
   về NGƯỜI GỬI, không phải về tiến độ xử lý.
   Xem migrations/009_guest_reports.sql. */
.rp-guest-chip {
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--warn);
  border-radius: 999px;
  background: var(--warn-soft);
  color: var(--warn);
  font-size: var(--fs-2xs);
  font-weight: 700;
  white-space: nowrap;
}

/* Huy hiệu "To-do" — ban quản trị đánh dấu luồng này còn việc phải làm
   (migrations/010_report_todo.sql). Nền đặc như huy hiệu Khách để không bị đọc
   nhầm là một trạng thái thứ ba của luồng: nó nói về việc của ADMIN, không phải
   về chuyện người gửi có đang chờ hay không. Chỉ có ở hộp thư admin. */
.rp-todo-chip {
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-size: var(--fs-2xs);
  font-weight: 700;
  white-space: nowrap;
}

/* Một hàng của hộp thư admin: nút To-do (một `<button>`) + thẻ (một `<a>`).
   Hai ANH EM chứ không lồng nhau — nút nằm trong link là HTML sai và bàn phím
   không tới được nó. `align-items: stretch` để nút cao bằng thẻ, thẻ dài mấy
   dòng thì vùng bấm cũng dài theo.

   Nút đứng BÊN TRÁI, và không phải vì thẩm mỹ: ba nút nổi (góp ý · trợ lý AI ·
   ủng hộ) ghim cứng ở góc PHẢI-DƯỚI mọi trang. Để nút To-do bên phải thì trên
   màn hẹp nó nằm đúng dưới chồng nút đó — che một cái nút là che hẳn, không
   như che một góc thẻ (vẫn bấm được chỗ khác). Bên trái thì không đụng ai ở
   mọi bề ngang, và một ô tick đứng trước mục cũng là hình dạng quen của danh
   sách việc cần làm. Trong DOM nút vẫn nằm SAU thẻ để bàn phím/đọc màn hình
   nghe tiêu đề trước rồi mới tới hành động — `order` chỉ đổi chỗ khi vẽ. */
.rp-card-row { display: flex; align-items: stretch; gap: 0.5rem; }
.rp-card-row .rp-todo-btn { order: -1; }
.rp-card-row .rp-card { flex: 1; min-width: 0; }

.rp-todo-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  flex: 0 0 auto;
  /* Sàn bề ngang để chữ "To-do" / "할 일" không ép nút co lại khác nhau giữa ba
     ngôn ngữ — hàng nào cũng phải thẳng mép với hàng trên. */
  min-width: 3.75rem;
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-3);
  font-size: var(--fs-2xs);
  font-weight: 700;
  cursor: pointer;
}
.rp-todo-btn:hover { border-color: var(--accent); color: var(--ink); }
.rp-todo-btn:disabled { opacity: .6; cursor: default; }
/* Ô ✓: dấu hiệu THỨ HAI bên cạnh màu, để người phân biệt màu kém vẫn thấy cờ
   đang bật hay tắt (docs/ui.md). Ô rỗng khi tắt — vẫn chiếm chỗ nên nút không
   nhảy bề cao lúc bấm. */
.rp-todo-btn__box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.125rem;
  height: 1.125rem;
  border: 1px solid var(--line-dark);
  border-radius: 0.25rem;
  font-size: var(--fs-2xs);
  line-height: 1;
}
.rp-todo-btn.is-on {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-dark);
}
.rp-todo-btn.is-on .rp-todo-btn__box {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}
/* Ở màn chi tiết nút đứng CẠNH cụm trạng thái trong một hàng ngang, nên nó xếp
   ngang để cao bằng cụm đó. Ở danh sách thì xếp dọc, vì chỗ trống bên phải thẻ
   là một cột hẹp và cao. Cùng một nút, hai khuôn — nhưng chỉ khác chiều xếp. */
.rp-statusbar .rp-todo-btn { flex-direction: row; padding: 0.4375rem 0.75rem; }

/* Cảnh báo ngay trên khung soạn tin: câu sắp gõ sẽ không tới được ai. */
.rp-guest-warn {
  margin: 0 0 0.75rem;
  padding: 0.625rem 0.75rem;
  border-left: 3px solid var(--warn);
  border-radius: 0.25rem;
  background: var(--warn-soft);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

/* Luồng tin nhắn — kiểu hộp thoại: người khác nép TRÁI, mình nép PHẢI.

   Trước đây mọi tin nhắn đều căn cùng một mép và chỉ khác nhau ở tông nền, nên
   phải đọc tên người gửi mới biết ai nói câu nào. Nay chỉ cần liếc mép.

   `list-style: none` là bắt buộc: `.rp-thread` là `<ol>` (thứ tự tin nhắn là
   thật, nên vẫn giữ thẻ ol cho trình đọc màn hình), mà `<li>` vẫn là list-item
   kể cả khi cha thành flex container — thiếu dòng này là mỗi bong bóng đeo một
   con số ở mép trái. */
.rp-thread {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: 1rem 0;
  list-style: none;
  padding: 0;

  /* Vùng tin nhắn CUỘN RIÊNG (chủ dự án chốt 2026-08-18): con trỏ nằm trong
     luồng thì lăn chuột chạy luồng, đưa ra ngoài mới cuộn cả trang.

     `overscroll-behavior: contain` là phần bắt buộc, không phải trang trí: thiếu
     nó thì cuộn hết luồng là trình duyệt "nối" đà sang trang (scroll chaining),
     tức đang đọc dở lại bị trang nhảy theo.

     Chiều cao KHÔNG đặt bằng số ở đây — nó do bố cục cao-bằng-màn-hình bên
     dưới chia phần còn lại. Bản đầu đặt `max-height: 58dvh` và hỏng đúng chỗ
     dễ bỏ sót: vùng tin nhắn kéo dài quá đáy khung nhìn, mà ô soạn tin thì dính
     đáy khung nhìn, nên **tin nhắn cuối cùng nằm khuất dưới ô soạn tin**. */
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Chừa chỗ cho thanh cuộn để chữ không dính sát nó */
  padding-inline-end: 0.375rem;
}

/* ---- Bố cục cao bằng màn hình cho HAI màn chi tiết report ----------------

   Cả khối vừa đúng một khung nhìn: header · đường dẫn · tiêu đề · LUỒNG (co
   giãn, tự cuộn) · ô soạn tin · footer. Nhờ vậy ô soạn tin luôn nằm NGAY DƯỚI
   luồng chứ không đè lên nó, và trang gần như không phải cuộn.

   Vì sao không đặt một con số `max-height` cho luồng: con số đó phải đoán chiều
   cao của header + tiêu đề + ô soạn tin, mà cả ba đổi theo ngôn ngữ, theo độ
   dài tiêu đề người dùng đặt và theo bề ngang màn hình. Đoán sai một lần là che
   mất tin nhắn — đúng lỗi vừa phải sửa.

   `min-height: 0` ở mọi tầng là bắt buộc: mặc định của flex item là
   `min-height: auto`, nó không cho con co nhỏ hơn nội dung, nên phần tử tự cuộn
   sẽ phình ra thay vì cuộn. */
body[data-page="my-report-details"],
body[data-page="report-inbox-details"] {
  /* `height`, KHÔNG phải `min-height`: min-height chỉ đặt sàn, nội dung dài hơn
     thì body cứ cao lên và chẳng có gì bị ép co — đo được lần đầu viết sai như
     vậy: body 1585px trong khung nhìn 860px, luồng vẫn phình 1058px thay vì
     cuộn. Có trần thì `flex-shrink` mới có chỗ làm việc.

     Chật quá (màn rất thấp) thì nội dung tràn ra ngoài body và trang cuộn như
     thường — `overflow: hidden` sẽ cắt mất chữ nên cố ý không dùng. */
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
}
body[data-page="my-report-details"] .main,
body[data-page="report-inbox-details"] .main {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  /* Hàng NGANG với đúng một con: `align-items: stretch` mặc định lo phần chiều
     cao, còn bề ngang thì `.container` tự giữ `min(92%, 44rem)` và `margin
     auto` căn giữa. ⚠ Đừng thêm `flex: 1` cho container — trục chính ở đây là
     trục ngang, thêm vào là nó kéo hết bề ngang màn hình và chữ chạy sát mép
     (đã đo bằng ảnh chụp một lần rồi). */
}
body[data-page="my-report-details"] .main > .container,
body[data-page="report-inbox-details"] .main > .container {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
body[data-page="my-report-details"] #thread,
body[data-page="report-inbox-details"] #thread {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
body[data-page="my-report-details"] .rp-thread,
body[data-page="report-inbox-details"] .rp-thread {
  flex: 1 1 auto;
  /* Sàn cho vùng đọc. Trên điện thoại, header hai hàng + tiêu đề dài + ô soạn
     tin ăn gần hết màn: đo ở 375×700 với sàn 7rem thì luồng chỉ còn 105px, tức
     một tin nhắn cũng không hiện trọn. 12rem là mức còn đọc được; chật hơn nữa
     thì cả trang cuộn như bình thường. */
  min-height: 12rem;
}
.rp-msg {
  /* Không kéo hết bề ngang: chừa mép đối diện để hai bên nhìn ra ngay là hai
     người khác nhau đang nói. */
  max-width: 82%;
  align-self: flex-start;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
}
/* Lời của chính người đang đọc — nép phải, và tô nền nhấn nhạt. Giữ CẢ HAI dấu
   hiệu: người phân biệt màu kém vẫn đọc được theo mép, còn mép thì trên màn
   rất hẹp gần như trùng nhau nên vẫn cần màu (docs/ui.md). */
.rp-msg--mine {
  align-self: flex-end;
  background: var(--accent-soft);
  border-color: var(--line-dark);
}
.rp-msg__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
}
/* Tên và giờ theo về mép của chính bong bóng đó. Phần NỘI DUNG thì vẫn đọc từ
   trái sang như mọi đoạn văn khác — căn phải cả đoạn nhiều dòng là bắt mắt phải
   dò lại đầu dòng mỗi lượt. */
.rp-msg--mine .rp-msg__head { justify-content: flex-end; }
.rp-msg__who { font-size: var(--fs-lg); font-weight: 600; color: var(--ink); }
.rp-msg__when { font-size: var(--fs-sm); color: var(--ink-3); }
.rp-msg__body {
  font-size: var(--fs-body);
  color: var(--ink-2);
  /* Giữ xuống dòng người dùng đã gõ. textContent không sinh <br>, nên nếu thiếu
     dòng này thì mọi report dài thành một khối chữ liền. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Ô soạn tin — luôn nằm ngay dưới luồng tin nhắn.

   Trước đây nó `position: sticky` để khỏi bị đẩy xuống cuối trang dài. Từ khi
   vùng tin nhắn tự cuộn trong bố cục cao-bằng-màn-hình thì **sticky thành cái
   hại**: nó dính lên đáy khung nhìn và đè mất tin nhắn cuối cùng của vùng ngay
   bên trên (chủ dự án báo 2026-08-18). Bố cục mới đã lo đúng việc mà sticky
   từng lo — trên màn đủ cao, ô soạn tin luôn ở đáy màn hình mà không đè lên gì.

   Nền đục + viền trên vẫn giữ: chúng tách khu vực gõ khỏi khu vực đọc. */
.rp-reply {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding-block: 0.625rem;
  background: var(--bg);
  border-top: 1px solid var(--line);
}
/* Ô gõ chiếm hết bề ngang thanh soạn tin: `.pf-input` chặn 20rem vì nó sinh ra
   cho mấy ô ngắn ở màn hồ sơ, còn đây là chỗ gõ cả đoạn.

   Trần chiều cao: dính đáy rồi thì một ô cao quá sẽ nuốt mất phần đang đọc.
   Quá trần thì ô tự cuộn bên trong. */
.rp-reply .rb__area {
  max-width: 100%;
  min-height: 4.5rem;
  max-height: 30vh;
}
.rp-reply__actions { display: flex; justify-content: flex-end; }

.rp-statusbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.625rem;
  margin-bottom: 0.875rem;
}
.rp-statusbar__label { font-size: var(--fs-lg); color: var(--ink-3); }

/* ------------------------------------------------------- Trang hồ sơ
   profile.html — một cột hẹp: nội dung là vài cặp nhãn–giá trị, kéo rộng bằng
   trang danh sách thì mắt phải quét ngang một quãng trống dài. */
.container--narrow { width: min(var(--page-w), 44rem); }

.pf-id {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding-bottom: 0.875rem;
  margin-bottom: 0.875rem;
  border-bottom: 1px solid var(--line);
}
.pf-id__avatar {
  display: grid;
  place-items: center;
  width: 3.5rem;
  height: 3.5rem;
  flex-shrink: 0;
  border-radius: 50%;
  overflow: hidden;
  background: var(--accent-soft);
}
.pf-id__avatar img { width: 100%; height: 100%; object-fit: cover; }
.pf-id__avatar--text {
  font-size: var(--fs-3xl);
  font-weight: 600;
  color: var(--accent-dark);
}
.pf-id__name {
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.pf-id__sub { font-size: var(--fs-lg); color: var(--ink-3); overflow-wrap: anywhere; }

/* Nhãn – giá trị. Cột nhãn cố định để chế độ xem và chế độ sửa thẳng hàng
   nhau: bấm "Chỉnh sửa" mà cả khối xê dịch một chút là nhìn như trang bị giật. */
.pf-rows { display: flex; flex-direction: column; gap: 0.75rem; }
.pf-row {
  display: grid;
  grid-template-columns: 9.5rem 1fr;
  align-items: center;
  gap: 0.75rem;
  min-height: 2.125rem;
}
.pf-row dt,
.pf-row__label {
  font-size: var(--fs-lg);
  color: var(--ink-3);
}
.pf-row dd,
.pf-row__value {
  font-size: var(--fs-body);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.pf-input {
  width: 100%;
  max-width: 20rem;
  padding: 0.4375rem 0.625rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  font-size: var(--fs-body);
  color: var(--ink);
}
.pf-input:focus { outline: none; border-color: var(--accent); }
.pf-input::placeholder { color: var(--ink-3); }
/* Ô ngày sinh chỉ chứa 10 ký tự — kéo rộng bằng ô tên là một khoảng trống dài
   vô nghĩa, và trông như đang chờ nhập thêm gì đó. */
.pf-input--date { max-width: 9.5rem; font-variant-numeric: tabular-nums; }

/* Ô ngày sinh + nút lịch đứng cạnh nhau như một cụm */
.pf-date { display: inline-flex; align-items: center; gap: 0.375rem; position: relative; }
.pf-date__btn {
  display: grid;
  place-items: center;
  width: 2.125rem;
  height: 2.125rem;
  flex-shrink: 0;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  color: var(--ink-3);
}
.pf-date__btn:hover { border-color: var(--accent); color: var(--ink); }
.pf-date__btn .icon { width: 1.125rem; height: 1.125rem; }
/* Ô date thật chỉ để mượn cái lịch — không hiện, không bấm trúng, nhưng KHÔNG
   `display:none`: ẩn kiểu đó thì showPicker() không có chỗ để neo bảng lịch. */
.pf-date__native {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

.pf-hint {
  display: block;
  margin-top: 0.25rem;
  font-size: var(--fs-sm);
  color: var(--ink-3);
}

/* Cụm nút nằm sát mép PHẢI của thẻ — cùng phía với nút Đăng xuất bên dưới, nên
   mắt chỉ phải theo dõi một cột thao tác thay vì hai. */
.pf-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1rem;
}
.pf-actions--end { justify-content: flex-end; margin-top: 1rem; }
.pf-saved { font-size: var(--fs-lg); color: var(--ok); }

.pf-links { display: flex; flex-direction: column; gap: 0.625rem; }
.pf-link {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.pf-link__name { font-size: var(--fs-body); font-weight: 600; color: var(--ink); }
.pf-link__email { font-size: var(--fs-lg); color: var(--ink-2); overflow-wrap: anywhere; }
.pf-link__since { font-size: var(--fs-sm); color: var(--ink-3); }

/* Trang hồ sơ có hai panel chồng nhau — tách chúng ra khỏi nhau và khỏi tiêu đề */
#profile .panel + .panel { margin-top: 1rem; }

/* ==========================================================================
   LIGHT MODE — bật khi công tắc trong header được tick.
   Tone be, cùng họ màu với dark mode ở trên.
   ========================================================================== */
:root[data-theme="light"] {
  color-scheme: light;

  /* Nền & bề mặt */
  --bg: #f6efe0;
  --surface: #ffffff;
  --surface-2: #ebe1cc;
  --field: #ffffff;

  /* Viền */
  --line: #ece3d0;
  --line-dark: #d9c9a9;

  /* Chữ — nâu ấm */
  --ink: #33291d;
  --ink-2: #6b5c48;
  --ink-3: #a3937c;

  /* Nhấn — be sáng */
  --accent: #eecfa1;
  --accent-dark: #e4bd85;
  --accent-soft: #f8ecd8;
  --on-accent: #33291d;
  /* Vàng ĐẬM hẳn, vì ở đây nó là màu CHỮ trên nền kem — xem ghi chú ở :root */
  --accent-ink: #8a6a2f;

  /* Kết quả */
  --ok: #4c7a52;
  --ok-soft: #eaf2e7;
  --err: #b2452f;
  --err-soft: #fbeae4;
  --warn: #96721a;
  --warn-soft: #f7edd2;
  /* Xem ghi chú --ok-vivid/--err-vivid ở :root (dark) — cặp riêng cho lúc chấm
     đúng/sai, đậm hơn --ok/--err để còn đọc rõ trên nền sáng. */
  --ok-vivid: #16a34a;
  --err-vivid: #dc2626;
}

/* Icon công tắc phản ánh mode đang bật: mặc định dark → mặt trăng */
:root[data-theme="light"] .theme-btn .icon-moon { display: none; }
:root[data-theme="light"] .theme-btn .icon-sun { display: block; }

/* Sang light thì bóng đổ dropdown nhẹ lại */
:root[data-theme="light"] .lang__menu,
:root[data-theme="light"] .navdrop__menu { box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / .12); }

/* ==========================================================================
   Responsive — breakpoint dùng em để cũng giãn theo cỡ chữ người dùng
   56.25em = 900px · 38.75em = 620px · 26.5em = 424px (ở cỡ chữ mặc định 16px)
   ========================================================================== */

/* Header hết chỗ SỚM HƠN phần thân nên có breakpoint riêng, đừng gộp vào
   56.25em bên dưới. Logo + menu + 3 nút công cụ cần ~933px ở bản tiếng Việt
   (bản dài nhất); vùng nội dung chỉ rộng 92% màn hình, cộng thêm cỡ chữ gốc
   giãn theo màn hình nữa, nên phải tới ~1200px mới đủ cho một hàng.
   Hẹp hơn thì menu xuống hàng riêng — làm vậy để tên site KHÔNG BAO GIỜ bị ngắt
   dòng, vì ngắt dòng là header cao thấp khác nhau giữa các ngôn ngữ. */
@media (max-width: 75em) {
  .header__inner {
    min-height: 0;
    padding-block: 0.625rem;
    flex-wrap: wrap;
    gap: 0.5rem 0.875rem;
  }
  .nav {
    order: 3;
    width: 100%;
    margin-left: 0;
    gap: 1.125rem;
  }
}

@media (max-width: 56.25em) {
  /* Xếp dọc thì nội dung tự đủ cao, không cần chặn chiều cao tối thiểu */
  .work { grid-template-columns: 1fr; min-height: 0; }

  /* Khung Learn more cũng xếp dọc: bản dịch → từ mới → ngữ pháp. Đường kẻ dọc
     giữa hai thẻ đổi thành kẻ ngang, không thì nó treo lơ lửng ở mép trái. */
  .learn__grid { grid-template-columns: 1fr; }
  .learn__card--vocab {
    margin-top: 0.875rem;
  }
  .logo { font-size: var(--fs-2xl); }

  /* Trang chi tiết từ xếp dọc: cột "đường đi sang phần khác" xuống dưới phần
     nghĩa và câu ví dụ — đọc nghĩa trước, đi tiếp sau. */
  .entry { grid-template-columns: 1fr; }

  /* Màn làm bài xếp dọc. Phiếu trả lời nằm SAU danh sách câu (đúng thứ tự DOM):
     hẹp thế này thì không nhìn được hai thứ cùng lúc, mà đẩy phiếu lên trên chỉ
     làm câu hỏi đầu tiên bị tụt khỏi màn hình. Bỏ luôn sticky — dính một khối
     cao bằng nửa màn hình thì không còn chỗ đọc đề. */
  .exam { grid-template-columns: 1fr; }
  /* Hết dính thì bỏ luôn trần chiều cao: xếp dọc rồi, phiếu cứ cao bao nhiêu
     cũng được vì cả trang cuộn chung. */
  .exam__side { position: static; max-height: none; }
  .exam__side .panel__body { overflow-y: visible; }

  /* Ô soạn tin dính đáy nằm ngang tầm nút nổi góp ý, mà nút Gửi lại căn phải —
     hai thứ chồng nhau. Từ đây trở xuống khung nội dung gần chạm mép màn hình
     nên phải chừa chỗ; rộng hơn 56.25em thì `.container--narrow` (44rem) đã
     cách mép đủ xa, đo được ≥98px so với 64px nút nổi chiếm. */
  .rp-reply__actions { padding-inline-end: 3.5rem; }
  .notes-app { grid-template-columns: 1fr; }
  .notes-list { max-height: 14rem; overflow-y: auto; }
}

@media (max-width: 38.75em) {
  /* Màn hẹp thì nới % ra để chữ không bị bó vào giữa */
  .container { width: 94%; }
  .container--narrow { width: 94%; }

  /* Trang hồ sơ: nhãn xuống dòng riêng. Giữ hai cột ở 320px thì cột giá trị
     chỉ còn hơn 100px và email dài bị bẻ vụn thành bốn dòng. */
  .pf-row { grid-template-columns: 1fr; align-items: start; gap: 0.125rem; }
  .pf-input { max-width: 100%; }

  /* Nút nổi thu nhỏ và nép sát góc trên màn hẹp — vẫn đủ 2.75rem cho đầu ngón
     tay (mốc chạm ở @media (hover:none) bên dưới). */
  .fab { width: 2.75rem; height: 2.75rem; right: 0.625rem; bottom: 4rem; }
  /* Notes tạm ẩn — xem ghi chú ở `.fab--notes` bản desktop bên trên. */
  .fab--notes { right: 0.625rem; bottom: 7.375rem; }
  /* AI xếp ngay trên Report (chỗ Notes bỏ trống): 4rem + 2.75rem + 0.625rem khe.
     Bật lại Notes thì trả mốc này về 10.75rem. */
  .fab--ai { right: 0.625rem; bottom: 7.375rem; }
  .notes-editor__actions { flex-wrap: wrap; }

  /* Màn hẹp: nút ủng hộ to lên CÙNG CỠ nút góp ý cho vừa đầu ngón tay. Nút vẫn
     có thể đè lên hàng nút thao tác khi cuộn tới đúng tầm — xem ghi chú ở
     `.actions` bên dưới. */
  .fab--donate-wrap {
    right: 0.625rem;
    bottom: 0.625rem;
  }
  .fab--donate {
    width: 2.75rem;
    height: 2.75rem;
  }
  .toast { right: 0.625rem; bottom: 8.25rem; }

  /* Chừa chỗ cho nút nổi ở những hàng thao tác CHẠY HẾT BỀ NGANG.
     Đo được ở 320×700: nút nổi (x 245–290) đè lên nút "Bỏ qua" (x 161–282) của
     màn chép chính tả. Nút nổi hiện ở mọi màn (chủ dự án chốt 2026-08-17) nên
     cách sửa là ĐẨY NỘI DUNG RA, không phải ẩn nút. Nút ủng hộ đứng NGAY DƯỚI
     nút góp ý và ở màn hẹp cũng là hình tròn 2.75rem (xem `.fab--donate`
     ở trên) nên dùng chung một mức chừa chỗ này, không cần padding riêng.
     ⚠ Đây chỉ giải quyết lúc hàng nút nằm ngang tầm nút nổi; cuộn trang thì mọi
     thứ vẫn đi qua dưới nó — bản chất của một nút `position: fixed`. Vùng gõ
     chính tả thì KHÔNG bao giờ bị đè (đã đo). */
  .actions,
  .result__actions,
  .rp-reply__actions,
  .pf-actions--end { padding-inline-end: 3.5rem; }

  /* Màn hẹp: thu nhỏ thêm, và chật thì menu XUỐNG HÀNG chứ không cuộn ngang.
     ⚠ ĐỪNG đặt lại `overflow-x: auto` ở đây: `.navdrop__menu` là phần tử
     position:absolute nằm NGAY DƯỚI mép dưới của `.nav`, mà overflow-x: auto
     làm overflow-y tính ra `auto` theo — tức `.nav` thành khung cắt và submenu
     bị cắt sạch. Trên điện thoại đó chính là lỗi "bấm Practice / My Pocket
     không thấy gì": menu có mở (`.is-open`, display:block, đo được cao 109px)
     nhưng nằm trọn ngoài vùng nhìn thấy của khung cuộn. */
  .logo { font-size: var(--fs-xl); }
  .logo__mark { width: 1.875rem; height: 1.875rem; }
  .nav {
    flex-wrap: wrap;
    gap: 0.5rem 1rem;
    font-size: var(--fs-md);
  }

  /* Tiêu đề bài và số file xếp dọc cho dễ đọc */
  .page-head { align-items: flex-start; gap: 0.375rem; }

  /* Hai tab chia đôi bề ngang, dễ bấm bằng ngón tay */
  .tabs .tab { flex: 1; }
  .detail-tabs-row { flex-wrap: wrap; gap: 0.5rem; }
  .detail-tabs-row .tabs { flex: 1 1 100%; }
  .lesson-nav { margin-left: auto; }

  /* Thanh play: hàng trên là các nút, thanh tua xuống hàng riêng */
  .player__row { flex-wrap: wrap; }
  .seek { order: 5; flex-basis: 100%; margin-top: 0.125rem; }
  .time { margin-left: auto; }

  .now { padding: 1.5rem 1rem 1.25rem; }
  .seg-track { padding-inline: 1rem; }
  /* Ẩn dòng chú thích ở giữa để Prev / Next không bị ép xuống dòng */
  .now-foot > span { display: none; }
  .now-foot { justify-content: space-between; }

  /* Liên hệ xếp dọc, email dài không đẩy tràn */
  .footer__inner { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
  .footer__contact { flex-direction: column; gap: 0.25rem; }

  /* Thanh trạng thái bài thi: đồng hồ và số câu ở hàng trên, nút Nộp bài trải
     hết hàng dưới cho dễ bấm bằng ngón cái. */
  .exam-bar__answered { margin-inline-start: 0; }
  .exam-bar #btn-submit { flex: 1 1 100%; }
}

@media (max-width: 26.5em) {
  /* Màn hình rất hẹp: nhóm nút hành động trải đều thay vì tràn */
  .actions .spacer { display: none; }
  .actions .btn { flex: 1 1 auto; }
  .btn--icon { flex: 0 0 auto; }

  /* Thẻ bài xuống 2 hàng: xem @container lessons ở khối DICTATION LIST — mốc đó
     tính theo bề ngang cột nên đúng cả khi danh sách chia 2 cột. */

  /* Nghĩa của từ xuống dòng dưới thay vì bị bóp ngang */
  .vocab__item { flex-wrap: wrap; }
  .vocab b { min-width: 3.75rem; }
}

/* Thiết bị cảm ứng: nới vùng bấm cho đạt cỡ ngón tay, không ảnh hưởng desktop */
@media (hover: none) {
  .btn { padding-block: 0.625rem; }
  /* `padding: 0` phải khai LẠI ở đây. `.btn--icon { padding: 0 }` ở đầu file
     cùng độ ưu tiên với `.btn` ngay trên mà lại đứng TRƯỚC, nên trên điện thoại
     nút icon ăn 0.625rem đệm dọc trong khi bề cao vẫn bị ghim cứng — chữ × bị
     đẩy lệch xuống khỏi tâm ô. Thấy rõ ở nút đóng của popup gửi góp ý. */
  .btn--icon { width: 2.5rem; height: 2.5rem; padding: 0; }
  /* Nút × của popup góp ý ghim 1.875rem cho chuột (`.rb__head #rb-close`), độ
     ưu tiên cao hơn dòng trên nên nó là nút icon DUY NHẤT không được nới. Trả
     lại cỡ ngón tay đúng bằng cách viết lại cùng bộ chọn ấy. */
  .rb__head #rb-close { width: 2.5rem; height: 2.5rem; }
  .step-btn { width: 2.5rem; height: 2.5rem; }
  .play-btn { width: 2.75rem; height: 2.75rem; }
  .vocab__item input { width: 1.125rem; height: 1.125rem; }
  /* Nút xoá từ khoá chỉ có icon 1rem — nới vùng bấm cho vừa đầu ngón tay */
  .filters__clear { padding: 0.6875rem; margin: -0.4375rem; }
  /* Dropdown trạng thái ở Dictation List: nút và từng hàng tích đạt ~44px, ô tích
     to lên cho khỏi trượt ngón. */
  .multi__btn { padding-block: 0.625rem; }
  .multi__opt { padding-block: 0.75rem; }
  .multi__opt input { width: 1.125rem; height: 1.125rem; }

  /* My Pocket: thẻ lật thấp lại một chút cho vừa màn điện thoại, và cụm nút
     Đã nhớ / Chưa nhớ đã đủ cao nhờ .btn ở trên. */
  .fc__card { min-height: 12rem; }

  /* Nút trái tim là mục tiêu bấm nhỏ nhất trên site — nới cho vừa đầu ngón tay */
  .heart { width: 2.75rem; height: 2.75rem; }

  /* Màn làm bài nghe: ô phiếu trả lời, nút ▶ của từng câu và nút chọn đáp án
     đều phải đạt cỡ đầu ngón tay (~44px). */
  .sheet__grid { grid-template-columns: repeat(auto-fill, minmax(2.75rem, 1fr)); }
  .sheet__cell { height: 2.75rem; }
  .q__play { width: 2.25rem; height: 2.25rem; }
  .opt { padding-block: 0.6875rem; }
  .opt input { width: 1.125rem; height: 1.125rem; }
  .mode input { width: 1.125rem; height: 1.125rem; }
}

/* Tôn trọng lựa chọn giảm chuyển động của người dùng */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}

/* ================================================= Admin › User Management */
.um-filters {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-block: 0.875rem 1.125rem;
}
.um-filter { display: flex; flex-direction: column; gap: 0.3rem; color: var(--ink-3); font-size: var(--fs-sm); }
.um-filter--search { flex: 1 1 19rem; }
.um-search { width: 100%; max-width: none; }
.um-select { width: auto; min-width: 9rem; max-width: 100%; }
.um-clear { flex-shrink: 0; }
.um-list-column[aria-busy="true"] { opacity: 0.68; }
.um-list-column[aria-busy="true"]::before { content: ""; display: block; width: 2.5rem; height: 2px; margin-bottom: -2px; background: var(--accent); animation: um-refresh 0.8s ease-in-out infinite alternate; }
@keyframes um-refresh { to { transform: translateX(5rem); } }
.um-skeleton { display: grid; gap: 0.625rem; min-height: 13rem; }
.um-skeleton span { display: block; height: 2.75rem; border-radius: var(--radius); background: var(--surface-2); animation: um-pulse 1.1s ease-in-out infinite alternate; }
@keyframes um-pulse { to { opacity: 0.45; } }
.um-load-error { margin-top: var(--gap-section); }
.um-container { width: min(var(--page-w), 94%); }
.um-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(22rem, 27%);
  align-items: start;
  gap: var(--gap-section);
}
.um-list-column { min-width: 0; transition: opacity 0.15s ease; }
.um-table-wrap { min-width: 0; }
.um-table { width: 100%; table-layout: fixed; border-collapse: collapse; }
.um-table th,
.um-table td {
  padding: 0.75rem 0.625rem;
  border-bottom: 1px solid var(--line);
  text-align: start;
  vertical-align: middle;
  overflow-wrap: anywhere;
}
.um-table th { color: var(--ink-3); font-size: var(--fs-sm); }
.um-table th:nth-child(1) { width: 43%; }
.um-table th:nth-child(2) { width: 16%; }
.um-table th:nth-child(3) { width: 18%; }
.um-table th:nth-child(4) { width: 19%; }
.um-table th:nth-child(5) { width: 4%; }
.um-table td:not(:first-child) { white-space: nowrap; }
.um-table tbody tr { cursor: pointer; transition: background 0.15s ease; }
.um-table tbody tr:hover { background: var(--surface-2); }
.um-table tbody tr:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.um-table tbody tr[aria-current="true"],
.um-card-list .rp-card[aria-current="true"] { background: var(--surface-2); box-shadow: inset 0 0 0 2px var(--accent); }
.um-card-list { display: none; }
.um-person { min-width: 0; }
.um-person strong, .um-person span {
  display: block;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.um-person span { margin-top: 0.2rem; color: var(--ink-3); font-size: var(--fs-sm); }
.um-chevron { color: var(--ink-3); font-size: var(--fs-xl); text-align: end !important; }
.um-detail-panel {
  position: sticky;
  top: calc(var(--gap-section) + 4rem);
  min-width: 0;
  max-height: calc(100vh - 6rem);
  overflow-y: auto;
  padding: 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.um-panel-close { display: none; margin-bottom: 1rem; }
.um-panel-prompt { margin: 0; color: var(--ink-3); text-align: center; padding-block: 3rem; }
.um-detail-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
.um-detail-head h1, .um-detail-head h2 { margin: 0; overflow-wrap: anywhere; }
.um-detail-email { margin: 0.3rem 0 0; color: var(--ink-3); overflow-wrap: anywhere; }
.um-role,
.um-badges {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
}
.um-role {
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--line-dark);
  border-radius: 999px;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  white-space: nowrap;
}
.um-role--admin { border-color: var(--accent); color: var(--accent-dark); }
.rp-status--user-active { border-color: var(--ok); color: var(--ok); }
.rp-status--user-disabled { border-color: var(--err); color: var(--err); }
.um-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 1rem;
}
.um-section { margin-top: var(--gap-section); }
.um-info, .um-identity-info { display: grid; grid-template-columns: minmax(7rem, 40%) minmax(0, 1fr); gap: 0.5rem 0.75rem; margin: 0.75rem 0 0; }
.um-info dt, .um-identity-info dt { color: var(--ink-3); }
.um-info dd, .um-identity-info dd { margin: 0; overflow-wrap: anywhere; }
.um-identities { display: flex; flex-direction: column; gap: 0.625rem; margin-top: 0.75rem; }
.um-identity { padding-block: 0.25rem 0.625rem; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
.um-identity-info { font-size: var(--fs-sm); }
.um-counts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.625rem;
  margin-top: 0.75rem;
}
.um-count {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
  padding: 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-align: center;
}
.um-count strong { font-size: var(--fs-xl); }
.um-count span { color: var(--ink-3); font-size: var(--fs-sm); overflow-wrap: anywhere; }
.um-actions { display: flex; flex-wrap: wrap; gap: 0.625rem; margin-top: 1rem; }
.um-action-message { margin-top: 1rem; }
.um-action-hint { margin: 0.75rem 0 0; color: var(--ink-3); font-size: var(--fs-sm); }
.um-admin-actions { position: sticky; bottom: 0; z-index: 1; background: var(--surface); }
.um-confirm { width: min(92%, 30rem); }
.um-confirm form { display: grid; gap: 1rem; }
.um-confirm h2, .um-confirm h3, .um-confirm p { margin: 0; }

/* ---- Admin › Retention (admin-retention.html) ----
   Hai ô số liệu, một donut "quay lại / chỉ ghé lần đầu", một bảng.
   Donut là dạng NHẤN MỘT NHÓM: "quay lại" mang màu vàng, nhóm còn lại xám —
   không phải bảng màu phân loại. Hai màu riêng cho từng mode vì be `--accent`
   quá nhạt để làm nét biểu đồ (đo bằng validate_palette của skill dataviz
   2026-09-13: ΔE giữa hai cung 19.2 ở dark · 20.0 ở light, mù màu vẫn ≥ 16.9.
   Cung xám dưới 3:1 trên nền ở cả hai mode — nên con số luôn in kèm ở chú
   giải và ở bảng; đừng bỏ hai chỗ đó đi mà giữ mỗi biểu đồ). */
:root {
  --rt-returned: #c38a33;
  --rt-once: #6f6353;
}
:root[data-theme="light"] {
  --rt-returned: #9a6a1c;
  --rt-once: #b9ab95;
}
.rt-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: var(--gap-section);
}
.rt-tile {
  min-width: 0;
  padding: 1rem 1.125rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.rt-tile__label { margin: 0; font-size: var(--fs-body); font-weight: 500; color: var(--ink-2); }
.rt-tile__value {
  margin: 0.25rem 0 0;
  font-size: 2.25rem;
  font-weight: 600;
  line-height: 1.15;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.rt-tile__hint { margin: 0.375rem 0 0; color: var(--ink-3); font-size: var(--fs-sm); overflow-wrap: anywhere; }
.rt-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
  align-items: start;
  gap: var(--gap-section);
  margin-top: var(--gap-section);
}
.rt-section-head { margin: 0 0 1rem; font-size: var(--fs-body); }
.rt-share { display: flex; flex-wrap: wrap; align-items: center; gap: 1.25rem 2rem; }
.rt-donut { flex: none; width: 10rem; height: 10rem; }
/* Cung xoay -90° để bắt đầu từ 12 giờ. Nét 14/120 bề ngang: đủ dày để bấm
   rê chuột trúng, không dày tới mức thành chiếc bánh. */
.rt-donut__seg {
  fill: none;
  stroke-width: 14;
  transform: rotate(-90deg);
  transform-origin: 60px 60px;
  transition: opacity 0.15s ease;
}
.rt-donut__seg--returned { stroke: var(--rt-returned); }
.rt-donut__seg--once { stroke: var(--rt-once); }
.rt-donut:hover .rt-donut__seg { opacity: 0.55; }
.rt-donut .rt-donut__seg:hover { opacity: 1; }
/* Chữ giữa donut dùng token chữ, không dùng màu của cung. */
.rt-donut__value { fill: var(--ink); font-size: 21px; font-weight: 600; text-anchor: middle; dominant-baseline: middle; }
.rt-donut__label { fill: var(--ink-3); font-size: 10px; text-anchor: middle; dominant-baseline: middle; }
.rt-legend { display: grid; gap: 0.75rem; min-width: 0; margin: 0; padding: 0; list-style: none; }
.rt-legend li { display: grid; grid-template-columns: 0.75rem minmax(0, 1fr); align-items: center; column-gap: 0.5rem; }
.rt-legend strong { font-weight: 600; overflow-wrap: anywhere; }
.rt-legend__value { grid-column: 2; color: var(--ink-2); font-size: var(--fs-md); font-variant-numeric: tabular-nums; }
.rt-swatch { width: 0.75rem; height: 0.75rem; border-radius: 3px; }
.rt-swatch--returned { background: var(--rt-returned); }
.rt-swatch--once { background: var(--rt-once); }
.rt-table-wrap { overflow-x: auto; }
.rt-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.rt-table th,
.rt-table td {
  padding: 0.625rem;
  border-bottom: 1px solid var(--line);
  text-align: end;
  white-space: nowrap;
}
.rt-table th:first-child,
.rt-table td:first-child { text-align: start; }
.rt-table thead th { color: var(--ink-3); font-size: var(--fs-sm); font-weight: 500; white-space: normal; }
.rt-table tbody th { font-weight: 400; }
.rt-table tfoot th,
.rt-table tfoot td { border-bottom: 0; font-weight: 600; }
.rt-note { max-width: 60rem; margin: var(--gap-section) 0 0; color: var(--ink-3); font-size: var(--fs-sm); }
/* Top người học nhiều bài nhất. `table-layout: fixed` + bề ngang cột cố định:
   tên và email là chữ người dùng tự đặt, để bảng tự co thì một email dài bóp
   các cột số (bài học 2026-08-19 ở User Management). Tràn thì cắt "…", chữ
   đầy đủ nằm ở `title`.
   Màn hẹp (< 48em) ẩn cột Email và đưa email xuống dưới tên: để bảng cuộn
   ngang thì ba cột số — thứ bảng này sinh ra để xem — bị đẩy khuất.
   Cột số hẹp chỉ chứa được TỪ dài ~48px, nên nhãn cột phải là từ ngắn (en:
   Total · Done · Active). "Completed" 68px, "progress" 55px từng lòi ra cột
   bên; bẻ giữa chữ bằng `overflow-wrap: anywhere` thì ra "Compl/eted". */
.rt-top { margin-top: var(--gap-section); }
.rt-top-table { table-layout: fixed; }
.rt-top-table th:nth-child(1) { width: 3rem; }
.rt-top-table th:nth-child(2) { width: 28%; }
.rt-top-table th:nth-child(3) { width: 32%; }
.rt-top-table tbody td:nth-child(3),
.rt-top__name,
.rt-top__email {
  overflow: hidden;
  text-align: start;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rt-top-table thead th:nth-child(2),
.rt-top-table thead th:nth-child(3) { text-align: start; }
/* Hangul mặc định gãy giữa hai âm tiết ("전체 수/업"); chỉ cho gãy ở dấu cách. */
.rt-top-table thead th { word-break: keep-all; }
.rt-top-table td:first-child { color: var(--ink-3); }
.rt-top-table td:nth-child(4) { font-weight: 600; }
.rt-top-table tbody td:nth-child(3) { color: var(--ink-2); }
.rt-top__name {
  display: block;
  color: var(--ink);
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-color: var(--ink-3);
}
.rt-top__name:hover { color: var(--ink); text-decoration-color: currentcolor; }
.rt-top__email { display: none; margin-top: 0.2rem; color: var(--ink-3); font-size: var(--fs-sm); }
.rt-top__hint { margin: 0.75rem 0 0; color: var(--ink-3); font-size: var(--fs-sm); }
@media (max-width: 47.999em) {
  .rt-top-table th,
  .rt-top-table td { padding-inline: 0.375rem; }
  .rt-top-table th:nth-child(1) { width: 1.75rem; }
  .rt-top-table th:nth-child(2) { width: auto; }
  .rt-top-table th:nth-child(n+4) { width: 3.5rem; }
  .rt-top-table th:nth-child(3),
  .rt-top-table td:nth-child(3) { display: none; }
  .rt-top__email { display: block; }
  /* Người không có email vẫn giữ đủ hai dòng, để các hàng cao bằng nhau. */
  .rt-top__email:empty::before { content: "\200b"; }
}

/* Ô số liệu BẤM ĐƯỢC — hai ô bài học mở popup danh sách (user-admin-panel.js).
   `<button>` mang sẵn font, màu và nền riêng của trình duyệt nên phải trả nó về
   đúng hình khối của mấy ô `<div>` bên cạnh, nếu không hai ô này lạc hẳn ra. */
.um-count--open {
  font: inherit;
  color: inherit;
  background: transparent;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.um-count--open:hover { border-color: var(--accent); background: var(--surface-2); }
.um-count--open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Popup "Bài đã và đang học". Hình khối mượn của `.um-confirm` (khung, nền và
   nút đóng do trình duyệt vẽ theo `color-scheme`, nên tự đúng cả 2 theme).
   Danh sách cuộn TRONG hộp: 57 bài mà để hộp tự cao thì nút Đóng bị đẩy khỏi
   màn hình. */
/* Căn giữa PHẢI tự làm: reset của site đặt `margin: 0` cho mọi thứ, thế là
   `margin: auto` trong stylesheet mặc định của trình duyệt — thứ vẫn căn giữa
   `<dialog>` — bị xoá, hộp dính lên góc trên bên trái. Cùng cách chữa với `.lp`
   và `.otp` ở trên. */
.um-lessons-dialog {
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(92%, 34rem);
  max-height: 90vh;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  padding: 1.25rem;
}
.um-lessons-dialog::backdrop { background: rgb(0 0 0 / .55); }
.um-lessons-dialog form { display: grid; gap: 1rem; }
.um-lessons-dialog h2, .um-lessons-dialog h3 { margin: 0; }
.um-lessons-dialog .confirm-dialog__actions { display: flex; justify-content: flex-end; }
.um-lessons { display: grid; gap: 1.25rem; max-height: min(60vh, 26rem); overflow-y: auto; }
.um-lessons-empty { margin: 0; color: var(--ink-3); }
.um-lesson-group__head { margin: 0 0 0.5rem; color: var(--ink-3); font-size: var(--fs-sm); }
.um-lesson-list { display: flex; flex-direction: column; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
.um-lesson {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--line);
  min-width: 0;
}
.um-lesson:last-child { padding-bottom: 0; border-bottom: 0; }
/* Tên bài là link thì phải TRÔNG như link NGAY, không đợi rê chuột: trong danh
   sách này có cả dòng bấm được lẫn dòng không (bài đã gỡ khỏi dữ liệu), mà màu
   `a` mặc định của site (`--ink-2`, gạch chân khi hover) thì hai loại nhìn y
   hệt nhau khi đứng yên. Dùng `--ink` + gạch chân chứ KHÔNG dùng `--accent`:
   ở bản sáng accent là #eecfa1, chữ màu đó trên nền trắng chỉ được 1.5:1. */
.um-lesson__title { overflow-wrap: anywhere; }
a.um-lesson__title {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-color: var(--ink-3);
}
a.um-lesson__title:hover { color: var(--ink); text-decoration-color: currentcolor; }
.um-lesson__meta { color: var(--ink-3); font-size: var(--fs-sm); overflow-wrap: anywhere; }

/* The persistent desktop panel must fit a standard 900px-tall viewport. */
@media (min-width: 90em) {
  #user-panel .um-detail-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.375rem 0.625rem;
    margin-bottom: 0.5rem;
  }
  #user-panel .um-detail-head h2 {
    line-height: 1.15;
    font-size: var(--fs-xl);
  }
  #user-panel .um-detail-email { margin-top: 0.15rem; font-size: var(--fs-sm); }
  #user-panel .um-badges { align-self: start; flex-direction: column; align-items: flex-end; }
  #user-panel .um-panel-content > section.panel {
    margin-top: 0;
    border: 0;
    border-top: 1px solid var(--line);
    border-radius: 0;
    background: transparent;
  }
  #user-panel .um-panel-content > section.panel > .panel__body { padding: 0.45rem 0; }
  #user-panel .um-panel-content > section.panel h3 { margin: 0; font-size: var(--fs-xl); }
  #user-panel .um-info {
    grid-template-columns: minmax(6.5rem, 38%) minmax(0, 1fr);
    gap: 0.2rem 0.5rem;
    margin-top: 0.35rem;
    font-size: var(--fs-sm);
  }
  #user-panel .um-counts {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.35rem;
    margin-top: 0.35rem;
  }
  #user-panel .um-count { gap: 0.1rem; padding: 0.35rem 0.2rem; }
  #user-panel .um-count strong { font-size: var(--fs-lg); }
  #user-panel .um-count span { line-height: 1.15; word-break: keep-all; }
  #user-panel #sessions { margin: 0.3rem 0 0; font-size: var(--fs-sm); }
  #user-panel .um-identities { gap: 0; margin-top: 0.25rem; }
  #user-panel .um-identity {
    display: grid;
    grid-template-columns: auto minmax(5rem, 1.5fr) repeat(2, minmax(4.5rem, 1fr));
    gap: 0.1rem 0.4rem;
    padding-block: 0.15rem 0.25rem;
    font-size: var(--fs-sm);
    line-height: 1.15;
  }
  #user-panel .um-identity > strong { grid-column: 1; min-width: 3.25rem; }
  #user-panel .um-identity-info {
    display: contents;
  }
  #user-panel .um-identity-field { min-width: 0; }
  #user-panel .um-identity-field--wide { grid-column: 2; }
  #user-panel .um-identity-field dt,
  #user-panel .um-identity-field dd { margin: 0; overflow-wrap: anywhere; }
  #user-panel .um-identity-field dt { font-size: 0.75em; line-height: 1; }
  #user-panel .um-actions { gap: 0.5rem; margin-top: 0.4rem; }
  #user-panel .um-action-message { margin-top: 0.4rem; }
  #user-panel .um-action-hint { margin-top: 0.3rem; line-height: 1.25; }
  #user-panel .um-admin-actions { position: static; }
}

@media (max-width: 89.999em) {
  .um-layout { display: block; }
  .um-detail-panel {
    position: fixed;
    z-index: 50;
    inset-block: 0;
    inset-inline: auto 0;
    width: min(32rem, 100%);
    max-height: none;
    padding: var(--gap-section);
    border-block: 0;
    border-inline-end: 0;
    border-radius: 0;
    transform: translateX(100%);
    visibility: hidden;
    transition: transform 0.22s ease, visibility 0s linear 0.22s;
  }
  .um-detail-panel.is-open {
    transform: translateX(0);
    visibility: visible;
    transition-delay: 0s;
  }
  .um-panel-close { display: inline-flex; margin-bottom: 1rem; }
  .um-panel-close { position: sticky; top: 0; z-index: 2; background: var(--surface); }
  body.um-panel-open { overflow: hidden; }
}

@media (max-width: 47.999em) {
  .um-table-wrap { display: none; }
  .um-card-list { display: flex; }
  .um-counts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .um-detail-panel { inset: 0; width: 100%; border: 0; }
  .um-filters { align-items: stretch; }
  .um-filter, .um-clear { flex: 1 1 100%; }
  .um-select { width: 100%; }
  .um-filters .um-search,
  .um-filters .um-select,
  .um-filters .um-clear { min-height: 44px; }
}

@media (max-width: 26.5em) {
  .um-counts { grid-template-columns: 1fr; }
  .um-actions { flex-direction: column; align-items: stretch; }
}

@media (hover: none) {
  .um-pagination .btn, .um-filters .btn, .um-filters input, .um-filters select { min-height: 44px; }
  /* Nút To-do ở hộp thư admin: trên máy cảm ứng nó đứng sát thẻ bài, nên phải
     đủ rộng cho ngón tay (docs/ui.md mục 6). Bề cao đã do `stretch` lo. */
  .rp-todo-btn { min-width: 44px; }
}

/* ------------------------------------------------ My Pocket › My Grammar

   Toàn bộ màu lấy từ token ở :root, KHÔNG gõ hex tại chỗ. Site có cả mode
   sáng (:root[data-theme="light"]) nên một mã màu cứng ở đây là một vệt tối
   nằm giữa nền be khi người dùng bật mode sáng. Bảng tối ấm mà thiết kế mô tả
   đã có sẵn trong token: --bg #17130e, --surface #241e17, --accent-dark
   #e6c795, --ink-2 #c2b3a0.
   ------------------------------------------------------------------------ */

/* Hàng thống kê đầu trang (số đã lưu / đã thuộc / cần ôn + thanh tiến độ % +
   hai nút "Ôn ngay"/"Tùy chỉnh") đã BỎ HẲN — chủ dự án chốt 2026-09-09. Số mẫu
   vẫn hiện ở .file-count trên page-head, còn hai cái nút chỉ lặp lại đúng hai
   nút mà tab "Luyện tập" ngay bên dưới đã có. */

/* Ô "Sắp xếp" (.sortdrop) đã bỏ hẳn — chủ dự án chốt 2026-09-09. Thứ tự danh
   sách giờ là một luật cố định trong sortEntries(), không còn gì để chọn. */

/* Nhóm lọc trình độ mượn nguyên `.seg-btns` của My Vocab, chỉ nới thêm đúng
   hai điều — và nới Ở ĐÂY theo id chứ KHÔNG sửa rule gốc `.seg-btns`, vì
   Dictation Details Transcript và My Vocab đang dùng chung nó.
   Vì sao phải nới: My Vocab nhét 2 nút vào khối này, My Grammar có 4, mà
   `.seg-btns` khai `flex-shrink: 0` nên nút không co được. Đo ở khung nhìn
   320px: bốn nút cộng lại 344px trong khung nội dung 294px — cả trang tràn
   ngang 355px và "Cao cấp" bị `overflow: hidden` cắt mất. Cho xuống dòng thì
   hết tràn, còn `flex: 1 1 auto` để hai nút của hàng dưới giãn ra cho đầy,
   không để hở một mảng nền trong khung viền. */
#grammar-levels {
  flex-wrap: wrap;
  max-inline-size: 100%;
}
#grammar-levels button { flex: 1 1 auto; }

/* Căn ĐÁY chứ không căn giữa như `.list-view` mặc định: khối `.seg-btns` có
   viền 1px trên + dưới nên cao hơn cặp nút chữ trơn bên phải 2.4px, căn giữa
   thì mép dưới hai bên lệch nhau 1.2px — đủ để nhìn ra là hàng không thẳng.
   Nới Ở ĐÂY theo id, KHÔNG sửa `.list-view`: My Vocab và Dictation List dùng
   chung rule đó và ở hai màn ấy căn giữa đang đúng. */
#bulk-row { align-items: flex-end; }

/* ---------------------------------------- Thẻ ngữ pháp */

/* HAI cột (chủ dự án chốt 2026-09-09). Bản trước để MỘT cột vì hàng thu gọn
   còn mang huy hiệu trình độ + nút trạng thái — cụm đó chiếm 172px cố định, chia
   đôi cột thì phần nghĩa chỉ còn 175px và bị cắt ngang. Cả cụm đã dời vào trong
   thẻ, hàng thu gọn giờ chỉ có mẫu + nghĩa + mũi tên, nên nửa cột vẫn đủ chỗ và
   một màn hiện được gấp đôi số mẫu.
   Thẻ hẹp đi thì hàng thu gọn tự xếp dọc — xem @container grammarcard bên dưới. */
/* Khoảng cách và mốc chia cột SAO Y `.wordlist` của My Vocab (chủ dự án chốt
   2026-09-09): một cột mặc định, hai cột từ 64em, hàng cách nhau 0.5rem còn cột
   cách nhau `--col-gap`.
   ⚠ Khe CỘT là 2% chứ không phải rem — đó là quy ước bề ngang bố cục của site
   (docs/ui.md), nên phải dùng `var(--col-gap)`, đừng quy ra rem cho "gọn".
   Nhưng khe HÀNG thì bắt buộc là rem: % trong `gap` tính theo bề ngang phần tử,
   nên ở cột hẹp 2% teo lại còn vài px, còn ở màn rộng thì phình ra — cùng cái
   bẫy đã ghi ở `.exam` (dòng ~2870). */
.grammar-library {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* `start` chứ không để mặc định `stretch`: mở một thẻ là hàng lưới cao bằng
     thẻ đó, mà `stretch` sẽ kéo thẻ BÊN CẠNH cao theo — người dùng thấy một
     thẻ rỗng cao ngoẵng chứ không phải khoảng trống. */
  align-items: start;
  gap: 0.5rem;
  max-width: none;
}
@media (min-width: 64em) {
  .grammar-library {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem var(--col-gap);
  }
}

/* ⚠ `container` đặt trên CHÍNH THẺ, không phải `.grammar-library` bao ngoài —
   cùng cái bẫy đã ghi ở `.word-card`: query trên danh sách thì đo bề ngang cả
   lưới hai cột, tức không bao giờ khớp dù mỗi thẻ đã chật một nửa. */
.grammar-card {
  container: grammarcard / inline-size;
  min-width: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  transition: border-color 180ms ease, background 180ms ease;
}
.grammar-card:hover { border-color: var(--accent-soft); }
.grammar-card.is-open { border-color: var(--line-dark); }
/* Thẻ đã thuộc: viền xanh, dùng chung cách đánh dấu với `.word-card.is-known`
   bên My Vocab. Khai SAU `.is-open` để mở thẻ ra vẫn giữ được viền xanh —
   hai class cùng độ ưu tiên, cái viết sau thắng. */
.grammar-card.is-known { border-color: var(--ok); }

/* Cả hàng thu gọn LÀ một cái nút. Bản trước phải tách thành hai nút anh em vì
   nút trạng thái đứng cạnh mà <button> không được lồng <button>; nút đó đã dời
   vào trong thẻ nên cấu trúc trở lại đơn giản. */
.grammar-card__toggle {
  inline-size: 100%;
  min-width: 0;
  display: grid;
  /* Mẫu tiếng Hàn ăn 3/4 hàng, nghĩa 1/4 (chủ dự án chốt 2026-09-09): mẫu là
     thứ người học quét để tìm, nghĩa chỉ để xác nhận "đúng cái này rồi". Bản
     trước chia gần ngược lại (0.9fr / 2fr) nên mẫu bị bẻ dòng trong khi nghĩa
     thừa chỗ. */
  grid-template-columns: minmax(0, 3fr) minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.875rem;
  /* Đệm 0.75rem ĐỀU BỐN PHÍA để thẻ ngữ pháp cao đúng bằng thẻ từ của My Vocab
     (chủ dự án chốt 2026-09-09). Đo ở root 18px: hai màn có phần nội dung y hệt
     nhau (34.2px — cùng cỡ chữ, cùng line-height), nên chênh lệch chiều cao
     TRƯỚC ĐÂY chỉ đến từ đệm dọc: vocab 13.5+13.5 cho thẻ 63.2px, còn ở đây
     9+9 cho thẻ 54.2px. Con số 0.75rem lấy thẳng của `.wordlist--flat
     .word-card` — sửa bên đó thì phải sửa kèm ở đây. */
  padding: 0.75rem;
  border: 0;
  background: transparent;
  color: inherit;
  text-align: start;
  cursor: pointer;
}
.grammar-card__toggle:hover { background: var(--surface-2); }
.grammar-card__toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

.grammar-card__title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.5rem; }
.grammar-card__pattern {
  /* Cỡ và độ đậm SAO Y từ tiếng Hàn của My Vocab — hai màn là hai nửa cùng một
     túi, chữ Hàn phải cùng một cỡ.
     ⚠ Chép từ `.word-card__term` là CHÉP HỤT: rule gốc đó khai --fs-2xl, nhưng
     kiểu xem MẶC ĐỊNH của My Vocab (danh sách phẳng) hạ xuống --fs-xl bằng
     `.wordlist--flat .word-card__term` — phải bám bản --fs-xl thì hai màn mới
     bằng nhau. Đo tận nơi 2026-09-09 ở 1440px: cả hai ra 21.375px.
     ⚠ Root font-size của site là 18px, KHÔNG phải 16px. Nhẩm --fs-xl + --fs-bump
     ra "19px" là nhẩm theo root 16px rồi kết luận sai; đo bằng
     getComputedStyle() chứ đừng tính tay.
     MÀU thì lấy của .lesson__title bên Dictation List (--ink): bản cũ tô
     --accent-dark, mà đó là màu sinh ra để TÔ NỀN, viết chữ bằng nó ở light
     mode chỉ được ~1.9:1 (xem ghi chú cạnh --accent-ink). */
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
  /* `anywhere` cắt giữa từ tiếng Hàn: "N + 이/가 아니에요" xuống dòng thành
     "아니 / 에요". Luật chung của file là keep-all + break-word (xem
     .lesson__title) — ngắt ở dấu cách và dấu "/", chỉ bẻ từ khi không còn cách. */
  word-break: keep-all;
  overflow-wrap: break-word;
}
.grammar-card__toggle:hover .grammar-card__pattern { color: var(--accent-dark); }

/* Nhãn trình độ dùng CHUNG .tag với màn Dictation List — chỉ viền mảnh + chữ
   màu, nền trong suốt. Bộ .badge--level cũ tô nền đặc (--ok-soft/--warn-soft)
   nên mỗi hàng mang thêm một mảng màu; hai màn cùng nói một điều thì phải nhìn
   giống nhau, và .tag đã có sẵn đúng ba cấp beginner/intermediate/advanced. */
.grammar-card__title .tag { flex: none; }

/* Nghĩa khoá đúng HAI DÒNG khi thu gọn, cùng lý do và cùng cách làm với
   .lesson__summary: giải thích trong grammar.json dài 45-169 ký tự, thả tự do
   thì thẻ cao 160-234px so le nhau và phần giải thích át cả mẫu ngữ pháp. Bản
   đầy đủ nằm ngay dưới, mở thẻ ra là thấy — không mất chữ nào. */
/* MỘT dòng, không phải hai: hàng thu gọn giờ chỉ mang NGHĨA ngắn (câu đầu của
   giải thích, trung vị 68 ký tự), còn bản đầy đủ đã dời vào trong thẻ. Khoá một
   dòng thì mọi hàng cao bằng nhau và mẫu ngữ pháp giữ được vị trí áp đảo. */
/* Không còn vạch --accent-soft ngăn giữa mẫu và nghĩa: lưới đã tách hai cột
   rồi, thêm một vạch màu trên MỖI hàng chỉ là màu thừa. Màu và cỡ chữ lấy đúng
   của .lesson__summary bên Dictation List. */
.grammar-card__meaning {
  --meaning-lines: 1;
  /* Nhích 15px sang trái (chủ dự án chốt 2026-09-09) — lấn vào khe 14px của
     lưới để nghĩa bám sát mẫu hơn, đổi lại được thêm chừng đó bề ngang cho chữ.
     Cố ý dùng margin ÂM chứ không thu `gap`: `gap` là khe chung, thu nó thì
     mũi tên bên phải cũng dính vào nghĩa theo.
     Chỉ đúng ở bố cục NGANG; khi thẻ hẹp và nghĩa xuống hàng riêng thì margin
     này lại đẩy chữ lòi khỏi lề thẻ, nên @container bên dưới trả nó về 0. */
  margin-inline-start: -0.9375rem;
  color: var(--ink-3);
  font-size: var(--fs-sm);
  line-height: 1.45;
  min-height: calc(var(--meaning-lines) * 1.45em);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--meaning-lines);
  overflow: hidden;
}
/* Mở thẻ ra KHÔNG bung nghĩa ngắn nữa: bản đầy đủ đã nằm ngay dưới trong thẻ,
   bung ra là đọc hai lần cùng một câu. */

.grammar-card__chevron { display: flex; color: var(--ink-3); }
.grammar-card__chevron svg { inline-size: 1.25rem; block-size: 1.25rem; transition: transform 180ms ease; }
.grammar-card.is-open .grammar-card__chevron svg { transform: rotate(180deg); }

/* ---------------------------------------- Phần mở ra */

/* Trình độ + trạng thái mở đầu phần bung ra, đúng chỗ My Vocab đặt từ loại.
   --ink-2 chứ không --ink-3, cùng lý do đã ghi ở .word-card__pos: --ink-3 ở
   light mode chỉ đạt 2.99 trong khi mọi chữ khác trong thẻ đều ≥ 6.46. */
.grammar-card__tags {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.375rem 0.5rem;
  margin: 0 0 0.5rem;
}
.grammar-card__level { font-size: var(--fs-md); color: var(--ink-2); }

/* Trượt mở/đóng — cùng cơ chế và cùng nhịp với `.word-card__panel` của My Vocab.
   Hai điều kiện để nó chạy được, thiếu một là thẻ bung ra một phát:
   1. my-grammar.js phải đổi class TẠI CHỖ, không được `renderList()` — vẽ lại
      là thẻ sinh ra đã mang sẵn `is-open`, không có trạng thái trước để chuyển.
   2. `padding` DỌC phải chạy theo hiệu ứng, và ở trạng thái đóng phải bằng 0.
      ⚠ Để padding cố định là SAI, đã đo 2026-09-09: `overflow: hidden` cắt nội
      dung nhưng KHÔNG cắt padding của chính cái hộp đó, nên thẻ đóng vẫn cao
      thêm đúng 20.2px và hiệu ứng trượt coi như không thấy. Còn nếu chỉ khai
      padding trong nhánh `.is-open` thì lúc đóng nó biến mất tức thì rồi hàng
      mới trượt — cũng giật. Cho nó cùng transition với hàng thì cả hai chiều
      đều mượt. Padding NGANG giữ cố định, không liên quan. */
.grammar-card__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .24s ease; }
.grammar-card.is-open .grammar-card__panel { grid-template-rows: 1fr; }
.grammar-card__inner {
  min-height: 0;
  overflow: hidden;
  visibility: hidden;
  padding: 0 0.75rem;
  /* Ẩn hẳn SAU khi trượt xong (không thì nội dung mất trước, hàng trượt sau),
     và ẩn bằng `visibility` để phần đóng lại rơi khỏi thứ tự Tab. */
  transition: visibility 0s .24s, padding .24s ease;
}
.grammar-card.is-open .grammar-card__inner {
  visibility: visible;
  padding: 0.25rem 0.75rem 0.875rem;
  transition: visibility 0s, padding .24s ease;
}

/* Cho rê chuột bôi đen và chép chữ trên thẻ. Hàng thu gọn là một <button>, mà
   UA stylesheet của WebKit/Blink đặt `user-select: none` cho button — nên mẫu
   ngữ pháp và nghĩa không chép ra được. Cú click ngay sau khi bôi đen đã được
   chặn trong my-grammar.js, không thì chọn chữ xong là thẻ bật ra/đóng sập. */
.grammar-card,
.grammar-card__toggle { -webkit-user-select: text; user-select: text; }
/* Mũi tên là nét vẽ, bôi đen trúng nó chỉ tổ dính rác vào chỗ copy. */
.grammar-card__chevron { -webkit-user-select: none; user-select: none; }
.grammar-card__detail {
  margin: 0 0 0.625rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field);
  color: var(--ink-2);
  font-size: var(--fs-lg);
  line-height: 1.55;
}
.grammar-card__wait { margin: 0; color: var(--ink-3); font-size: var(--fs-sm); }
.grammar-card__examples { display: grid; gap: 0.5rem; }

.grammar-example {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: 0.5rem 0.75rem;
  padding: 0.625rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
}
.grammar-example__body { min-width: 0; }
.grammar-example__ko {
  margin: 0;
  font-size: calc(var(--fs-body) + var(--fs-bump));
  line-height: 1.6;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* Chỗ mang ngữ pháp — cùng màu nhấn với mẫu ở hàng đầu, để mắt nối được
   "mẫu này" với "chỗ này trong câu". */
.grammar-example__hit {
  padding: 0.05em 0.2em;
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-weight: 800;
}
.grammar-example__vi { margin: 0.3rem 0 0; color: var(--ink-2); line-height: 1.5; }
.grammar-example__from {
  grid-column: 1 / -1;
  font-size: var(--fs-sm);
  color: var(--ink-3);
}
.grammar-example__from:hover { color: var(--accent-dark); }

.grammar-example__play {
  grid-column: 2;
  grid-row: 1;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  border: 1px solid var(--line-dark);
  border-radius: 50%;
  background: var(--field);
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color 160ms ease, color 160ms ease, background 160ms ease;
}
.grammar-example__play:hover { border-color: var(--accent); color: var(--accent-dark); }
.grammar-example__play:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.grammar-example__play .icon { inline-size: 1.125rem; block-size: 1.125rem; }
.grammar-example__play.is-playing {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-dark);
}
/* Sóng âm chỉ hiện khi đang phát — nút đứng yên thì chỉ còn cái loa, gọn hơn. */
.grammar-example__wave { opacity: 0; transition: opacity 160ms ease; }
.grammar-example__play.is-playing .grammar-example__wave { opacity: 1; }

/* Bố cục chân thẻ (.grammar-card__foot/__meta/__stat/__actions/__remove) khai
   chung với .word-card__* ở phần My Vocab — tìm "Chân thẻ của My Vocab". */

/* ---------------------------------------- Tab Flashcard

   Thẻ lật dùng NGUYÊN khuôn `.fc*` của My Vocab (assets/js/flashcard.js), chỉ
   chỉnh ba chỗ riêng của ngữ pháp. */

/* Khe với thanh tab lấy đúng 1.125rem như hai tab kia (`#practice-setup`,
   `#view-list .filters`), để đổi qua lại ba tab không thấy nội dung nhích.
   `.fc` gốc để 0.875rem vì bên My Vocab còn hàng chip chủ đề nằm giữa. */
#view-flashcard > .fc { margin-top: 1.125rem; }

/* Mẫu ngữ pháp dài hơn từ vựng nhiều: trung vị 10 ký tự, p90 22, dài nhất 53
   (g-0074, đo 2026-09-15) — từ vựng chỉ vài âm tiết. Giữ cỡ chữ của thẻ từ
   (`--fs-3xl` + 3 nấc bump) thì mẫu dài nhất xuống 4 dòng ở 320px và mặt sau đẩy
   cặp nút Đã nhớ/Chưa nhớ ra khỏi màn. Bớt MỘT nấc bump, vẫn là chữ to nhất
   thẻ. */
.fc__term--grammar { font-size: calc(var(--fs-3xl) + var(--fs-bump) * 2); }

/* Ba tab ở màn hẹp. Luật chung `.tabs .tab { flex: 1 }` chia đều bề ngang, mà
   với ba tab và padding 1rem mỗi bên thì ở 320px lòng tab chỉ còn ~60px — nhãn
   "Ngữ pháp của tôi" vỡ ba dòng, cả hàng tab cao 85px. Nới padding còn 0.5rem
   thì nhãn dài nhất xuống tối đa hai dòng (cao 62px). Chỉ áp cho hàng tab của
   My Grammar (`.tabs--three`), My Vocab hai tab vẫn đủ chỗ. */
@media (max-width: 38.75em) {
  .tabs--three .tab { padding-inline: 0.5rem; }
}

/* ---------------------------------------- Khung câu hỏi ôn ngữ pháp */
/* Chỉ phần LÀM bài (#practice-body, khung .drill) to hơn phần còn lại của site
   ĐÚNG 2px (chủ dự án chốt 2026-09-23): ở đây người học đọc câu tiếng Hàn và
   gõ lại, không phải liếc một cái nhãn. Màn CHỌN ngữ pháp/hình thức
   (#practice-setup) giữ nguyên cỡ chữ gốc — chốt lại 2026-09-23, ban đầu từng
   áp cho cả #view-practice rồi thu hẹp vì màn chọn không cần to hơn.
   Khai lại chính các biến cỡ chữ thay vì sửa từng rule — mọi con chữ bên trong
   #practice-body đều đi qua các biến này, kể cả nút và khung hiện ngữ pháp.
   0.125rem = 2px ở cỡ chữ gốc 16px; để rem nên nó co giãn cùng `html` (clamp
   93.75%–112.5%), tức 1.9–2.25px tuỳ bề ngang — cùng lối với `--fs-bump`. */
#practice-body {
  --fs-2xs: calc(0.71875rem + 0.125rem);
  --fs-xs: calc(0.75rem + 0.125rem);
  --fs-sm: calc(0.78125rem + 0.125rem);
  --fs-md: calc(0.8125rem + 0.125rem);
  --fs-lg: calc(0.84375rem + 0.125rem);
  --fs-xl: calc(0.875rem + 0.125rem);
  --fs-body: calc(0.9375rem + 0.125rem);
  --fs-2xl: calc(1rem + 0.125rem);
  --fs-3xl: calc(1.0625rem + 0.125rem);
}
.drill {
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 1.25rem;
}
.drill__head {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: .625rem;
}
.drill__nav { display: inline-flex; align-items: center; gap: .375rem; }
/* Canh lề theo CLASS RIÊNG, đừng dùng :first-child/:last-child. Khung này đã có
   thêm nút "Hiện ngữ pháp" và ô ngữ pháp ở hàng dưới (2026-09-16), nên "con
   cuối" không còn là nút Câu sau — nó mất `justify-self` và giãn hết cột 1fr,
   thành một cái nút dài ngoằng. */
.drill__nav--prev { justify-self: start; }
.drill__nav--next { justify-self: end; }
.drill__mode { margin: 0; text-align: center; font-size: var(--fs-sm); color: var(--ink-3); }
.drill__count { display: block; margin-top: .25rem; }
/* Nút hiện/ẩn ngữ pháp rơi xuống hàng hai của lưới `.drill__head`, thẳng dưới
   nút "Câu trước". Nút và ô nội dung gói chung một hàng flex chiếm trọn bề
   ngang, để ô bám ngay sau nút (cách 20px) chứ không bắt đầu từ cột giữa của
   lưới như trước (chủ dự án chốt 2026-09-22). */
.drill__reveal-row {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 20px;
  min-width: 0;
  margin-top: .5rem;
}
.drill__reveal { flex: none; }
.drill__grammar {
  /* NGANG HÀNG với nút hiện/ẩn, không xuống dòng riêng: một dòng duy nhất, cắt
     đuôi bằng ellipsis. Nhờ vậy bấm hiện/ẩn KHÔNG làm cao thấp đổi, cả khung
     bên dưới đứng yên (chủ dự án chốt 2026-09-16). Chữ bị cắt vẫn đọc được đủ
     qua `title` — xem renderDrill(). Không viền, không nền (2026-09-22). */
  flex: 1 1 auto;
  /* `min-width: 0` là BẮT BUỘC: ô để `nowrap`, mà phần tử flex mặc định không co
     nhỏ hơn nội dung — thiếu dòng này thì câu dài đẩy tràn ngang ở 320px. */
  min-width: 0;
  margin: 0;
  color: var(--ink-2);
  font-size: var(--fs-md);
  line-height: 1.6;
  text-align: start;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Khai lại cho chắc: luật class ở trên có `display` thì nó đè mất thuộc tính
   `hidden` của HTML — đúng cái bẫy đã ghi ở CLAUDE.md mục "kiểm cái người dùng
   nhìn thấy". Ở đây chưa khai `display`, nhưng ai thêm sau sẽ dính. */
.drill__grammar[hidden] { display: none; }
.drill__grammar-pattern { color: var(--ink); }
.drill__question { margin-top: 1.75rem; text-align: center; }
.drill__prompt { margin: 0 0 .75rem; color: var(--ink-3); font-size: var(--fs-sm); }
.drill__sentence {
  margin: 0;
  color: var(--ink);
  font-size: calc(var(--fs-2xl) + var(--fs-bump) * 2);
  font-weight: 700;
  line-height: 1.65;
  word-break: keep-all;
  overflow-wrap: break-word;
}
.drill__hit {
  color: var(--ink);
  background: var(--accent-soft);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: .25em;
  border-radius: .2rem;
}
.drill__answers { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .625rem; margin-top: 1.75rem; }
.drill-option {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: 1rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-lg);
  line-height: 1.5;
  text-align: start;
  cursor: pointer;
}
.drill-option__key { flex-shrink: 0; color: var(--ink-3); font-size: var(--fs-sm); font-weight: 700; }
.drill-option__text { min-width: 0; word-break: keep-all; overflow-wrap: break-word; }
.drill-option:hover:not([aria-disabled="true"]) { border-color: var(--accent); background: var(--accent-soft); }
.drill-option:focus-visible { outline: .125rem solid var(--accent); outline-offset: .1875rem; }
.drill-option[aria-disabled="true"] { cursor: default; }
.drill-option.is-correct { border-color: var(--ok-vivid); background: var(--ok-soft); }
.drill-option.is-wrong { border-color: var(--err-vivid); background: var(--err-soft); }
@media (max-width: 38.75em) {
  .drill { padding: .875rem; }
  .drill__head { grid-template-columns: auto minmax(0, 1fr) auto; gap: .375rem; }
  .drill__answers { grid-template-columns: minmax(0, 1fr); }
}

/* Trả lời viết: dùng chung ô gõ, compose có vùng viết và câu mẫu riêng. */
.drill__translation { margin: .75rem 0 0; color: var(--ink-3); font-size: var(--fs-md); line-height: 1.6; }
/* Bài sắp xếp: câu dịch là ĐỀ BÀI chứ không phải chú thích, nên to và đậm
   đúng bằng câu đề của bài "tự dựng câu". */
.drill__translation--lead {
  color: var(--ink);
  font-size: calc(var(--fs-2xl) + var(--fs-bump) * 2);
  font-weight: 700;
  line-height: 1.65;
  word-break: keep-all;
  overflow-wrap: break-word;
}
.drill__formula { display: flex; flex-wrap: wrap; justify-content: center; align-items: baseline; gap: .75rem; }
.drill__plus { color: var(--ink-3); font-weight: 400; }
.drill__pattern { font-size: calc(var(--fs-xl) + var(--fs-bump)); }
.drill__blank {
  display: inline-block;
  inline-size: var(--blank-size);
  max-inline-size: 100%;
  block-size: 1.3em;
  margin-inline: .2em;
  vertical-align: -.2em;
  border-bottom: .125rem solid var(--accent);
  border-radius: .2rem .2rem 0 0;
  background: var(--accent-soft);
}
.drill__hints { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .5rem; margin-top: 1rem; }
/* Hàng gợi ý thứ hai (ngữ pháp) đi ngay dưới hàng từ vựng, không cách ra như
   một khối mới. */
.drill__hints + .drill__hints { margin-top: .5rem; }
/* Nút "Làm lại": nằm trong lưới đáp án (chọn nghĩa) hay trong khối gõ (điền,
   chia từ) đều phải ra giữa, nên căn bằng margin auto chứ không justify-self. */
.drill__retry { grid-column: 1 / -1; display: block; margin: .75rem auto 0; }
/* Đúng/sai phải nói ra bằng CHỮ, không chỉ bằng màu ô đáp án — người không phân
   biệt được màu vẫn phải biết mình vừa làm đúng hay sai. */
/* Ô tick "Xáo câu" đứng cùng hàng với hai nút của màn ôn. */
.practice-bar__toggle {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  color: var(--ink-2);
  font-size: var(--fs-sm);
  white-space: nowrap;
  cursor: pointer;
}
.practice-bar__toggle input { accent-color: var(--accent); cursor: pointer; }
.drill__verdict { grid-column: 1 / -1; text-align: center; font-weight: 600; }
/* Bài sắp xếp: hàng trên là kho cụm chưa dùng, hàng dưới là câu đang xếp. */
.drill__arrange { grid-column: 1 / -1; min-width: 0; }
.drill__line {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: .5rem;
  /* Chiều cao sàn do `.drill__line-spacer` giữ, không phải `min-block-size`. */
  padding: .625rem;
  border: 1px dashed var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
}
.drill__line.is-correct { border-style: solid; border-color: var(--ok-vivid); background: var(--ok-soft); }
.drill__line.is-wrong { border-style: solid; border-color: var(--err-vivid); background: var(--err-soft); }
.drill__bank { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem; margin-bottom: .875rem; }
.drill__chunk {
  padding: .5rem .875rem;
  /* Viền dày, nét liền, màu nhấn: mấy cụm này là thứ phải BẤM, mà viền 1px xám
     mảnh thì nhìn ra như chữ trang trí. */
  border: 2px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-lg);
  line-height: 1.5;
  word-break: keep-all;
  cursor: pointer;
  /* Kéo cụm đổi chỗ dịch cả hàng ngay lập tức (thứ tự DOM đổi) — không có
     transition thì mắt chỉ thấy các cụm NHẢY chỗ, không phân biệt được đang
     kéo qua hay kéo về. Chuyển động mượt cho cảm giác "lướt qua nhau". */
  transition: transform .15s ease, background-color .15s ease,
    border-color .15s ease, opacity .15s ease;
}
.drill__chunk:hover:not(:disabled) { background: var(--accent-soft); }
.drill__chunk:disabled { cursor: default; }
.drill__chunk--placed { background: var(--accent-soft); border-color: var(--accent); }
/* Cụm đang nằm DƯỚI con trỏ lúc kéo — tức chỗ sẽ đổi vào nếu thả ra ngay bây
   giờ. Phải RÕ hơn hẳn viền màu nhấn mặc định của `--placed`, nên đổi cả nền
   lẫn viền sang tông đậm và nhích lên một chút, không chỉ đổi màu suông. */
.drill__chunk--dragover {
  background: var(--accent-dark);
  border-color: var(--accent-dark);
  color: var(--on-accent);
  transform: translateY(-.1875rem) scale(1.05);
}
/* Cụm đã bấm: vẫn nằm nguyên chỗ trong kho để hàng không co lại, nhưng mờ đi
   cho khỏi lẫn với cụm còn dùng được. Dùng `opacity` chứ không đổi màu chữ —
   phải thấy được là ô ĐÓ đã dùng rồi, chứ không phải một ô trống. */
.drill__chunk.is-used { border-color: var(--line-dark); background: transparent; opacity: .4; }
/* Miếng chèn giữ chiều cao cho ô đáp án — xem ghi chú ở phần dựng ô. Rộng 0,
   không bắt chuột, không chiếm khe `gap` của hàng cụm. */
.drill__line-spacer {
  inline-size: 0;
  padding-inline: 0;
  border-inline-width: 0;
  margin-inline-end: -.5rem;
  visibility: hidden;
  pointer-events: none;
}
/* Chỗ của nút "Xếp lại" luôn được giữ, chỉ ẩn phần nhìn thấy — xem paint(). */
.drill__retry.is-blank { visibility: hidden; }
/* Cụm đã xếp thì KÉO được để đổi chỗ. `touch-action: none` là bắt buộc: thiếu
   nó thì trên điện thoại cú kéo bị hiểu thành cuộn trang. */
.drill__chunk--placed:not(:disabled) { cursor: grab; touch-action: none; }
.drill__chunk.is-dragging { opacity: .55; cursor: grabbing; }
.drill__verdict.is-correct { color: var(--ok-vivid); }
.drill__verdict.is-wrong { color: var(--err-vivid); }
.drill__hint-label { color: var(--ink-3); font-size: var(--fs-sm); }
.drill__hint { padding: .25rem .625rem; border: 1px solid var(--line-dark); border-radius: var(--radius); color: var(--ink-2); background: var(--field); font-size: var(--fs-md); word-break: keep-all; overflow-wrap: break-word; }
.drill__written { grid-column: 1 / -1; min-width: 0; width: 100%; text-align: center; }
.drill__input-row { display: flex; justify-content: center; align-items: center; gap: .625rem; }
.drill__input {
  box-sizing: border-box;
  min-width: 0;
  width: 100%;
  max-width: 28rem;
  padding: .75rem 1rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--field);
  color: var(--ink);
  font: inherit;
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  line-height: 1.6;
}
.drill__input::placeholder { color: var(--ink-3); font-size: var(--fs-md); }
.drill__input:focus-visible { outline: .125rem solid var(--accent); outline-offset: .1875rem; }
.drill__input.is-correct { border-color: var(--ok-vivid); }
.drill__input.is-wrong { border-color: var(--err-vivid); }
/* Ô gõ chữ khớp `:focus-visible` cả khi bấm chuột, nên đã chấm xong mà bấm lại
   vào ô thì vòng vàng đè lên viền xanh/đỏ. Vòng focus lấy luôn màu kết quả:
   vẫn thấy ô đang được chọn, nhưng không mất màu đúng/sai (2026-09-22). */
.drill__input.is-correct:focus-visible { outline-color: var(--ok-vivid); }
.drill__input.is-wrong:focus-visible { outline-color: var(--err-vivid); }
.drill__input-row > .btn { flex-shrink: 0; }
.drill__feedback { margin: .75rem 0 0; font-size: var(--fs-md); color: var(--ink-2); line-height: 1.6; }
.drill__feedback:empty { margin: 0; }
/* "Chính xác!" / "Kiểm tra lại nha!" của bài gõ (chia từ/điền chỗ trống/viết
   câu) và bài sắp xếp — đậm + xanh hoặc đỏ như dòng phán đúng/sai của bài
   chọn đáp án (`.drill__verdict.is-correct`/`.is-wrong`), để cùng một tín
   hiệu nhất quán giữa các hình thức (chủ dự án chốt 2026-09-23). */
.drill__correct-text { color: var(--ok-vivid); font-weight: 600; }
.drill__wrong-text { color: var(--err-vivid); font-weight: 600; }
/* Bài sắp xếp: chừa sẵn HAI dòng cho lời báo đúng/sai. Cú bấm cuối cùng là cú
   vừa xếp nốt cụm cuối vừa hiện lời báo — không chừa chỗ thì đúng lúc đó nút
   "Xếp lại" bị đẩy xuống. Hai dòng vì lời báo lúc nào cũng hai dòng: xếp đúng
   thì thêm dòng đáp án tham khảo, còn ở bề ngang điện thoại thì lời báo sai tự
   xuống dòng. `.375rem` là khoảng cách giữa hai dòng đó, xem `.drill__why`. */
.drill__arrange .drill__feedback,
.drill__arrange .drill__feedback:empty { margin: .75rem 0 0; min-block-size: calc(3.2em + .1875rem); }
/* Dòng đáp án tham khảo nằm sát ngay dưới lời báo đúng — hai dòng này là
   MỘT ý, kéo xa nhau ra thì đọc thành hai mẩu rời. */
.drill__arrange .drill__why { margin-block-start: .1875rem; }
/* Sai ở đâu — câu giải thích đi kèm lỗi hay gặp, lấy từ kho câu mẫu. */
.drill__why { margin: .375rem 0 0; color: var(--ink-2); font-size: var(--fs-md); line-height: 1.6; }
.drill__compose > textarea { display: block; margin: 0 auto .75rem; max-width: 38rem; resize: vertical; }
.drill__sample { max-width: 38rem; margin: 1.25rem auto 0; padding: 1rem; border: 1px solid var(--line-dark); border-radius: var(--radius); background: var(--accent-soft); }
.drill__sample-sentence { margin: 0; color: var(--ink); font-size: calc(var(--fs-xl) + var(--fs-bump)); line-height: 1.7; word-break: keep-all; overflow-wrap: break-word; }
@media (max-width: 38.75em) {
  .drill__input-row { flex-direction: column; }
}

/* ---------------------------------------- Tab luyện tập */

#practice-setup, #practice-run { min-width: 0; }
/* Khe với thanh tab ngay trên. Lấy đúng 1.125rem của `#view-list .filters` để
   hai tab mở ra ở cùng một độ cao, đổi qua lại không thấy nhích. */
#practice-setup, #practice-run, #practice-empty { margin-top: 1.125rem; }
#practice-setup > * + *, #practice-run > * + * { margin-top: var(--gap-section); }
/* ---- Hai bước của màn dựng lượt ôn ----
   Trước đây màn này là MỘT tiêu đề gộp rồi thả thẳng nút + lưới thẻ xuống dưới,
   nhìn vào không ra được thứ tự thao tác. Giờ chia hai bước đánh số.

   ⚠ TÊN CLASS là `.setup-step*`, KHÔNG phải `.step*`: `.step` đã có chủ — đó là
   thẻ "3 bước học" ở trang chủ (tìm `.step__top`), và nó mang sẵn viền, nền
   `--surface`, padding cùng một mũi tên nối `::before` neo theo `--steps-gap`.
   Dùng lại tên đó là hai bước ở đây tự mọc ra khung thẻ và một mũi tên treo lơ
   lửng giữa trang.

   Ranh giới giữa hai bước giao cho KHOẢNG TRẮNG + một hairline, không dựng thẻ
   có viền/bóng riêng: thêm một lớp khung nữa quanh lưới sáu thẻ (bản thân chúng
   đã có viền) là màn hình thành khung lồng khung. */
/* Mỗi bước là một HỘP riêng. Ba tầng nền, đếm từ trang đi vào: trang `--bg`
   → hộp bước `--surface-2` → thẻ hình thức `--surface`. Hộp bước KHÔNG dùng
   `--surface` như panel thường: lưới sáu thẻ bên trong đã là `--surface` rồi,
   cho hộp cùng màu là hai lớp dính vào nhau, không còn thấy thẻ nổi lên. */
.setup-step {
  /* Cỡ vòng tròn số và khe sau nó khai Ở ĐÂY, không rải vào từng rule: dòng
     nhắc bên dưới phải thụt vào ĐÚNG bằng tổng hai con số này mới thẳng hàng
     với chữ tiêu đề. Để hai chỗ tự ghi số riêng là sửa một bên quên bên kia,
     lệch vài pixel — thứ chỉ lộ ra khi nhìn kỹ nên rất lâu mới có ai báo. */
  --num-size: 1.375rem;
  --num-gap: 0.5rem;

  /* LƯỚI, không phải hàng flex bọc tiêu đề + nút.
     Thứ tự DOM là thứ tự của màn HẸP (tiêu đề › nhắc › nút › lưới thẻ); ở màn
     rộng chỉ MỘT dòng bên dưới nhấc nút lên ô hàng-1/cột-2. Bản trước bọc hai
     món đó trong một hàng flex, và ở 375px nút xuống dòng NGAY TRONG hàng ấy →
     chen vào giữa tiêu đề và dòng nhắc, đẩy dòng nhắc của bước ② xuống 27,38px
     trong khi bước ① là 0. Lưới không có kiểu hỏng đó: nút muốn nằm đâu là do
     ô của nó, không do chỗ trống còn lại của hàng. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  column-gap: 0.875rem;

  padding: 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
}
/* Cột 2 CHỈ tồn tại cho ô của nút ở hàng 1. Mọi thứ khác trải hết bề ngang
   (`1 / -1`) — để `grid-column: 1` thì lưới thẻ và dòng nhắc bị hụt đúng bằng
   bề ngang cái nút, tự thu hẹp lại khi nhãn nút dài ra. */
.setup-step > * { grid-column: 1 / -1; }
.setup-step__head { grid-column: 1; grid-row: 1; }
/* Nút trong hộp bước KHÔNG được kéo giãn: ô lưới mặc định `stretch`, nên
   "Chọn ngữ pháp (1/7)" bị căng hết chiều ngang hộp, trông như một thanh chứ
   không còn ra cái nút. Lưới thẻ và dòng chữ thì vẫn để giãn bình thường. */
.setup-step > .btn { justify-self: start; }
.setup-step > * + * { margin-top: 0.5rem; }
/* Nút/lưới cách tiêu đề xa hơn dòng nhắc cách tiêu đề — đủ để đọc ra "dòng chữ
   này thuộc về tiêu đề trên, còn thứ bấm được thì ở dưới". */
.setup-step > :last-child { margin-top: 0.875rem; }

/* MÀN RỘNG: nhấc nút lên ngang hàng tiêu đề, dồn sát lề phải của hộp.
   Ô riêng nên nó KHÔNG còn ảnh hưởng gì tới dòng nhắc bên dưới — chiều cao
   hàng 1 do tiêu đề quyết định, y như bước ① không có nút.
   `padding-block: 0` để nút không cao hơn tiêu đề: cùng `align-items` thì món
   cao hơn vẫn kéo hàng cao theo. Vùng CHẠM bù lại bằng lớp phủ `::after` ở khối
   `@media (hover: none)` phía dưới. */
.setup-step .modes-switch {
  grid-column: 2;
  grid-row: 1;
  align-self: center;
  justify-self: end;
  margin-top: 0;
  padding-block: 0;
}
/* MÀN HẸP: trả nút về đúng chỗ của nó trong DOM — dưới dòng nhắc, trên lưới
   thẻ — và vẫn dồn phải. Mốc lấy đúng 38.75em của `#practice-modes` (chỗ lưới
   thẻ về một cột), để hai thay đổi xảy ra cùng một lúc. */
@media (max-width: 38.75em) {
  .setup-step { grid-template-columns: minmax(0, 1fr); }
  .setup-step .modes-switch {
    grid-column: 1;
    grid-row: auto;
    margin-top: 0.5rem;
  }
}

.setup-step__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--num-gap);
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--ink);
}
/* Vòng tròn số. Đây là chỗ DUY NHẤT của hai bước dùng màu nhấn đặc — nó nhỏ nên
   không tranh chỗ với CTA, mà vẫn đủ để mắt bắt được "1 rồi tới 2" trước cả khi
   đọc chữ. Chữ số nằm trong HTML chứ không phải `content` của CSS (docs/i18n.md).
   `line-height: 1`: chữ số không có phần thò xuống, để line-height thường là nó
   lệch lên trong vòng tròn. */
.setup-step__num {
  display: inline-grid;
  place-items: center;
  flex: none;
  inline-size: var(--num-size);
  block-size: var(--num-size);
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1;
}
/* Trạng thái đã xong. `--ok` là token site dùng sẵn cho lời báo thành công
   (xem `.pocket__msg`), không phải một màu mới nhập về.
   Dấu ✓ vẽ bằng MẶT NẠ `--tick-mask` có sẵn, không phải `content: "✓"` —
   docs/i18n.md cấm nhét chữ vào CSS, và mặt nạ thì ăn theo `currentColor` nên
   tự đúng màu ở cả hai mode. */
.setup-step__done {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--ok);
}
.setup-step__done::before {
  content: "";
  inline-size: 0.75rem;
  block-size: 0.75rem;
  background: currentColor;
  -webkit-mask: var(--tick-mask) center / contain no-repeat;
  mask: var(--tick-mask) center / contain no-repeat;
}
/* Dòng nhắc canh theo CHỮ TIÊU ĐỀ, không theo mép hộp: thụt vào đúng bề ngang
   vòng tròn số cộng khe sau nó, nên hai dòng thành một cột chữ thẳng và vòng
   tròn số đứng riêng ra ngoài lề như một dấu đánh số thật.
   `margin-top` khai riêng chứ không ăn 0.5rem của `> * + *`: dòng này phải ngồi
   SÁT tiêu đề — gần như dính vào chân nó — để đọc ra ngay là lời chú của tiêu
   đề, không phải một khối mới.
   `0` chứ không phải một số âm: hộp chữ vẫn còn ~4px nửa-khe của `line-height:
   1.6` ở phía trên, nên nhìn ra vẫn có khoảng thở và chữ không bao giờ chạm
   chân tiêu đề. Kéo âm là ăn vào phần đó, tới cỡ chữ gốc lớn (màn rộng, site
   nới cỡ chữ theo bề ngang) thì hai dòng chồng mép nhau. */
.setup-step__hint {
  margin-inline-start: calc(var(--num-size) + var(--num-gap));
  margin-top: 0;
  font-size: var(--fs-sm);
  font-style: italic;
  color: var(--ink-3);
}
/* LIGHT MODE PHẢI ĐẬM HƠN MỘT NẤC. Đo thật: `--ink-3` (#a3937c) trên nền hộp
   `--surface-2` (#ebe1cc) chỉ được 2,30:1 — nền hộp tối hơn nền trang nên dòng
   này mất thêm tương phản so với chỗ khác của site đang dùng `--ink-3`.
   `--ink-2` được 5,44:1, vẫn nhạt hơn hẳn chữ tiêu đề nên không phá ý đồ "dòng
   nhắc phải lùi về sau". Dark giữ nguyên `--ink-3` (4,53:1, đã đủ). */
:root[data-theme="light"] .setup-step__hint { color: var(--ink-2); }

/* Đúng 4 thẻ hình thức (`DRILLS` trong my-grammar.js) — 2 cột thì chia đều
   2×2, không lẻ ra một thẻ trơ trọi ở hàng dưới như lưới 3 cột. */
#practice-modes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
  align-items: stretch;
}
.practice-mode {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-content: start;
  gap: 0.5rem;
  padding: 0.875rem;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  min-width: 0;
  word-break: keep-all;
  overflow-wrap: anywhere;
}
.practice-mode.is-active { border-color: var(--accent); background: var(--accent-soft); }
.practice-mode:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
/* NÚT TRÒN TỰ VẼ, không dùng ô mặc định của trình duyệt — cùng một lý do đã
   ghi ở `.grammar-dialog__row input` (tìm "Ô tick tự vẽ"): `accent-color` chỉ
   nhuộm được cái chấm lúc ĐÃ chọn, còn SÁU nút chưa chọn vẫn là vòng tròn xám
   lạnh do trình duyệt vẽ theo `color-scheme: dark`, lạc hẳn khỏi bảng nâu ấm
   của site. Mà ở lưới này số nút chưa chọn luôn áp đảo nút đã chọn, nên cái
   xám đó chính là thứ đập vào mắt trước.

   KIỂU VẼ LÀ "VÒNG + CHẤM", không phải "tô đặc cả ô" như ô tick vuông trong
   popup: lòng ô GIỮ NGUYÊN nền `--field` ở cả hai trạng thái, chọn rồi thì chỉ
   đổi màu vòng sang `--accent` và thả một chấm `--accent` vào giữa. Nhờ lòng ô
   không đổi màu, cái vành sáng vẫn còn nguyên nên nhìn ra ngay đây là nút TRÒN
   (chọn một trong nhiều) chứ không phải ô tick vuông (chọn nhiều) — đúng cái
   khác biệt mà người dùng cần đọc được trước khi bấm.

   Ba lớp vòng / khe / chấm ăn theo `--rb`, đừng ghim số rời: đổi cỡ nút là cả
   ba cùng giãn, và dòng `margin` canh mép dưới cũng tự theo. */
.practice-mode input {
  --rb: 1.0625rem;        /* đường kính nút */
  --rb-ring: 0.125rem;    /* bề dày vòng ngoài */
  --rb-dot: 0.4375rem;    /* đường kính chấm giữa — khe hở còn lại ≈ 3px */
  --rb-on: var(--accent); /* màu lúc đã chọn / rê chuột — xem ghi chú light mode */
  --rb-ink: var(--on-accent); /* MỰC nằm TRÊN --rb-on (dấu ✓ của ô vuông) */

  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  inline-size: var(--rb);
  block-size: var(--rb);
  border: var(--rb-ring) solid var(--line-dark);
  border-radius: 50%;
  background: var(--field);
  cursor: pointer;
  transition: border-color 140ms ease;
  /* Canh vào ĐÚNG DÒNG CHỮ ĐẦU của tên hình thức, không canh giữa cả ô: tên
     dài (bản tiếng Hàn, hoặc màn hẹp một cột) xuống hai dòng thì `align-self:
     center` đẩy nút trôi xuống khoảng giữa hai dòng. Số nhích tự tính từ chính
     cỡ chữ đang dùng nên không phải con số phép thuật: nửa phần thừa giữa
     chiều cao dòng (1.6em) và chiều cao nút. */
  font-size: var(--fs-md);
  margin: calc((1.6em - var(--rb)) / 2) 0 0;
}
.practice-mode:hover input { border-color: var(--rb-on); }
.practice-mode input:checked { border-color: var(--rb-on); }
.practice-mode input:checked::after {
  content: "";
  inline-size: var(--rb-dot);
  block-size: var(--rb-dot);
  border-radius: 50%;
  background: var(--rb-on);
}
/* LIGHT MODE PHẢI ĐỔI SANG `--accent-ink`, nếu không nút ĐÃ CHỌN nhìn còn mờ
   hơn nút CHƯA CHỌN — tức là ngược hẳn ý nghĩa.
   Đo thật ở light: vòng chưa chọn `--line-dark` là #d9c9a9, còn `--accent` lại
   là #eecfa1 — NHẠT HƠN. Bản tô đặc cũ giấu được chuyện này vì cả ô là một
   mảng màu liền; đổi sang vòng 1,5px + chấm 6,5px thì lượng mực còn quá ít,
   chấm trên nền `--field` trắng chỉ được 1,5:1, dưới xa sàn 3:1 cho dấu hiệu
   trạng thái của một nút bấm.
   `--accent-ink` chính là token site dựng sẵn cho đúng tình huống này — vẫn là
   vàng của site, chỉ là biến thể đủ đậm để làm MỰC trên nền kem (xem ghi chú ở
   `:root`). Nó cho 4,9:1. Dark mode không đụng tới, giữ nguyên `--accent`. */
:root[data-theme="light"] .practice-mode input {
  --rb-on: var(--accent-ink);
  /* Mực phải LẬT theo nền. `--on-accent` ở light là #33291d, đặt lên nền
     `--accent-ink` #8a6a2f chỉ được 2,83:1 — dấu ✓ gần như chìm hẳn, ô tick
     nhìn ra chỉ còn là một mảng nâu đặc. Trắng trên nền đó được 4,9:1.
     Ở dark thì ngược lại: nền `--accent` sáng nên mực tối `--on-accent` mới
     đúng, nên KHÔNG gộp hai mode vào một giá trị. */
  --rb-ink: var(--surface);
}
/* CHẾ ĐỘ KẾT HỢP: cùng một <input>, chỉ đổi `type` sang checkbox — nên chỉ cần
   bẻ lại bo góc và thay chấm tròn bằng dấu ✓. Vuông/tròn là dấu hiệu DUY NHẤT
   phân biệt "chọn một" với "chọn nhiều", nên nó phải đổi thật, không chỉ đổi
   chữ trên nút gạt. Dấu ✓ mượn đúng `--tick-mask` của ô tick trong popup chọn
   ngữ pháp, không vẽ thêm hình mới. */
.practice-mode input[type="checkbox"] { border-radius: 0.25rem; }
.practice-mode input[type="checkbox"]:checked {
  background: var(--rb-on);
}
.practice-mode input[type="checkbox"]:checked::after {
  inline-size: 0.6875rem;
  block-size: 0.6875rem;
  border-radius: 0;
  background: var(--rb-ink);
  -webkit-mask: var(--tick-mask) center / contain no-repeat;
  mask: var(--tick-mask) center / contain no-repeat;
}

/* NÚT ĐỔI CHẾ ĐỘ — cố ý KHÔNG phải `.btn`. Nó là lối đi phụ: người mới chỉ cần
   bấm một hình thức rồi bắt đầu, không phải hiểu khái niệm một/nhiều trước.
   Nên vẽ như một dòng chữ bấm được, nhẹ hơn cả thẻ hình thức phía trên và nhẹ
   hơn hẳn CTA phía dưới.
   Dấu ＋ / ↩ vẽ bằng ::before chứ không nhét vào chuỗi dịch: lockWidth() ghi đè
   textContent của nút để đo, chữ nào nằm trong đó cũng bị cuốn theo. */
.modes-switch {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.25rem 0;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--accent-ink);
  text-align: left;
}
/* Icon là SVG trong HTML, KHÔNG phải `content` của CSS (docs/i18n.md). Hai cái
   nằm sẵn trong nút, mỗi chế độ hiện một cái — rẻ hơn việc thay path bằng JS và
   không có nhịp nào nút trống icon. */
.modes-switch .icon { inline-size: 0.9rem; block-size: 0.9rem; }
.modes-switch .modes-switch__back,
.modes-switch.is-multi .modes-switch__plus { display: none; }
.modes-switch.is-multi .modes-switch__back { display: block; }
.modes-switch:hover { text-decoration: underline; }
.modes-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Viền tiêu điểm vẽ ở chính nút chứ không ở cả thẻ: `.practice-mode
   :focus-within` phía trên bọc quanh nguyên khối thẻ, hợp lý khi nút là ô
   mặc định, nhưng giờ nút đã có hình riêng thì khoanh đúng nó rõ hơn. Giữ cả
   hai cũng được — chúng không chồng nét lên nhau. */
.practice-mode input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.practice-mode__name { font-size: var(--fs-md); font-weight: 600; color: var(--ink); }
.practice-mode__desc { grid-column: 2; font-size: var(--fs-sm); color: var(--ink-2); }
/* NÚT MỞ POPUP CHỌN NGỮ PHÁP — đây là việc BẮT BUỘC phải làm trước khi bấm
   "Bắt đầu ôn tập" được, nhưng ở dạng `.btn--ghost .btn--sm` trần nó mảnh hơn
   cả mấy nút lọc ở tab 1: nền `--field` gần như trùng nền trang, chữ `--ink-2`.
   Nâng lên một nấc bằng cách ĐỔ NỀN `--accent-soft` + chữ `--ink`, KHÔNG đổi
   sang `.btn--primary`: nền be đặc đang là dấu hiệu riêng của "Bắt đầu ôn tập"
   ở cuối màn, hai nút đặc cạnh nhau là mắt không biết bấm cái nào trước.
   Thành thứ bậc ba nấc: đặc (làm cuối) › nền mờ (làm trước) › ghost (phụ).

   Chữ để `--ink` chứ không `--accent-ink`: cặp accent-ink trên accent-soft ở
   LIGHT mode chỉ được ~4,2:1, hụt sàn 4,5:1 cho cỡ chữ 13,5px. `--ink` thì dư
   ở cả hai mode.

   Cỡ chữ/độ đậm đổi thì bề ngang đo được cũng đổi, nhưng `lockWidth()` đo bằng
   `getBoundingClientRect()` lúc chạy nên tự ăn theo — không có số nào phải
   chỉnh tay ở lockPracticeWidths(). */
#pick-grammar {
  padding: 0.375rem 0.875rem;
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--ink);
  font-size: var(--fs-lg);
  font-weight: 600;
  transition: background 140ms ease, color 140ms ease;
}
#pick-grammar:hover { background: var(--accent); color: var(--on-accent); }
/* LIGHT MODE PHẢI ĐỔI NỀN, không dùng chung được `--accent-soft`.
   Đo thật: ở light, `--accent-soft` là #f8ecd8 còn nền trang `--bg` là #f6efe0
   — lệch nhau 2 nấc, mắt không thấy cái nút nào nổi lên cả, nó chỉ còn mỗi
   đường viền. (Ở dark thì ngược lại: #2f2618 trên nền #17130e nổi rõ.)
   Cái CẦN giữ nguyên là Ý ĐỒ chứ không phải mã màu: "nền cao hơn nền trang
   một nấc". Ở light nấc đó là `--surface` (#ffffff trên nền kem), đúng token
   mà thẻ panel dùng để nổi lên khỏi trang. Viền lấy `--accent-dark` đậm hơn
   một chút cho cạnh nút không bị nền trắng nuốt mất. */
:root[data-theme="light"] #pick-grammar {
  background: var(--surface);
  border-color: var(--accent-dark);
}
:root[data-theme="light"] #pick-grammar:hover { background: var(--accent); }

.practice-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.625rem; }
/* `inline-flex` để mũi tên có khe riêng; `justify-content: center` thay cho
   `text-align` của `.btn`, vì sàn `min-width` mà lockWidth() đặt rộng hơn nhãn
   ngắn — không căn giữa là chữ dán vào mép trái. */
#practice-start {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
}
/* Mũi tên LUÔN có mặt, kể cả lúc nút đang khoá: nó là một phần của bao hình mà
   lockWidth() đã đo, ẩn đi là chữ nhảy ngang mỗi lần đổi trạng thái. Lúc khoá
   thì cả nút đã mờ đi theo `.btn:disabled` nên nó không hứa hẹn gì sai. */
.btn__arrow { inline-size: 0.9rem; block-size: 0.9rem; }
#start-hint, #dialog-warn { color: var(--ink-2); font-size: var(--fs-sm); }
/* Dòng nhắc "chọn ít nhất một ngữ pháp" GIỮ CHỖ sẵn kể cả lúc không hiện: nó
   nằm ngay trên cặp nút Huỷ/Xong, mà `display: none` thì bỏ chọn hết một cái là
   cặp nút nhảy lên rồi hiện nhắc lại nhảy xuống — đúng lúc con trỏ đang ở đó.
   `visibility` giữ nguyên ô chữ trong bố cục nên chiều cao popup đứng yên, và
   vẫn giấu dòng này khỏi trình đọc màn hình y như `hidden`. Cao bao nhiêu thì
   để chính nó tự đo, không neo `min-height` bằng tay: ở 320px bản tiếng Việt
   câu này xuống hai dòng, neo một dòng là hụt. */
.grammar-dialog__warn.is-off { visibility: hidden; }
/* Hàng đầu màn ôn: breadcrumb trái, cụm nút phải. `margin-inline-start: auto`
   trên cụm nút chứ không `space-between`: màn hẹp cụm nút rớt xuống hàng riêng
   thì `space-between` thả nó về mép TRÁI, còn auto margin vẫn giữ nó bên phải.
   Breadcrumb bỏ `margin-bottom` của `.crumbs` — ở đây nó đứng chung hàng với
   nút, khe dưới là việc của `#practice-run > * + *`. */
.practice-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem 1rem;
}
.practice-bar .crumbs { margin-bottom: 0; min-width: 0; }
.practice-bar__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-inline-start: auto;
}
#practice-crumbs { flex-wrap: wrap; overflow-wrap: anywhere; }
#practice-crumbs .crumbs__current { min-width: 0; }
#back-to-modes { gap: 0.375rem; }
/* Hai nút phải chung một hàng ở mọi bề ngang. Đo ở 320px (2026-09-15): cụm nút
   có 286.7px, mà "Ngữ pháp đang ôn (3)" khoá sàn 10.7rem = 160.5px + khe 7.5px +
   "← Đổi hình thức" 8.2rem = 123px = 291px → nút sau rớt tầng. Sàn là nhãn rộng
   nhất của cả BA ngôn ngữ nên không đổi chữ mà lách được; bỏ sàn thì vừa khít
   0.5px — tức phá luật "nút không đổi kích thước theo ngôn ngữ" để đổi lấy một
   hàng chưa chắc giữ nổi. Thu padding ngang + khe lấy lại ~14.7px: đủ cả khi con
   số lên hai chữ số ("(12)" rộng thêm ~8px). lockWidth() đo lại khi resize nên
   sàn co theo padding mới, ba ngôn ngữ vẫn bằng nhau. */
@media (max-width: 26.5em) {
  .practice-bar__actions { gap: 0.375rem; }
  .practice-bar__actions > .btn { padding-inline: 0.4375rem; }
  /* Ô tick "Xáo câu" (thêm 2026-09-16) xuống HÀNG RIÊNG ở bề ngang hẹp. Phép đo
     ở comment trên cho thấy hai nút chỉ vừa đúng một hàng sau khi đã bóp padding;
     để ô tick ~100px chen cùng hàng là nút "Đổi hình thức" rớt tầng ngay
     (e2e `khung luyện tập…` bắt được: hai nút lệch 38.75px ở 320px). */
  .practice-bar__toggle { flex: 1 0 100%; justify-content: flex-end; }
}
.grammar-dialog {
  position: fixed;
  inset: 0;
  margin: auto;
  width: 38rem;
  max-width: calc(100% - 2rem);
  max-height: 90vh;
  max-height: 90dvh;
  padding: 0;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  overflow: auto;
}
.grammar-dialog::backdrop { background: var(--bg); opacity: 0.65; }
.grammar-dialog__body { display: flex; flex-direction: column; gap: 0.75rem; padding: 1rem; min-width: 0; }
/* Tiêu đề popup lấy ĐÚNG cỡ `.lp__head` của popup mời đăng nhập — cỡ tiêu đề
   hộp thoại của site đã có sẵn, đừng đẻ thêm một cỡ thứ hai. Bản trước để
   `--fs-lg` (13.5px), tức chỉ nhỉnh hơn chữ thường 0.5px: nhìn lướt không ra
   đâu là tên hộp. */
.grammar-dialog__head { display: flex; align-items: center; gap: 0.625rem; }
#dialog-title {
  flex: 1;
  min-width: 0;
  font-size: calc(var(--fs-xl) + var(--fs-bump));
  font-weight: 600;
  color: var(--ink);
}
/* Nút × là icon trần, không phải `.btn`: một cái nút viền ở góc trên hút mắt
   ngang với nút Xong, mà đóng hộp không phải việc chính ở đây. */
.grammar-dialog__x {
  display: flex;
  flex-shrink: 0;
  padding: 0.25rem;
  border-radius: var(--radius);
  color: var(--ink-3);
}
.grammar-dialog__x:hover { color: var(--ink); }
.grammar-dialog__x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.grammar-dialog__x .icon { inline-size: 1.125rem; block-size: 1.125rem; }

/* ⚠ `.filters__search` khai `flex: 1 1 16rem`. Ở tab 1 nó nằm trong một hàng
   NGANG nên 16rem là bề ngang cơ sở; trong đây khối cha xếp DỌC, mà flex-basis
   ăn theo trục chính — để nguyên là ô tìm cao 16rem. Phải trả nó về tự nhiên. */
.grammar-dialog__search { flex: 0 0 auto; }
/* Hai hàng chip lọc của popup. `flex-wrap` là bắt buộc chứ không phải cho đẹp:
   ở 320px bản tiếng Việt, ba chip trạng thái ("Tất cả · Chưa thuộc · Đã thuộc")
   cộng lại đã rộng hơn lòng hộp — không cho xuống dòng là hàng đẩy ngang cả
   popup. */
#dialog-status, #dialog-status, #dialog-levels { flex-wrap: wrap; }

/* Hộp riêng cho phần ngữ pháp: nền lùi một bậc so với thân popup (`--surface-2`
   trên `--surface`) cộng một đường viền, đủ để tách "chỗ lọc" khỏi "chỗ chọn"
   mà không phải tô thêm màu nhấn nào. */
.grammar-dialog__box {
  display: flex;
  flex-direction: column;
  min-height: 0;
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  background: var(--surface-2);
}
.grammar-dialog__tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--line);
}
.grammar-dialog__tools button { color: var(--ink-2); font-size: var(--fs-md); }
.grammar-dialog__tools button:hover { color: var(--accent-ink); }
.grammar-dialog__tools button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Số đếm dạt hẳn về mép phải của hộp — nó là kết quả, không phải một cái nút
   thứ ba đứng cùng hàng với hai nút kia. */
#dialog-count { margin-inline-start: auto; color: var(--ink-3); font-size: var(--fs-sm); }
#dialog-list { padding-inline: 0.75rem; overflow-y: auto; max-height: 36vh; max-height: 36dvh; min-height: 0; flex-shrink: 1; }
/* Dòng cuối không kẻ gạch: đã có viền hộp làm mép dưới rồi, kẻ thêm là hai
   đường sát nhau. */
.grammar-dialog__row:last-child { border-bottom: 0; }
.grammar-dialog__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.25rem 0.625rem;
  padding-block: 0.625rem;
  border-bottom: 1px solid var(--line);
  word-break: keep-all;
  overflow-wrap: anywhere;
  cursor: pointer;
}
/* Ô tick tự vẽ, KHÔNG dùng ô mặc định của trình duyệt: `accent-color` chỉ đổi
   được màu lúc ĐÃ tick, còn ô trống vẫn là hộp xám lạnh do trình duyệt vẽ theo
   `color-scheme: dark` — lạc hẳn khỏi bảng nâu ấm của site. Vẽ tay thì ô trống
   dùng đúng `--field` + `--line-dark` như mọi ô nhập khác.

   `grid-row: 1` + `align-self: center` chứ không `span 2`: trải hai hàng là ô
   tick rơi vào khoảng giữa mẫu tiếng Hàn và dòng nghĩa, không thẳng hàng với
   thứ nó đang đánh dấu. */
.grammar-dialog__row input {
  grid-row: 1;
  align-self: center;
  appearance: none;
  -webkit-appearance: none;
  /* Ô tick TỰ NÓ là một khối flex căn giữa, thay vì thả dấu tick bằng `margin`.
     Bản trước để `margin: -1px auto 0`, mà `auto` chỉ căn được theo chiều
     NGANG — theo chiều dọc dấu tick rơi thẳng lên mép trên của lòng ô rồi còn
     bị kéo lên thêm 1px nữa. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  inline-size: 1.0625rem;
  block-size: 1.0625rem;
  margin: 0;
  border: 1px solid var(--line-dark);
  border-radius: 0.25rem;
  background: var(--field);
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease;
}
.grammar-dialog__row:hover input { border-color: var(--accent); }
.grammar-dialog__row input:checked { border-color: var(--accent); background: var(--accent); }
/* Dấu tick vẽ bằng MẶT NẠ SVG, không phải hai cạnh viền xoay 45° như bản trước.
   Lý do bỏ cách cũ: mực của nó chỉ nằm ở hai cạnh phải + dưới của một ô chữ
   nhật, nên sau khi xoay 45° phần nhìn thấy được KHÔNG trùng tâm ô chứa —
   phải nhích tay, mà lượng nhích lại đổi theo cỡ ô. Đo thật (chụp phóng 10×,
   đếm pixel): cùng một mức nhích −1,5px cho ra lệch −0,10px ở 1280 nhưng
   −0,70px ở 320 và −0,95px ở 768, vì ô tick co giãn theo cỡ chữ gốc
   (17 / 18 / 20px) trong khi nét viền ghim cứng 2px — hình không còn đồng dạng.

   Mặt nạ thì không có số phép thuật nào: nét ✓ nằm giữa khung nhìn 24×24 của
   chính file SVG (bao hình của nét chạy 3,5..20,5 theo cả hai chiều), nên
   `center / contain` căn đúng ở MỌI cỡ. Tô bằng `background` nên vẫn ăn biến
   màu `--on-accent`, tức vẫn đổi theo sáng/tối — đây là lý do dùng mặt nạ chứ
   không phải `background-image` thẳng (SVG nền không đọc được biến CSS của
   trang). Trình duyệt nào không hiểu mặt nạ thì mất dấu ✓ nhưng ô vẫn tô đặc
   màu nhấn, vẫn phân biệt được đã tick hay chưa. */
.grammar-dialog__row input:checked::after {
  content: "";
  inline-size: 0.6875rem;
  block-size: 0.6875rem;
  background: var(--on-accent);
  -webkit-mask: var(--tick-mask) center / contain no-repeat;
  mask: var(--tick-mask) center / contain no-repeat;
}
.grammar-dialog__row input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.grammar-dialog__meaning { grid-column: 2; color: var(--ink-2); font-size: var(--fs-sm); }
/* Hàng ở chế độ XEM (popup mở từ màn ôn, chưa bấm Thay đổi): không có cột ô
   tick, nên mẫu và nghĩa về cột 1 và bỏ con trỏ bàn tay — hàng này không bấm
   được gì. */
.grammar-dialog__row--view { grid-template-columns: minmax(0, 1fr); cursor: default; }
.grammar-dialog__row--view .grammar-dialog__meaning { grid-column: 1; }
/* Dồn phải, và Xong là nút đứng SÁT MÉP — chỗ mắt dừng lại cuối cùng.
   Khác `.lp__actions`/`.otp__actions` (căn giữa, nút chính đứng trước): hai hộp
   kia chỉ có một việc để làm nên nút chính nằm giữa là đúng, còn hộp này là
   một biểu mẫu có xác nhận/huỷ, mà kiểu xếp quen thuộc của biểu mẫu là cặp nút
   nằm ở góc dưới bên phải. Đổi lại, đây là hộp DUY NHẤT của site xếp kiểu này —
   ai thêm hộp thoại có cặp xác nhận/huỷ về sau thì theo hộp này cho đều. */
.grammar-dialog__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.5rem;
}
@media (max-width: 38.75em) {
  #practice-modes { grid-template-columns: minmax(0, 1fr); }
}
@media (hover: none) {
  #view-practice .btn, #dialog-status .chip, #dialog-levels .chip { min-height: 2.75rem; }
  /* `.modes-switch` KHÔNG dùng `min-height` như mấy cái trên: nó nằm chung hàng
     flex với tiêu đề bước ②, mà cao lên là kéo cả hàng cao theo rồi đẩy dòng
     nhắc xuống, lệch với bước ①. Nới vùng chạm bằng một lớp phủ trong suốt —
     bao hình của nút giữ nguyên nên bố cục không nhúc nhích.
     Nút cao ~21px, phủ thêm 12px mỗi chiều thành ~45px, qua mốc 44px. */
  .modes-switch { position: relative; }
  .modes-switch::after {
    content: "";
    position: absolute;
    inset: -0.75rem -0.25rem;
  }
}

/* Thẻ hẹp: mẫu ở trên, nghĩa xuống dòng dưới, chevron giữ ở góc phải. Đo theo
   bề ngang CỦA THẺ chứ không của khung nhìn — từ khi danh sách chia hai cột thì
   một thẻ ở màn 1280px cũng chỉ rộng ~34rem, hẹp hơn cả thẻ một cột ở 768px. */
@container grammarcard (max-width: 34rem) {
  .grammar-card__toggle {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.25rem 0.5rem;
  }
  .grammar-card__meaning { grid-column: 1 / -1; grid-row: 2; margin-inline-start: 0; }
  .grammar-card__chevron { grid-column: 2; grid-row: 1; }
}

@container grammarcard (max-width: 26rem) {
  /* Chân thẻ: cụm nút xuống hàng riêng, không bóp phần ngày lưu — cùng cách
     .word-card đã xử lý ở My Vocab. */
  .grammar-card__foot { align-items: stretch; flex-direction: column; }
  .grammar-card__actions .btn { flex: 1 1 auto; }
}

@media (max-width: 48em) {
  .grammar-example { grid-template-columns: minmax(0, 1fr) auto; }
}

/* Ai bật "giảm chuyển động" thì bỏ hết hiệu ứng trượt/xoay — accordion vẫn
   mở đóng đúng, chỉ là không có đoạn chuyển. */
@media (prefers-reduced-motion: reduce) {
  .grammar-card,
  .grammar-card__panel,
  .grammar-card__chevron svg,
  .grammar-example__play { transition: none; }
}

/* ==========================================================================
   ABOUT US — about.html

   Một trang chữ, không có gì để bấm ngoài đúng một nút ở cuối. Nên nó chỉ có
   bốn công cụ để dẫn mắt người đọc, và KHÔNG thêm màu nào ngoài bảng ở :root
   (docs/ui.md mục 4):

     1. BỀ NGANG — CĂN THEO MENU, không phải theo một con số tự chọn.
        Chủ dự án chốt 2026-09-03: mép trái chữ phải thẳng hàng với chữ
        "Dictation" trên menu, và khoảng trống hai bên phải bằng nhau.

        MỘT luật lo cả hai việc, dùng cho mọi bề ngang:

            padding-inline: max(--nav-inset, nửa phần thừa của trần đọc)

        `padding-inline` đặt ĐỀU hai bên nên khoảng trống trái = phải, luôn
        luôn, không cần đo lại sau mỗi lần sửa.

        `--nav-inset` = mép trái chữ "Dictation" tính từ mép trái `.container`,
        và nó chỉ có hai giá trị vì header chỉ có hai trạng thái:

          · dưới 75em — menu xuống hàng riêng, nằm sát mép container → 0
          · từ 75.0625em — menu cùng hàng với logo → 16.122rem (ĐO ĐƯỢC, giống
            hệt nhau ở 1280 · 1440 · 1600 · 1920 và ở cả ba ngôn ngữ, vì logo là
            tên thương hiệu không dịch)

        Vế thứ hai của `max()` là trần chống dòng quá dài: khi khung rộng tới
        mức luật căn menu cho ra dòng dài hơn `--about-max`, nó thắng và khối tự
        căn giữa lại. Đo được: căn khớp tuyệt đối ở 375 · 620 · 768 · 1280 ·
        1440 · 1512 · 1600; từ khoảng 1024–1200 và trên ~1650 thì trần thắng và
        mép trái lệch khỏi menu. Đó là chỗ hai yêu cầu (thẳng hàng · dòng đọc
        được) không cùng thoả được — ưu tiên dễ đọc, vì chính dòng quá dài là
        thứ phải sửa ở lượt trước.

        Vì sao `calc(var(--page-w) / 2 - var(--about-max) / 2)`: `%` trong
        `padding` tính theo khối CHỨA `.about` (cả bề ngang màn hình), chứ không
        theo bề ngang của chính `.about` — mà `.about` chỉ rộng `--page-w`
        (92%). Nửa của 92% trừ nửa trần mới ra đúng phần đệm cần.

     2. NHỊP — bốn chương treo trên một dòng thời gian, xen giữa là ba khối
        NỔI KHÁC HÌNH NHAU: câu dẫn ở đầu (viền trái), câu trích giữa chương 2
        (viền trái mảnh hơn, chữ nhỏ hơn), đoạn đỉnh cảm xúc (nền vàng nhạt,
        gạch trên gạch dưới). Không cái nào là "thẻ có viền bốn cạnh" —
        chủ dự án chốt 2026-09-03: trang kể chuyện, không phải trang bán hàng.

     3. KHOẢNG TRẮNG — `--about-gap` là khe DUY NHẤT giữa các phần lớn
        (5.5rem ≈ 96px desktop, 3.75rem ≈ 56px dưới 56.25em). Một con số cho mọi
        khe, giống `--gap-section` của màn Dictation.

     4. DÒNG THỜI GIAN — ba chấm tròn và hai đoạn nối, đều là HÌNH HỌC THUẦN
        trong CSS. Không ảnh, không ký tự, không nằm trong luồng đọc của trình
        đọc màn hình. Đường kẻ cố ý mờ (28%): đủ để mắt thấy có một trục, không
        đủ để thành cái khung như timeline của CV.

   Dải sóng `.wave` chỉ xuất hiện ĐÚNG MỘT LẦN, ở khối chữ ký cuối trang. Trước
   đó nó còn một bản nữa ở hero, và khi có mặt hai chỗ thì nó không còn là chữ
   ký của cái gì cả.
   ========================================================================== */

.about {
  /* Trần bề ngang dòng chữ. 52rem ≈ 936px ở cỡ chữ gốc desktop — rộng hơn mức
     lý tưởng (680–760px) một nhịp, nhưng đây là con số do LUẬT CĂN MENU quyết
     định chứ không phải chọn tự do: hạ xuống nữa là mép trái rời khỏi chữ
     "Dictation" ngay từ khung 1440. */
  --about-max: 52rem;
  --about-gap: 5.5rem;
  /* Menu xuống hàng riêng thì nó nằm sát mép `.container` → không lệch. */
  --nav-inset: 0rem;

  padding-block: 1.5rem 2rem;
  padding-inline: max(var(--nav-inset), calc(var(--page-w) / 2 - var(--about-max) / 2));
  transform: translateY(-5px);
}

/* 75.0625em = 1201px = ngay TRÊN `@media (max-width: 75em)` của header. Phải
   nhích 1px chứ không dùng `min-width: 75em`: ở đúng 1200px thì cả hai truy vấn
   cùng khớp, tức menu đã xuống hàng riêng mà trang vẫn căn theo kiểu một hàng.

   Sửa bề ngang logo, khe của `.header__inner` hay padding mục menu thì phải ĐO
   LẠI con số này — lệch đi thì trang About rời khỏi menu mà không test nào bắt
   được. Cách đo: mép trái text node của mục menu đầu tiên, trừ mép trái
   `.header__inner`. */
@media (min-width: 75.0625em) {
  .about { --nav-inset: 16.122rem; }
}

.about-wish {
  margin: 0;
  color: var(--accent-ink);
  color: color-mix(in srgb, var(--ink-2) 60%, var(--accent-ink));
  font-size: calc(var(--fs-sm) + 3px);
  font-style: italic;
  line-height: 1.5;
  text-align: right;
  transform: translateY(-15px);
  word-break: keep-all;
}

/* --------------------------------------------------------------- Mở đầu */
.about-hero { padding-top: 1.25rem; }
.about-hero__eyebrow {
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.about-hero__title {
  margin-top: 1rem;
  /* Trần hẹp hơn cột chữ: tiêu đề cỡ này mà trải hết bề ngang cột thì ra một
     dòng dài ngoằng. 32rem cho bản tiếng Việt gãy thành 2 dòng, đúng dáng chủ
     dự án mô tả — nhưng để CHỮ tự gãy, không chèn <br>, vì mỗi ngôn ngữ gãy một
     chỗ khác nhau. */
  max-width: 32rem;
  font-size: 2.25rem;
  font-weight: 700;
  line-height: 1.24;
  letter-spacing: -.02em;
  /* Tiêu đề dài, tiếng Hàn phải ngắt theo từ chứ không cắt giữa chữ. */
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* Câu dẫn — chính là câu mà cả trang xoay quanh. Nghiêng, vàng, có vạch bên
   trái. KHÔNG dùng ::before để chèn dấu ngoặc kép: dấu đó nằm trong chuỗi dịch
   (docs/i18n.md cấm chữ hiển thị nằm trong CSS). */
.about-hero__lead {
  max-width: 34rem;
  margin-top: 2rem;
  padding-left: 1.25rem;
  border-left: 2px solid var(--accent);
  font-size: 1.375rem;
  font-style: italic;
  line-height: 1.55;
  color: var(--accent-ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}

/* ------------------------------------------------- Dòng thời gian 4 chương

   Mỗi chương tự vẽ phần của mình: `::before` là chấm của nó, `::after` là đoạn
   nối xuống chấm của chương KẾ TIẾP. Chương cuối không có `::after`.

   Vì sao không phải MỘT đường kẻ chạy suốt `.about-chapters`: đường đó dừng ở
   đáy khối, tức đáy đoạn văn cuối cùng — thõng xuống dưới chấm cuối cả trăm
   pixel, nhìn ra như sợi chỉ bị bỏ dở. Cắt theo từng đoạn thì trục bắt đầu ở
   chấm đầu và kết thúc đúng ở chấm cuối, không cần biết chương dài bao nhiêu.

   Bốn con số dưới đây khai thành biến vì đoạn nối phải tính TỪ chúng
   (`bottom` = khe giữa hai chương + phần chấm sau nhô xuống). Đổi cỡ chấm hay
   khe mà quên sửa `bottom` là đoạn nối hụt hoặc thừa — để calc tự lo. */
.about-chapters {
  --rail: 2.25rem;              /* bề ngang chừa cho trục */
  --dot: 0.5rem;
  --dot-top: 0.3125rem;         /* mép trên chấm, so với mép trên chương */
  --chapter-gap: 3.25rem;

  position: relative;
  margin-top: var(--about-gap);
  padding-left: var(--rail);
}
.chapter { position: relative; }
.chapter + .chapter { margin-top: var(--chapter-gap); }
.chapter::before {
  content: '';
  position: absolute;
  left: calc(-1 * var(--rail));
  top: var(--dot-top);
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--accent);
}
.chapter:not(:last-child)::after {
  content: '';
  position: absolute;
  left: calc(-1 * var(--rail) + var(--dot) / 2);
  top: calc(var(--dot-top) + var(--dot));
  bottom: calc(-1 * (var(--chapter-gap) + var(--dot-top)));
  width: 1px;
  background: var(--accent);
  opacity: .28;
}
.chapter:last-child::after {
  content: '';
  position: absolute;
  left: calc(-1 * var(--rail) + var(--dot) / 2);
  top: calc(var(--dot-top) + var(--dot));
  bottom: 0;
  width: 1px;
  background: var(--accent);
  opacity: .28;
}
.chapter__title {
  font-size: 1.3125rem;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -.01em;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}
.chapter__body { margin-top: 1.125rem; }

/* Cỡ chữ và nhịp của MỌI đoạn văn trong trang — khai một chỗ, không rải ra
   từng khối. `--fs-body` + 0.0625rem đo được ~17.5px ở desktop, ~18px ở mobile
   (thang --fs-* dưới 56.25em đã tự cộng thêm 2px). */
.about-chapters p,
.about-ending__mission {
  font-size: calc(var(--fs-body) + 0.0625rem);
  line-height: 1.8;
  color: var(--ink-2);
  word-break: keep-all;
  overflow-wrap: break-word;
}
.chapter__body p + p { margin-top: 1.375rem; }
.about-say + p { margin-top: 1.375rem; }
/* Chỗ nhấn trong câu: đậm hơn VÀ sáng hơn phần chữ quanh nó — chỉ một trong
   hai thì gần như không nhận ra trên nền tối. */
.about b { font-weight: 700; color: var(--ink); }

/* Câu trích trong chương 2. Cùng ngôn ngữ với câu dẫn ở hero (nghiêng, vàng,
   vạch trái) nhưng NHỎ HƠN và vạch MẢNH HƠN: nó là một ý nghĩ thoáng qua giữa
   mạch kể, không phải câu mở màn. */
.about-say {
  margin-top: 1.375rem;
  padding-left: 1.125rem;
  border-left: 1px solid var(--accent);
}
.about-chapters .about-say p {
  font-size: 1.125rem;
  font-style: italic;
  line-height: 1.7;
  color: var(--accent-ink);
}

/* ------------------------------------------------------ Đoạn đỉnh cảm xúc

   Rộng hơn cột đọc một nhịp, nền vàng 7%, CHỈ có gạch trên và gạch dưới. Không
   viền bốn cạnh, không bo góc, không đổ bóng: một quãng lặng vắt ngang trang,
   không phải thêm một cái thẻ nữa (docs/ui.md mục 5).

   `color-mix` cho nền theo đúng màu accent của mode đang bật, nên không phải
   khai hai lần ở hai bảng màu. */
.about-featured {
  margin-top: var(--about-gap);
  padding: 3.5rem 2rem;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  text-align: center;
}
.about-featured__lead {
  font-size: 1.1875rem;
  line-height: 1.7;
  color: var(--ink-2);
  word-break: keep-all;
  overflow-wrap: break-word;
}
.about-featured__punch {
  max-width: 38rem;
  margin: 1.125rem auto 0;
  font-size: 1.4375rem;
  line-height: 1.65;
  color: var(--ink-2);
  word-break: keep-all;
  overflow-wrap: break-word;
}

/* ------------------------------------------------------------------ Kết */
.about-ending {
  margin-top: var(--about-gap);
  text-align: center;
}

/* Chữ ký. Dải sóng nằm trên, chữ nằm dưới — nên chữ phải là `display: block`
   (cả cụm nằm trong một <p>, `.wave` thì là inline-flex). */
.about-sign { margin-top: 3rem; }
.about-sign .wave { justify-content: center; }
.about-sign__text {
  display: block;
  max-width: 34rem;
  margin: 1rem auto 0;
  font-size: 1.1875rem;
  font-weight: 600;
  line-height: 1.7;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: break-word;
}

.about-cta { margin-top: 2.5rem; }
/* `.btn` thường, KHÔNG phải `.btn--hero`: đây là một lối đi tiếp cho ai vừa đọc
   xong, không phải nút chính của cả site như ở trang chủ. */
.about-cta .btn { display: inline-block; }

@media (max-width: 56.25em) {
  .about { --about-gap: 3.75rem; }
  .about-hero { padding-top: 0.25rem; }
  .about-hero__title,
  .about-hero__lead { max-width: none; }
  .about-featured { padding: 2.5rem 1.25rem; }
}

@media (max-width: 38.75em) {
  /* Trục hẹp lại và chấm nhỏ đi; đoạn nối tự theo vì nó tính bằng calc từ
     chính bốn biến này. */
  .about-chapters {
    --rail: 1.5rem;
    --dot: 0.375rem;
    --dot-top: 0.375rem;
    --chapter-gap: 2.5rem;
  }
  .about-hero__lead { padding-left: 1rem; }
  .about-say { padding-left: 0.875rem; }
  .about-featured { padding-inline: 1rem; }
}

/* ----------------------------------------------- Cỡ chữ mobile (+2px)
   Dưới 56.25em (mốc hai cột xếp dọc — xem docs/ui.md mục 6) mọi cỡ chữ tăng
   thêm 0.125rem, tức +2px theo cách quy đổi của bảng token ở :root đầu file.
   Chỉ đụng thang --fs-*, không đụng --fs-bump (bump là phần CỘNG THÊM cho khối
   nội dung; sửa nó nữa thì phần đó bị cộng hai lần).

   Vài rule khai cứng rem thay vì dùng token nên phải bump tay ở dưới. Khối này
   nằm CUỐI FILE có chủ ý: cùng độ đặc hiệu thì rule sau thắng, nên nó phải đứng
   sau @media (max-width: 38.75em) ở giữa file — chỗ .hero__title bị khai lại.
   Thêm rule khai cứng cỡ chữ ở đâu đó trong file thì nhớ bump ở đây luôn. */
@media (max-width: 56.25em) {
  :root {
    --fs-2xs: 0.84375rem;  /* 13.5px */
    --fs-xs: 0.875rem;     /* 14px   */
    --fs-sm: 0.90625rem;   /* 14.5px */
    --fs-md: 0.9375rem;    /* 15px   */
    --fs-lg: 0.96875rem;   /* 15.5px */
    --fs-xl: 1rem;         /* 16px   */
    --fs-body: 1.0625rem;  /* 17px   */
    --fs-2xl: 1.125rem;    /* 18px   */
    --fs-3xl: 1.1875rem;   /* 19px   */
  }

  .page-head h1 { font-size: 1.375rem; }
  .step-btn { font-size: 1.25rem; }
  .now__ko { font-size: clamp(1.375rem, 1.125rem + 1.6vw, 1.6875rem); }
  .hero__title { font-size: 2.25rem; }
  .entry__title h1 { font-size: 1.875rem; }
  .about-hero__title { font-size: 2.125rem; }
  .about-hero__lead { font-size: 1.5rem; }
  .chapter__title { font-size: 1.4375rem; }
  .about-chapters .about-say p { font-size: 1.25rem; }
  .about-featured__lead { font-size: 1.3125rem; }
  .about-featured__punch { font-size: 1.5rem; }
  .about-sign__text { font-size: 1.3125rem; }
}

@media (max-width: 38.75em) {
  .hero__title { font-size: 1.75rem; }
  .about-hero__title { font-size: 1.75rem; }
  .about-hero__lead { font-size: 1.3125rem; }
  .chapter__title { font-size: 1.3125rem; }
  .about-chapters .about-say p { font-size: 1.125rem; }
  .about-featured__lead { font-size: 1.1875rem; }
  .about-featured__punch { font-size: 1.3125rem; }
  .about-sign__text { font-size: 1.1875rem; }
}
