/* ===================================================================
   MINADORI — TẦNG THÀNH PHẦN DÙNG CHUNG (common component layer)
   ===================================================================

   VÌ SAO FILE NÀY TỒN TẠI
   Đo ngày 2026-10-18 trên 167 trang HTML: mỗi trang tự viết lại trong
   <style> của mình cùng một bộ thành phần, với các biến thể lệch nhau:

     .admin-bar + .brand-mini + .logo   98 trang (3 biến thể padding/margin)
     modal (.modal/.modal-box/.modal-overlay)  43 trang (3 hệ song song)
     .toolbar + .search + select        35 trang (4 biến thể)
     .pagination-bar + .pg-btn          25 trang (3 biến thể)
     .ed-card/.edit-head/.save-bar      18 trang
     .tabs/.tab-btn                     16 trang (3 biến thể)

   Hệ quả thực tế, không chỉ là chuyện gọn code:
     — Sửa 1 lỗi giao diện phải sửa 25–98 chỗ, và lần nào cũng sót vài chỗ.
       Đó chính là lý do cùng một nút "Trang sau" có 3 kiểu khác nhau tuỳ
       màn, và cùng một ô tìm kiếm cao 36px ở màn này, 42px ở màn kia.
     — 126 chỗ `outline:none` rải khắp 83 trang xoá vòng focus của bàn phím
       mà KHÔNG thay bằng gì → người dùng tab bằng bàn phím mất dấu hoàn
       toàn (WCAG 2.4.7). Sửa ở đây một lần là xong cả 83 trang.
     — Không trang nào xử lý riêng mobile cho modal/toolbar/bảng dữ liệu.

   THỨ TỰ CASCADE — ĐỌC TRƯỚC KHI SỬA
   File này được <link> SAU style.css và TRƯỚC khối <style> của từng trang.
   Nên:
     • Trang nào còn giữ rule riêng thì rule của trang vẫn THẮNG (cùng
       specificity, nằm sau) → gỡ trùng lặp dần dần được, không phải sửa
       98 trang trong một lần.
     • Trang nào đã gỡ rule trùng thì nhận bản chuẩn ở đây.
   Giá trị trong file này được chọn là BIẾN THỂ ĐA SỐ (xem chú thích tỉ lệ
   ở từng khối) nên gỡ trùng lặp không làm đổi diện mạo.

   `!important` chỉ dùng ở ĐÚNG MỘT chỗ: vòng focus bàn phím — vì phải
   thắng 126 dòng `outline:none` nằm sau trong cascade. Mọi chỗ khác không
   dùng.

   MỤC LỤC
     1. Thanh đầu trang (.admin-bar)
     2. Thanh công cụ danh sách (.toolbar)
     3. Khối nội dung (.panel, .ed-card, .edit-head, .save-bar)
     4. Tab (.tabs/.tab-btn)
     5. Phân trang (.pagination-bar/.pg-btn)
     6. Modal — 3 hệ gộp về một diện mạo
     7. Bảng dữ liệu đáp ứng (.table-scroll)
     8. Trạng thái: rỗng / đang tải / lỗi
     9. Nhãn trạng thái (.badge-*, .chip)
    10. Hành động theo lô (.bulk-actions)
    11. Tiện ích + hệ thang chữ
    12. A11Y — vòng focus bàn phím (toàn cục)
    13. Mobile (≤640px) + vùng chạm 44px
   =================================================================== */


/* ── 1. Thanh đầu trang ─────────────────────────────────────────────
   Repo có HAI kiểu thanh đầu trang dùng CHUNG class `.admin-bar`:
     • kiểu SÁNG (54 trang học viên) — nền trong suốt, `.brand` + `.nav-pills`,
       đã được định nghĩa trong style.css.
     • kiểu TỐI (98 trang quản trị) — nền var(--ink), `.brand-mini` + <nav>,
       mà 98 trang kia TỰ VIẾT LẠI trong <style> của mình.
   Không có class nào phân biệt hai kiểu, nên ở đây dùng chính dấu hiệu có
   thật trong markup: `:has(.brand-mini)`. Bọc trong `:where()` để
   specificity vẫn chỉ bằng `.admin-bar` — nhờ vậy rule riêng của trang
   (nằm sau file này) vẫn thắng, gỡ trùng lặp được từng trang một.
   Biến thể đa số: 40/98 trang. */
.admin-bar:where(:has(.brand-mini)) {
  display: flex; align-items: center; gap: 14px;
  background: var(--ink); color: #fff;
  border-radius: var(--radius);
  padding: 14px 22px; margin-bottom: 24px;
}
.admin-bar .brand-mini {
  display: flex; align-items: center; gap: 9px;
  font-weight: 800; flex-shrink: 0;
  color: inherit; text-decoration: none;
}
.admin-bar .brand-mini .logo {
  width: 30px; height: 30px; border-radius: 9px;
  background: linear-gradient(135deg, var(--c-lesson), var(--c-ai));
  display: grid; place-items: center;
  font-family: var(--font-jp); font-weight: 900;
  flex-shrink: 0;
}
/* 98/98 trang để <img class="logo"> — ảnh cần object-fit, nếu không sẽ méo
   khi tỉ lệ file khác 1:1. */
.admin-bar .brand-mini img.logo { object-fit: cover; }
.admin-bar .badge {
  background: var(--c-quiz); color: var(--ink);
  font-weight: 800; font-size: .74rem; letter-spacing: .05em;
  padding: 4px 10px; border-radius: 999px;
  flex-shrink: 0; white-space: nowrap;
}
/* Chỉ thanh TỐI: <nav> không class. Trang học viên dùng .nav-pills (style.css). */
.admin-bar:where(:has(.brand-mini)) nav {
  margin-left: auto; display: flex; gap: 4px;
  min-width: 0; overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
}
.admin-bar:where(:has(.brand-mini)) nav::-webkit-scrollbar { display: none; }
.admin-bar:where(:has(.brand-mini)) nav a,
.admin-bar:where(:has(.brand-mini)) nav .nav-item-has-children { flex-shrink: 0; }
.admin-bar:where(:has(.brand-mini)) nav a {
  color: rgba(255,255,255,.75); text-decoration: none;
  font-weight: 600; font-size: .86rem; white-space: nowrap;
  padding: 8px 13px; border-radius: 999px;
  transition: background .18s var(--ease), color .18s var(--ease);
}
.admin-bar:where(:has(.brand-mini)) nav a:hover,
.admin-bar:where(:has(.brand-mini)) nav a.on { background: rgba(255,255,255,.15); color: #fff; }
.admin-bar .an-avatar-btn,
.admin-bar .lang-select { flex-shrink: 0; }

/* Ô chọn ngôn ngữ ở thanh SÁNG của học viên.
   Rule `.lang-select` trong style.css viết cho thanh TỐI (nền var(--ink)):
   background rgba(255,255,255,.12) + color #fff. Mang nguyên sang thanh SÁNG
   (nền trang --bg #F0EDE6) thì nền hòa ra gần như trắng, còn chữ vẫn trắng
   → "trắng trên trắng", đo được tương phản 1.15:1, KHÔNG đọc được
   (User report 2026-10-21 — task 73755234, trang chủ học viên).
   Thanh SÁNG không có .brand-mini nên phải LOẠI TRỪ nó — cùng cơ chế với
   các rule .admin-bar:where(:has(.brand-mini)) ở trên.
   Dùng `> .lang-select` (con trực tiếp của thanh) để KHÔNG đụng ô chọn
   ngôn ngữ riêng trong settings.html — ô đó nằm trong .set-row. */
.admin-bar:not(:has(.brand-mini)) > .lang-select {
  background: var(--surface);
  color: var(--ink);
  border-color: var(--line);
}


/* ── 2. Thanh công cụ danh sách ─────────────────────────────────────
   Khuôn chung của mọi màn danh sách: [ô tìm kiếm co giãn] [bộ lọc…]
   [nút thêm]. Biến thể đa số 17/35. */
.toolbar {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 10px; margin-bottom: 16px;
}
/* `.search` đứng một mình (KHÔNG chỉ trong .toolbar): 10 màn đặt ô tìm kiếm
   ngay trên danh sách chứ không nhét vào thanh công cụ, và trước đây chúng
   hiện ra dưới dạng <input> trần của trình duyệt giữa một trang đã bo tròn
   hết — lạc hẳn khỏi phần còn lại. `flex: 1` thì vẫn CHỈ trong .toolbar:
   ngoài ngữ cảnh flex nó vô nghĩa, mà trong .toolbar nó là thứ giữ ô tìm
   kiếm giãn hết chỗ trống. */
.search {
  display: flex; align-items: center; gap: 9px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 999px; padding: 9px 16px;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.toolbar .search { flex: 1; min-width: 180px; }
/* Ô tìm kiếm là nơi gõ nhưng viền lại nằm ở thẻ bọc — người dùng không có
   dấu hiệu nào cho biết con trỏ đang ở trong đó. :focus-within chuyển dấu
   hiệu đó ra thẻ bọc, đúng chỗ mắt đang nhìn. */
.search:focus-within {
  border-color: var(--c-ai);
  box-shadow: 0 0 0 3px var(--c-ai-soft);
}
.search input {
  flex: 1; border: none; background: none; outline: none;
  font-family: inherit; font-size: .92rem; color: var(--ink);
  min-width: 0;
}
/* Nút ✕ mặc định của <input type="search"> trên WebKit nhỏ hơn 44px và
   không nhận được vòng focus — ẩn đi, người dùng xoá bằng phím như mọi ô
   khác. */
.search input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
.toolbar select {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 999px; padding: 9px 16px;
  font-family: inherit; font-weight: 600; font-size: .86rem;
  color: var(--ink); cursor: pointer;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.toolbar select:hover { border-color: var(--c-ai); }
/* `:not(.btn)` là CỐ Ý: 18 trang dùng `class="btn add"`, và `.toolbar .add`
   (specificity 0,2,0) sẽ thắng `.btn` (0,1,0) của style.css → nút tụt từ
   44px xuống 34px, mất cả vùng chạm. Nút nào đã là `.btn` thì giữ nguyên
   kiểu `.btn`; `.add` ở đây chỉ dành cho nút pill nhỏ không có `.btn`. */
.toolbar .add:not(.btn) {
  background: var(--c-quiz); color: var(--ink);
  border: none; border-radius: 999px;
  padding: 9px 16px;
  font-family: inherit; font-weight: 700; font-size: .86rem;
  cursor: pointer; text-decoration: none;
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap;
  transition: transform .15s var(--ease), box-shadow .2s var(--ease), filter .2s;
}
.toolbar .add:not(.btn):hover { transform: translateY(-1px); box-shadow: var(--shadow-soft); }
.toolbar .add:not(.btn):active { transform: translateY(0); }
.toolbar .add:not(.btn):disabled,
.toolbar .add:not(.btn)[aria-disabled="true"] {
  opacity: .45; cursor: not-allowed; transform: none; filter: grayscale(.35);
}


/* ── 3. Khối nội dung ───────────────────────────────────────────────── */
.panel { padding: 24px; }
.panel-head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 10px; margin-bottom: 16px;
}
.panel-head h2 { font-size: 1.05rem; font-weight: 700; }

/* Màn soạn/sửa: tiêu đề + nút quay lại + nhóm hành động bên phải */
.edit-head {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 14px; margin-bottom: 20px;
}
.edit-head h1 { font-size: 1.5rem; font-weight: 800; letter-spacing: -.02em; }
.edit-head .back {
  color: var(--ink-soft); text-decoration: none;
  font-weight: 600; font-size: .9rem;
  display: inline-flex; align-items: center; gap: 6px;
  transition: color .18s var(--ease);
}
.edit-head .back:hover { color: var(--ink); }
.edit-head .actions {
  margin-left: auto; display: flex; gap: 10px; flex-wrap: wrap;
}

.back-link {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: .88rem; font-weight: 600; color: var(--ink-soft);
  text-decoration: none; margin-bottom: 16px;
  transition: color .18s var(--ease), gap .18s var(--ease);
}
.back-link:hover { color: var(--ink); gap: 9px; }

/* Thẻ form của màn soạn nội dung */
.ed-card { padding: 22px; margin-bottom: 18px; }
.ed-card h3 { font-size: .98rem; font-weight: 800; margin-bottom: 14px; }
.ed-card label {
  display: block; font-size: .82rem; font-weight: 600;
  color: var(--ink-soft); margin-bottom: 4px;
}
.ed-card input, .ed-card select, .ed-card textarea {
  width: 100%; box-sizing: border-box;
  border: 2px solid var(--line); border-radius: 10px;
  background: var(--bg); padding: 10px 13px; margin-bottom: 13px;
  font-family: inherit; font-size: .95rem; color: var(--ink);
  outline: none;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.ed-card input:focus, .ed-card select:focus, .ed-card textarea:focus {
  border-color: var(--c-ai); background: var(--surface);
}
.ed-card textarea { resize: vertical; min-height: 50px; }

/* Thanh lưu dính đáy màn hình */
.save-bar {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; flex-wrap: wrap; gap: 14px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 20px;
}
.save-bar .btn-delete { background: #fdecea; color: #c62828; margin-left: auto; }
.save-err { color: #c62828; font-size: .86rem; }


/* ── 4. Tab ─────────────────────────────────────────────────────────── */
.tabs {
  display: flex; gap: 2px; margin-bottom: 16px;
  border-bottom: 2px solid var(--line);
  overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
}
.tabs::-webkit-scrollbar { display: none; }
.tab-btn {
  flex-shrink: 0;
  padding: 10px 18px; margin-bottom: -2px;
  border: none; border-bottom: 3px solid transparent; background: none;
  font-family: inherit; font-weight: 600; font-size: .9rem;
  color: var(--ink-soft); white-space: nowrap; cursor: pointer;
  transition: color .15s var(--ease), border-color .15s var(--ease);
}
.tab-btn:hover { color: var(--ink); }
.tab-btn.on { border-bottom-color: var(--c-ai); color: var(--c-ai); }


/* ── 5. Phân trang ─────────────────────────────────────────────────── */
.pagination-bar {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 14px; margin-top: 14px;
}
.pagination-bar:empty { display: none; }
.pagination-bar .pg-info { font-size: .82rem; color: var(--ink-soft); }
.pagination-bar .pg-buttons { display: flex; gap: 6px; flex-wrap: wrap; }
.pg-btn {
  border: 1px solid var(--line); background: var(--surface);
  border-radius: 8px; padding: 6px 11px;
  font-family: inherit; font-size: .82rem; font-weight: 600;
  color: var(--ink); cursor: pointer;
  transition: background .15s var(--ease), border-color .15s var(--ease), color .15s var(--ease);
}
.pg-btn:hover:not(:disabled) { background: var(--c-ai-soft); border-color: var(--c-ai); }
.pg-btn.pg-active { background: var(--c-ai); border-color: var(--c-ai); color: #fff; }
.pg-btn:disabled { opacity: .4; cursor: not-allowed; }


/* ── 6. Modal ───────────────────────────────────────────────────────
   Repo có 3 hệ đặt tên song song, sinh ra do 3 giai đoạn khác nhau:
     .modal-backdrop > .modal        (41 trang)
     .modal-overlay  > .modal-box    (27 trang)
     .modal-field / .modal-actions   (hỗn hợp cả hai)
   Không đổi tên class ở đây (đổi là phải sửa JS của 43 trang). Thay vào
   đó cho cả 3 hệ CÙNG một diện mạo và CÙNG hành vi mobile. */
.modal-backdrop, .modal-overlay {
  display: none; position: fixed; inset: 0; z-index: 200;
  align-items: center; justify-content: center;
  padding: 16px;
  /* Giữ ĐÚNG giá trị 37 trang đang dùng để gỡ trùng lặp được; lớp mờ phía
     sau là thuộc tính MỚI nên không cản việc đó. */
  background: rgba(0,0,0,.45);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
.modal-backdrop.open, .modal-overlay.open, .modal-overlay.show { display: flex; }

.modal, .modal-box {
  background: #fff; border-radius: var(--radius);
  box-shadow: 0 8px 40px rgba(0,0,0,.18);
  width: 100%;
  max-height: min(90vh, 900px); overflow-y: auto;
  padding: 28px 26px;
  /* Hiện ra có hướng (đi lên + lớn dần) thay vì nhảy thẳng vào mặt.
     @media (prefers-reduced-motion) ở style.css đã tắt animation toàn cục
     nên không cần lặp lại ở đây. */
  animation: modal-in .22s var(--ease);
}
/* `max-width` CHỈ cho `.modal` (460px — biến thể đa số của 10 trang).
   KHÔNG đặt cho `.modal-box`: 24 trang dùng nó với bề rộng riêng khai báo
   bằng `width: min(540px, 94vw)`, mà `max-width` thì ĐÈ ĐƯỢC `width` —
   đo được modal "Thêm Quản trị viên" bị bóp từ 540px xuống 460px. */
.modal { max-width: 460px; }

@keyframes modal-in {
  from { opacity: 0; transform: translateY(12px) scale(.98); }
  to   { opacity: 1; transform: none; }
}
.modal h3, .modal-box h3 { font-size: 1.1rem; font-weight: 700; margin-bottom: 18px; }
.modal .ed-sub, .modal-sub {
  font-size: .85rem; color: var(--ink-soft); margin-bottom: 14px; line-height: 1.5;
}
.modal label, .modal-field label {
  display: block; font-size: .82rem; font-weight: 600;
  color: var(--ink-soft); margin-bottom: 4px;
}
.modal input, .modal select, .modal textarea,
.modal-field input, .modal-field select, .modal-field textarea {
  width: 100%; box-sizing: border-box;
  border: 1px solid var(--line); border-radius: 8px;
  padding: 9px 12px; margin-bottom: 13px;
  font-family: inherit; font-size: .92rem; color: var(--ink);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.modal input:focus, .modal select:focus, .modal textarea:focus,
.modal-field input:focus, .modal-field select:focus, .modal-field textarea:focus {
  outline: none; border-color: var(--c-ai); box-shadow: 0 0 0 3px var(--c-ai-soft);
}
.modal textarea, .modal-field textarea { resize: vertical; min-height: 70px; }
.modal .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.modal-field { margin-bottom: 14px; }
.modal .actions, .modal-actions {
  display: flex; gap: 10px; justify-content: flex-end; margin-top: 6px;
  flex-wrap: wrap;
}
.modal-err { color: #c0392b; font-size: .85rem; margin-top: 8px; display: none; }
.modal-ok, .modal-success {
  display: none; background: var(--c-vocab-soft); color: #0a7d5a;
  border-radius: var(--radius-sm); padding: 12px 14px; margin-top: 10px;
  font-size: .88rem; line-height: 1.6;
}


/* ── 7. Bảng dữ liệu đáp ứng ────────────────────────────────────────
   Màn danh sách của repo dùng CSS grid cột cố định (.st-head/.st-row…).
   Trên điện thoại chúng đẩy trang rộng quá khung nhìn, và Chrome khi
   `isMobile:true` không cho cuộn ngang mà THU NHỎ CẢ TRANG — chữ co lại
   còn ~57% (đo trong tests/mobile-fit.spec.mjs).
   Cách đúng, cũng là cách spec đó coi là chủ ý: bọc bảng trong một vùng
   cuộn ngang riêng. Dùng: <div class="table-scroll"> …bảng… </div> */
.table-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
/* Nền chuyển màu ở rìa phải để người dùng thấy "còn nữa bên phải" — không
   có nó thì vùng cuộn ngang trên mobile vô hình. */
.table-scroll {
  background:
    linear-gradient(to right, var(--surface) 30%, rgba(250,250,248,0)) left center / 28px 100% no-repeat,
    linear-gradient(to left,  var(--surface) 30%, rgba(250,250,248,0)) right center / 28px 100% no-repeat;
  background-attachment: local, local;
}
.table-scroll > * { min-width: max-content; }

/* Dòng danh sách bấm được: phản hồi khi trỏ vào */
.row-hover { transition: background .15s var(--ease); }
.row-hover:hover { background: var(--c-ai-soft); }


/* ── 8. Trạng thái: rỗng / đang tải / lỗi ──────────────────────────── */
/* KHÔNG đưa `.empty` vào đây: 4 trang dùng nó làm MODIFIER
   (`.cq-preview.empty`, `.jlb-count.empty`, `.cc-teacher.empty`,
   `.vg-word-answer.empty`) nên một rule `.empty` dùng chung sẽ chèn padding
   vào khối của họ — đo được +32px chiều cao ở admin-jft-exam-edit.html.
   Dùng `.empty-state` hoặc `.list-empty` cho trạng thái rỗng. */
.empty-state {
  color: var(--ink-soft); font-size: .9rem;
  padding: 30px; text-align: center;
}
.hub-access-denied {
  padding: 40px; text-align: center;
  color: var(--ink-soft); font-size: .94rem;
}

/* Khung chờ (skeleton) — thay cho chữ "Đang tải…" trần.
   Dùng: <div class="skeleton" style="height:56px"></div> */
.skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--line) 25%, var(--surface) 37%, var(--line) 63%);
  background-size: 400% 100%;
  animation: skeleton-sweep 1.3s ease-in-out infinite;
}
@keyframes skeleton-sweep {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}
.skeleton + .skeleton { margin-top: 10px; }

/* Vòng xoay chờ dùng chung */
.spinner {
  width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--c-ai);
  animation: spin .7s linear infinite;
  display: inline-block; vertical-align: -3px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Khối "đang tải" căn giữa.
   `.spinner` ở trên CHÍNH LÀ cái vòng tròn đang quay, nên không được gắn nó
   lên thẻ bọc có chữ: thẻ bọc sẽ thành hình tròn 2px quay tròn và dòng chữ
   quay theo. 10 trang học viên mắc đúng lỗi này (phát hiện 2026-10-21 trên
   jft-exams.html) vì chúng khai `.spinner { text-align:center; padding:40px }`
   trong <style> của trang — rule trang chỉ ĐÈ 3 thuộc tính đó, còn
   width/height/border-radius/border/animation của component vẫn nguyên.

   Đúng cách: thẻ bọc là `.loading-block`, vòng xoay nằm BÊN TRONG.
     <div class="loading-block" role="status" aria-live="polite">
       <span class="spinner" aria-hidden="true"></span>
       <span data-i18n="...">Đang tải…</span>
     </div>
   (đừng đặt `data-i18n` lên thẻ bọc — applyStaticTranslations() ghi thẳng
   textContent nên nó xoá luôn vòng xoay bên trong.)

   `grid-column` để khối trải hết hàng khi cha là lưới; thiếu nó thì khối co
   vào 1 ô lưới và dính góc trên bên trái thay vì ra giữa. Thuộc tính này vô
   hại khi cha không phải lưới, nên dùng được cho cả danh sách dọc. */
.loading-block {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; min-height: 42vh; padding: 40px 20px;
  color: var(--ink-soft); font-size: .9rem; text-align: center;
}
.loading-block .spinner { width: 30px; height: 30px; border-width: 3px; vertical-align: baseline; }

/* ── Lớp chờ TOÀN MÀN (`.page-loading`) — tiêu chuẩn chung từ 2026-10-25 ──
   JS dựng khối này: assets/loading.js (số đo + lý do từng nét ở đầu file đó).
   Đo trước đợt này bằng tests/_loading-audit.mjs trên 175 trang: 82 trang
   không hiện dấu hiệu chờ nào (4 trang 0 ký tự = màn trắng trơn), 45 trang
   có hiện nhưng lệch tâm tới (-528,-187) hoặc (0,+492).

   `position:fixed; inset:0; place-items:center` → tâm KHUNG NHÌN, độc lập
   bố cục trang. `.loading-block` ngay trên phải nhờ `grid-column:1/-1` mới ra
   giữa khi cha là lưới; cách này không cần biết cha là gì — đó là lý do nó
   thay cho mọi biến thể tự chế. */
.page-loading {
  position: fixed; inset: 0; z-index: 3500;   /* > nav 1000, < toast 4000 */
  display: grid; place-items: center; padding: 20px;
  background: rgba(240, 237, 230, .66);       /* = --bg pha mờ */
  -webkit-backdrop-filter: saturate(130%) blur(3px);
  backdrop-filter: saturate(130%) blur(3px);
  opacity: 0; transition: opacity .18s ease-out;
  /* Chặn click xuyên màn hình trong lúc loading (sửa Bug #74048187):
     Khi chưa hiển thị/đang mờ, pointer-events: none để không che chuột.
     Khi lớp loading kích hoạt (.is-in), chuyển pointer-events: auto để chặn
     người dùng bấm vào các menu/màn hình khác trong lúc đang tải dữ liệu.
     Chỉ .pl-retry bên trong thẻ mở lại chuột khi có lỗi mạng. */
  pointer-events: none;
}
/* [hidden] (0,2,0) phải thắng display:grid (0,1,0) — cùng lớp bẫy đã làm
   .hide thua display:flex ba lần trong repo này. Viết tường minh. */
.page-loading[hidden] { display: none; }
.page-loading.is-in { opacity: 1; pointer-events: auto; }
.page-loading.is-error { pointer-events: none; }

.page-loading-card {
  display: flex; flex-direction: column; align-items: center; gap: 13px;
  min-width: 186px; max-width: min(340px, 86vw);
  padding: 26px 32px 24px; text-align: center;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  transform: translateY(8px) scale(.96);
  transition: transform .26s var(--ease);
}
.page-loading.is-in .page-loading-card { transform: none; }

/* Vòng xoay: viền-và-animation là bản nền chạy ở mọi trình duyệt; bản cung
   gradient (đẹp hơn) chỉ bật khi có `mask`, vì không có mask thì conic-
   gradient hiện ra cả một ĐĨA tròn đặc đang quay. */
.pl-ring {
  width: 52px; height: 52px; border-radius: 50%;
  border: 4px solid var(--line); border-top-color: var(--c-ai);
  animation: spin .75s linear infinite;
}
@supports ((-webkit-mask: radial-gradient(#000, #000)) or (mask: radial-gradient(#000, #000))) {
  .pl-ring {
    border: 0;
    background: conic-gradient(from 90deg, rgba(108, 92, 231, 0) 0deg,
                var(--c-ai) 250deg, var(--c-vocab) 360deg);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4.5px), #000 calc(100% - 4px));
    mask: radial-gradient(farthest-side, transparent calc(100% - 4.5px), #000 calc(100% - 4px));
  }
}
/* Thanh chạy vô định: nói được "dữ liệu đang về", thứ một vòng xoay đơn
   không nói — và là cái duy nhất còn nhúc nhích nếu font/màu bị chặn. */
.pl-bar { width: 128px; height: 3px; border-radius: 999px; background: var(--line); overflow: hidden; }
.pl-bar > i {
  display: block; width: 42%; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--c-ai), var(--c-vocab));
  animation: pl-sweep 1.15s ease-in-out infinite;
}
@keyframes pl-sweep {
  from { transform: translateX(-110%); }
  to   { transform: translateX(240%); }
}
.pl-label { font-size: .92rem; font-weight: 600; color: var(--ink); }
.pl-hint  { font-size: .78rem; color: var(--ink-soft); line-height: 1.4; }
.page-loading .pl-retry { pointer-events: auto; margin-top: 2px; }
/* Lần thứ tư trong repo này: `.btn` (style.css) đặt `display:inline-flex` với
   specificity 0,1,0, còn `hidden` chỉ là rule của TRÌNH DUYỆT — rule tác giả
   nào cũng thắng nó. Nên `<button class="btn" hidden>` vẫn HIỆN. Đo được: nút
   "Tải lại" nằm sẵn trong thẻ chờ ngay từ giây đầu. Phải khai tường minh. */
.page-loading .pl-retry[hidden],
.page-loading .pl-hint[hidden] { display: none; }

/* Hết giờ (32s, quá hạn đọc 30s của apiFetch): đổi hẳn sang trạng thái lỗi
   thay vì quay mãi. Vòng xoay vĩnh viễn là bản đẹp hơn của màn trắng xoá
   (PR #231) — người dùng vẫn mắc kẹt, chỉ là có cái nhìn. */
.page-loading.is-error .pl-ring,
.page-loading.is-error .pl-bar { display: none; }
.page-loading.is-error .page-loading-card::before { content: '⚠️'; font-size: 1.5rem; line-height: 1; }

@media (prefers-reduced-motion: reduce) {
  /* style.css đã tắt animation toàn cục, nên vòng xoay đứng im: cho viền kín
     + mờ để nó trông như "đang chờ", không phải một hình bị lỗi. */
  .pl-ring { background: none; border: 4px solid var(--c-ai); opacity: .45; }
  .pl-bar { display: none; }
}

/* Lớp chờ giữa màn đang hiện thì ẩn khối chờ tại chỗ của trang: HAI vòng xoay
   cùng lúc là lỗi giao diện.
   `visibility` chứ không `display`: (a) không đổi chiều cao nên danh sách
   không nhảy một nhịp lúc lớp chờ tắt; (b)
   tests/loading-block-centered.spec.mjs đo hình học + computed style của
   `.loading-block`, `display:none` sẽ đưa số đo về 0 và spec đỏ vì lý do không
   liên quan.
   Chỉ áp trong đợt đọc ĐẦU của trang — sau đó assets/loading.js tắt chế độ tự
   động, nên skeleton của mọi lần tìm kiếm/phân trang về sau hiện bình thường. */
body:has(#page-loading.is-in) .loading-block,
body:has(#page-loading.is-in) .skeleton,
/* …và cả dòng chữ "Đang tải…" tự chế của từng trang. 128 trang có một biến
   thể riêng (<p>, <div class="empty">, <td colspan>…) nên không có class
   chung để bắt — nhưng gần như tất cả đều mang khoá i18n kết thúc bằng
   "loading" (`data-i18n="loading"`, `adm_ldet_loading`, `stu_kana_loading`…).
   Bắt theo khoá là cách duy nhất phủ hết mà không phải sửa 128 file. */
body:has(#page-loading.is-in) [data-i18n="loading"],
body:has(#page-loading.is-in) [data-i18n$="_loading"] { visibility: hidden; }

/* Vòng xoay của `.loading-block` dùng chung diện mạo với lớp trên, để hai
   kiểu chờ trong cùng một sản phẩm không trông như hai sản phẩm. */
@supports ((-webkit-mask: radial-gradient(#000, #000)) or (mask: radial-gradient(#000, #000))) {
  .loading-block .spinner {
    border: 0;
    background: conic-gradient(from 90deg, rgba(108, 92, 231, 0) 0deg,
                var(--c-ai) 250deg, var(--c-vocab) 360deg);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3.5px), #000 calc(100% - 3px));
    mask: radial-gradient(farthest-side, transparent calc(100% - 3.5px), #000 calc(100% - 3px));
  }
}


/* ── 9. Nhãn trạng thái ────────────────────────────────────────────── */
.chip {
  display: inline-block;
  font-size: .68rem; font-weight: 700; padding: 2px 9px;
  border-radius: 999px; background: var(--bg); color: var(--ink-soft);
  white-space: nowrap;
}
.badge-pass, .badge-section {
  display: inline-block;
  font-size: .72rem; font-weight: 700; padding: 3px 9px;
  border-radius: 999px; white-space: nowrap;
}
.badge-pass.pass { background: var(--c-vocab-soft); color: var(--c-vocab); }
.badge-pass.fail { background: var(--c-lesson-soft); color: var(--c-lesson); }
.badge-section.safety { background: var(--c-lesson-soft); color: var(--c-lesson); }
.badge-section.manner { background: var(--c-quiz-soft); color: #c77f00; }
.badge-st-na { color: var(--ink-soft); font-size: .72rem; }


/* Nhãn trạng thái xuất bản — 13 trang danh sách nội dung dùng chung bộ này */
.status-badge {
  font-size: .72rem; font-weight: 700; padding: 4px 10px;
  border-radius: 999px; white-space: nowrap; text-align: center;
}
.status-badge.draft { background: var(--c-quiz-soft); color: #8a6500; }
.status-badge.published { background: var(--c-vocab-soft); color: var(--c-vocab); }

/* Nhãn trạng thái đơn tuyển dụng — 10 trang */
.tag-open { background: var(--c-vocab-soft); color: #0a7d5a; }
.tag-draft { background: var(--line); color: var(--ink-soft); }
.tag-closed { background: var(--c-lesson-soft); color: var(--c-lesson); }
.tag-other { background: var(--c-quiz-soft); color: #8a6500; }

/* KHÔNG đưa `.fg` (form group) vào đây — cùng lớp bẫy với `.empty`: 16 trang
   định nghĩa nó như "nhóm trường trong form" (có margin đáy), nhưng 3 trang
   khác (admin-safety-results, admin-vocab-progress, admin-interview-sessions)
   dùng CÙNG tên class cho ô lọc nằm NGANG hàng và không khai báo gì cả — thêm
   margin dùng chung làm thanh lọc của họ cao thêm 20px. Tên class 2 chữ cái
   quá chung để một tầng dùng chung sở hữu. */

/* Thẻ "mục" có đầu mục riêng, nội dung tràn sát mép — 9 trang */
.section-card { margin-bottom: 20px; padding: 0; overflow: hidden; }

/* ── 10. Hành động theo lô ─────────────────────────────────────────── */
.bulk-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.bulk-actions button {
  height: 34px; padding: 0 12px;
  border: 1.5px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--ink);
  font-family: inherit; font-size: .8rem; font-weight: 700;
  white-space: nowrap; cursor: pointer;
  transition: all .15s var(--ease);
}
.bulk-actions button:disabled { opacity: .45; cursor: not-allowed; }
.bulk-actions .bulk-publish {
  border-color: var(--c-vocab); color: #0a7d5a; background: var(--c-vocab-soft);
}
.bulk-actions .bulk-publish:hover:not(:disabled) { background: var(--c-vocab); color: #fff; }
.bulk-actions .bulk-unpublish {
  border-color: var(--c-quiz); color: #8a6500; background: var(--c-quiz-soft);
}
.bulk-actions .bulk-unpublish:hover:not(:disabled) { background: var(--c-quiz); color: #fff; }


/* ── 11. Tiện ích + hệ thang chữ ───────────────────────────────────── */

/* `!important` là CÓ CHỦ Ý, không phải cho tiện.
 *
 * File này nạp TRƯỚC khối <style> của từng trang. `.hide` và một class
 * thường như `.ei-panel` có CÙNG độ ưu tiên (0,1,0), nên khi trang viết
 * `.ei-panel { display:flex }` thì theo thứ tự nguồn nó THẮNG, và thêm class
 * `hide` vào phần tử đó KHÔNG ẩn được gì. Hỏng hoàn toàn IM LẶNG: markup
 * đúng, JS đúng, DevTools hiện `class="... hide"` — chỉ có phần tử vẫn nằm
 * đó.
 *
 * ĐÃ ĐẾM 2026-11-05: 50 chỗ trên 19 trang viết đúng hình dạng đó trong
 * markup tĩnh, và con số thật còn cao hơn vì phần lớn `hide` được JS gắn lúc
 * chạy (quét tĩnh không thấy) — đúng như ca user báo ở
 * admin-life-vocab-edit.html: ô giữ chỗ 🖼️ mang class `hide` mà
 * `getComputedStyle` vẫn trả `display:grid`, nên màn hiện HAI ảnh.
 * Hai trang (admin-jft-exam-edit, admin-jft-listening-bank) đã tự phát hiện
 * và chép tay đúng dòng `!important` này vào <style> của riêng mình — tức
 * bẫy đã được tìm ra hai lần mà không ai sửa ở gốc, nên nó cứ quay lại.
 *
 * AN TOÀN, đã đo chứ không đoán: (1) không selector nào trong toàn bộ
 * repo đặt `display` KHÁC `none` cho `.hide`; (2) không phần tử nào vừa
 * được bật/tắt bằng class `hide` vừa được JS gán `style.display` nội tuyến —
 * đó là chỗ duy nhất `!important` có thể ẩn nhầm.
 *
 * Cùng lý do Bootstrap để `.d-none { display:none !important }`: lớp tiện
 * ích phải thắng, nếu không nó chẳng tiện gì.
 */
.hide { display: none !important; }
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Thang chữ dùng chung — để các màn mới không lại tự chọn cỡ chữ riêng.
   Khớp với cỡ đang dùng ở đa số trang hiện tại. */
.t-h2 { font-size: 1.25rem; font-weight: 800; letter-spacing: -.02em; }
.t-h3 { font-size: 1.05rem; font-weight: 700; }
.t-h4 { font-size: .95rem; font-weight: 700; }
.t-body { font-size: .95rem; }
.t-sm { font-size: .86rem; }
.t-xs { font-size: .78rem; }
.t-soft { color: var(--ink-soft); }


/* ── 12. A11Y — vòng focus bàn phím (toàn cục) ─────────────────────
   Đo được: 126 dòng `outline:none`/`outline: none` nằm trong <style> của
   83 trang, không trang nào thay bằng dấu hiệu khác. Người dùng bàn phím
   (và người dùng phần mềm đọc màn hình đi kèm bàn phím) mất hoàn toàn dấu
   chỉ vị trí — WCAG 2.1 AA, mục 2.4.7 Focus Visible.

   `!important` ở đây là CẦN THIẾT, không phải tiện tay: những dòng
   `outline:none` kia nằm trong <style> của trang, tức SAU file này trong
   cascade và cùng specificity — không có !important thì chúng thắng và
   vòng focus không bao giờ hiện.

   Dùng `:focus-visible` (không phải `:focus`) nên CHUỘT bấm sẽ KHÔNG hiện
   vòng — diện mạo hiện tại của người dùng chuột không đổi một pixel nào. */
:is(a, button, input, select, textarea, summary, [tabindex], [role="button"], [role="tab"]):focus-visible {
  outline: 3px solid var(--c-ai) !important;
  outline-offset: 2px !important;
  border-radius: inherit;
}
/* User report 2026-10-20 (ảnh ô nhập Luyện AI) — hai vòng focus lồng nhau,
   lệch hẳn sang trái. Nguyên nhân: ô nhập nằm trong một "vỏ" đã vẽ sẵn viền
   (`.field`, bo tròn 999px, có padding và nút bên trong). Vòng focus vẽ lên
   INPUT lấy `border-radius: inherit` của vỏ nên ra hình viên thuốc, nhưng ôm
   đúng phần input — tức lệch khỏi vỏ đúng bằng phần padding.

   Vẽ lên VỎ thay vì lên input. Vẫn đúng yêu cầu trợ năng (bàn phím vẫn thấy
   vòng), mà hình thì khớp. 5 trang đang dùng khuôn `.field` này.

   Offset 0 (không phải 2px như mặc định): `.field` đã tự đổi màu viền khi
   focus, để hở 2px thì vòng ngoài và viền trong thành HAI vòng tím lồng
   nhau — vẫn đúng cái người dùng chụp. Áp sát thì thành một vòng dày.

   KHÔNG dùng `:where(.field)` cho vế tắt vòng: luật trên có `[tabindex]`
   trong `:is()`, mà `:is()` lấy độ ưu tiên của VẾ CAO NHẤT — tức (0,2,0),
   không phải (0,0,1) như nhìn vào chữ `input`. `:where()` góp 0 nên
   `:where(.field) > input:focus-visible` chỉ (0,1,1) và THUA, dù có
   `!important` và nằm sau. Viết thẳng `.field` để lên (0,2,1). */
.field:has(> input:focus-visible) {
  outline: 2px solid var(--c-ai) !important;
  outline-offset: 0 !important;
}
.field > input:focus-visible { outline: none !important; }

/* User report 2026-10-26 — Y HỆT lỗi trên, lần này ở ô tìm kiếm dùng chung
   `.search` (41 trang: 4 màn học viên + `.toolbar .search` của màn quản trị).
   Đo được trên notifications.html/assignments.html: `<input>` nhận vòng tím
   3px NẰM TRONG viên thuốc, lệch vào đúng phần padding trái (icon 🔍 nằm
   ngoài vòng), còn vỏ thì vẽ quầng `--c-ai-soft` của riêng nó — thành hai
   vòng đồng tâm.

   `.search input { outline: none }` ở mục 2 KHÔNG cứu được: nó không có
   `!important` nên thua luật a11y ở trên. Chữa đúng như `.field`: vẽ lên VỎ,
   tắt vòng của input.

   Vì sao `.search input` (descendant) chứ không phải `> input`: cả 41 chỗ
   hiện đều để input làm con trực tiếp, nhưng dấu `>` sẽ im lặng bỏ sót ngày
   có ai bọc thêm một lớp — mà triệu chứng của việc bỏ sót chính là lỗi này
   quay lại. Specificity vẫn (0,2,1), đủ thắng luật a11y (0,2,0).

   Offset 0: `.search:focus-within` đã đổi viền sang `--c-ai` rồi, để hở là
   lại thành hai vòng tím lồng nhau — cùng lý do với `.field`. */
.search:has(input:focus-visible) {
  outline: 2px solid var(--c-ai) !important;
  outline-offset: 0 !important;
}
.search input:focus-visible { outline: none !important; }

/* Trên nền tối (.admin-bar) màu tím không đủ tương phản → dùng trắng. */
.admin-bar:where(:has(.brand-mini)) :is(a, button, select, [tabindex]):focus-visible {
  outline-color: #fff !important;
}

/* Link "nhảy tới nội dung" cho người dùng bàn phím: ẩn tới khi được focus.
   Markup: <a class="skip-link" href="#main">Nội dung chính</a> */
.skip-link {
  position: absolute; left: 50%; top: -60px; transform: translateX(-50%);
  z-index: 999; background: var(--ink); color: #fff;
  padding: 10px 18px; border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  font-weight: 700; font-size: .88rem; text-decoration: none;
  transition: top .18s var(--ease);
}
.skip-link:focus-visible { top: 0; }


/* ── 13. Mobile ────────────────────────────────────────────────────── */
@media (max-width: 640px) {
  /* Thanh đầu trang (kiểu TỐI, 98 màn quản trị).
     Trước: 4 hàng xếp chồng — thương hiệu / chọn ngôn ngữ / nav / avatar —
     cao ~210px trên iPhone 13, tức gần 1/4 màn hình đầu tiên chỉ để hiện
     thanh điều hướng, nội dung thật bị đẩy xuống dưới nếp gấp.
     Sau: hàng 1 = thương hiệu + ngôn ngữ + avatar, hàng 2 = nav cuộn ngang.
     Thứ tự DOM là brand → lang → nav → avatar (site-nav.js nối thêm nav và
     avatar vào cuối), nên phải dùng `order` chứ không đổi được markup. */
  /* `body:not(.has-student-tabbar) .admin-bar` trong style.css đặt
     flex-direction:column cho MỌI thanh đầu trang ở màn hẹp (0,2,1) — cao
     hơn `.admin-bar:where(...)` (0,1,0) nên phải lặp lại cùng dạng selector
     ở đây thì mới thắng (file này nạp sau style.css). Thanh SÁNG của trang
     học viên vẫn giữ nguyên kiểu cột như cũ. */
  body:not(.has-student-tabbar) .admin-bar:where(:has(.brand-mini)) {
    flex-direction: row; align-items: center;
    flex-wrap: wrap; padding: 12px 14px; gap: 10px; border-radius: var(--radius-sm);
  }
  .admin-bar:where(:has(.brand-mini)) .brand-mini { order: 0; margin-right: auto; }
  .admin-bar:where(:has(.brand-mini)) .badge { order: 1; }
  .admin-bar:where(:has(.brand-mini)) .lang-select { order: 2; }
  /* Khối tài khoản do site-nav.js nối thêm là một <div> KHÔNG class (xem
     buildAvatarMenu) — trong thanh này chỉ nó là <div>, nên chọn như vậy
     là đủ và không cần sửa site-nav.js. Để cuối hàng 1, giống bố cục PC. */
  .admin-bar:where(:has(.brand-mini)) > :is(div, a.acct, .sa-menu-wrap) { order: 3; }
  .admin-bar:where(:has(.brand-mini)) nav { order: 4; margin-left: 0; flex-basis: 100%; }

  /* Thanh công cụ: ô tìm kiếm chiếm trọn hàng đầu, bộ lọc xuống dưới.
     Trước đây flex-wrap đẩy chúng thành từng hàng rời rạc cao 5–6 hàng. */
  .toolbar { gap: 8px; }
  .toolbar .search { flex-basis: 100%; min-width: 0; order: -1; }
  .toolbar select { flex: 1 1 140px; min-width: 0; max-width: 100%; }
  .toolbar .add { flex: 1 1 100%; justify-content: center; }

  /* Modal thành tấm trượt từ đáy — ngón tay với tới nút ở dưới, và không
     còn modal cao hơn màn hình bị cắt mất nút như trước. */
  .modal-backdrop, .modal-overlay { align-items: flex-end; padding: 0; }
  .modal, .modal-box {
    max-width: none; max-height: 92vh;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: 22px 18px calc(22px + env(safe-area-inset-bottom));
    animation: sheet-in .24s var(--ease);
  }
  @keyframes sheet-in {
    from { opacity: 0; transform: translateY(100%); }
    to   { opacity: 1; transform: none; }
  }
  .modal .row { grid-template-columns: 1fr; }
  .modal .actions, .modal-actions { justify-content: stretch; }
  .modal .actions > *, .modal-actions > * { flex: 1; justify-content: center; }

  .panel, .ed-card { padding: 16px 14px; }
  .edit-head { gap: 10px; margin-bottom: 14px; }
  .edit-head h1 { font-size: 1.2rem; }
  .edit-head .actions { margin-left: 0; width: 100%; }
  .edit-head .actions > * { flex: 1; justify-content: center; }
  .save-bar { padding: 12px 14px; gap: 10px; }
  .pagination-bar { justify-content: center; }

  /* Vùng chạm 44×44 (WCAG 2.5.5 / tests/touch-target.spec.mjs).
     Dùng min-height + padding dọc chứ không set height cố định, để nút có
     2 dòng chữ không bị tràn. */
  .pg-btn, .tab-btn, .toolbar select, .toolbar .add, .bulk-actions button {
    min-height: 44px;
  }
  .pg-btn { padding: 0 14px; }

  /* Thanh đầu trang cũng phải đạt 44px — đo được trên iPhone 13:
     mục nav 37px, ô chọn ngôn ngữ 29px, nút tài khoản 38px. Đây là những
     thứ người dùng bấm NHIỀU NHẤT trên điện thoại.
     Dùng min-height + inline-flex (không đặt height cứng) để nhãn dài 2
     dòng vẫn không bị tràn. */
  .admin-bar:where(:has(.brand-mini)) nav a {
    min-height: 44px; display: inline-flex; align-items: center; padding: 0 14px;
  }
  .admin-bar .lang-select { min-height: 44px; }
  .admin-bar :is(.sa-acct, .an-avatar-btn) { min-height: 44px; }
}

/* ===========================================================================
   Tiến độ bài học của một học viên — dùng chung cho 4 màn của quản lý
   (company-/admin-/kumiai-/kigyou-student-progress). Đi cùng
   assets/student-lesson-progress.js; tiền tố `slp-` để không đụng `lp-`
   (hàng bài học cũ) hay `prog-` (thanh tổng) đã có trong từng trang.
   User request 2026-10-19.
   =========================================================================== */
.slp-level { margin-bottom: 22px; }
.slp-level-head { display: flex; align-items: baseline; gap: 10px; font-size: .9rem; }
.slp-level-head .slp-level-sub { color: var(--ink-soft); font-size: .8rem; }
.slp-level-head .slp-level-pct { margin-left: auto; font-size: 1.15rem; font-weight: 800; color: var(--c-ai); }
.slp-level-bar { height: 8px; border-radius: 999px; background: var(--line); overflow: hidden; margin: 7px 0 12px; }
.slp-level-bar i { display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--c-ai), var(--c-lesson)); }

.slp-row { border: 1px solid var(--line); border-radius: var(--radius-sm); margin-bottom: 8px; }
.slp-row.is-current { border-color: var(--c-quiz); background: var(--c-quiz-soft); }
.slp-row > summary { display: grid; grid-template-columns: 40px 1fr auto 56px 86px;
  gap: 12px; align-items: center; padding: 11px 14px; cursor: pointer; list-style: none; }
.slp-row > summary::-webkit-details-marker { display: none; }
.slp-row > summary:hover { background: var(--bg); }
.slp-row .num { font-weight: 800; color: var(--c-ai); font-size: .95rem; }
.slp-row .info b { font-size: .92rem; }
.slp-row .slp-now-tag { font-style: normal; font-size: .7rem; font-weight: 700;
  color: #8a6500; margin-left: 7px; }
.slp-row .slp-pct { font-weight: 800; font-size: .95rem; text-align: right; }
.slp-row .st { font-size: .72rem; font-weight: 700; padding: 4px 9px;
  border-radius: 999px; text-align: center; }
.slp-row .st.done { background: var(--c-vocab-soft); color: #0a7d5a; }
.slp-row .st.now { background: var(--c-quiz-soft); color: #8a6500; }
.slp-row .st.todo { background: var(--bg); color: var(--ink-soft); }
/* "Đã làm xong, chưa đạt" — đã nộp bài 問 nhưng điểm dưới ngưỡng. Màu
   cam-đỏ (--c-lesson) để KHÁC hẳn "đang học" (vàng) và "đã xong" (xanh):
   nội dung đã 100% mà vẫn chưa đạt thì không được tô như đang làm dở.
   (Freedcamp 73873895 / 73874152 / 73874330) */
.slp-row .st.retry { background: var(--c-lesson-soft); color: #c0392b; }

.slp-badges { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; }
/* Điểm Quiz của bài — trước đây dòng bài không in điểm nào, nên trung tâm
   không thấy kết quả quiz dù học viên có (Freedcamp 73874350). Đặt chung ô
   với badge kỹ năng để không phá lưới 5 cột của dòng. */
.slp-quiz { font-size: .7rem; font-weight: 700; padding: 2px 7px; border-radius: 999px;
  background: var(--c-ai-soft); color: var(--c-ai); white-space: nowrap; }
.slp-badge { width: 22px; height: 22px; border-radius: 7px; display: grid; place-items: center;
  font-family: var(--font-jp); font-size: .74rem; font-weight: 800; box-sizing: border-box;
  border: 2px solid transparent;   /* chỗ dành sẵn cho viền, để ô không đổi cỡ */
  background: var(--line); color: var(--ink-soft); }
.slp-badge.done { background: var(--c-vocab); color: #fff; }
/* User request 2026-10-20 — "đang học" phải có VIỀN, mirror đúng thẻ kỹ năng
   mà học viên thấy ở lesson-detail.html (viền nét đứt màu hổ phách): nền mờ
   một mình quá nhẹ ở ô 22px, nhìn lướt không khác ô "chưa bắt đầu". */
.slp-badge.partial { background: var(--c-quiz-soft); color: #8a6500;
  border: 2px dashed var(--c-quiz); }
/* "Đã làm xong, chưa đạt" — xem `.st.retry` ở trên. */
.slp-badge.retry { background: var(--c-lesson); color: #fff; }
/* `empty` (bài không có phần đó) mờ hẳn và KHÔNG tô màu "chưa làm": tab đó bị
   loại khỏi mẫu số của bài, báo "chưa bắt đầu" sẽ gây hiểu nhầm là còn việc. */
.slp-badge.empty { opacity: .35; }

.slp-level-note { margin: 0 0 6px; padding: 0 14px; font-size: .76rem; color: var(--ink-soft); }

.slp-detail { padding: 4px 14px 13px; border-top: 1px solid var(--line); }
.slp-sec { display: grid; grid-template-columns: 24px 1fr 54px 1fr 44px 92px; gap: 10px;
  align-items: center; padding: 6px 0; font-size: .84rem; }
.slp-sec.is-empty { opacity: .45; }
.slp-sec-icon { font-family: var(--font-jp); font-weight: 800; color: var(--ink-soft); }
.slp-sec-count { text-align: right; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.slp-sec-bar { height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; }
.slp-sec-bar i { display: block; height: 100%; border-radius: 999px; background: var(--c-ai); }
.slp-sec-pct { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
.slp-sec-st { justify-self: end; font-size: .72rem; font-weight: 700; padding: 2px 8px;
  border-radius: 999px; white-space: nowrap; }
.slp-sec-st.done { background: var(--c-vocab-soft); color: var(--c-vocab); }
.slp-sec-st.partial { background: var(--c-quiz-soft); color: #8a6500;
  border: 1px dashed var(--c-quiz); }
/* "Đã làm xong, chưa đạt" — xem `.st.retry` ở trên. */
.slp-sec-st.retry { background: var(--c-lesson-soft); color: #c0392b; }
.slp-sec-st.todo { background: var(--bg); color: var(--ink-soft); }
/* Tab bài không có nội dung: KHÔNG tô màu trạng thái — nó bị loại khỏi mẫu số
   của bài, gắn nhãn "chưa bắt đầu" sẽ như còn việc phải làm. */
.slp-sec-st.empty { background: transparent; color: var(--ink-soft); font-weight: 600; }

@media (max-width: 720px) {
  /* Điện thoại: 5 cột không đủ chỗ — đẩy badge + % xuống hàng dưới thay vì
     để chữ tên bài bị bóp còn vài ký tự. */
  .slp-row > summary { grid-template-columns: 40px 1fr 86px; row-gap: 8px; }
  .slp-row .slp-badges { grid-column: 2; }
  .slp-row .slp-pct { grid-column: 3; grid-row: 2; }
  /* Nhãn trạng thái của BÀI: không khai chỗ thì nó rơi xuống hàng 3, nằm lệch
     hẳn sang trái dưới số thứ tự — đặt vào ô trống hàng 1, cạnh tên bài. */
  .slp-row > summary .st { grid-column: 3; grid-row: 1; justify-self: end; }
  /* Hàng kỹ năng: 6 cột không vừa — xuống 2 hàng. Hàng trên: biểu tượng,
     tên, số, %. Hàng dưới: thanh tiến độ kéo dài + nhãn trạng thái bên phải.
     Cột 3/4 để `auto`: nhãn "Chưa bắt đầu" rộng hơn 44px, ép cứng là tràn
     ngang cả trang (đo ở 390px). */
  .slp-sec { grid-template-columns: 24px 1fr auto auto; row-gap: 6px; }
  .slp-sec-bar { grid-column: 1 / 4; grid-row: 2; }
  .slp-sec-st { grid-column: 4; grid-row: 2; justify-self: end; }
}

/* ===========================================================================
   Phạm vi LỚP của màn giáo viên — hàng tab chọn lớp + thẻ thông tin lớp.
   Dùng bởi teacher-class / teacher-grading / teacher-dashboard /
   teacher-schedule qua assets/class-scope.js. Một giáo viên có thể dạy nhiều
   lớp (ClassTeacher M2M, phase 86) nên "đang xem lớp nào" phải nhìn là thấy.

   Đặt ở đây thay vì trong <style> từng trang vì bốn trang dùng y hệt: tô lẻ
   thì bốn chỗ lệch nhau dần, đúng cái đã xảy ra với chính nav của hai vai này.
   =========================================================================== */
.class-tabs { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 18px; }
.class-tab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 15px; border-radius: 999px;
  border: 1.5px solid var(--line, #E2DCD2); background: var(--surface, #fff);
  color: var(--ink, #1F1B2E); font-family: inherit; font-size: .88rem; font-weight: 700;
  cursor: pointer; transition: background .15s, border-color .15s, color .15s;
}
.class-tab:hover { background: var(--bg, #F0EDE6); }
.class-tab.on {
  background: var(--c-ai, #6C5CE7); border-color: var(--c-ai, #6C5CE7); color: #fff;
}
/* Số học viên trong tab: nền phải ĐỔI theo trạng thái tab, không thì ở tab
   đang chọn (nền tím) con số nằm trên nền sáng gần như vô hình. */
.class-tab-n {
  font-size: .72rem; font-weight: 800; padding: 1px 7px; border-radius: 999px;
  background: var(--bg, #F0EDE6); color: var(--ink-soft, #6B6577);
}
.class-tab.on .class-tab-n { background: rgba(255, 255, 255, .25); color: #fff; }

.class-header-card {
  background: var(--surface, #fff); border: 1px solid var(--line, #E2DCD2);
  border-radius: var(--radius-lg, 16px); padding: 16px 18px; margin: 0 0 18px;
}
.class-header-card .chc-top {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 4px;
}
.class-header-card .chc-name { font-size: 1.05rem; font-weight: 800; }
.class-header-card .chc-pills { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.class-header-card .chc-row {
  font-size: .85rem; color: var(--ink-soft, #6B6577); line-height: 1.55; margin-top: 3px;
}
.class-header-card .chc-label { font-weight: 700; color: var(--ink, #1F1B2E); }

@media (max-width: 560px) {
  /* Dạy 5 lớp trên điện thoại: cuộn ngang một hàng, không xuống 5 dòng đẩy
     hết nội dung khỏi màn hình đầu. */
  .class-tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
  .class-tabs::-webkit-scrollbar { display: none; }
  .class-tab { flex-shrink: 0; }
}

/* Tách số theo lớp ở teacher-dashboard.html — mỗi lớp một dòng bấm được,
   dẫn sang đúng lớp đó ở teacher-class.html (?class=). */
.cb-rows { display: flex; flex-direction: column; }
.cb-row {
  display: grid; grid-template-columns: 1.6fr .9fr .9fr .7fr 1fr;
  gap: 10px; align-items: center;
  padding: 11px 4px; border-top: 1px solid var(--line, #E2DCD2);
  color: inherit; text-decoration: none; font-size: .88rem;
}
.cb-rows .cb-row:first-child { border-top: none; }
.cb-row:hover { background: var(--bg, #F0EDE6); }
.cb-row .cb-name { font-weight: 700; }
.cb-row .cb-cell { color: var(--ink-soft, #6B6577); }
@media (max-width: 680px) {
  /* Năm cột trên điện thoại là năm cột không đọc được — xuống hai hàng, tên
     lớp chiếm cả hàng trên. */
  .cb-row { grid-template-columns: 1fr 1fr 1fr; }
  .cb-row .cb-name { grid-column: 1 / -1; }
}

/* ════════════════════════════════════════════════════════════════════════
   Luồng tiến độ (user request 2026-11-03)

   Cùng một bộ số được bày ở BỐN màn: trang chủ học viên (dashboard.html),
   trang chủ trung tâm (company-dashboard.html), trang chủ giáo viên
   (teacher-dashboard.html) và hồ sơ một học viên
   (company-student-progress.html). Để ở đây thay vì chép vào `<style>` của
   từng trang, vì bốn bản sao gần giống nhau là cách chắc chắn nhất để bốn
   màn dần trông khác nhau về cùng một thứ. Mã vẽ dùng chung nằm ở
   `assets/study-lanes.js`.
   ════════════════════════════════════════════════════════════════════════ */

/* ── Hai luồng RÀNG BUỘC, nằm ở mép phải một dòng danh sách ───────────── */
/* Hai con số CHỨ KHÔNG PHẢI một: 社内教育 và chương trình lớp là hai thứ
   trung tâm điều khiển bằng hai thao tác khác nhau, gộp lại thì nhìn vào
   không biết phải đi sửa chỗ nào. */
.lanes2 { display: flex; gap: 14px; flex-shrink: 0; }
.lanes2 .ln { width: 86px; text-align: right; }
.lanes2 .ln .cap {
  font-size: .68rem; color: var(--ink-soft); font-weight: 700;
  text-transform: uppercase; letter-spacing: .03em;
}
.lanes2 .ln .pct { font-weight: 800; font-size: .92rem; }
.lanes2 .ln .bar {
  height: 5px; border-radius: 999px; background: var(--line);
  margin-top: 4px; overflow: hidden;
}
.lanes2 .ln .bar span { display: block; height: 100%; border-radius: 999px; }
.lanes2 .ln .note { font-size: .68rem; margin-top: 3px; }
.lanes2 .ln .note.warn { color: var(--c-lesson); font-weight: 700; }
.lanes2 .ln.empty .pct { color: var(--ink-soft); font-weight: 600; font-size: .78rem; }
@media (max-width: 760px) { .lanes2 .ln { width: 70px; } }

/* ── Ba luồng, dạng thẻ ───────────────────────────────────────────────── */
.lane-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
@media (max-width: 860px) { .lane-row { grid-template-columns: 1fr; } }
.lane {
  border-radius: 16px; padding: 14px; background: var(--surface);
  border: 1px solid var(--line); border-top: 4px solid var(--lane-c);
  display: flex; flex-direction: column; gap: 5px;
  text-decoration: none; color: var(--ink);
}
a.lane { transition: all .2s var(--ease, ease); }
a.lane:hover { transform: translateY(-3px); box-shadow: 0 15px 35px rgba(32,35,58,.07); }
.lane h3 { font-size: .92rem; font-weight: 800; margin: 0; }
.lane .lane-jp { font-size: .74rem; color: var(--ink-soft); font-weight: 500; }
.lane .lane-num { display: flex; align-items: baseline; gap: 8px; margin-top: 2px; }
.lane .lane-pct { font-size: 1.55rem; font-weight: 800; line-height: 1; color: var(--lane-c); }
.lane .lane-frac { font-size: .78rem; color: var(--ink-soft); font-weight: 600; }
.lane .mini-prog { height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; }
.lane .mini-prog span { display: block; height: 100%; border-radius: 999px; background: var(--lane-c); }
.lane .lane-note { font-size: .74rem; color: var(--ink-soft); }
.lane .lane-note.warn { color: var(--c-lesson); font-weight: 700; }
.lane.empty .lane-pct { color: var(--ink-soft); }

/* ── Dòng học viên trong danh sách tiến độ ────────────────────────────── */
/* CẢ DÒNG bấm được: xem tiến độ xong thì việc tiếp theo luôn là mở hồ sơ
   em đó ra. */
a.stu-row { text-decoration: none; color: inherit; }
a.stu-row:hover { background: var(--bg); }
.stu-row {
  display: flex; align-items: center; gap: 13px; padding: 12px 0;
  border-bottom: 1px solid var(--line);
}
.stu-row:last-child { border-bottom: none; }
.stu-row .ava {
  width: 34px; height: 34px; border-radius: 50%; display: grid;
  place-items: center; color: #fff; font-weight: 700; font-size: .78rem; flex-shrink: 0;
}
.stu-row .info { flex: 1; min-width: 0; }
.stu-row .info b { font-size: .9rem; display: block; }
.stu-row .info span { font-size: .78rem; color: var(--ink-soft); display: block; }
.stu-toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; flex-wrap: wrap; }
.stu-toolbar input, .stu-toolbar select {
  padding: 8px 11px; border: 1px solid var(--line); border-radius: 10px;
  font-family: inherit; font-size: .85rem; background: var(--surface); color: var(--ink);
}
.stu-toolbar input { flex: 1; min-width: 150px; }

/* ── Dòng lớp trong danh sách tiến độ ─────────────────────────────────── */
/* Cùng hình dáng với `.stu-row`, đổi huy hiệu tròn thành vuông bo góc để
   phân biệt "một lớp" với "một người". company-dashboard.html có rule
   `.cls-row` riêng trong `<style>` của trang (nạp SAU file này) nên nó vẫn
   thắng — khối này là để các màn KHÁC dùng được cùng một dòng. */
.cls-row {
  display: flex; align-items: center; gap: 13px; padding: 13px 0;
  border-bottom: 1px solid var(--line);
}
.cls-row:last-child { border-bottom: none; }
.cls-row .ic {
  width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0;
  display: grid; place-items: center; font-weight: 800; color: #fff; font-size: .85rem;
}
.cls-row .info { flex: 1; min-width: 0; }
.cls-row .info b { font-size: .92rem; }
.cls-row .info span { font-size: .79rem; color: var(--ink-soft); display: block; }

/* ════════════════════════════════════════════════════════════════════════
   Biểu đồ so sánh của trung tâm (user request 2026-11-04)

   Vẽ bằng CSS, không thư viện — theo đúng lối `.dist-bar` đã có ở
   company-reports.html, và để không thêm một CDN nữa vào trang mà service
   worker phải lo. Mã dựng ở `assets/study-charts.js`.

   Nguyên tắc chung: nét MẢNH, lưới và trục là chỉ một bậc khác nền, khe hở
   2px giữa các mảng kề nhau (không vẽ viền quanh mảng), đầu thanh bo 4px
   và neo vào vạch gốc.
   ════════════════════════════════════════════════════════════════════════ */

.ch-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 14px; }
.ch-key { display: inline-flex; align-items: center; gap: 7px; font-size: .8rem; color: var(--ink-soft); font-weight: 600; }
.ch-key i { width: 11px; height: 11px; border-radius: 3px; flex-shrink: 0; }
.ch-empty, .ch-note { font-size: .82rem; color: var(--ink-soft); padding: 10px 0 0; }

/* Bảng số đi kèm — đường đọc cho người không phân biệt được màu. */
.ch-table { margin-top: 14px; }
.ch-table summary { font-size: .8rem; color: var(--ink-soft); font-weight: 600; cursor: pointer; }
.ch-table summary:hover { color: var(--c-ai); }
.ch-table table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: .8rem; }
.ch-table th, .ch-table td { padding: 6px 8px; border-bottom: 1px solid var(--line); text-align: right; }
.ch-table thead th:first-child, .ch-table tbody th { text-align: left; font-weight: 700; }
.ch-table thead th { color: var(--ink-soft); font-weight: 600; }

/* ── A. Cột ngang ghép đôi: so sánh giữa các lớp ──────────────────────── */
.hbars { display: flex; flex-direction: column; gap: 14px; }
.hb-row { display: grid; grid-template-columns: minmax(90px, 170px) 1fr; gap: 14px; align-items: center; }
.hb-name {
  font-size: .82rem; font-weight: 700; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.hb-plot { display: flex; flex-direction: column; gap: 5px; }
.hb-line { display: flex; align-items: center; gap: 9px; }
/* Đường ray = trục 0–100%. Thanh ăn % CỦA ĐƯỜNG RAY, nhãn nằm NGOÀI nó —
   để nhãn trong cùng flex với thanh thì nó bóp thanh lại và hình vẽ ngắn
   hơn con số nó đang in ra.
   Lưới dọc 25/50/75/100 là hairline ĐẶC, không nét đứt: nét đứt đọc ra như
   "ngưỡng" trong khi nó chỉ là lưới. */
.hb-track {
  flex: 1; min-width: 0; height: 9px; border-radius: 999px;
  background-color: var(--bg);
  background-image: repeating-linear-gradient(to right,
    transparent 0 calc(25% - 1px), var(--line) calc(25% - 1px) 25%);
}
.hb-bar { height: 9px; border-radius: 0 4px 4px 0; min-width: 2px; }
.hb-val {
  font-size: .74rem; color: var(--ink-soft); font-weight: 700;
  flex-shrink: 0; width: 38px; text-align: right;
}

/* ── B. Cột phân đoạn: tương quan giữa học viên ───────────────────────── */
.segbar-wrap { display: flex; gap: 18px; align-items: stretch; }
/* Khe 2px giữa các đoạn thay cho đường viền. */
.segbar {
  width: 54px; min-height: 200px; display: flex; flex-direction: column-reverse;
  gap: 2px; flex-shrink: 0;
}
/* Thẻ bên phải thấp hơn thẻ bên trái nhiều, để nguyên là một mảng trống
   không giải thích được — cho cột cao bằng thẻ. */
.chart-row > .panel { display: flex; flex-direction: column; }
.chart-row > .panel > div:last-child { flex: 1; display: flex; flex-direction: column; }
.segbar-wrap { flex: 1; }
.segbar .seg {
  border-radius: 3px; display: grid; place-items: center;
  color: #fff; font-weight: 800; font-size: .78rem; min-height: 4px;
}
.segbar-keys { display: flex; flex-direction: column; justify-content: center; gap: 10px; }
.segbar-keys .sk { display: flex; align-items: center; gap: 9px; font-size: .82rem; }
.segbar-keys .sk i { width: 11px; height: 11px; border-radius: 3px; flex-shrink: 0; }
.segbar-keys .sk b { min-width: 22px; font-weight: 800; }
.segbar-keys .sk span { color: var(--ink-soft); }

/* ── C. Biểu đồ nhiệt: lớp × tuần ─────────────────────────────────────── */
.heatmap { display: flex; flex-direction: column; gap: 3px; overflow-x: auto; }
.hm-row { display: grid; grid-template-columns: minmax(80px, 150px) 1fr; gap: 10px; align-items: center; }
.hm-name {
  font-size: .78rem; font-weight: 700; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.hm-cells { display: flex; gap: 3px; min-width: 0; }
.hm-cell, .hm-wk { flex: 1 1 0; min-width: 12px; }
.hm-cell { height: 20px; border-radius: 3px; }
.hm-cell.zero { background: var(--line); }
.hm-wk { font-size: .64rem; color: var(--ink-soft); text-align: center; white-space: nowrap; }
.hm-head { margin-bottom: 2px; }
.hm-scale { display: flex; align-items: center; gap: 5px; margin-top: 12px; font-size: .72rem; color: var(--ink-soft); }
.hm-scale i { width: 15px; height: 11px; border-radius: 3px; display: inline-block; }
.hm-scale i.zero { background: var(--line); }

/* Hai biểu đồ đầu đứng cạnh nhau trên màn rộng, xuống hàng khi hẹp. */
.chart-row { display: grid; grid-template-columns: 1.45fr 1fr; gap: 18px; }
@media (max-width: 900px) { .chart-row { grid-template-columns: 1fr; } }

/* ── Màu huy hiệu chữ cái đầu (.a1–.a6) ───────────────────────────────── */
/* Năm trang (company-dashboard, company-classes, company-class-detail,
   teacher-dashboard, teacher-class) đang khai bộ này trong `<style>` riêng
   với GIÁ TRỊ GIỐNG HỆT. Bản ở đây để các màn khác — kumiai-dashboard là
   màn đầu tiên — dùng được mà không phải chép lần thứ sáu.
   CỐ Ý KHÔNG gỡ năm bản kia trong lần này: chúng nạp SAU file này nên vẫn
   thắng, giá trị trùng khớp nên không đổi gì trên màn, và gỡ chúng là đụng
   vào năm trang không liên quan tới việc đang làm. */
.a1 { background: linear-gradient(135deg, #FF6B5B, #FFB02E); }
.a2 { background: linear-gradient(135deg, #00B894, #0984E3); }
.a3 { background: linear-gradient(135deg, #6C5CE7, #FF6B5B); }
.a4 { background: linear-gradient(135deg, #0984E3, #6C5CE7); }
.a5 { background: linear-gradient(135deg, #FFB02E, #00B894); }
.a6 { background: linear-gradient(135deg, #1A8E8E, #6C5CE7); }

/* ── Nhật ký học tập: lịch ngày của MỘT học viên ──────────────────────── */
/* Ô vuông bo góc, xuống dòng tự do — 28 ngày trên màn rộng là hai hàng 14,
   trên điện thoại tự gập thành nhiều hàng hơn thay vì co ô lại tới mức
   không phân biệt được đậm nhạt. */
/* Ô GHIM kích thước, không `1fr`: khối này nằm trong thẻ rộng cả màn, để
   ô co giãn thì 14 cột kéo mỗi ô thành ~140px — một lịch 28 ngày to bằng
   nửa trang. `minmax(0, 34px)` giữ ô vuông nhỏ như lịch đóng góp quen
   thuộc, và `justify-content:start` dồn chúng về trái thay vì dàn đều. */
.daylog {
  display: grid; grid-template-columns: repeat(14, minmax(0, 34px));
  gap: 6px; justify-content: start;
}
@media (max-width: 560px) { .daylog { grid-template-columns: repeat(7, minmax(0, 34px)); } }
.daylog .dl-cell { aspect-ratio: 1; border-radius: 7px; min-height: 18px; }
.daylog .dl-cell.zero { background: var(--line); }
.dl-foot {
  display: flex; justify-content: space-between; align-items: center;
  gap: 16px; flex-wrap: wrap; margin-top: 14px;
  background: var(--c-vocab-soft); border-radius: 14px; padding: 12px 16px;
}
.dl-facts { display: flex; gap: 20px; flex-wrap: wrap; font-size: .82rem; }
.dl-facts span { color: var(--ink); font-weight: 600; }
