/* ============================================================================
   HAPPIPHONE · TẦNG TOKEN DÙNG CHUNG
   Nạp file này TRƯỚC mọi <style> của trang. Trang nào cũng nạp, kể cả trang có
   style riêng: token khai ở đây, trang chỉ dùng.

   Vì sao có file này: trước đây mỗi trang tự khai một bảng màu trong <style>
   riêng — bàn làm việc lấy tím #4f46e5 làm thương hiệu, bàn cuộc gọi và tra cứu
   lấy teal #0d7a84, báo cáo lại một bộ khác. Cùng một khái niệm hiện ra ba màu,
   và sửa một màu phải sửa bảy chỗ.

   Quy ước màu:
     · teal  = thương hiệu, hành động chính
     · tím   = CHỈ dành cho Mya (AI). Thấy tím là biết máy đang nói, không phải người.
     · lục / hổ phách / đỏ = ngữ nghĩa trạng thái, khai một lần dùng chung
   Quy tắc bất di bất dịch của dự án: chữ luôn ngược màu nền.

   Ba chế độ nền: theo hệ thống (không đặt data-theme) / sáng / tối.
   Hai mật độ: gọn (mặc định) / thoáng. Xem hp-ui.js.
   ============================================================================ */

/* Form control native (select, input date, thanh cuộn) KHÔNG đi theo token — chúng
   đi theo `color-scheme`. Chỉ khai ở <meta> thì chúng đi theo HỆ ĐIỀU HÀNH, nên
   người ép nền tối trên máy đang để nền sáng sẽ thấy trang tối mà select vẫn sáng.
   Buộc `color-scheme` đi cùng `data-theme` là hết lệch, và làm ở đây thì mọi trang
   được luôn — kể cả trang quên khai <meta>. */
:root { color-scheme: light dark; }
:root[data-theme="sang"] { color-scheme: light; }
:root[data-theme="toi"] { color-scheme: dark; }

/* ============================================================================
   PHÔNG ROBOTO — TỰ HOST, GIỐNG CRM CŨ  (01/10/2026)

   Hub dùng ĐÚNG phông của CRM cũ (`scrm-fe`, `src/assets/fonts/font.scss`): Roboto,
   ba độ đậm 300 · 400 · 500, mỗi độ đậm tách ba bộ chữ vietnamese / latin-ext / latin.
   Chín tệp ấy chép NGUYÊN từ CRM cũ — người đã quen nét chữ bên đó không phải làm quen lại.
   (Trước ngày này Hub chạy Inter theo thang của Jira — CRM-212; đã thay.)

   Thêm một độ đậm 700 mà CRM cũ không có: bên đó chữ `font-weight: 600/700` là chữ đậm
   GIẢ LẬP từ bản 500 (trình duyệt tự bôi dày), nét nhoè ở cỡ 12–14px. Ba tệp 700 cắt từ
   Roboto-Bold bản Google (Apache 2.0 — `public/fonts/roboto-LICENSE.txt`) theo ĐÚNG ba dải
   ký tự của CRM cũ. Hub khai `font-weight: 600` thì trình duyệt lấy bản 700 gần nhất.

   TỰ HOST chứ không gọi Google Fonts, vì hai lý do độc lập:
     · CSP của hub là `default-src 'self'` không mở `font-src` (xem src/app.ts) —
       gọi fonts.googleapis.com là bị trình duyệt chặn thẳng, không có lỗi nào hiện ra.
     · Hub chạy nội bộ trên k3s; để trình duyệt nhân viên gọi thẳng sang Google là
       thêm một đường rò và một điểm hỏng ngoài tầm với.

   Thứ tự vietnamese → latin-ext → latin là thứ tự của Google Fonts (và của CRM cũ): các dải
   chồng nhau thì face khai SAU thắng. `font-display: swap` để chữ hiện ngay bằng phông dự
   phòng rồi mới đổi — không có màn trắng chữ, đúng luật "không màn nào bắt đầu bằng khoảng
   trắng".
   ============================================================================ */
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("/app/fonts/roboto-300-vietnamese.woff2?v=1") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("/app/fonts/roboto-300-latin-ext.woff2?v=1") format("woff2");
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("/app/fonts/roboto-300-latin.woff2?v=1") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/app/fonts/roboto-400-vietnamese.woff2?v=1") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/app/fonts/roboto-400-latin-ext.woff2?v=1") format("woff2");
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/app/fonts/roboto-400-latin.woff2?v=1") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/app/fonts/roboto-500-vietnamese.woff2?v=1") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/app/fonts/roboto-500-latin-ext.woff2?v=1") format("woff2");
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/app/fonts/roboto-500-latin.woff2?v=1") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/app/fonts/roboto-700-vietnamese.woff2?v=1") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/app/fonts/roboto-700-latin-ext.woff2?v=1") format("woff2");
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/app/fonts/roboto-700-latin.woff2?v=1") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* --- nền & mặt phẳng ---
     BẢNG MÀU CỦA CRM CŨ (01/10/2026). Người đặt hàng chốt: chữ, màu nền, phông, cỡ chữ của Hub
     phải GIỐNG CRM cũ (`scrm-fe`) — KHÔNG đổi bố cục. Mọi giá trị dưới đây đọc từ mã nguồn bên
     đó, ghi kèm nơi lấy, không phỏng đoán:
       · thanh trên + dải menu trắng (`header`/`sidebar` component)       → `--shell`
       · vùng nội dung xám nhạt `#EEF0F1` (`.main-container`)             → `--page`, `--bg`
       · khối, bảng, ô nhập trắng (`.search-container`, `.main-table`)    → `--surface`, `--field`
       · nền phụ hay dùng nhất `#F3F3F3` (47 lần) · `#E5E5E5` (25 lần)     → `--surface-2/-3`
       · viền `#E3E5EC` (37 lần) · `#D3D4D8` (62 lần)                      → `--line`, `--line-2`
         (ĐẬM HƠN từ 04/10/2026: `#C4C8D0` · `#A3A8B3` — vạch bảng và viền khung của CRM cũ
         gần như biến mất trên màn hình cũ, mờ của khách; xem khối chữ đậm ngay dưới)
     CHỮ ĐẬM HƠN CRM CŨ (04/10/2026) — ngoại lệ có chủ ý của bảng này. Khách dùng màn hình cũ, mờ:
     chữ chính `#232735` và chữ phụ `#666666` của CRM cũ (14,9:1 · 5,7:1) nhoè trên màn ấy. Người
     đặt hàng chốt "màu chữ đen đậm hơn, để nhìn rõ hơn". Ba bậc chữ giữ khoảng cách để còn đọc ra
     thứ bậc, mỗi bậc tối hơn hẳn:
       · chữ chính `--ink` `#14161C` (18,1:1 trên trắng)
       · chữ phụ `--ink-2` `#363940` (11,6:1) — nhãn, nội dung phụ
       · chú thích `--muted` `#4A4E57` (8,3:1) — giờ, gợi ý, dòng phụ
       · chữ gợi ý trong ô `--placeholder` `#707480` (4,7:1 trên ô trắng) — CỐ Ý nhạt hơn chữ đã
         gõ, để ô trống không bị đọc nhầm là đã có giá trị
     (Trước ngày này là bảng "dịu mắt" CRM-359: hai sắc xám ấm gần nhau; đã thay.)
     Tên token GIỮ NGUYÊN — chỉ đổi giá trị, nên mọi trang đổi theo không cần sửa. Hàng rào
     `src/core/token-contrast.test.ts` khoá bảng này với CRM cũ và tính lại tương phản. */
  --shell: #ffffff;
  --page: #eef0f1;
  --bg: #eef0f1;
  --surface: #ffffff;
  --surface-2: #f3f3f3;
  --surface-3: #e5e5e5;
  --field: #ffffff;
  --ink: #14161c;
  --ink-2: #363940;
  --muted: #4a4e57;
  --placeholder: #707480;
  --line: #c4c8d0;
  --line-2: #a3a8b3;
  /* Vạch mảnh giữa các dòng bảng — trong suốt nên đúng trên mọi mặt phẳng. */
  --hair: rgba(20, 22, 28, .14);
  /* Tấm nội dung (`.hp-page`) cùng màu vùng nội dung như CRM cũ, nên không viền không bóng —
     ở nền tối tấm vẫn là một mặt phẳng riêng và giữ viền + bóng. */
  --page-line: transparent;
  --page-sh: none;
  /* Dải menu: mục đang mở nền xanh chữ trắng, rê chuột `#EEF0F1` (`.sidebar-child a` của CRM cũ).
     CRM cũ dùng `#00B1D2` — chữ trắng trên đó chỉ 2,55:1, mờ hẳn trên màn hình cũ của khách; từ
     04/10/2026 hạ xuống cùng sắc xanh nhưng sẫm hơn `#007D96` để chữ trắng đạt 4,8:1. */
  --nav-on: #007d96;
  --nav-on-ink: #ffffff;
  --nav-hover: #eef0f1;

  /* --- thương hiệu --- hạ bão hoà một nấc (#0d7a84 → #137679) để teal đứng cạnh nền ấm
     không "phát sáng"; chữ trắng trên nó vẫn 5,4:1. */
  --brand: #137679;
  --brand-2: #0e5f62;
  --brand-ink: #ffffff;
  --brand-soft: #e0eeec;
  --brand-line: #b5d5d2;

  /* --- Mya (AI) --- */
  --mya: #5e4bcf;
  --mya-2: #4b3ab5;
  --mya-soft: #edeafb;
  --mya-line: #cec6f3;

  /* --- ngữ nghĩa trạng thái --- */
  /* Bốn nền nhạt của nhãn trạng thái lấy của CRM cũ: `#E0FDEC` · `#FFF6ED` · `#FBE7E7` · `#D0E7FF`. */
  --ok: #287347;      --ok-soft: #e0fdec;   --ok-line: #b9dbc5;
  --warn: #875a06;    --warn-soft: #fff6ed; --warn-line: #e6d3a6;
  --bad: #ae3b30;     --bad-soft: #fbe7e7;  --bad-line: #ebc2bb;
  --info: #2c6390;    --info-soft: #d0e7ff; --info-line: #bcd2e6;
  /* Chữ đặt TRÊN nền `--bad` đặc (huy hiệu đếm đỏ). Trước đây viết cứng `#fff` — đúng ở nền
     sáng, nhưng ở nền tối `--bad` là đỏ nhạt và chữ trắng tụt còn ~2,5:1. */
  --bad-ink: #ffffff;

  /* --- lớp phủ ---
     MỘT màu cho mọi hộp thoại (thay năm màu rgba khác nhau từng trang tự chọn) và một màu
     nhẹ hơn cho ngăn kéo bên phải — ngăn kéo cố ý để danh sách phía sau còn đọc được. */
  --scrim: rgba(21, 24, 25, .36);
  --scrim-soft: rgba(21, 24, 25, .16);

  /* --- chữ --- */
  --f-sans: Roboto, "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, Ubuntu, "Helvetica Neue", sans-serif;
  /* `--f-mono` là phông của MÃ, ID, số điện thoại, giờ, serial, JSON — và từ 01/10/2026 nó CŨNG
     là Roboto: CRM cũ hiện mọi dữ liệu bằng Roboto, không có chữ đơn cách nào. Token GIỮ để
     đánh dấu chỗ nào là mã (muốn quay về đơn cách là đổi MỘT dòng này). Chữ số của Roboto
     vốn đều bề rộng (tabular) nên cột số và giờ vẫn thẳng hàng. */
  --f-mono: Roboto, "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, Ubuntu, "Helvetica Neue", sans-serif;

  /* --- THANG CHỮ: bảy bậc, không có bậc thứ tám (CRM-212; cỡ theo CRM cũ từ 01/10/2026) ---
     Bảy bậc là các cỡ CRM cũ (`scrm-fe`) dùng nhiều nhất, thân chữ 14px như `html, body` bên
     đó: 12 (57 lần) · 14 (101) · 16 (83) · 18 (32) · 24 (tiêu đề trang 24/28) · 30 (16) · 32.
     Trước ngày này hai bậc 4 và 6 là 20px và 28px theo thang của Jira (`@atlaskit/tokens`);
     năm bậc kia trùng sẵn với CRM cũ nên giữ nguyên.

     Vì sao phải có: trước đợt này `public/` có 783 khai báo cỡ chữ dùng tới 41 cỡ khác
     nhau, trong đó 18 cỡ lẻ tới phần mười pixel (9.5 · 10.4 · 11.2 · 12.3 · 13.4 …).
     Không con số nào có lý lẽ — chúng là dấu vết của những lần "nhích cho vừa ô này".
     Cùng một bài học đã ghi ở đầu tệp này về màu thương hiệu: khai một chỗ, dùng chung.

     Mỗi bậc là một CẶP cỡ + chiều cao dòng, đúng cách Jira khai composite
     (`--ds-font-body: normal 400 0.875rem/1.25rem`). Đặt đúng cỡ mà bỏ chiều cao dòng
     thì con số đúng nhưng nhịp đọc vẫn lệch.

     BẬC NHỎ NHẤT LÀ 12px, KHÔNG CÓ NGOẠI LỆ. Atlassian đã nâng bậc nhỏ nhất từ 11px lên
     12px vì khả năng đọc. Chừa một ngoại lệ 11px "cho pill thôi" là mở lại đúng cái cửa
     đã đẻ ra 41 cỡ. Hàng rào: Luật 32 trong `src/core/ui-shell.test.ts` cấm mọi cỡ chữ
     số cứng ở ngoài tệp này.

     `--lh-*` là chiều cao dòng ĐI KÈM từng bậc, dùng khi một khối cần nhịp đọc của
     thang. Component nào đang khai chiều cao dòng KHÔNG đơn vị (1 cho pill, 1.45 cho
     bong bóng tin) thì giữ nguyên số đó — đấy là nhịp riêng đã cân theo padding của
     chính nó, đổi sang px là đổi chiều cao khối, việc khác hẳn. */
  --fs-1: 12px;   --lh-1: 16px;   /* font.body.small     — chú thích, nhãn phụ, pill, dấu giờ */
  --fs-2: 14px;   --lh-2: 20px;   /* font.body           — THÂN, cỡ mặc định của mọi thứ */
  --fs-3: 16px;   --lh-3: 24px;   /* font.body.large     — tiêu đề khối, chữ đọc liền */
  --fs-4: 18px;   --lh-4: 24px;   /* tiêu đề hộp thoại, số liệu nhỏ — 18px là cỡ CRM cũ dùng (32 lần) */
  --fs-5: 24px;   --lh-5: 28px;   /* font.heading.large  — tiêu đề trang */
  --fs-6: 30px;   --lh-6: 32px;   /* số liệu lớn ở bảng điều hành — 30px như CRM cũ (16 lần) */
  --fs-7: 32px;   --lh-7: 36px;   /* font.heading.xxlarge— dùng dè: đồng hồ màn cuộc gọi */

  /* --- hình khối --- */
  --r1: 6px; --r2: 10px; --r3: 14px; --r4: 20px; --rp: 999px;
  --side-w: 244px;    /* dải menu bên trái khi mở rộng */
  --side-w-min: 64px; /* ...và khi thu gọn (chỉ icon) */
  /* Chiều cao thanh trên. Là TOKEN chứ không phải con số rải rác, vì từ khi thanh
     được ghim đứng yên (xem `.topbar` bên dưới) nó không còn chỉ là chiều cao của
     chính nó: mọi thứ dính khác trên trang (menu Quản trị, dải điều hướng của màn
     tạo sự vụ, khung ba cột của bàn làm việc) phải trừ đúng con số này ra thì mới
     không chui xuống dưới thanh. Trước đây `50px` được chép tay ở bốn chỗ; đổi một
     chỗ mà quên ba chỗ kia thì không có lỗi nào nổ ra, chỉ có một dải bị che. */
  --top-h: 50px;
  /* Tổng chiều cao các BĂNG BÁO ngay dưới thanh trên (chưa gán phòng ban · bị chuyển khỏi màn
     không có quyền). KHÔNG phải hằng số: rolebar.js đo thật rồi ghi đè trên <html>
     (`doBang`), nên ở đây chỉ là giá trị khi không có băng. Khung toàn màn hình trừ nó cạnh
     `--top-h` — xem `.hp-page` và Luật 47 (`src/core/ui-cuon.test.ts`). */
  --hp-band: 0px;
  /* Khe giữa tấm nội dung và mép khung nhìn (mép phải, mép dưới) — xem `.hp-page`. */
  --sheet-gap: 10px;
  /* Lề trong hai bên của mọi khung trang (xem `.hp-page` bên dưới). Là token vì tám
     trang từng tự chọn một con số riêng: 18px ở Sự vụ và Cuộc gọi, 22px ở Lịch sử,
     24px ở Báo cáo, 20px ở Quản trị — cùng một khái niệm hiện ra năm bề rộng. Dùng
     `clamp` để màn 1280 không bị lề ăn mất chỗ, mà màn 2560 cũng không dán nội dung
     sát mép kính. */
  --gut: clamp(14px, 1.1vw, 24px);
  --sh1: 0 1px 2px rgba(31, 37, 40, .05);
  --sh2: 0 6px 18px rgba(31, 37, 40, .08), 0 1px 2px rgba(31, 37, 40, .05);
  --sh3: 0 20px 48px rgba(31, 37, 40, .16), 0 2px 6px rgba(31, 37, 40, .06);

  /* LỚP (z) CÓ TÊN (CRM-361). Mọi z-index từ 40 trở lên PHẢI đọc một tên ở đây — hàng rào Luật 44
     (`src/core/ui-diu-mat.test.ts`) chặn số trần; số dưới 40 là xếp lớp CỤC BỘ trong một khối và
     được viết số. Giá trị GIỮ NGUYÊN các con số đã chạy: đổi số là đổi quan hệ chồng lớp của
     từng màn, còn đặt tên thì không — thang 10/20/30/40/50 của bản thiết kế chỉ là THỨ TỰ, và
     thứ tự dưới đây đúng thứ tự ấy. Thêm một lớp mới = thêm một dòng ở đây, không phải một số
     lớn hơn mọi số đang có (đó là cách 9999 và 2147483647 ra đời). */
  --z-drawer-scrim: 40;   /* lớp mờ sau ngăn kéo sự vụ */
  --z-drawer: 50;         /* ngăn kéo sự vụ */
  --z-page-dialog: 50;    /* hộp thoại riêng của một trang (Dữ liệu nguồn, Đồng bộ, Tạo sự vụ, Hướng dẫn) */
  --z-menu: 60;           /* menu thả, khay chuông — trên ngăn kéo: menu mở TỪ ngăn kéo phải nổi lên */
  --z-page-dialog-top: 60; /* hộp thoại phải đè hộp thoại khác của cùng trang (thẻ kết thúc Tạo sự vụ) */
  --z-fly: 61;            /* thẻ nổi của dải menu thu gọn */
  --z-pop: 70;            /* hộp chọn nổi và lớp phủ hộp thoại dùng chung `.hp-scrim` */
  --z-modal: 80;          /* hộp thoại nhập tệp, hộp thoại cũ của ĐMX / Danh sách yêu cầu */
  --z-callcards: 900;     /* cột thẻ cuộc gọi đến */
  --z-callcard-scrim: 1000; /* lớp phủ khi mở một thẻ cuộc gọi */
  --z-toast: 1000;        /* toast */
  --z-toast-top: 1100;    /* toast của thẻ / cửa sổ cuộc gọi — phải nổi trên lớp phủ thẻ */
  --z-support: 9999;      /* cửa sổ Hỗ trợ nội bộ */
  --z-support-btn: 10000; /* bong bóng Hỗ trợ — không bao giờ chìm nửa sau cửa sổ của chính nó */
  --z-ticket-popup: 10010; /* popup Tạo sự vụ nhúng — mở từ thẻ cuộc gọi và từ khay Hỗ trợ */
  --z-progress: 2147483647; /* vạch tiến trình chuyển trang — trên tất cả */
  --focus: 0 0 0 3px rgba(19, 118, 121, .26);

  /* --- chuyển động ---
     Ba nấc thời lượng, hai đường cong. Khai ở đây vì trước kia mỗi chỗ tự chế một
     con số (.15s ở .hp-menu-btn .chev, .1s ở .sup-star, 2.2s ở .msg.found) — cùng
     một loại hiệu ứng chạy ba tốc độ khác nhau thì mắt đọc ra là "hệ chắp vá".

       --dur-1  nhấn/nhả, đổi màu, hiện dấu — nhanh tới mức không kịp thấy là animation
       --dur-2  mở/đóng menu, chuyển trang, mờ chéo — nấc mặc định
       --dur-3  thứ chiếm cả màn: lớp phủ, ngăn kéo, hộp thoại

     `--motion` là CÔNG TẮC TOÀN HỆ: 1 = bật, 0 = tắt. Người bật "giảm chuyển động"
     trong hệ điều hành thì mọi thời lượng nhân 0 — tắt một chỗ, cả hệ theo, không
     phải nhớ thêm @media vào từng file. Xem khối prefers-reduced-motion cuối file. */
  --motion: 1;
  --dur-1: calc(120ms * var(--motion));
  --dur-2: calc(220ms * var(--motion));
  --dur-3: calc(320ms * var(--motion));
  --ease-out: cubic-bezier(.22, .8, .3, 1);      /* vào nhanh, dừng êm — dùng cho thứ HIỆN ra */
  --ease-in-out: cubic-bezier(.5, 0, .3, 1);     /* hai đầu êm — dùng cho thứ DI CHUYỂN */

  /* --- mật độ: một biến đổi cả hệ --- */
  --row: 36px;          /* chiều cao dòng bảng */
  --fz: var(--fs-2);    /* cỡ chữ thân — LÀ một bậc của thang, không phải số tự chọn */
  --tap: 34px;          /* chiều cao tối thiểu của thứ bấm được */

  /* --- BÍ DANH TƯƠNG THÍCH ---
     Tên biến các trang cũ đang dùng. Giữ lại để trang không phải sửa từng chỗ,
     nhưng giá trị nay chảy từ token chung ở trên — nên đổi thương hiệu một lần
     là mọi trang đổi theo. Code MỚI đừng dùng những tên này. */
  --card: var(--surface);
  --card2: var(--surface-2);
  --panel: var(--surface);
  --sunken: var(--surface-2);
  --ink2: var(--ink-2);
  --ink3: var(--muted);
  --line2: var(--line);
  --teal: var(--brand);
  --teal2: var(--brand-2);
  --tealink: var(--brand-2);
  --teal-soft: var(--brand-soft);
  --tealw: var(--brand-soft);
  --good: var(--ok);
  --good-soft: var(--ok-soft);
  --green: var(--ok);
  --greenw: var(--ok-soft);
  --amber: var(--warn);
  --amberw: var(--warn-soft);
  --red: var(--bad);
  --redw: var(--bad-soft);
  --violet: var(--mya);
  --violetw: var(--mya-soft);
  --blue: var(--info);
  --blue-soft: var(--info-soft);
  --in-bubble: var(--surface-3);
  --out-bubble: var(--brand-soft);
  --brand-dark: var(--brand-2);
}

/* Mật độ thoáng: cho màn hình treo tường và người cần chữ to hơn. */
:root[data-density="thoang"] {
  --row: 44px;
  --fz: var(--fs-3);  /* một bậc lên, không phải "14.5px cho to hơn tí" */
  --tap: 42px;
}

/* Bảng tối — than chì, không xanh-đen (CRM-359): nền cũ `#0a1219` + chữ `#e6eef4` là 16:1,
   chữ sáng "loang" trên nền gần đen. Nay chữ chính 12:1 trên `#1b1f21`.
   CRM cũ KHÔNG có nền tối, nên đợt "giống CRM cũ" (01/10/2026) giữ nguyên bảng này; các token
   mới (`--shell` · `--page` · `--nav-*`) nhận đúng màu nền tối đang dùng ở chỗ ấy.
   Khai hai lần có chủ đích:
   · trong @media  → người chưa chọn gì thì đi theo hệ thống
   · trong [data-theme="toi"] → người đã chọn tay thì thắng cả hệ thống
   Không gộp được: gộp thì bộ chọn tay không kéo nổi máy đang để nền sáng. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="sang"]) {
    --shell: #151819;
    --page: #1b1f21;
    --page-line: var(--line);
    --page-sh: var(--sh1);
    --nav-on: #1b3231;
    --nav-on-ink: #7fc8c5;
    --nav-hover: #202528;
    --bg: #151819;
    --surface: #1b1f21;
    --surface-2: #202528;
    --surface-3: #272d30;
    --field: #181b1d;
    --ink: #d8dddf;
    --ink-2: #aab3b7;
    --muted: #8a949a;
    --placeholder: #7d878d;
    --line: #2b3235;
    --line-2: #3a4246;
    --hair: rgba(255, 255, 255, .06);
    --brand: #4aaeac;
    --brand-2: #7fc8c5;
    --brand-ink: #0b1e1e;
    --brand-soft: #1b3231;
    --brand-line: #2c5452;
    --mya: #a597f0;
    --mya-2: #bdb2f7;
    --mya-soft: #25213b;
    --mya-line: #3e3767;
    --ok: #6dbf8e;      --ok-soft: #1a2c22;   --ok-line: #2a4a37;
    --warn: #d6a452;    --warn-soft: #302818; --warn-line: #54452a;
    --bad: #e48a7f;     --bad-soft: #34211f;  --bad-line: #5a3531;
    --info: #80aed6;    --info-soft: #1b2935; --info-line: #2e4658;
    --bad-ink: #1b0f0e;
    --scrim: rgba(0, 0, 0, .5);
    --scrim-soft: rgba(0, 0, 0, .28);
    --sh1: 0 1px 2px rgba(0, 0, 0, .3);
    --sh2: 0 8px 22px rgba(0, 0, 0, .35), 0 1px 2px rgba(0, 0, 0, .3);
    --sh3: 0 22px 56px rgba(0, 0, 0, .5);
    --focus: 0 0 0 3px rgba(74, 174, 172, .32);
  }
}
:root[data-theme="toi"] {
  --shell: #151819;
  --page: #1b1f21;
  --page-line: var(--line);
  --page-sh: var(--sh1);
  --nav-on: #1b3231;
  --nav-on-ink: #7fc8c5;
  --nav-hover: #202528;
  --bg: #151819;
  --surface: #1b1f21;
  --surface-2: #202528;
  --surface-3: #272d30;
  --field: #181b1d;
  --ink: #d8dddf;
  --ink-2: #aab3b7;
  --muted: #8a949a;
  --placeholder: #7d878d;
  --line: #2b3235;
  --line-2: #3a4246;
  --hair: rgba(255, 255, 255, .06);
  --brand: #4aaeac;
  --brand-2: #7fc8c5;
  --brand-ink: #0b1e1e;
  --brand-soft: #1b3231;
  --brand-line: #2c5452;
  --mya: #a597f0;
  --mya-2: #bdb2f7;
  --mya-soft: #25213b;
  --mya-line: #3e3767;
  --ok: #6dbf8e;      --ok-soft: #1a2c22;   --ok-line: #2a4a37;
  --warn: #d6a452;    --warn-soft: #302818; --warn-line: #54452a;
  --bad: #e48a7f;     --bad-soft: #34211f;  --bad-line: #5a3531;
  --info: #80aed6;    --info-soft: #1b2935; --info-line: #2e4658;
  --bad-ink: #1b0f0e;
  --scrim: rgba(0, 0, 0, .5);
  --scrim-soft: rgba(0, 0, 0, .28);
  --sh1: 0 1px 2px rgba(0, 0, 0, .3);
  --sh2: 0 8px 22px rgba(0, 0, 0, .35), 0 1px 2px rgba(0, 0, 0, .3);
  --sh3: 0 22px 56px rgba(0, 0, 0, .5);
  --focus: 0 0 0 3px rgba(74, 174, 172, .32);
}

/* ============================================================================
   NGUYÊN TỐ DÙNG CHUNG
   Chỉ khai theo LỚP, không khai theo thẻ (trừ :focus-visible). Trang nào đã có
   lớp cùng tên thì <style> của trang nằm sau <link> nên vẫn thắng — thêm file
   này vào một trang cũ không làm trang đó đổi hình.
   ============================================================================ */

:focus-visible { outline: 0; box-shadow: var(--focus); border-radius: var(--r1); }

/* CHỮ CHỈ DÀNH CHO TRÌNH ĐỌC MÀN HÌNH.
   Trước đây hub không có lớp này: chỗ duy nhất cần nó (`.tb-shift .lbl`) tự khai một bản
   riêng, nên người viết màn mới không có gì để dùng lại. Hệ quả đã xảy ra thật: chấm "chưa
   đọc" của khay thông báo kèm một <span class="sr-only">Chưa đọc</span>, lớp không tồn tại
   nên chữ HIỆN RA giữa mỗi dòng và khay tràn ngang.
   Một lớp không tồn tại KHÔNG ném lỗi nào — nó chỉ lặng lẽ hiện ra. Đó là lý do lớp này
   phải sống ở đây, và có một hàng rào canh (src/notify/catalog.test.ts). */
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

/* NGOẠI LỆ THỨ HAI của luật "chỉ khai theo lớp" (sau :focus-visible), và có lý do
   giống nó: đây là chỗ mặc định của TRÌNH DUYỆT sai, không phải chỗ của trang.
   Ô nhập, nút, ô chọn KHÔNG thừa kế phông từ thân trang — không khai gì thì chúng
   dùng phông hệ điều hành (Chrome/Windows: Arial 13.33px). Đã đếm được trên bản
   chạy: hai nút gập dải menu và mọi nút/ô chọn nào chưa tự khai phông đều rơi ra
   Arial giữa một màn Segoe UI.
   CRM-116 lúc đó CỐ Ý chỉ đồng bộ họ chữ, không đụng cỡ — vì chưa có thang chữ nên
   kéo 13.33px lên 14px là "đổi hình", không phải "đồng bộ". CRM-212 dựng thang xong
   thì lý lẽ ấy đảo chiều: 13.33px KHÔNG nằm trên thang. Đo trên bản chạy sau khi quy
   đổi 795 khai báo, 279 phần tử vẫn lạc ngoài thang và tất cả đều là nút / ô nhập /
   ô chọn chưa tự khai cỡ (cùng đám icon SVG nằm trong chúng, thừa kế theo). Nên nay
   đồng bộ CẢ CỠ — control nào đã tự khai cỡ thì vẫn thắng, vì chọn theo thẻ (0,0,1)
   thua mọi lớp của trang. Vẫn KHÔNG đụng độ đậm: `font: inherit` sẽ kéo theo cả
   font-weight và làm mọi nút mất nét đậm riêng. */
button, input, select, textarea, optgroup { font-family: inherit; font-size: inherit; }
/* Bốn thẻ trình duyệt tự gán phông ĐƠN CÁCH mặc định (Consolas/Courier) — không có dòng này thì
   `<pre>` của khay payload, `<code>` trong bài hướng dẫn vẫn hiện nét khác mọi chữ quanh nó dù
   token đã đổi. Đi qua `--f-mono` để còn MỘT chỗ quyết định. */
pre, code, kbd, samp { font-family: var(--f-mono); }
/* `<small>` trơn lấy cỡ "nhỏ hơn cha" của trình duyệt (11,67px trong chữ 14px, 10px trong chữ 12px)
   — lệch thang và dưới bậc nhỏ nhất. Kéo về `--fs-1`; lớp của trang (0,1,1) vẫn thắng. */
small { font-size: var(--fs-1); }
/* Chữ gợi ý trong ô (04/10/2026): mặc định của trình duyệt là xám rất nhạt (Firefox còn chồng
   `opacity:.54`), gần như mất trên màn hình cũ của khách. Một màu riêng, nhạt hơn chữ đã gõ. */
::placeholder { color: var(--placeholder); opacity: 1; }

/* Khối "Cần đăng nhập" / "Cần quyền quản trị viên" là vỏ DÙNG CHUNG, nhưng `<h2>` bên
   trong nó chưa trang nào khai cỡ — nên nó rơi vào mặc định `1.5em` của trình duyệt,
   tức 21px, lạc ngoài thang ở bảy màn cùng lúc. Cùng lý lẽ với khối control ở trên:
   chỗ sai là mặc định của trình duyệt, và sửa ở đây thì bảy màn cùng đúng. */
.deny h1, .deny h2, .idle h1, .idle h2 { font-size: var(--fs-4); line-height: var(--lh-4); }

.hp-mono { font-family: var(--f-mono); font-variant-numeric: tabular-nums; }

/* --- Nhãn trạng thái --- */
.hp-pill { display: inline-flex; align-items: center; gap: 5px; font: 600 var(--fs-1)/1 var(--f-sans); padding: 4px 9px; border-radius: var(--rp); white-space: nowrap; border: 1px solid transparent; }
.hp-pill.ok { background: var(--ok-soft); color: var(--ok); border-color: var(--ok-line); }
.hp-pill.warn { background: var(--warn-soft); color: var(--warn); border-color: var(--warn-line); }
.hp-pill.bad { background: var(--bad-soft); color: var(--bad); border-color: var(--bad-line); }
.hp-pill.info { background: var(--info-soft); color: var(--info); border-color: var(--info-line); }
.hp-pill.mya { background: var(--mya-soft); color: var(--mya); border-color: var(--mya-line); }
.hp-pill.brand { background: var(--brand-soft); color: var(--brand-2); border-color: var(--brand-line); }
.hp-pill.neu { background: var(--surface-3); color: var(--ink-2); border-color: var(--line); }
.hp-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.hp-dot.live { animation: hp-pulse calc(1500ms * var(--motion)) infinite; }
@keyframes hp-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .25 } }
@media (prefers-reduced-motion: reduce) { .hp-dot.live { animation: none } }

/* --- Icon system: một khổ, một nét, dùng chung mọi nơi (icons.js) --- */
.hp-i { display: inline-block; vertical-align: -3px; flex: none; }
.hp-i.sm { width: 14px; height: 14px; }

/* --- Thanh trên cùng: khung dùng chung cho MỌI trang.
   Trước đây mỗi trang tự khai .topbar/.top/.adm-top trong <style> riêng — tám cách viết
   khác nhau cho cùng một thanh, giống hệt bài học màu thương hiệu ở đầu file này. Khai
   MỘT lần ở đây; trang không tự khai lại (xem docs/HE-THONG-THIET-KE.md).

   TỪ BẢN "MENU DỌC": thanh này KHÔNG còn mang điều hướng. Mọi mục đã dời sang dải
   menu bên trái (`.hp-side`, do rolebar.js dựng) — xem khối ngay bên dưới. Thanh
   trên giờ chỉ còn nút ba-gạch của màn hẹp (bên trái) và cụm phải (tenant · ngôn
   ngữ · nền/mật độ · người dùng). Nhờ vậy nó KHÔNG BAO GIỜ hết chỗ nữa, nên cả bộ
   máy đo-và-co "+ Thêm ▾" (fitNav) đã bỏ hẳn: dải dọc cao bao nhiêu cũng cuộn được,
   không có thứ gì phải giấu đi vì thiếu bề ngang. */
/* GHIM ĐỨNG YÊN, khai MỘT lần ở đây — không phải mười một lần trong <style> của
   từng trang. Trước bản này, tám trang tự chép `header.topbar{position:sticky;top:0;
   z-index:10}` vào khối <style> riêng, còn Quản trị và màn Tạo sự vụ thì quên: cuộn
   xuống là thanh trôi mất, và người dùng mất luôn đường đổi tenant/ngôn ngữ/đăng
   xuất cho tới khi cuộn ngược lên đầu. Đúng lớp lỗi mà mục 5 của
   docs/HE-THONG-THIET-KE.md sinh ra để chặn: một thứ dùng chung mà khai ở tám chỗ
   thì chỗ thứ chín luôn thiếu.

   `sticky` chứ KHÔNG phải `fixed`: dải menu trái đã `position: fixed` và trang nhường
   chỗ cho nó bằng `padding-left` trên <body> (xem `html.hp-side-on body`). Thanh trên
   nằm TRONG <body> nên `sticky` tự ăn theo lề đó — đúng bề rộng còn lại, ở cả hai
   trạng thái mở rộng/thu gọn, và không phải tính tay `left: var(--side-w)` rồi nhớ
   sửa ở nhánh thu gọn.

   z-index 10 là có chủ ý và phải nằm DƯỚI hai thứ: lớp phủ đăng nhập (.login-overlay,
   10 — nhưng thanh mang `data-hp-when="auth"` nên không cùng lúc hiện) và hộp thoại
   Kết ca (.sh-overlay, 11). Nằm TRÊN dải menu (9) ở màn rộng; ở màn hẹp dải thành
   ngăn kéo và tự vượt lên 12 (xem media query bên dưới). */
/* KHUNG NẰM TRÊN NỀN TRANG (CRM-361): thanh trên và dải menu không còn nền trắng riêng + vạch
   viền — mắt chỉ còn thấy HAI độ sáng: nền trang và tấm nội dung (`.hp-page`). Nền vẫn ĐẶC
   (`--bg`), không trong suốt: trang cuộn ở <body> phải chui xuống dưới thanh chứ không lộ qua. */
.topbar { position: sticky; top: 0; z-index: 10; height: var(--top-h); display: flex; align-items: center; gap: 2px; padding: 0 16px; background: var(--shell); border-bottom: 1px solid transparent; }
/* .tb-brand/.tb-logo giờ đứng TRONG dải menu bên trái: rolebar.js DI CHUYỂN đúng
   phần tử đã có sẵn trong HTML của từng trang (không nhân bản một cái thứ hai), nên
   trang nào không nạp rolebar.js — /app/ticket/ chẳng hạn — vẫn thấy logo ở chỗ cũ
   trên thanh trên, không mất gì. */
.tb-brand { display: flex; align-items: center; gap: 8px; font: 600 var(--fs-2)/1 var(--f-sans); color: var(--ink); letter-spacing: -.01em; white-space: nowrap; margin-right: 14px; flex: none; }
.tb-logo { width: 26px; height: 26px; border-radius: var(--r1); background: var(--brand); color: var(--brand-ink); display: grid; place-items: center; font-weight: 600; font-size: var(--fs-2); flex: none; }
/* `flex: 0 1 auto` + `min-width: 0`, KHÔNG `flex: none` (CRM-320 #82): cụm phải của vai
   Nhân viên có thêm hai nút chữ ("Chia sẻ cho tôi" · "Kết ca"), cộng badge công ty và tên tài
   khoản — `flex: none` giữ nó đúng bằng bề rộng nội dung, nên ở 900px nó thò ra mép phải và
   trên điện thoại nút Tài khoản nằm TRỌN ngoài khung nhìn. Ở màn hẹp nó còn làm `flex-wrap`
   của chính cụm này thành luật chết: một hộp không bao giờ hẹp hơn nội dung thì không có gì để
   xuống dòng. Co được thì hai mảnh mang CHỮ TỰ DO (badge công ty, tên tài khoản) cắt bớt có dấu
   ba chấm — xem hai luật ngay dưới. */
.tb-user { margin-left: auto; display: flex; align-items: center; gap: 8px; flex: 0 1 auto; min-width: 0; }
/* TÊN TRANG ĐANG MỞ — đẩy LÊN thanh ngang, không đặt trong thân trang.
   Từ bản "menu dọc", logo dời sang dải bên trái nên nửa trái thanh trên bỏ trống,
   trong khi mỗi trang lại tự dành nguyên một dòng trong thân trang chỉ để nhắc lại
   đúng cái tên vừa bấm ở menu. Đưa tên lên đây được cả hai: chỗ trống có việc, và
   thân trang bắt đầu thẳng bằng NỘI DUNG (ô tìm kiếm, bộ lọc, bảng) chứ không phải
   một dòng chữ lặp lại.
   Chữ do rolebar.js đặt theo ĐÚNG mục menu đang sáng — một nguồn tên duy nhất cho
   cả menu lẫn thanh trên, nên hai chỗ không thể lệch nhau.
   `flex: 0 1 auto` + `min-width: 0`: tên dài gặp cửa sổ hẹp thì CẮT BỚT có dấu ba
   chấm, không đẩy cụm phải (người dùng · ngôn ngữ · nền) tràn khỏi khung nhìn. */
.tb-title { margin: 0; padding-right: 14px; min-width: 0; flex: 0 1 auto; font: 500 var(--fs-2)/1.4 var(--f-sans); color: var(--ink);  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Không cho từng mảnh trong cụm phải co lại: co một hộp chứa chữ nowrap chỉ làm chữ
   tràn ra ngoài, không làm chữ nhỏ lại. */
.tb-user > .hp-prefs, .tb-user > .hp-pill { flex-shrink: 0; }
.tb-user > .tb-shift { flex-shrink: 0; }
/* ...TRỪ hai mảnh mà chữ là TÊN RIÊNG, dài bao nhiêu tuỳ dữ liệu: badge công ty và tên tài
   khoản. Chữ nằm trong một `.lbl` riêng nên co là cắt có dấu ba chấm, không phải tràn; tên đầy
   đủ ở `title`. Badge vẫn LỘ RA (luật "Đa tenant" của CLAUDE.md) — sàn 72px giữ icon + vài chữ
   đầu, không bao giờ co về một vệt màu. */
.tb-user > #me-tenant { flex-shrink: 1; min-width: 72px; }
#me-tenant > .lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.tb-user > .tb-acct { flex-shrink: 1; }
.tb-acct > .hp-menu-btn { max-width: 100%; min-width: 0; }
.tb-acct > .hp-menu-btn .lbl { min-width: 0; }
/* NÚT KẾT CA trên thanh trên (CRM-119).
   Trước đây nó là một icon trong dải lọc của cột hội thoại, tức CHỈ có ở Omnichat —
   người trực đang mở Cuộc gọi hay Sự vụ muốn kết ca phải quay về. Nay rolebar.js gắn
   nó vào `.tb-user` nên mọi trang đều có, và chỉ dựng cho vai `agent`: admin và
   supervisor không nằm trong luồng chia việc.

   `min-width` khoá sẵn bề ngang cho nhãn DÀI hơn trong hai nhãn ("Vào ca lại"), nên
   đổi trạng thái không làm cụm phải nhảy chỗ — mốc M0-M3, docs/CHUYEN-TRANG-MUOT.md. */
.tb-shift { display: inline-flex; align-items: center; gap: 6px; min-width: 104px; height: 32px; padding: 0 11px; border: 1px solid var(--brand-line); background: var(--brand-soft); color: var(--brand-2); border-radius: var(--r1); font: 600 var(--fs-1)/1 var(--f-sans); cursor: pointer; white-space: nowrap; }
.tb-shift:hover { border-color: var(--brand); }
.tb-shift:focus-visible { outline: 0; box-shadow: 0 0 0 3px var(--brand-soft), 0 0 0 4px var(--brand); }
/* Đã kết ca: nền hổ phách. Nút nói VIỆC SẼ LÀM khi bấm ("Vào ca lại"), còn màu nói
   TRẠNG THÁI ĐANG CÓ — hai thứ khác nhau nên phải mã hoá bằng hai kênh khác nhau. */
.tb-shift.shift-off { border-color: var(--warn-line); background: var(--warn-soft); color: var(--warn); }

/* BĂNG "CHƯA ĐƯỢC GÁN PHÒNG BAN" (CRM-181) — dựng bởi rolebar.js, có mặt ở MỌI trang.
   Nằm ngay dưới thanh trên và chiếm cả bề ngang: đây là câu giải thích cho một màn hình
   trống, nên nó phải đọc được trước khi mắt kịp kết luận "hệ không có việc gì".
   Không `position:fixed` — băng đẩy nội dung xuống thay vì che mất dòng đầu tiên. */
.hp-nodept {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 16px; font-size: var(--fs-2); font-weight: 550;
  background: var(--warn-soft); color: var(--warn);
  border-bottom: 1px solid var(--warn-line);
}
.hp-nodept[hidden] { display: none; }
.tb-shift.shift-off:hover { border-color: var(--warn); }

/* NÚT 📘 HƯỚNG DẪN trên thanh trên (khu /app/guide/).
   Do `rolebar.js` gắn vào `.tb-user` lúc chạy, KHÔNG viết vào HTML từng trang: Luật 28 so cả
   khối `<header class="topbar">` của mọi màn với nhau, nên một nút viết tay ở đây là hai mươi
   tệp phải sửa giống hệt — và một tệp quên là test đỏ.

   Nhạt hơn nút Kết ca một bậc, và đó là chủ ý: Kết ca là HÀNH ĐỘNG có hậu quả (tắt nhận việc,
   chuyển chat), còn đây là một lối tra cứu. Hai thứ cạnh nhau thì thứ nặng hơn phải nổi hơn. */
/* CHỈ ICON (CRM-361): nút có viền + chữ "Hướng dẫn" là một ô nữa trong cụm phải vốn đã chật; nay
   cùng khổ 32px với chuông. Tên vẫn còn trong `.lbl` (ẩn khỏi mắt, còn trong cây trợ năng) và
   `title`, nên trình đọc màn hình và người rê chuột vẫn biết nút là gì. */
.tb-guide { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: 0; background: transparent; color: var(--ink-2); border-radius: var(--r2); font: 500 var(--fs-1)/1 var(--f-sans); cursor: pointer; white-space: nowrap; text-decoration: none; flex-shrink: 0; }
.tb-guide .lbl { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.tb-guide:hover { background: var(--surface-3); color: var(--ink); }
/* Đang mở khu Hướng dẫn. Dải menu không có mục nào sáng cho màn này, nên nút tự nhận mình. */
.tb-guide[aria-current='page'] { border-color: var(--brand-line); background: var(--brand-soft); color: var(--brand-2); }
.tb-guide:focus-visible { outline: 0; box-shadow: 0 0 0 3px var(--brand-soft), 0 0 0 4px var(--brand); }
/* Màn hẹp: thu về đúng icon. Ẩn bằng một phần tử con riêng chứ không `font-size:0` trên nút —
   icon cũng là chữ, và `font-size:0` sẽ nuốt luôn nó. */


.tb-shift svg { flex: none; }
/* MÀN HẸP: thanh trên phải XUỐNG DÒNG, không đẩy cụm phải ra ngoài mép.
   Hai luật dưới đây trước nằm trong `style.css` — tức CHỈ bàn làm việc và Quản trị
   có, còn mười ba trang kia thì cụm phải (nền · mật độ · ngôn ngữ · người dùng, rộng
   khoảng 430px) chạy thẳng ra khỏi khung nhìn 390px. Không ai kêu, vì hiếm ai mở màn
   Sự vụ bằng điện thoại — nhưng từ khi TÊN TRANG lên thanh này thì nó là thứ đầu
   tiên bị ép, còn đúng hai chữ cái. Khai một lần ở đây thì mọi trang được, y như
   chính khung `.topbar` bên trên. */
/* Lề ngang hẹp lại từ 1000px. Luật này trước nằm ở `style.css` — tệp CHỈ màn Omnichat
   nạp — nên đúng một màn có lề 10px và khe 9px, mười lăm màn kia giữ 16px/2px của rule
   gốc. Không ai gọi đó là lỗi, nhưng nó là hai cái thanh trên khác nhau, và người đi
   qua hai màn liền nhau ở khổ tablet thấy cả hàng nhích ngang một nhịp.
   Viết LONGHAND, không `padding: 0 10px`: shorthand đặt luôn trên/dưới về 0, mà từ
   ngưỡng 860px thanh được phép xuống dòng và cần đúng khoảng đệm dọc ở khối dưới. */
@media (max-width: 1000px) {
  .topbar { gap: 9px; padding-left: 10px; padding-right: 10px; }
}
@media (max-width: 860px) {
  /* `padding` chỉ đặt TRÊN/DƯỚI ở khối này — lề ngang đã do ngưỡng 1000px lo, và đặt
     cả bốn phía ở đây là át mất nó. */
  .topbar { height: auto; min-height: var(--top-h); flex-wrap: wrap; row-gap: 6px; padding-top: 7px; padding-bottom: 7px; }
  .tb-user { flex-wrap: wrap; row-gap: 6px; justify-content: flex-end; min-width: 0; }
  /* Nút Kết ca THU VỀ ICON: chữ đi, `title` và `aria-label` ở lại (rolebar.js đặt cả
     hai), nên trình đọc màn hình và tooltip vẫn nói đủ câu. Ô vuông đúng bằng chiều
     cao nút để nó không lệch hàng với mấy nút icon bên cạnh. */
  .tb-shift { min-width: 0; width: 32px; padding: 0; justify-content: center; }
  .tb-shift .lbl { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
}
@media (max-width: 700px) {
  /* Giấu TÊN trong nút người dùng, giữ vòng tròn viết tắt. Ngưỡng cũ là 1500px vì
     hồi đó cả dải điều hướng còn nằm trên thanh này và phải nhường chỗ cho nó; giờ
     thanh chỉ còn cụm phải nên chỉ hạ chữ khi thật sự chật. */
  .tb-user .hp-menu-btn .lbl { display: none; }
  .tb-user .hp-menu-btn { padding: 7px 8px; }
  /* KHÔNG còn luật ẩn bộ chọn nền/mật độ ở đây.
     Ba luật cũ (`.topbar .hp-prefs { display:none }` cộng hai ngoại lệ cho âm báo và
     thông báo màn hình) sinh ra để giành bề ngang cho tên trang khi bốn cặp công tắc
     còn nằm thẳng trên thanh. Nay cả bốn nằm trong menu ⚙ (`mountPrefsMenu` ở
     rolebar.js) — thanh chỉ còn MỘT nút 32px ở mọi bề rộng, nên không còn gì để giành.
     Giữ lại thì tệ hơn là thừa: bộ chọn nền sẽ biến mất ngay BÊN TRONG menu vừa mở ở
     màn hẹp, tức người dùng điện thoại mở ⚙ ra và thấy một khay thiếu hai hàng. */
  .tb-title { font-size: var(--fs-2); }
}

/* ============================================================================
   DẢI MENU DỌC BÊN TRÁI (.hp-side) — rolebar.js dựng, cho mọi trang có #role-bar.
   Vì sao đổi từ thanh ngang sang dải dọc: số mục chỉ có tăng (riêng cụm "Sự vụ" đã
   chín mục), mà bề NGANG là tài nguyên hữu hạn còn bề DỌC thì cuộn được. Thanh ngang
   buộc phải giấu bớt mục vào "+ Thêm ▾", và mục bị giấu mỗi máy một khác vì phụ
   thuộc bề rộng cửa sổ — cùng một hướng dẫn sử dụng không dùng chung được.

   Hai trạng thái, người dùng tự chọn và được NHỚ (localStorage `hp_side_collapsed`):
     · mở rộng  → icon + tên
     · thu gọn  → chỉ icon; rê chuột (hoặc focus bàn phím) thì hiện tên trong một
                  thẻ nổi bên phải, nhóm thì hiện luôn danh sách mục con.
   Thẻ nổi đó (.side-fly) do JS đặt vị trí và gắn thẳng vào <body> — KHÔNG đặt trong
   dải: dải phải `overflow-y:auto` để menu dài còn cuộn được, mà overflow đã cắt theo
   chiều dọc thì cũng cắt luôn theo chiều ngang, thẻ nổi sẽ bị xén mất.
   ============================================================================ */
.hp-side { position: fixed; top: 0; left: 0; bottom: 0; width: var(--side-w); display: flex; flex-direction: column; background: var(--shell); border-right: 1px solid transparent; z-index: 9; transition: width var(--dur-1) var(--ease-in-out), transform var(--dur-2) var(--ease-out); }
/* Nội dung trang nhường đúng bề rộng của dải. Đặt trên <body> nên MỌI trang được
   ngay, không phải sửa bố cục riêng của từng trang. Chỉ áp dụng khi trang có khai
   dải (html.hp-side-on) — trang chưa đăng nhập không bị thụt lề vô cớ.

   KHÔNG có `transition` ở đây — xem `.hp-side-anim` ngay dưới. */
html.hp-side-on body { padding-left: var(--side-w); }
/* Lề trái CHỈ được phép chạy mượt khi người dùng tự bấm thu gọn/mở rộng, KHÔNG phải
   lúc mở trang.
   Lỗi đã trả giá: `hp-side-on` do rolebar.js dán ở CUỐI <body>, tức sau khi <body>
   đã có kiểu tính lần đầu với `padding-left: 0`. Lớp dán muộn một nhịp + transition
   = mỗi lượt bấm menu, TOÀN BỘ nội dung trang trượt ngang 244px trong 120ms. Người
   dùng đọc ra đó là "hiệu ứng trượt khi chuyển trang", còn theo luật ở đầu
   docs/CHUYEN-TRANG-MUOT.md thì nó đúng là thứ bị cấm: một thứ đã hiện rồi mới đổi
   chỗ.
   Hai lớp chắn, cố ý không phải một:
     · hp-ui.js đặt `hp-side-on` ở mốc M1 (trong <head>, trước khi <body> được phân
       tích) nên lề đã ĐÚNG ngay từ kiểu tính đầu tiên — không có gì để chuyển động.
     · `hp-side-anim` chỉ được dán SAU khi dải dựng xong (rolebar.js, qua hai khung
       hình). Đường nào khiến lề tới muộn — đăng nhập SSO trả token ở fragment, trang
       quên khai `data-hp-side` — thì lề nhảy thẳng tới đúng chỗ thay vì trượt. */
html.hp-side-anim body { transition: padding-left var(--dur-1) var(--ease-in-out); }
html.hp-side-min .hp-side { width: var(--side-w-min); }
html.hp-side-on.hp-side-min body { padding-left: var(--side-w-min); }
/* Nhánh anon: dải đã ẩn theo data-hp-when, nên lề chừa chỗ cho nó phải về 0 — nếu
   không thì màn đăng nhập / câu "không vào được" bị đẩy lệch 244px sang phải để
   nhường chỗ cho một thứ không còn hiện. */
html.hp-anon.hp-side-on body,
html.hp-anon.hp-side-on.hp-side-min body { padding-left: 0; }
.side-head { display: flex; align-items: center; gap: 8px; padding: 0 10px; min-height: 50px; border-bottom: 1px solid var(--line); }
.hp-side .tb-brand { margin-right: 0; min-width: 0; overflow: hidden; }
.hp-side .tb-brand .lbl { overflow: hidden; text-overflow: ellipsis; }
/* Thu gọn: logo và nút gập xếp DỌC — 26px logo + 32px nút không đứng vừa một hàng
   rộng 64px, mà giấu nút gập đi thì không còn đường mở lại dải. */
html.hp-side-min .side-head { flex-direction: column; gap: 5px; padding: 9px 0; }
html.hp-side-min .hp-side .tb-brand .lbl { display: none; }
.side-toggle, .side-burger { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 0; background: transparent; color: var(--muted); border-radius: var(--r1); cursor: pointer; flex: none; }
.side-toggle { margin-left: auto; }
html.hp-side-min .side-toggle { margin-left: 0; }
.side-toggle:hover, .side-burger:hover { background: var(--surface-2); color: var(--ink); }
.side-toggle:focus-visible, .side-burger:focus-visible { outline: 0; box-shadow: var(--focus); }
/* Nút ba gạch chỉ có nghĩa ở màn hẹp, nơi dải nằm ngoài khung nhìn (xem media query). */
.side-burger { display: none; margin-right: 4px; }

.side-nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 8px; display: flex; flex-direction: column; gap: 1px; }
.side-link { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; text-decoration: none; border: 0; background: transparent; color: var(--ink); border-radius: var(--r2); padding: 0 10px; min-height: var(--tap); font: 500 var(--fs-2)/1.4 var(--f-sans); white-space: nowrap; cursor: pointer; position: relative; }
.side-link:hover { background: var(--nav-hover); color: var(--ink); }
.side-link:focus-visible { outline: 0; box-shadow: var(--focus); }
.side-link .lbl { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Mục đang mở: nền xanh chữ trắng như CRM cũ (`--nav-on`, xem đầu tệp). */
.side-link.on { background: var(--nav-on); color: var(--nav-on-ink); font-weight: 500; }
/* Nhãn nhóm "Thiết lập" (CRM-361) — chữ thường, màu phụ, không phải một mục bấm được. Thu gọn
   (chỉ icon) hay ngăn kéo đóng thì ẩn như mọi nhãn chữ khác của dải. */
.side-sec { margin: 14px 10px 4px; font: 500 var(--fs-1)/var(--lh-1) var(--f-sans); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
html.hp-side-min .side-sec { display: none; }
.side-link.on:hover { background: var(--nav-on); color: var(--nav-on-ink); }
.side-group { display: flex; flex-direction: column; gap: 1px; }
/* Nút nhóm khi trang đang mở nằm BÊN TRONG nhóm: chỉ đổi màu chữ, không tô nền —
   nền dành riêng cho đúng MỘT mục đang mở, không thì hai chỗ cùng tự nhận là "đây". */
.side-group-btn.on-group { color: var(--brand-2); }
.side-group-btn .chev { flex: none; opacity: .7; transition: transform var(--dur-1) var(--ease-out); }
.side-group.open > .side-group-btn .chev { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .side-group-btn .chev { transition: none; } }
.side-sub { display: none; flex-direction: column; gap: 1px; margin: 2px 0 4px 21px; padding-left: 7px; border-left: 1px solid var(--line); }
.side-group.open > .side-sub { display: flex; }
.side-sub-link { display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--ink); border-radius: var(--r1); padding: 0 9px; min-height: calc(var(--tap) - 4px); font: 400 var(--fs-2)/1.4 var(--f-sans); white-space: nowrap; }
.side-sub-link:hover { background: var(--nav-hover); color: var(--ink); }
.side-sub-link:focus-visible { outline: 0; box-shadow: var(--focus); }
.side-sub-link .lbl { overflow: hidden; text-overflow: ellipsis; }
/* Icon mục con nhỏ hơn một bậc — cùng cỡ icon cha thì hai cấp đọc ra ngang hàng. */
.side-sub-link .hp-i { width: 14px; height: 14px; }
.side-sub-link.on { background: var(--nav-on); color: var(--nav-on-ink); font-weight: 500; }

/* --- MENU KÊNH CỦA OMNICHAT (CRM-326) ---
   Vùng `#rb-omni` là `display:contents`: nó chỉ là chỗ để rolebar.js dựng lại riêng nhóm
   Omnichat khi danh sách kênh đổi, còn về bố cục thì các con của nó vẫn là mục trực tiếp
   của `.side-nav` (cùng khoảng cách, cùng flex như mọi mục khác). */
.rb-omni { display: contents; }
/* Hai con số của Omnichat đứng sát nhau: nút nhóm còn phải chừa chỗ cho mũi tên gập/mở,
   cách nhau 10px như mọi phần tử khác của dòng là chữ "Omnichat" bị cắt ở dải có thanh cuộn. */
.rb-bs { flex: none; display: inline-flex; gap: 3px; }
/* Nút nhóm con (một loại kênh có nhiều trang) mang CỠ mục con — nó là `<button>`, nên phải
   tự gỡ viền/nền/căn chữ mà thẻ `<a>` không có sẵn.
   Các luật dưới KHÔNG gắn vào `.rb-omni`: thẻ nổi lúc dải thu gọn là BẢN SAO nằm ở <body>,
   ngoài vùng ấy — gắn vào đó là số trên mục kênh trong thẻ nổi hết đứng ở mép phải. */
button.side-sub-link { width: 100%; border: 0; background: transparent; text-align: left; cursor: pointer; }
a.side-sub-link[data-kenh] .lbl, button.side-sub-link .lbl { flex: 1 1 auto; min-width: 0; }
.side-sub-link .rb-badge { padding: 2px 5px; }
.side-sub-link.on-group { color: var(--brand-2); }
/* Mục vừa ĐANG MỞ vừa là nhóm chứa trang đang xem (nút nhóm Omnichat khi đứng ở "mọi kênh") mang cả
   `.on` lẫn `.on-group`. Luật `.on-group` khai sau nên từng thắng: chữ `--brand-2` trên nền `--nav-on`
   gần như mất khi nền sẫm xuống (04/10/2026). Mục đang mở thì chữ luôn là `--nav-on-ink`. */
.side-group-btn.on-group.on, .side-sub-link.on-group.on { color: var(--nav-on-ink); }
/* Tầng 3 (trang/OA trong một loại kênh) lùi ÍT hơn tầng 2: dải chỉ rộng 244px, lùi đủ 29px
   lần nữa là tên trang còn chưa tới 80px. */
.rb-omni .side-sub .side-sub { margin-left: 13px; padding-left: 6px; }
/* Chữ cái đầu của tên trang thay cho ảnh Fanpage/OA — cùng cỡ icon mục con. */
.ib-av { flex: none; width: 14px; height: 14px; border-radius: 50%; display: grid; place-items: center; background: var(--surface-3); color: var(--ink-2); font: 600 var(--fs-1)/1 var(--f-sans); }
.side-sub-link.on .ib-av { background: var(--surface); color: var(--brand-2); }

/* --- Thu gọn: chỉ còn icon --- */
html.hp-side-min .side-link { justify-content: center; gap: 0; padding: 0; }
html.hp-side-min .side-link .lbl, html.hp-side-min .side-link .chev { display: none; }
/* Danh sách con KHÔNG mở tại chỗ khi thu gọn (không còn chỗ cho chữ) — nó hiện trong
   thẻ nổi bên phải, xem .side-fly.
   Bộ chọn phải nhắc lại `.side-group.open >`: rule mở nhóm ở trên có ba lớp, còn
   `html.hp-side-min .side-sub` chỉ có hai — thiếu vế đó thì rule này THUA về độ ưu
   tiên và nhóm đang mở vẫn bung ra trong dải rộng 64px (lỗi đã gặp thật khi nghiệm
   thu: chữ mục con tràn ra ngoài dải). */
html.hp-side-min .side-group.open > .side-sub { display: none; }
html.hp-side-min .side-nav { padding: 8px 9px; }
/* Số việc tồn: thu gọn thì không còn chỗ cho con số cạnh chữ — treo lên góc icon. */
html.hp-side-min .side-link .rb-badge { position: absolute; top: 2px; right: 2px; margin: 0; padding: 1px 4px; font-size: var(--fs-1); }

/* --- Khối TÀI KHOẢN ở GÓC PHẢI THANH TRÊN (CRM-152) ---
   rolebar.js dời `.hp-user-menu` vào `.tb-user` và giữ nó ở CUỐI cụm phải, tức đúng
   góc phải trên của màn hình (xem `placeUserMenu`). Trước đó nó nằm ở đáy dải menu —
   chỗ chỉ với tới được khi dải đang mở, mà dải tự thu về 64px ở mọi bề rộng ≤1200px
   và nằm hẳn ngoài khung nhìn ở màn hẹp.
   KHÔNG có rule riêng nào cho nó ở đây: `.hp-menu-btn` + `.hp-menu-list` của khung
   chung (phía dưới) đã đúng cho một nút đứng trên thanh ngang, và `@media
   (max-width: 700px)` cạnh `.tb-user` đã thu nó về vòng chữ tắt. Khối cũ phải khai
   lại từ bề rộng tới hướng bung menu chỉ vì nó đang đứng trong một dải dọc.
   Badge tenant đứng liền BÊN TRÁI khối này, không gấp vào trong menu — xem chú thích
   ở `buildBar()` trong rolebar.js. */

/* --- Thẻ nổi khi thu gọn: tên mục, hoặc tên nhóm + danh sách mục con --- */
.side-fly { position: fixed; z-index: var(--z-fly); display: none; min-width: 190px; max-width: 290px; max-height: calc(100vh - 16px); overflow-y: auto; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r2); box-shadow: var(--sh2); padding: 6px; }
.side-fly.open { display: block; }
.side-fly .side-fly-h { font: 500 var(--fs-2)/1.3 var(--f-sans); color: var(--ink); padding: 6px 9px; }
.side-fly .side-fly-list { display: flex; flex-direction: column; gap: 1px; margin-top: 4px; padding-top: 5px; border-top: 1px solid var(--line); }
/* Tiêu đề của một cụm CON đã bung phẳng trong thẻ nổi (xem `bungNhomCon` ở rolebar.js).
   Nhạt hơn tiêu đề chính một bậc, và KHÔNG mang class `.side-link`: mọi thứ mang class
   đó đều bị chính rule thu gọn ẩn mất chữ — mà thẻ nổi chỉ hiện lúc dải đang thu gọn. */
.side-fly .side-fly-sub-h { display: flex; align-items: center; gap: 6px; font: 500 var(--fs-2)/1.3 var(--f-sans); color: var(--muted); padding: 8px 9px 2px; }
/* Số tổng của một loại kênh (CRM-326) đứng mép phải tiêu đề nhóm trong thẻ nổi. */
.side-fly .side-fly-sub-h .rb-badge { margin-left: auto; padding: 2px 5px; }
/* Tiêu đề nhóm KÊNH là liên kết chọn cả nhóm (dòng "Tất cả" đã bỏ — xem `bungNhomCon` ở
   rolebar.js): giữ nguyên dáng tiêu đề, chỉ thêm con trỏ và màu thương hiệu khi rê/đang chọn. */
.side-fly a.side-fly-h, .side-fly a.side-fly-sub-h { text-decoration: none; cursor: pointer; border-radius: var(--r1); }
.side-fly a.side-fly-h:hover, .side-fly a.side-fly-sub-h:hover { color: var(--ink); background: var(--nav-hover); }
.side-fly a.side-fly-h.on, .side-fly a.side-fly-sub-h.on { color: var(--nav-on-ink); background: var(--nav-on); }
/* Mục đơn: thẻ nổi rút về đúng một mẩu chú thích, không giả làm menu rỗng. */
.side-fly.titleonly { min-width: 0; padding: 0; }
.side-fly.titleonly .side-fly-h { padding: 7px 11px; white-space: nowrap; }

/* --- Màn hẹp: dải thành NGĂN KÉO, mở bằng nút ba gạch trên thanh trên ---
   Ở đây KHÔNG có trạng thái "thu gọn": dải vốn đã nằm ngoài khung nhìn, thu gọn thêm
   chỉ còn là một dải icon khó đọc. rolebar.js gỡ `hp-side-min` khi rơi vào ngưỡng này
   (và trả lại lựa chọn đã nhớ khi cửa sổ rộng ra), nên mọi rule "thu gọn" ở trên tự
   động không áp dụng — không phải viết lại lần thứ hai ở đây. */
.hp-side-backdrop { position: fixed; inset: 0; background: var(--scrim); z-index: 8; display: none; }
.hp-side-backdrop.on { display: block; }
@media (max-width: 860px) {
  /* Nhắc lại cả biến thể `.hp-side-min` — KHÔNG thừa: rule "thu gọn" ở trên có HAI
     lớp (`html.hp-side-on.hp-side-min body`) nên thắng rule một lớp ở đây, dù rule
     này nằm trong media query (media query không cộng độ ưu tiên). Bình thường
     `applyMode()` đã gỡ lớp đó trước khi vào ngưỡng này, nhưng thế là CSS phải TIN
     vào JS chạy đúng lúc; hễ lớp còn sót một nhịp là lòi ra khe trống 64px cạnh nội
     dung và ngăn kéo co lại chỉ còn icon. Khai đủ ở đây thì CSS tự đứng vững một
     mình, không phụ thuộc thời điểm JS chạy. */
  html.hp-side-on body, html.hp-side-on.hp-side-min body { padding-left: 0; }
  /* `transform` mới chỉ đẩy dải RA KHỎI KHUNG NHÌN — nó vẫn nằm nguyên trong thứ tự
     Tab và trong cây trợ năng. Ngăn kéo đóng mà bấm Tab thì con trỏ chạy hết mười
     mấy mục KHÔNG NHÌN THẤY (đo thật: mục đầu nằm ở x = -10), Enter thì mở một link
     không ai biết là link nào; trình đọc màn hình cũng đọc hết cả dải đang ẩn.
     `visibility: hidden` gỡ khỏi CẢ HAI, và không cần `inert` nên không kèm điều
     kiện trình duyệt. Hoãn đúng bằng thời lượng trượt để còn thấy animation đóng;
     lúc mở thì bỏ hoãn để hiện ngay từ khung hình đầu. */
  .hp-side, html.hp-side-min .hp-side { width: min(84vw, var(--side-w)); }
  .hp-side { transform: translateX(-100%); visibility: hidden;
             transition: width var(--dur-1) var(--ease-in-out), transform var(--dur-2) var(--ease-out), visibility 0s linear var(--dur-2); }
  .hp-side.is-open { transform: none; visibility: visible; transition-delay: 0s; box-shadow: var(--sh3); }
  /* Ngăn kéo phải nằm TRÊN thanh trên, nền mờ nằm giữa hai thứ. `.topbar` mang
     `position:sticky; z-index:10` (nay khai một lần ở khối thanh trên bên trên, trước
     đây mỗi trang tự chép), mà dải chỉ có `z-index:9` — hệ quả ĐO ĐƯỢC: 50px đầu của ngăn kéo, tức
     ĐÚNG hàng có logo, bị thanh trên (nền đặc) che kín; và nền mờ `z-index:8` cũng
     nằm dưới thanh, nên bấm vào dải thanh trên không đóng được ngăn kéo mà lại bấm
     trúng nút bên dưới. Chỉ nâng TRONG ngưỡng này: ở màn rộng dải là một cột tĩnh và
     PHẢI nằm dưới lớp phủ đăng nhập (z-index 10) cùng hộp thoại kết ca (11). */
  .hp-side { z-index: 12; }
  .hp-side-backdrop { z-index: 11; }
  .side-burger { display: inline-flex; }
  .side-toggle { display: none; }
}
/* ĐẶT SAU khối ngăn kéo, không phải trước: khối đó khai lại `transition` cho
   `.hp-side`, nên rule này để trên sẽ bị ghi đè đúng ở màn hẹp — tức đúng chỗ dải
   trượt nhiều nhất. `visibility` giữ nguyên hoãn 0s ở đây: nó không phải chuyển động,
   mà là thứ quyết định dải có nằm trong thứ tự Tab hay không. */
@media (prefers-reduced-motion: reduce) {
  .hp-side, html.hp-side-on body { transition: none; }
  .hp-side { transition: visibility 0s linear var(--dur-2); }
  .hp-side.is-open { transition-delay: 0s; }
}

/* --- TAY KÉO ĐỔI BỀ RỘNG (`.hp-grip`, dựng bởi `HPUI.keoRong` ở hp-ui.js) ---
   MỘT khuôn cho mọi ranh giới kéo được: cạnh phải dải menu (ngay dưới) và cạnh trái cột
   phải Omnichat (style.css). Trang chỉ đặt VỊ TRÍ; hình dạng và trạng thái ở đây.
   Vùng bấm 8px (16px với màn cảm ứng) rộng hơn vạch nhìn thấy — một đường 1px là cái bẫy
   chuột. Cái NẮM giữa chiều cao lúc nào cũng hiện, mờ: ai cũng phải biết chỗ này kéo được
   mà không cần tình cờ rê chuột qua đúng 8px ấy (bài học tay kéo ngăn kéo sự vụ). */
.hp-grip { position: absolute; top: 0; bottom: 0; width: 8px; z-index: 2; cursor: col-resize; touch-action: none; }
/* Vạch chạy hết chiều cao — chỉ hiện khi rê, focus hoặc đang kéo. */
.hp-grip::before { content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 2px; background: transparent; transition: background var(--dur-1) var(--ease-out); }
/* Cái nắm: một viên thuốc dọc, đứng ĐÈ lên đường viền của khối. */
.hp-grip::after { content: ''; position: absolute; top: 50%; left: -1px; width: 4px; height: 28px; margin-top: -14px; border-radius: var(--rp); background: var(--line-2); transition: background var(--dur-1) var(--ease-out); }
.hp-grip:hover::before, .hp-grip:focus-visible::before, .hp-grip.on::before,
.hp-grip:hover::after, .hp-grip:focus-visible::after, .hp-grip.on::after { background: var(--brand-2); }
.hp-grip:focus-visible { outline: 0; }
@media (pointer: coarse) { .hp-grip { width: 16px; } }
/* TRONG LÚC KÉO: con trỏ giữ nguyên hình kéo dù đi qua nút hay ô chữ, không bôi đen chữ,
   và khung nhúng (popup tạo sự vụ) không nuốt mất lượt di chuột. Tắt chuyển động của lề
   <body> và bề rộng dải: kéo mà mỗi khung hình còn trượt 120ms là tay kéo lết sau con trỏ.
   ĐẶT SAU `html.hp-side-anim body` — hai bộ chọn cùng độ ưu tiên, luật sau thắng. */
html.hp-dang-keo, html.hp-dang-keo * { cursor: col-resize; user-select: none; }
html.hp-dang-keo iframe { pointer-events: none; }
html.hp-dang-keo body, html.hp-dang-keo .hp-side { transition: none; }
/* Cạnh PHẢI dải menu: 1px đè lên đường viền của dải, phần còn lại thò ra ngoài — tuyệt
   đối không lấn vào trong, vì sát mép trong là thanh cuộn của `.side-nav`: đè lên đó là
   không kéo được thanh cuộn nữa. */
.side-grip { right: -7px; }
@media (pointer: coarse) { .side-grip { right: -15px; } }
/* Dải đang thu về icon (`--side-w-min`), hoặc đang là ngăn kéo phủ lên nội dung ở màn
   hẹp: không có ranh giới nào giữa dải và trang để mà kéo. */
html.hp-side-min .side-grip { display: none; }
@media (max-width: 860px) { .side-grip { display: none; } }

/* --- Dropdown/popover dùng chung: menu Báo cáo/Quản trị/Ngôn ngữ/Người dùng.
   Một khuôn duy nhất (HPUI.dropdown() trong hp-ui.js lo phần mở/đóng/Escape). --- */
.hp-menu { position: relative; display: inline-flex; min-width: 0; flex-shrink: 0; }
.hp-menu-btn { display: inline-flex; align-items: center; gap: 6px; border: 0; background: transparent; color: var(--ink); border-radius: var(--r1); padding: 7px 9px; font: 500 var(--fs-2)/1 var(--f-sans); cursor: pointer; min-height: var(--tap); white-space: nowrap; }
.hp-menu-btn:hover { color: var(--ink); background: var(--surface-2); }
.hp-menu-btn:focus-visible { outline: 0; box-shadow: var(--focus); }
.hp-menu-btn[aria-expanded="true"] { color: var(--ink); background: var(--surface-2); }
.hp-menu-btn .chev { transition: transform var(--dur-1) var(--ease-out); opacity: .7; flex: none; }
.hp-menu-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
.hp-menu-list { position: absolute; top: calc(100% + 6px); right: 0; min-width: 200px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r2); box-shadow: var(--sh2); padding: 6px; z-index: var(--z-menu); display: none; }
.hp-menu.left .hp-menu-list { right: auto; left: 0; }
.hp-menu.open .hp-menu-list { display: block; }
.hp-menu-item { display: flex; align-items: center; gap: 9px; width: 100%; text-align: left; border: 0; background: transparent; color: var(--ink); border-radius: var(--r1); padding: 8px 10px; font: 400 var(--fs-2)/1.4 var(--f-sans); cursor: pointer; text-decoration: none; white-space: nowrap; }
.hp-menu-item:hover { background: var(--surface-2); }
.hp-menu-item:focus-visible { outline: 0; box-shadow: var(--focus); }
/* Mục đang mở trong menu — chữ ngược nền như mọi trạng thái "đang chọn" khác của hệ
   (xem quy tắc màu ở đầu tệp). Cần từ khi cụm "Sự vụ" có chín mục: nút cụm sáng chỉ
   nói đang ở trong cụm, không nói đang ở mục nào. */
.hp-menu-item.on { background: var(--brand-soft); color: var(--brand-2); }
.hp-menu-item.on:hover { background: var(--brand-soft); }
.hp-menu-item.danger { color: var(--bad); }
.hp-menu-item.danger:hover { background: var(--bad-soft); }
/* Mục bị khoá (ví dụ: lời mời chia sẻ còn treo — `applyShareGate` ở app.js). Phải có
   kiểu riêng, không thì mục vô hiệu hoá trông y hệt mục bấm được và người dùng bấm mãi. */
.hp-menu-item:disabled { opacity: .5; cursor: default; }
.hp-menu-item:disabled:hover { background: transparent; }
/* NÚT MỞ MENU bị khoá — cùng lý lẽ, và là chỗ dễ sót hơn hẳn: `applyShareGate` khoá
   `#mya-btn` với `#conv-acts` bằng `disabled`, nhưng hai nút ấy mang `.hp-menu-btn` chứ
   KHÔNG mang `.mya-btn`, nên luật `.mya-btn:disabled` của style.css không với tới. Người
   dùng thấy hai cái nút sáng như thường, bấm vào thì menu không mở, và không có gì nói vì
   sao. Khai ở đây vì `.hp-menu-btn` là component dùng chung, không riêng của đầu khung chat. */
.hp-menu-btn:disabled { opacity: .5; cursor: default; }
.hp-menu-btn:disabled:hover { background: transparent; color: var(--ink-2); }
/* Dòng tiêu đề của menu: nói menu này là menu gì, hoặc trạng thái của thứ nó điều khiển.
   KHÔNG bấm được nên không phải `.hp-menu-item` — một dòng trông bấm được mà không bấm
   được là lời hứa suông. */
.hp-menu-h { padding: 6px 10px 5px; font: 500 var(--fs-1)/1.3 var(--f-sans); color: var(--muted); }

/* --- Ô ẢNH ĐẠI DIỆN (CRM-142) ---
   Dùng cho ảnh KHÁCH ở màn nhân viên. Khác `.hp-avatar` ngay dưới — cái đó là chấm
   viết tắt tên của CHÍNH nhân viên trên thanh trên, luôn có tên và không bao giờ có
   ảnh; ô này phải chịu được cả hai trạng thái và chuyển giữa chúng mà không nhảy.

   XẾP LỚP: chữ tắt vẽ SẴN, ảnh nằm ĐÈ lên bằng position:absolute. Ảnh đại diện đến từ
   sàn ngoài nên tải hỏng là chuyện thường; `onerror="this.remove()"` bên app.js gỡ thẻ
   ảnh và để lộ đúng cái đã có ở dưới. Không nhánh nào đổi kích thước, nên không có cú
   nhảy layout — đúng luật "không gì đổi kích thước sau khi đã hiện".

   MÀU: sáu tông suy từ tên khách (xem `avatarTone`), tất cả đều là cặp token đã khai
   sẵn nên cả hai chế độ nền đều có chữ ngược màu, đúng quy tắc UI của dự án. */
.hp-av { position: relative; width: 34px; height: 34px; border-radius: 50%; flex: none; overflow: hidden; display: grid; place-items: center; background: var(--brand-soft); color: var(--brand-2); }
.hp-av.big { width: 40px; height: 40px; }
/* line-height chừa dư cho dấu tiếng Việt, cùng lý do đã ghi ở `.hp-menu-btn .lbl`. */
.hp-av-ini { font-family: var(--f-sans); font-size: var(--fs-1); font-weight: 600; line-height: 1.3; }
.hp-av.big .hp-av-ini { font-size: var(--fs-2); }
/* `inset: 0` + `object-fit: cover`: ảnh sàn trả về đủ mọi tỉ lệ, `cover` cắt cho vừa
   vòng tròn thay vì bóp méo mặt người. */
.hp-av-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.hp-av.t0 { background: var(--brand-soft); color: var(--brand-2); }
.hp-av.t1 { background: var(--info-soft);  color: var(--info); }
.hp-av.t2 { background: var(--ok-soft);    color: var(--ok); }
.hp-av.t3 { background: var(--warn-soft);  color: var(--warn); }
.hp-av.t4 { background: var(--bad-soft);   color: var(--bad); }
.hp-av.t5 { background: var(--surface-3);  color: var(--ink-2); }

/* --- Chip người dùng: chấm tròn viết tắt tên + tên, mở menu người dùng ---
   `.lbl` ở đây LÀ chính phần tử #me-name được rolebar.js di chuyển vào (một <small>,
   cùng một id trên mọi màn) — ép font/color kế thừa nút cha, không theo cỡ chữ nhỏ
   mặc định của <small>. Thuộc tính `style` viết thẳng trên phần tử vẫn thắng khối
   này, nên rolebar.js gỡ nó khi gộp menu; xem Luật 28. */
.hp-avatar { width: 24px; height: 24px; border-radius: 50%; background: var(--brand-soft); color: var(--brand-2); display: grid; place-items: center; font: 600 var(--fs-1)/1 var(--f-sans); flex: none; }
/* line-height RIÊNG (không phải 1 kế thừa từ nút cha): overflow:hidden ở đây clip cả
   theo chiều dọc, và line-height:1 không chừa chỗ cho dấu nặng tiếng Việt (ự, ị…) — nó
   nằm dưới đường cơ sở, bị cắt mất, chữ "Sự vụ"/"Quản trị" hiện thành "Sư vu"/"Quan
   tri". Cỡ nào cũng phải chừa dư đủ cho dấu, không riêng gì `13px`. */
.hp-menu-btn .lbl { font: inherit; line-height: 1.4; color: inherit; max-width: 160px; overflow: hidden; text-overflow: ellipsis; }
/* Ẩn ở màn hẹp: xem media query (max-width: 700px) cạnh .tb-user phía trên. Luật đó
   lại là luật THẬT từ CRM-152: `.hp-user-menu` đứng ở `.tb-user` (góc phải trên) trên
   MỌI trang có thanh trên, nên dưới 700px chữ đi và chỉ còn vòng chữ tắt — đúng thứ
   phải nhường bề ngang cho TÊN TRANG. Vì luật này đã kẹp bề ngang ở màn hẹp, trang
   KHÔNG được tự kẹp thêm: `style.css` từng khai riêng `.topbar #me-name{max-width:
   40vw}` cho màn Omnichat, một luật chết (dưới 700px chữ đã ẩn) mà lại là một thanh
   trên khác với mười lăm màn kia. Đã bỏ. */

/* Số việc tồn trên nút "Omnichat" (CRM-83) — gắn vào .side-link qua rolebar.js
   (sideLinkHtml). Hổ phách = việc đang chờ, theo đúng bảng ngữ nghĩa trạng thái ở đầu
   file — không lấy màu thương hiệu, vì teal ở đây có nghĩa "hành động chính", không
   có nghĩa "còn tồn".
   Cặp nền-nhạt / chữ-đậm (--warn-soft / --warn) tự đảo theo nền: nền sáng thì nền badge
   nhạt chữ đậm, nền tối thì ngược lại. Đặt thẳng một màu nền là hỏng đúng một trong hai
   nền — quy tắc "chữ luôn ngược màu nền" của dự án. */
.rb-badge { background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn-line); border-radius: var(--rp); font: 600 var(--fs-1)/1 var(--f-sans); padding: 3px 6px; margin-left: 2px; }
.rb-badge[hidden] { display: none; }
/* Badge thứ hai: việc CHƯA AI NHẬN (CRM-145). Đỏ chứ không hổ phách, theo đúng bảng ngữ
   nghĩa ở đầu file: hổ phách = "đang chờ" (có người cầm, chỉ là chưa xong), đỏ = "hỏng
   luồng" — mà một khách đang đợi không có ai cầm việc thì đúng là luồng đã hở.
   Cùng cặp nền-nhạt / chữ-đậm (--bad-soft / --bad) nên tự đảo theo nền sáng/tối. */
.rb-badge-hot { background: var(--bad-soft); color: var(--bad); border-color: var(--bad-line); }
/* Dải THU GỌN treo badge lên góc icon (xem `html.hp-side-min .side-link .rb-badge` bên
   trên). Hai badge chồng đúng một chỗ thì cái dưới mất hút, nên đẩy badge đỏ xuống góc
   ĐỐI DIỆN: tồn ở trên-phải, chưa nhận ở dưới-phải. */
html.hp-side-min .side-link .rb-badge-hot { top: auto; bottom: 2px; }

/* --- Bộ chọn ngôn ngữ (i18n.js dựng) --- */
.hp-lang { display: inline-flex; border: 1px solid var(--line); border-radius: var(--rp); overflow: hidden; vertical-align: middle; }
.hp-lang button { border: 0; background: var(--surface-2); color: var(--muted); font: 600 var(--fs-1)/1 var(--f-sans); padding: 6px 11px; cursor: pointer; min-height: 26px; }
.hp-lang button:hover { color: var(--ink); }
.hp-lang button + button { border-left: 1px solid var(--line); }
.hp-lang button.on { background: var(--brand); color: var(--brand-ink); }

/* --- Bộ chọn nền & mật độ, hp-ui.js dựng vào thanh dùng chung --- */
.hp-prefs { display: inline-flex; gap: 3px; align-items: center; background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--rp); padding: 3px; }
.hp-prefs button { border: 0; background: transparent; color: var(--muted); border-radius: var(--rp); padding: 5px 9px; font: 600 var(--fs-1)/1 var(--f-sans); cursor: pointer; min-height: 26px; }
.hp-prefs button:hover { color: var(--ink); }
.hp-prefs button[aria-pressed="true"] { background: var(--brand-soft); color: var(--brand-2); }

/* --- KHAY TUỲ CHỌN HIỂN THỊ (nút ⚙ ở thanh trên) ---
   Bốn cặp bật/tắt vốn xếp ngang trên thanh nay thành bốn HÀNG trong một menu: nhãn bên
   trái, cặp nút bên phải. Xem `mountPrefsMenu` ở rolebar.js.

   Nhãn lấy bằng `content: attr(aria-label)` từ chính hộp. Ba module dựng ba hộp ấy theo
   ba nhịp khác nhau và mỗi hộp đã tự đặt `aria-label` đã dịch — đọc bằng CSS thì nhãn
   luôn đúng, kể cả khi bản dịch về sau lượt dựng, mà không phải hẹn giờ với ai. */
.hp-prefs-btn { padding: 7px 8px; }
.hp-prefs-menu .hp-menu-list { min-width: 268px; padding: 6px 4px 8px; }
.hp-prefs-body > span { display: block; }
.hp-prefs-body .hp-prefs { display: flex; align-items: center; justify-content: space-between; gap: 14px; width: 100%; background: transparent; border: 0; border-radius: 0; padding: 6px 8px; }
.hp-prefs-body .hp-prefs::before { content: attr(aria-label); flex: 1; min-width: 0; font: 600 var(--fs-1)/1.3 var(--f-sans); color: var(--ink); }
/* Cặp nút dính thành MỘT dải: viền chung, lề âm 1px để hai viền chồng làm một. Bản trên
   thanh dùng một cái pill bọc ngoài, nhưng ở đây hộp ngoài đã phải nhường chỗ cho nhãn. */
.hp-prefs-body .hp-prefs button { flex: none; border: 1px solid var(--line); background: var(--surface-2); border-radius: 0; }
.hp-prefs-body .hp-prefs button:first-of-type { border-radius: var(--rp) 0 0 var(--rp); }
.hp-prefs-body .hp-prefs button:last-of-type { border-radius: 0 var(--rp) var(--rp) 0; }
.hp-prefs-body .hp-prefs button + button { margin-left: -1px; }
/* NỀN THƯƠNG HIỆU PHẢI KHAI LẠI Ở ĐÂY.
   `.hp-prefs-body .hp-prefs button` ngay trên đặt `background: var(--surface-2)` và có độ
   ưu tiên (0,2,1) — BẰNG ĐÚNG `.hp-prefs button[aria-pressed="true"]` ở khối gốc, mà lại
   đứng sau, nên nó đè mất nền `--brand-soft` của nút đang chọn. Chỉ đổi `border-color` thì
   trạng thái bật chỉ còn một đường viền nhạt: bốn cặp Nền · Mật độ · Âm báo · Thông báo
   gần như không đọc ra đang chọn cái nào.
   Khai lại cả nền lẫn chữ ở đây thay vì hạ độ ưu tiên của luật trên: luật trên cần đúng
   `background` ấy cho nút KHÔNG được chọn (dải hai nút dính nhau phải có nền đặc mới thành
   một dải), nên hai trạng thái phải nói rõ từng cái. */
.hp-prefs-body .hp-prefs button[aria-pressed="true"] { background: var(--brand-soft); color: var(--brand-2); border-color: var(--brand-line); position: relative; z-index: 1; }

/* ============================================================================
   KHUNG TRANG TOÀN MÀN HÌNH (.hp-page)

   Vì sao có khối này: tám trang từng tự khai một trần bề ngang riêng —
   `main{max-width:1180px}` ở Cuộc gọi và Báo cáo, 1200 ở Chi tiết sự vụ, 1280 ở
   Lịch sử và Đồng bộ, 1440 ở Sự vụ, 1600 ở Danh sách yêu cầu, `.screen{900px}` ở
   Báo cáo đội. Trên màn 1920 (vùng nội dung 1676px sau dải menu) nghĩa là bỏ trống
   76–518px, trong khi chính bảng bên trong đang phải cuộn ngang vì thiếu chỗ. Đúng
   cái bệnh "mỗi trang một cách viết" mà file này ra đời để chữa.

   BỐN LUẬT — "toàn màn hình" KHÔNG phải là kéo mọi thứ dài ra:
     1. Bảng & danh sách  → tràn sát mép, cuộn TRONG khung (.hp-tbl)
     2. Biểu mẫu          → rộng ra thì thêm CỘT, ô giữ nguyên bề rộng (.hp-fgrid)
     3. Màn chi tiết      → rộng ra thì thêm KHOANG (.hp-panes)
     4. Chữ đọc liền mạch → vẫn khoá ở 80ch (.hp-read) — ngoại lệ CÓ CHỦ Ý

   NGƯỠNG ĐO VÙNG NỘI DUNG, KHÔNG ĐO KHUNG NHÌN. `@media (min-width: N)` đo khung
   nhìn, nhưng nội dung nằm trong phần còn lại sau dải menu — rộng 244px khi mở và
   64px khi thu. Cùng một khung nhìn cho ra hai bề rộng nội dung lệch nhau tới
   180px, nên một con số ngưỡng không thể đúng cho cả hai trạng thái. `container-type`
   + `@container` đo đúng bề rộng chỗ đứng, và không phải nhớ trừ 244.

   NGỮ CẢNH XẾP LỚP RIÊNG — KHAI, KHÔNG SUY RA (01/10/2026). Chú thích cũ ở đây viết
   `container-type` "bật luôn `contain: layout`" nên `.hp-page` là khung neo của mọi con
   `position: fixed`. Đặc tả đã gỡ vế ấy; đo trên Chromium 145: `container-type:
   inline-size` KHÔNG tạo ngữ cảnh xếp lớp, cũng KHÔNG thành khung neo. Hệ quả đã cắn
   trên dev: `z-index` của một khối trong trang thi đấu THẲNG với thanh trên (10) và dải
   menu (9), mà mọi menu thả của thanh trên bị nhốt trong ngữ cảnh z 10 của chính thanh —
   `.barwrap{z-index:31}` ở Tra cứu CRM cũ đè mất menu Tài khoản (nút Đăng xuất), menu ⚙
   và khay chuông. Nên khung tự khai `isolation: isolate` (ba khung: `.hp-page`,
   `.hp-measure`, `.workspace` ở style.css): con số trong trang từ nay chỉ xếp lớp VỚI
   NHAU. Hàng rào: Luật 45 (`src/core/ui-diu-mat.test.ts`).
   Cái giá, và vì sao luật cũ vẫn giữ: lớp phủ toàn màn hình đặt TRONG khung sẽ bị nhốt
   DƯỚI thanh trên. Nên lớp phủ PHẢI đứng ngoài khung — `#peek` ở màn Đồng bộ và `.modal`
   ở Danh sách yêu cầu nằm sau `</main>`; lớp phủ dựng bằng JS (`.sh-overlay`,
   `.hp-scrim`, ngăn kéo, ô chọn nổi lúc mở) gắn thẳng vào <body>. Lượt soát 01/10/2026
   trên mọi màn: không còn phần tử `position: fixed` nào đang hiện mà nằm trong khung.
   ============================================================================ */
/* Trang có khung `.hp-page` KHÔNG cuộn ở <body>: chiều cao đã khoá đúng bằng khung
   nhìn, nên chỉ một pixel lẻ do làm tròn cũng đủ sinh ra thanh cuộn dọc thứ hai —
   thứ trông y như lỗi mà không lỗi nào nổ ra. */
body:has(.hp-page) { overflow: hidden; }

/* HỘP ĐO, KHÔNG ĐỔI BỐ CỤC. Cho trang nào đã tràn hết mép sẵn (Tra cứu, Tạo sự vụ,
   Quản trị) mà chỉ cần chữa hộp đo ngưỡng: gắn lớp này lên khung ngoài cùng rồi đổi
   `@media` thành `@container page`. Không đụng gì tới chiều cao hay cách cuộn của
   trang — chỉ khai ra một hộp để `@container` có cái mà đo. */
.hp-measure { container-type: inline-size; container-name: page; isolation: isolate; }

.hp-page {
  container-type: inline-size;
  container-name: page;
  isolation: isolate; /* z-index trong trang không đè được thanh trên — xem chú thích khối */
  display: flex; flex-direction: column;
  /* Hai dòng, cố ý: `dvh` là dòng đúng (Safari iOS thu thanh địa chỉ khi cuộn, `vh`
     tính cả thanh đã thu nên thanh công cụ dính bị đẩy khuất dưới mép), `vh` đứng
     trước làm dòng dự phòng cho trình duyệt chưa biết `dvh`.
     `--hp-band` là tổng chiều cao các BĂNG BÁO mà rolebar.js chèn ngay dưới thanh trên
     (chưa gán phòng ban · bị chuyển khỏi màn không có quyền). Băng nằm trong luồng nên đẩy
     khung xuống; không trừ nó ra thì đúng bấy nhiêu điểm ảnh ĐÁY khung rơi ra ngoài khung
     nhìn, mà <body> khoá cuộn — thanh phân trang, dòng cuối, nút Gửi của Omnichat không còn
     đường nào xem tới (đo 05/10/2026). Không có băng thì biến vắng mặt, rơi về 0px. Hàng
     rào: Luật 47 (`src/core/ui-cuon.test.ts`). */
  height: calc(100vh - var(--top-h) - var(--hp-band, 0px) - var(--sheet-gap));
  height: calc(100dvh - var(--top-h) - var(--hp-band, 0px) - var(--sheet-gap));
  min-height: 0;
  /* TẤM NỘI DUNG (CRM-361): MỘT mặt phẳng bo góc nằm trên nền trang, thay cho thân trang
     trắng tràn mép giáp dải menu. Khe `--sheet-gap` ở mép phải và mép dưới; mép trái giáp
     dải menu (đã nằm trên nền trang) nên viền của tấm tự làm vạch chia. `overflow: hidden`
     để thanh lọc dính và bảng tràn mép vẫn bị bo theo góc tấm. */
  margin: 0 var(--sheet-gap) var(--sheet-gap) 0;
  background: var(--page);
  border: 1px solid var(--page-line);
  border-radius: var(--r3);
  box-shadow: var(--page-sh);
  overflow: hidden;
}
/* Cùng luật với `.hp-page-bar[hidden]`/`.hp-page-body[hidden]` bên dưới: `display:flex` ở
   trên là luật của tác giả nên THẮNG `[hidden]{display:none}` của trình duyệt. Màn Cuộc gọi
   rớt ẩn cả `#shell` khi vai không có quyền (CRM-300) — thiếu dòng này thì thanh lọc và
   skeleton vẫn hiện dưới hộp "chưa được cấp quyền" (CRM-303 #9). */
.hp-page[hidden] { display: none; }

/* Thanh công cụ + bộ lọc: DÍNH, không cuộn theo nội dung. Trước đây ô lọc cuộn mất
   khi kéo xuống bảng, và một con số đã lọc rất dễ bị đọc thành con số tổng. */
.hp-page-bar {
  flex: none; display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding: 11px var(--gut);
  background: var(--surface); border-bottom: 1px solid var(--line);
}
.hp-page-bar > .sp { flex: 1 1 12px; }
/* `display:flex` thắng thuộc tính `hidden` của HTML — thanh lọc nào dựng bằng JS và
   mang `hidden` cho tới khi có dữ liệu sẽ HIỆN RA rỗng nếu thiếu dòng này. */
.hp-page-bar[hidden] { display: none; }

/* DẢI KHU (CRM-385) — hàng thẻ ở đầu các trang cùng một khu của nhóm Thiết lập (Giám sát & kiểm
   soát: Kiểm soát tra cứu · Nhịp sử dụng · Thời gian chết · Đồng bộ). Thẻ do rolebar.js dựng
   (`veDaiKhu`) từ chính các mảng lái dải trái; ô chứa nằm tĩnh trong HTML và KHOÁ chiều cao để
   thẻ hiện ra trong chỗ đã giữ — không đẩy thân trang xuống sau khung hình đầu. */
.hp-khu { flex: none; display: flex; align-items: stretch; gap: 2px; height: 42px; padding: 0 var(--gut);
  background: var(--surface); border-bottom: 1px solid var(--line); overflow-x: auto; scrollbar-width: none; }
.hp-khu::-webkit-scrollbar { display: none; }
.hp-khu-ten { display: inline-flex; align-items: center; padding-right: 12px; margin-right: 6px;
  border-right: 1px solid var(--line); color: var(--muted); font: 500 var(--fs-1)/var(--lh-1) var(--f-sans); white-space: nowrap; }
.hp-khu-tab { display: inline-flex; align-items: center; padding: 0 12px; margin-bottom: -1px; color: var(--ink-2);
  text-decoration: none; white-space: nowrap; font: 400 var(--fs-2)/var(--lh-2) var(--f-sans);
  border-bottom: 2px solid transparent; }
.hp-khu-tab:hover { color: var(--ink); }
.hp-khu-tab:focus-visible { outline: 0; box-shadow: var(--focus); }
.hp-khu-tab.on { color: var(--brand-2); font-weight: 500; border-bottom-color: var(--nav-on); }

/* BỐ CỤC TÌM KIẾM – LỌC DÙNG CHUNG của các màn danh sách (cùng khuôn màn Sự vụ):
   dòng 1 `.hp-page-bar` = ô tìm · Tra cứu · (đẩy phải) Cột · Bộ lọc yêu thích;
   dòng 2 `.hp-page-bar.hp-fbar` = các ô lọc chính, mỗi ô một nhãn (`.hp-fil`), rồi popover
   "Bộ lọc khác" (`.hp-fmore`) cho ô phụ. `hp-ui.js` tự gắn mở/đóng cho `.hp-fmore`. */
.hp-page-bar.hp-fbar { padding-top: 8px; padding-bottom: 8px; gap: 8px 12px; }
.hp-fil { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-2); color: var(--ink); }
.hp-fil > span { white-space: nowrap; font-weight: 500; color: var(--ink); }
.hp-fil select, .hp-fil input:not([type="checkbox"]) {
  border: 1px solid var(--line); border-radius: 8px; padding: 4px 8px; font: inherit; font-size: var(--fs-2);
  line-height: var(--lh-2); background: var(--surface); color: var(--ink); max-width: 200px;
}
.hp-fil select:focus, .hp-fil input:focus { outline: 0; border-color: var(--brand); box-shadow: var(--focus); }
.hp-fil input[type="checkbox"] { accent-color: var(--brand); margin: 0; }
.hp-fil.chk { cursor: pointer; user-select: none; }
.hp-fil.chk > span { font-weight: 500; }
.hp-frange > i { font-style: normal; color: var(--muted); }
.hp-fmore > .hp-menu-btn { border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  font-size: var(--fs-2); padding: 5px 10px; min-height: 0; }
.hp-fmore > .hp-menu-btn:hover { border-color: var(--line-2); background: var(--surface); }
.hp-fmore .hp-menu-list { min-width: 250px; max-width: calc(100cqw - 24px); max-height: min(60vh, 420px);
  overflow: auto; padding: 10px; }
.hp-fmore .hp-fil { display: flex; flex-direction: column; align-items: stretch; gap: 4px; margin-bottom: 10px; }
.hp-fmore .hp-fil:last-child { margin-bottom: 0; }
.hp-fmore .hp-fil select, .hp-fmore .hp-fil input:not([type="checkbox"]) { max-width: none; width: 100%; }
.hp-fmore .hp-frange { flex-direction: row; flex-wrap: wrap; align-items: center; }
.hp-fmore .hp-frange > span { flex-basis: 100%; }
.hp-fmore .hp-frange input { flex: 1 1 0; min-width: 0; width: auto; }

/* Vùng cuộn DUY NHẤT của trang — <body> không còn cuộn dọc. `min-height:0` là dòng
   bắt buộc: thiếu nó thì con của flex không chịu co, khung cao quá màn hình, và
   trang lại cuộn ở <body> — tức quay về đúng chỗ cũ, chỉ thêm một thanh cuộn nữa. */
.hp-page-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 14px var(--gut) 28px; }
.hp-page-body.flush { padding: 0; }   /* bảng tràn sát mép, không lề */
/* Thân trang xếp dọc nhiều khối, khối cuối cùng ăn hết chiều cao còn lại. */
.hp-page-body.col { display: flex; flex-direction: column; gap: 13px; }
/* KHỐI TRONG THÂN TRANG KHÔNG CO — thiếu chỗ thì THÂN TRANG cuộn (05/10/2026).
   Con của flex mặc định `flex-shrink:1`, và con nào có `overflow` khác `visible` (thẻ bo góc
   `overflow:hidden`, bảng `overflow-x:auto`) thì `min-height:auto` tính ra 0 — đặc tả flexbox
   chỉ cho sàn theo nội dung với khối KHÔNG phải khung cuộn. Nên hễ tổng các khối cao hơn chỗ
   trống là trình duyệt lấy chỗ thiếu từ đúng những khối ấy: khối co lại, phần dưới bị cắt, và
   thân trang không thấy gì tràn nên không cuộn. Đo trên màn Tra cứu: 30 cuộc gọi mà tab chỉ
   hiện 6 dòng, thẻ khách bị bóp còn 24px — không có đường nào xem tiếp. Báo cáo, Báo cáo sự vụ,
   Dữ liệu nguồn từng tự vá TỪNG KHỐI bằng `flex:none`; đây là luật chung thay cho bản vá rời.
   `:where()` để độ ưu tiên bằng 0: khối CÓ CHỦ Ý co và cuộn riêng (`.hp-tbl`, `.card.grow`,
   `.split` của Lịch sử) tự khai `flex` nên vẫn thắng — kèm SÀN chiều cao, đừng để `min-height:0`
   trơn. Hàng rào: Luật 46 (`src/core/ui-cuon.test.ts`). */
:where(.hp-page-body.col) > * { flex-shrink: 0; }
/* CÙNG LUẬT với `.hp-page-bar[hidden]` ngay trên, và phải có vì cùng lý do: `display:flex`
   của `.col` là luật của tác giả nên nó THẮNG `[hidden]{display:none}` của trình duyệt.
   Trang nào chia thân ra nhiều mục rồi bật/tắt bằng `hidden` — màn Báo cáo chia Chat ·
   Cuộc gọi là trang đầu tiên — sẽ hiện CẢ HAI mục chồng nhau, và lệnh gán `hidden` trong
   JS trông như không chạy. (Cursor Bugbot, PR #378, High.) */
.hp-page-body[hidden] { display: none; }

/* --- Luật 1 · bảng dữ liệu ---
   `width:max-content` là cả câu chuyện: cột lấy đủ bề rộng đọc được rồi MỚI cuộn.
   Với `width:100%` thì trình duyệt phải nhồi mọi cột vào bề rộng khung, và nó lấy
   chỗ thiếu từ đúng những cột biết ngắt dòng — bóp xuống ~58px, đẩy một dòng địa chỉ
   cao 5 dòng chữ, mà bảng thì VẪN cuộn ngang. `min-width:100%` để bảng ít cột vẫn
   tràn hết mép.

   CHỈ dùng `.hp-tbl` cho bảng mà MỌI ô đều `white-space: nowrap`. Bảng có cột chữ tự
   do (thông báo lỗi, payload) thì `max-content` lấy bề rộng của chuỗi dài nhất — một
   dòng lỗi Odoo 400 ký tự sẽ kéo bảng rộng vài nghìn điểm ảnh. Bảng như vậy giữ
   `width:100%` và chỉ cần một hộp cuộn dọc riêng của trang. */
.hp-tbl { flex: 1 1 auto; min-height: 0; overflow: auto; background: var(--surface); }
/* `separate` + `spacing:0`, KHÔNG `collapse` (CRM-501): đầu bảng ở dưới dính `top:0`, mà ở
   `collapse` đường kẻ thuộc về BẢNG chứ không thuộc ô — ô dính để lại vạch dưới ở chỗ cũ, và ô ghim
   (`hp-ghim-*`) kéo ngang để lại vạch dọc. Bảng `.hp-tbl` chỉ kẻ vạch dưới nên đứng yên thì y hệt.
   Đặt ở ĐÂY chứ không ở từng trang: `.hp-tbl > table` nặng hơn `table{…}` của trang, nên trang tự
   khai `separate` bằng `table{}` là không có tác dụng (Bugbot bắt ở ba PR cùng lúc, #585–#587). */
.hp-tbl > table { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; }
/* Đầu bảng dính bên trong hộp cuộn hai chiều: `top:0` neo theo hộp `.hp-tbl`, không
   theo khung nhìn — nên nó đứng yên khi cuộn dọc và vẫn trôi ngang cùng cột.
   KHÔNG viết `> thead > tr > th`: vài chỗ dựng bảng bằng JS ghi thẳng `<table><tr><th>`
   không có `<thead>` (report.js, manager.js), trình duyệt tự bọc vào `<tbody>` nên
   bộ chọn qua `thead` không khớp gì — và đầu bảng thì trôi mất khi cuộn. */
.hp-tbl > table th { position: sticky; top: 0; z-index: 2; background: var(--surface-2); }

/* --- GHIM CỘT THAO TÁC (CRM-500 · CRM-501…510) ---
   Cột nút của dòng (Gọi lại, + Thêm sự vụ, Sửa/Xoá, ⋯) nằm cuối một bảng tràn ngang thì khuất
   ngoài khung: muốn bấm phải kéo hết bảng, rồi kéo ngược về để đọc dòng kế tiếp. Trang chỉ việc gắn
   lớp `hp-ghim-phai` (hoặc `hp-ghim-trai` cho cột thao tác đứng đầu) lên CẢ ô đầu cột lẫn ô của
   từng dòng — theo KHOÁ cột, không theo `:last-child`, vì cột đổi thứ tự được và ô `colspan` của
   dòng rỗng / dòng cảnh báo cũng đứng cuối hàng. Ô dính neo vào hộp cuộn ngang gần nhất.

   Xếp lớp: ô ghim của thân 1 < đầu bảng 2 < ô góc (đầu cột ghim) 3 — dòng cuộn lên chui dưới đầu
   bảng, đầu các cột khác trôi ngang dưới ô góc. Vì vậy đầu bảng của `.hp-tbl` ở trên là 2 chứ không
   1: cùng 1 thì ô ghim của thân (đứng SAU trong DOM) vẽ đè lên đầu bảng khi cuộn dọc. Trang tự dựng
   đầu bảng dính thì cũng phải đặt z-index ≥ 2.

   Nền ô thân ĐẶC — ô ghim mà trong suốt là chữ cột bên dưới lộ qua. Dòng có nền riêng (rê chuột,
   chưa đọc, đang chọn) thì đặt `--hp-ghim-nen` trên `<tr>` ấy để ô ghim đổi cùng. Nền ô ĐẦU cột thì
   trang tự lo (mỗi trang một màu đầu bảng) và phải đặc.

   Vạch tách ở mép trong của cột ghim CHỈ hiện khi nó đang đè lên cột khác (`hp-khuat-phai` /
   `hp-khuat-trai` trên hộp cuộn, hp-ui.js tự đo — xem `ghimCot`): bảng không tràn ngang thì giữ
   nguyên dáng cũ, không một vạch dọc thừa giữa bảng. */
table .hp-ghim-phai, table .hp-ghim-trai { position: sticky; z-index: 1; }
table .hp-ghim-phai { right: 0; }
table .hp-ghim-trai { left: 0; }
table td.hp-ghim-phai, table td.hp-ghim-trai { background: var(--hp-ghim-nen, var(--surface)); }
table th.hp-ghim-phai, table th.hp-ghim-trai { z-index: 3; }
/* Menu thả ra từ ô ghim (⋯ của Job card ĐMX): ô ghim là một lớp xếp chồng, nên ô ghim của các dòng
   SAU (cùng z 1, đứng sau trong DOM) vẽ đè lên menu. Nâng ô đang mở menu / đang có tiêu điểm. */
table td.hp-ghim-phai:focus-within, table td.hp-ghim-trai:focus-within,
table td.hp-ghim-phai:has(.open), table td.hp-ghim-trai:has(.open) { z-index: 4; }
.hp-khuat-phai .hp-ghim-phai { box-shadow: -1px 0 0 var(--line), -10px 0 12px -10px var(--scrim); }
.hp-khuat-trai .hp-ghim-trai { box-shadow: 1px 0 0 var(--line), 10px 0 12px -10px var(--scrim); }

/* --- Luật 3 · khoang ---
   Chỗ mới dùng để đưa một khối đang phải cuộn xuống mới thấy lên thành khoang thứ
   hai, thứ ba. Đổi chiều cuộn dọc thành chiều ngang của mắt. */
.hp-panes { display: grid; gap: var(--gut); min-height: 0; align-items: start; grid-template-columns: minmax(0, 1fr); }
.hp-panes > * { min-width: 0; min-height: 0; }
@container page (min-width: 1000px) {
  .hp-panes { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
}
@container page (min-width: 1420px) {
  .hp-panes.three { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) 380px; }
}

/* --- Luật 2 · biểu mẫu ---
   Rộng ra thì THÊM CỘT, ô giữ nguyên bề rộng đọc được. Một ô "Số điện thoại" giãn
   theo bề ngang màn hình là màn full mà không ai điền nổi. */
.hp-fgrid { display: grid; gap: 15px 24px; grid-template-columns: minmax(0, 1fr); }
.hp-fgrid > * { min-width: 0; }
@container page (min-width:  700px) { .hp-fgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container page (min-width: 1080px) { .hp-fgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container page (min-width: 1460px) { .hp-fgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* --- Luật 4 · chữ đọc liền mạch ---
   Câu chú thích, đoạn giải thích, khối cấu hình dạng "nhãn — ô nhập". Đây là ngoại
   lệ CÓ CHỦ Ý, không phải chỗ bị bỏ quên: một dòng chữ dài 1676px thì mắt mất dòng
   ở mỗi lần xuống hàng. */
.hp-read { max-width: 80ch; }

/* --- ĐIỆN THOẠI: TRẢ CUỘN VỀ CHO TRANG ---
   Khung cố định là đúng trên máy tính: ba thanh dính chiếm ~130px trong 900px, đổi lại
   là bảng không bao giờ mất đầu và nút "trang sau" luôn ở chỗ cũ. Trên màn 390×800 thì
   phép tính đảo chiều — ba thanh đó ăn 340px, còn lại chưa đầy hai bản ghi, mà trình
   duyệt di động lại còn tự thu thanh địa chỉ theo chiều cuộn của TRANG (thứ mà một
   khung khoá chiều cao không bao giờ kích hoạt). Nên ở đây bỏ khung: mọi thanh cuộn
   theo nội dung như một trang bình thường.
   Ngưỡng 700px lấy đúng bằng ngưỡng "bảng → thẻ" bên dưới, để hai cách xếp không đổi
   ở hai chỗ khác nhau. */
@media (max-width: 700px) {
  body:has(.hp-page) { overflow: visible; }
  .hp-page { display: block; height: auto; min-height: 0; margin: 0; border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
  .hp-page-body, .hp-page-body.flush { overflow: visible; padding: 12px var(--gut) 40px; }
  .hp-tbl { overflow-x: auto; }
  .hp-panes { display: block; }
  .hp-panes > * + * { margin-top: var(--gut); }
  /* Bảng thành THẺ (khối ngay dưới) và cả trang cuộn như trang thường: không còn gì để ghim.
     Gỡ CẢ `right`/`left`, không chỉ `position`: bảng không có `<thead>` thì không thành thẻ, và
     trang nào tự khai đầu bảng dính bằng bộ chọn nặng hơn (`#cr-rows th{position:sticky}`) vẫn
     thắng `static` ở đây — còn `right:0` thì ô góc đứng dính mép phải trong khi cột của nó đã
     trôi đi (đo ở 390px: ô góc x=322, ô dòng x=1168). Không `right` thì nó chỉ còn dính `top`. */
  table .hp-ghim-phai, table .hp-ghim-trai { position: static; right: auto; left: auto; }
  .hp-khuat-phai .hp-ghim-phai, .hp-khuat-trai .hp-ghim-trai { box-shadow: none; }
}

/* ============================================================================
   BẢNG → THẺ Ở MÀN HẸP
   Bảng số phải giữ nowrap trên máy tính: số điện thoại và mốc giờ ngắt dòng là
   không đọc được. Nhưng dưới 700px thì cuộn ngang bốn nhịp mới hết một dòng —
   nên bảng nào nhiều hơn 3 cột sẽ nằm dọc thành thẻ.
   Nhãn từng ô lấy từ <thead>, do hp-ui.js dán vào data-label — không phải sửa
   từng chỗ dựng bảng trong JS.
   ============================================================================ */
@media (max-width: 700px) {
  table.hp-cards { display: block; width: 100%; }
  table.hp-cards thead { display: none; }
  table.hp-cards tbody, table.hp-cards tr, table.hp-cards td { display: block; width: auto; }
  table.hp-cards tr {
    border: 1px solid var(--line); border-radius: var(--r2); background: var(--surface);
    padding: 9px 11px; margin: 0 0 9px;
  }
  table.hp-cards tr:last-child { margin-bottom: 0; }
  table.hp-cards td {
    border: 0; padding: 4px 0; height: auto; white-space: normal; word-break: break-word;
    display: flex; gap: 10px; align-items: baseline;
  }
  table.hp-cards td::before {
    content: attr(data-label); flex: 0 0 40%; max-width: 132px;
    color: var(--muted); font: 500 var(--fs-1)/1.35 var(--f-sans); padding-top: 2px;
  }
  /* Ô không có nhãn (cột nút) thì trải hết chiều ngang, không chừa cột nhãn rỗng. */
  table.hp-cards td[data-label=""]::before { display: none; }
  /* Dòng "đang tải" / "chưa có dữ liệu" dùng colspan — để nguyên một khối. */
  table.hp-cards td[colspan]::before { display: none; }
  table.hp-cards td[colspan] { text-align: center; }
}

/* ============================================================================
   BONG BÓNG "HỖ TRỢ" — nổi trên mọi màn hình, kéo đặt được
   rolebar.js dựng và ĐO VỊ TRÍ (left/top tính bằng pixel, ghi thẳng vào style).
   Ở đây chỉ khai HÌNH DÁNG và chuyển động; toạ độ là việc của JS vì nó phải kẹp
   theo khung nhìn thật và theo chiều cao đo được của thanh trên.

   `--sup-size` phải khớp `SUP_SIZE` trong rolebar.js: JS lấy con số đó để kẹp bong
   bóng vào trong khung nhìn, lệch là bong bóng thò ra ngoài mép đúng bằng phần lệch.

   z-index CAO HƠN `#sup-panel`: bong bóng không bao giờ được chìm một nửa sau cửa
   sổ chat (nửa chìm thì cú bấm rơi vào ô nhập tin bên dưới). Chồng HẲN lên cửa sổ
   thì rolebar.js gắn `.behind` để giấu nó đi — xem chú thích ở `supSyncOverlap()`.
   ============================================================================ */
.hp-sup-bubble {
  --sup-size: 52px;
  position: fixed; left: 0; top: 0; width: var(--sup-size); height: var(--sup-size);
  z-index: var(--z-support-btn); display: grid; place-items: center; padding: 0;
  border: 0; border-radius: 50%; background: var(--brand); color: var(--brand-ink);
  box-shadow: var(--sh2); cursor: grab; touch-action: none;
  transition: box-shadow var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out);
}
.hp-sup-bubble:hover { box-shadow: var(--sh3); }
.hp-sup-bubble:focus-visible { outline: 0; box-shadow: var(--focus), var(--sh2); }
/* Đang kéo: nhấc lên khỏi mặt phẳng (bóng đậm + to hơn một chút) và KHÔNG có
   transition vị trí — bong bóng phải dính ngón tay, đi trễ là đọc ra máy lag. */
.hp-sup-bubble.dragging { cursor: grabbing; box-shadow: var(--sh3); transform: scale(1.06); }
/* Chỉ lúc TRƯỢT BÁM MÉP (sau khi thả) mới có chuyển động vị trí — rolebar.js gắn rồi
   gỡ lớp này quanh đúng lượt trượt đó. */
.hp-sup-bubble.snapping { transition: left var(--dur-2) var(--ease-out), top var(--dur-2) var(--ease-out), box-shadow var(--dur-1) var(--ease-out); }
.hp-sup-bubble .hp-sup-ic { display: grid; place-items: center; pointer-events: none; }
/* Huy hiệu "có trả lời mới". Đỏ = cần người xem, theo bảng ngữ nghĩa ở đầu tệp;
   viền cùng màu nền trang để con số tách khỏi vòng teal phía dưới. */
.hp-sup-bubble .hp-sup-badge {
  position: absolute; top: -2px; right: -2px; min-width: 19px; height: 19px; padding: 0 4px;
  border-radius: var(--rp); background: var(--bad); color: var(--bad-ink); border: 2px solid var(--surface);
  font: 600 var(--fs-1)/var(--fs-3) var(--f-sans); text-align: center;
}
.hp-sup-bubble .hp-sup-badge[hidden] { display: none; }
/* Mở: đổi thành nút đóng — nền trầm hơn để nó không còn là "hành động chính" nữa
   (hành động chính lúc đó nằm trong cửa sổ chat: ô nhập và nút Gửi). */
.hp-sup-bubble.is-open { background: var(--ink); color: var(--surface); }
/* Đang đứng chồng lên cửa sổ chat: biến mất trong lúc cửa sổ mở. Lý do đầy đủ ở
   `supSyncOverlap()` trong rolebar.js — tóm tắt: chỗ chồng đúng vào nút Gửi. */
.hp-sup-bubble.behind { display: none; }
/* NÚT HỖ TRỢ TRÊN THANH TRÊN (CRM-361) — dạng mặc định ở mọi trang có thanh trên chung. Cùng
   khổ 32px với chuông và menu ⚙; không nền đặc, không bóng, không kéo thả. Huy hiệu chưa đọc
   treo ở góc như huy hiệu của chuông. Đứng SAU mọi luật `.hp-sup-bubble` ở trên nên thắng
   cùng độ ưu tiên. */
.hp-sup-bubble.tb-sup {
  position: relative; left: auto; top: auto; width: 32px; height: 32px; z-index: auto;
  border-radius: var(--r2); background: transparent; color: var(--ink-2);
  box-shadow: none; cursor: pointer; touch-action: auto; flex-shrink: 0;
}
.hp-sup-bubble.tb-sup:hover { background: var(--surface-3); color: var(--ink); box-shadow: none; }
.hp-sup-bubble.tb-sup:focus-visible { box-shadow: var(--focus); }
.hp-sup-bubble.tb-sup.is-open { background: var(--brand-soft); color: var(--brand-2); }
.hp-sup-bubble.tb-sup .hp-sup-badge { top: 2px; right: 1px; }

/* ============================================================================
   CỬA SỔ KÊNH HỖ TRỢ NỘI BỘ
   Trước đây cả khối này là một chuỗi JavaScript trong rolebar.js: sửa màu thương
   hiệu phải sửa trong chuỗi, và cửa sổ không theo được nền tối.

   VỊ TRÍ DÁN CHẾT ở góc phải-dưới, CỐ Ý — và cố ý KHÔNG đi theo bong bóng Hỗ trợ
   (thứ mở ra nó). Bong bóng kéo được nên nó ở đâu là tuỳ người dùng; nếu cửa sổ bám
   theo thì mỗi lần mở lại thấy nó ở một chỗ khác, mà một khung để ĐỌC và GÕ thì phải
   đứng yên — con mắt và ngón tay đều tìm nó theo trí nhớ cơ bắp. Bong bóng là nút
   bấm (một cú, xong việc), cửa sổ là chỗ làm việc: hai thứ khác loại, không buộc
   phải đi cùng nhau.
   ============================================================================ */
#sup-panel {
  position: fixed; right: 18px; bottom: 18px; width: 390px; max-width: calc(100vw - 30px);
  height: 520px; max-height: calc(100vh - 90px);
  background: var(--surface); border: 1px solid var(--line-2); border-radius: var(--r3);
  box-shadow: var(--sh3); display: none; flex-direction: column; z-index: var(--z-support);
  overflow: hidden; font-family: var(--f-sans);
}
#sup-panel.open { display: flex; }
#sup-head { display: flex; align-items: center; gap: 9px; padding: 10px 13px; background: var(--brand); color: var(--brand-ink); font-weight: 600; font-size: var(--fs-2); }
#sup-head small { font-weight: 400;  display: block; font-size: var(--fs-1); }
#sup-close { margin-left: auto; background: transparent; border: 0; color: var(--brand-ink); font-size: var(--fs-3); cursor: pointer; line-height: 1; }
#sup-msgs { flex: 1; overflow-y: auto; padding: 12px; background: var(--surface-2); display: flex; flex-direction: column; gap: 8px; }
.sup-m { max-width: 85%; padding: 8px 11px; border-radius: 11px; font-size: var(--fs-2); line-height: 1.45; white-space: pre-wrap; word-break: break-word; }
.sup-m.user { align-self: flex-end; background: var(--brand); color: var(--brand-ink); }
.sup-m.ai { align-self: flex-start; background: var(--surface); border: 1px solid var(--line-2); color: var(--ink); }
.sup-m .sup-who { font-size: var(--fs-1);  margin-bottom: 2px; font-weight: 600; }
.sup-m a { color: inherit; text-decoration: underline; }
#sup-foot { padding: 9px 11px; border-top: 1px solid var(--line); background: var(--surface); display: flex; flex-direction: column; gap: 6px; }
#sup-file-tag { font-size: var(--fs-1); color: var(--brand); display: none; }
#sup-row { display: flex; gap: 7px; align-items: flex-end; }
#sup-text { flex: 1; border: 1px solid var(--line-2); border-radius: 9px; padding: 8px 10px; font: inherit; font-size: var(--fs-2); resize: none; min-height: 38px; max-height: 110px; background: var(--surface); color: var(--ink); }
#sup-attach { border: 1px solid var(--line-2); background: var(--surface-2); color: var(--ink); border-radius: 9px; width: 38px; height: 38px; font-size: var(--fs-3); cursor: pointer; }
#sup-send { border: 0; background: var(--brand); color: var(--brand-ink); border-radius: 9px; padding: 0 15px; height: 38px; font-weight: 600; font-size: var(--fs-2); cursor: pointer; }
#sup-hint { font-size: var(--fs-1); color: var(--muted); }
#sup-rate { padding: 9px 11px; border-top: 1px solid var(--line); background: var(--surface-2); }
.sup-rate-q { font-size: var(--fs-1); color: var(--muted); margin-bottom: 5px; }
.sup-stars { font-size: var(--fs-4); letter-spacing: 3px; line-height: 1; margin-bottom: 7px; }
.sup-star { cursor: pointer; color: var(--line-2); transition: color var(--dur-1) var(--ease-out); }
.sup-star.on { color: var(--warn); }
.sup-rate-btns { display: flex; gap: 8px; }
.sup-rb { flex: 1; border: 1px solid var(--line-2); background: var(--surface); color: var(--ink); border-radius: var(--r1); padding: 6px 8px; font: inherit; font-size: var(--fs-1); cursor: pointer; }
.sup-rb.ok:hover { border-color: var(--ok); color: var(--ok); }
.sup-rb.no:hover { border-color: var(--bad); color: var(--bad); }
.sup-rated { font-size: var(--fs-1); color: var(--ink-2); }
.sup-rated a { color: var(--brand); margin-left: 6px; font-size: var(--fs-1); }
/* Màn hẹp: cửa sổ dán xuống đáy, chiếm cả bề rộng — 390px cố định trên điện thoại
   375px sẽ tràn ra ngoài mép phải. max-width phải ghi đè: quy tắc gốc có
   max-width:calc(100vw - 30px), để nguyên thì panel chỉ rộng 345/375px và lệch
   hẳn sang trái. */
@media (max-width: 560px) {
  #sup-panel { left: 0; right: 0; bottom: 0; width: auto; max-width: none; height: 82vh; max-height: none; border-radius: var(--r3) var(--r3) 0 0; }
}
/* Màn thấp: chừa chỗ cho bàn phím ảo, không thì ô nhập bị bàn phím che. */
@media (max-height: 560px) {
  #sup-panel { height: 92vh; }
}

/* ============================================================================
   KHUNG KHỞI ĐỘNG · SKELETON · CHUYỂN CẢNH
   Xem docs/CHUYEN-TRANG-MUOT.md. Ba khối dưới đây là TẦNG DÙNG CHUNG của việc
   "chuyển trang không giật" — trang KHÔNG tự khai lại, chỉ dùng.
   ============================================================================ */

/* ---- 1. Khung khởi động (hp-ui.js đặt class lên <html> TRƯỚC lần vẽ đầu tiên) ----

   Trước đây mỗi trang tự quyết "ai đang xem" bằng cách để sẵn `hidden` trên thân
   trang rồi bỏ `hidden` sau khi API trả lời. Hậu quả thấy rõ nhất ở Omnichat: lớp
   phủ đăng nhập (nền teal kín màn hình) là trạng thái MẶC ĐỊNH của HTML, nên nhân
   viên ĐANG đăng nhập bấm sang Omnichat vẫn thấy nó loé lên — trông như vừa bị
   đăng xuất — cho tới khi /api/agent/login đi xong một vòng mạng.

   Nay quyết định đó xảy ra ĐỒNG BỘ, trước khi vẽ, chỉ đọc localStorage, không chạm
   mạng. Trang chỉ cần dán hai thuộc tính lên phần tử của mình:
       data-hp-when="auth"  — chỉ hiện khi ĐÃ có phiên
       data-hp-when="anon"  — chỉ hiện khi CHƯA có phiên
   `visibility: hidden` chứ không `display: none` cho nhánh auth: khối bàn làm việc
   vẫn chiếm đúng chỗ của nó nên thanh cuộn và bố cục không nhảy khi đổi nhánh. */
:root:not(.hp-anon) [data-hp-when="anon"] { display: none !important; }
:root.hp-anon [data-hp-when="auth"] { display: none !important; }

/* HAI LÝ DO của nhánh anon — CRM-206.
   "Chưa có phiên" và "có phiên nhưng trang này không vào được" đều là nhánh anon, và
   mười bốn màn chỉ có MỘT khối cho nhánh ấy nên `="anon"` là đủ cho chúng. Màn Quản trị
   có CẢ HAI khối (ô nhập mã + khối chối), nên phải chọn đúng một — không thì lật deny
   xong người dùng nhìn thấy ô nhập mã, một lời mời sai vì họ đã đăng nhập rồi.
       data-hp-when="login" — CHƯA có phiên (và không phải một lượt chối)
       data-hp-when="deny"  — CÓ phiên nhưng bị chối
   Lớp `hp-denied` do `HPUI.denyBranch()` dán và `setSignedIn()` gỡ ở cả hai chiều, nên
   nó luôn đọc được là "có ai đó CHỦ Ý chối". Hai luật này KHÔNG đụng tới mười bốn màn
   kia: chúng vẫn khai `="anon"`, vốn không nằm trong bộ chọn nào ở đây. */
:root:not(.hp-anon) [data-hp-when="login"],
:root.hp-denied [data-hp-when="login"] { display: none !important; }
:root:not(.hp-denied) [data-hp-when="deny"] { display: none !important; }

/* ---- 2. Skeleton ----
   Ô xám nhấp nháy giữ ĐÚNG chỗ của dữ liệu sắp về. Cố ý KHÔNG dùng chữ
   "Đang tải…": chữ đó dài ngắn khác nhau giữa hai ngôn ngữ, và biến mất khi dữ
   liệu về — đổi kích thước là đúng thứ gây giật mà cả khối này sinh ra để chặn. */
.hp-skel { background: var(--surface-3); border-radius: var(--r1); position: relative; overflow: hidden; }
.hp-skel::after {
  content: ""; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--surface) 55%, transparent), transparent);
  animation: hp-skel-sweep calc(1400ms * var(--motion)) infinite;
}
@keyframes hp-skel-sweep { to { transform: translateX(100%); } }
/* Một dòng skeleton: cao bằng một dòng chữ. Nhân với --skel-w để dài ngắn khác
   nhau — mấy dòng dài bằng nhau chằn chặn trông như lỗi hiển thị, không như đang tải. */
.hp-skel-line { height: 11px; margin: 7px 0; width: var(--skel-w, 100%); }

/* ---- 3. Thanh tiến trình khi chuyển trang ----
   Cú bấm phải có phản hồi NGAY, không đợi trang mới về. Vạch mảnh ở đỉnh màn hình
   chạy tới 80% rồi dừng — 100% là lời hứa "xong rồi" mà ta chưa giữ được. */
#hp-progress {
  position: fixed; top: 0; left: 0; height: 2px; width: 0; z-index: var(--z-progress);
  background: var(--brand); box-shadow: 0 0 8px var(--brand); pointer-events: none;
  transition: width var(--dur-3) var(--ease-out), opacity var(--dur-2) var(--ease-out);
}

/* ---- 3b. Lớp phủ + vòng xoay trong lúc CHỜ trang mới ----
   Vạch tiến trình ở trên trả lời câu "máy có nhận cú bấm không". Khối này trả lời
   câu còn lại: "chỗ tôi đang nhìn sắp đổi". Vùng nội dung của trang CŨ mờ đi sau
   một lớp phủ, và giữa lớp phủ là một vòng xoay.

   Vì sao lớp phủ MỜ chứ không đục: nội dung cũ nhìn thấy lờ mờ phía sau vẫn nói
   đúng một điều — chỗ này còn nội dung, chỉ là sắp được thay. Đục hẳn thì màn hình
   thành một ô trống có vòng xoay ở giữa, tức lại rơi vào đúng câu thứ nhất của
   docs/CHUYEN-TRANG-MUOT.md: "không màn hình nào bắt đầu bằng khoảng trắng".

   Ba điều kiện làm nó không phản tác dụng:
   1. Chỉ phủ vùng NỘI DUNG. Dải menu và thanh trên mang `view-transition-name`, tức
      là hai khung đứng yên xuyên hai trang — phủ lên chúng là tự tay xoá đi cái mốc
      mà mắt đang bám vào. hp-ui.js đo lề trái của <body> và đáy thanh trên rồi đặt
      khung, không suy từ token: ở màn hẹp dải nằm ngoài khung nhìn còn lề bằng 0,
      hai trường hợp mà một con số cứng không kể hết.
   2. HOÃN trước khi hiện (xem NAV_WAIT ở hp-ui.js). Mục đã prefetch thì trang mới về
      gần như tức thì; hiện ngay là một cú loé — đúng thứ đang đi xoá, chỉ khác chỗ.
   3. `pointer-events: none` — nó là lời kể, không phải hàng rào. Người dùng đổi ý
      bấm sang mục khác giữa chừng vẫn phải bấm được.
   Không có `position: fixed` nào phải hoãn theo `visibility` ở đây: lớp phủ sinh ra
   rồi mất theo lượt điều hướng, không có trạng thái nào cần giữ. */
#hp-loading {
  position: fixed; z-index: 8; overflow: hidden; pointer-events: none;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--surface) 78%, transparent); opacity: 0;
  transition: opacity var(--dur-1) var(--ease-out);
}
#hp-loading.on { opacity: 1; }
/* Vòng xoay: một vành tròn khuyết một cung.
   Vành nền lấy `--line` (cùng sắc với mọi đường kẻ trong hệ) còn cung chạy lấy
   `--brand` — teal ở đây đúng nghĩa "hành động chính đang diễn ra", không phải màu
   trang trí. Xem bảng ngữ nghĩa màu ở đầu tệp; tím là của riêng Mya, không dùng.
   Thời lượng KHÔNG lấy `--dur-*`: ba token đó là thời lượng của một lượt chuyển
   trạng thái có điểm kết, còn đây là vòng lặp không có đích. Vẫn phải nhân
   `var(--motion)` để nằm trong công tắc giảm chuyển động của cả hệ. */
#hp-loading .hp-spin {
  width: 34px; height: 34px; border-radius: 50%;
  border: 3px solid var(--line);
  border-top-color: var(--brand);
  animation: hp-spin calc(820ms * var(--motion)) linear infinite;
}
@keyframes hp-spin { to { transform: rotate(360deg); } }

/* ---- 4. Chuyển cảnh giữa hai trang ----
   Hệ là MPA: mỗi mục menu là một trang thật. View Transitions cho trình duyệt mờ
   chéo giữa trang cũ và trang mới thay vì nháy trắng, và `view-transition-name`
   giữ khung điều hướng ĐỨNG YÊN xuyên hai trang — mắt bám vào dải menu, chỉ vùng
   nội dung đổi. Trình duyệt chưa hỗ trợ thì rơi về hành vi cũ, không vỡ gì.
   (Yêu cầu trình duyệt: xem mục 7 docs/HE-THONG-THIET-KE.md.)

   BA khung được đặt tên, không phải một. `.hp-side` là quan trọng nhất: nó chiếm
   244px bên trái MỌI trang, nên nó mà mờ vào mờ ra mỗi lượt chuyển thì chẳng còn
   ý nghĩa gì cả — đó mới đúng là thứ mắt bám vào.

   `#hp-side` do rolebar.js DỰNG BẰNG JS, không nằm trong HTML tĩnh. Nó vẫn kịp có
   mặt trước lần vẽ đầu vì mọi trang gọi mountRoleBar() sớm và mốc M1 dựng dải ngay
   từ ảnh chụp — nếu lần nào đó không kịp thì tệ nhất là lượt ấy mất hiệu ứng liên
   tục, không phải vỡ trang. */
@view-transition { navigation: auto; }
.hp-side { view-transition-name: hp-side; }
.topbar { view-transition-name: hp-topbar; }
.mob-nav { view-transition-name: hp-mobnav; }
::view-transition-group(hp-side),
::view-transition-group(hp-topbar),
::view-transition-group(hp-mobnav) { animation-duration: var(--dur-2); }
/* Vùng nội dung: NẠP LÊN, không trượt sang.
   Khai rõ hai animation thay vì để mặc định của trình duyệt lo, vì hai vế cố ý
   KHÔNG cân nhau: trang cũ tắt nhanh (--dur-1) để nhường chỗ ngay, trang mới hiện
   dần (--dur-2, --ease-out) như thứ vừa nạp xong. Chỉ đụng `opacity` — không
   `transform`, không `translate`: mọi thứ nằm nguyên chỗ của nó. */
@keyframes hp-vt-out { to { opacity: 0; } }
@keyframes hp-vt-in { from { opacity: 0; } }
::view-transition-old(root) { animation: hp-vt-out var(--dur-1) var(--ease-out) both; }
::view-transition-new(root) { animation: hp-vt-in var(--dur-2) var(--ease-out) both; }
/* Khung điều hướng là MỘT phần tử liên tục qua hai trang, nên KHÔNG mờ chéo nó —
   mờ chéo một thứ đứng yên chỉ làm nó nhấp nháy. */
::view-transition-old(hp-side),
::view-transition-new(hp-side),
::view-transition-old(hp-topbar),
::view-transition-new(hp-topbar),
::view-transition-old(hp-mobnav),
::view-transition-new(hp-mobnav) { animation: none; mix-blend-mode: normal; }

/* ---- 5. Giảm chuyển động: một công tắc cho cả hệ ----
   Chỉ cần hạ --motion về 0 — mọi thời lượng đều là calc(... * var(--motion)) nên
   tự về 0 theo. Trang mới thêm hiệu ứng bằng token thì được che luôn, không phải
   nhớ thêm @media riêng. */
@media (prefers-reduced-motion: reduce) {
  :root { --motion: 0; }
  .hp-skel::after { animation: none; }
  /* Vòng xoay đứng yên thì phải trông như CỐ Ý đứng yên, không như kẹt giữa chừng.
     Bỏ luôn animation (thời lượng đã về 0, nhưng khai rõ thì không phụ thuộc cách
     trình duyệt xử lý `0s`) và cho vành khuyết đều hai bên — đọc ra là một dấu hiệu
     "đang chờ", không phải một vòng xoay chết máy. Lớp phủ và vạch tiến trình vẫn
     còn, nên tín hiệu "đang tải" không mất. */
  #hp-loading .hp-spin { animation: none; border-color: var(--line); border-top-color: var(--brand); border-bottom-color: var(--brand); }
  @view-transition { navigation: none; }
}

/* Dòng skeleton trong bảng — xem HPUI.skeletonRows() ở hp-ui.js. Cao đúng bằng một
   dòng bảng (`--row`) nên lúc dữ liệu về không có gì phải dịch chuyển. Bề rộng so le
   để mắt đọc ra là "đang tải", không phải "bảng hỏng". */
.hp-skel-row td { padding: 0; border: 0; }
.hp-skel-row .hp-skel-line { height: 11px; margin: calc((var(--row) - 11px) / 2) 12px; }
.hp-skel-row:nth-child(3n)   .hp-skel-line { --skel-w: 82%; }
.hp-skel-row:nth-child(3n+1) .hp-skel-line { --skel-w: 94%; }
.hp-skel-row:nth-child(3n+2) .hp-skel-line { --skel-w: 71%; }

/* Khối skeleton cho ô KHÔNG phải bảng (thẻ số liệu, danh sách trong <div>). Bề rộng
   so le như .hp-skel-row, cùng lý do. */
.hp-skel-block .hp-skel-line { height: 12px; margin: 9px 0; }
.hp-skel-block .hp-skel-line:nth-child(4n)   { --skel-w: 64%; }
.hp-skel-block .hp-skel-line:nth-child(4n+1) { --skel-w: 92%; }
.hp-skel-block .hp-skel-line:nth-child(4n+2) { --skel-w: 78%; }
.hp-skel-block .hp-skel-line:nth-child(4n+3) { --skel-w: 85%; }

/* ============================================================================
   CA LÀM VIỆC — HỘP THOẠI KẾT CA & CHUYỂN GIAO CHAT
   Khối này trước nằm ở public/style.css, tức chỉ Omnichat mới có. Từ khi nút Kết ca
   lên thanh trên (CRM-119) thì hộp thoại phải dựng được ở MỌI trang, mà mười ba
   trang kia chỉ nạp tệp này — nên nó về đây.
   Chữ luôn ngược màu nền (nguyên tắc UI của dự án): thẻ nền trắng chữ --ink, dải
   cảnh báo nền vàng nhạt chữ nâu đậm, nút chính nền đậm chữ trắng.
   ============================================================================ */
.sh-overlay { position: fixed; inset: 0; z-index: 11; display: flex; align-items: center; justify-content: center; background: var(--scrim); padding: 16px; }
.sh-overlay[hidden] { display: none; }
.sh-card { background: var(--panel); color: var(--ink); border-radius: 14px; width: 520px; max-width: 100%; max-height: 86vh; display: flex; flex-direction: column; box-shadow: var(--sh3); }
.sh-head { padding: 15px 18px 11px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 8px; }
.sh-head h2 { font: 500 var(--fs-5)/var(--lh-5) var(--f-sans); }
.sh-head .sh-x { margin-left: auto; border: 0; background: transparent; color: var(--muted); font-size: var(--fs-4); cursor: pointer; line-height: 1; padding: 2px 6px; }
.sh-body { padding: 14px 18px; overflow: auto; }
.sh-foot { padding: 12px 18px 15px; border-top: 1px solid var(--line); display: flex; gap: 8px; flex-wrap: wrap; }
.sh-foot .sh-grow { flex: 1 1 auto; }
.sh-sub { font-size: var(--fs-1); color: var(--muted); margin-bottom: 9px; }
.sh-list { border: 1px solid var(--line); border-radius: 9px; max-height: 210px; overflow: auto; margin-bottom: 13px; }
.sh-row { padding: 7px 10px; border-bottom: 1px solid var(--line); font-size: var(--fs-2); display: flex; gap: 8px; align-items: baseline; }
.sh-row:last-child { border-bottom: 0; }
.sh-row b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh-row time { margin-left: auto; color: var(--muted); font-size: var(--fs-1); white-space: nowrap; }
.sh-tag { font-size: var(--fs-1); background: var(--in-bubble); color: var(--ink-2); padding: 1px 6px; border-radius: 8px; white-space: nowrap; }
/* Sắc thái của `.sh-tag` — khay chi tiết cuộc gọi (CRM-220) phân biệt lượt chào ĐƯỢC
   NHẬN với lượt bị từ chối. Không có hai lớp này thì cả năm kết quả chào máy trông y hệt
   nhau và ô nhãn chỉ còn là chữ; theo đúng bảng ngữ nghĩa của tệp này (xanh = xong, hổ
   phách = còn treo), không đặt màu cứng. */
.sh-tag.ok { background: var(--ok-soft); color: var(--ok); }
.sh-tag.warn { background: var(--warn-soft); color: var(--warn); }
/* Popup lịch sử cuộc gọi theo số điện thoại (CRM-221, public/hp-callhist.js) — BẢNG chín cột
   theo đúng bảng "Lịch sử cuộc gọi của số điện thoại" CSKH quen dùng. Thẻ rộng hơn `.sh-card`
   mặc định vì chín cột không đứng nổi trong 520px; màn hẹp thì bảng cuộn ngang TRONG khung
   `.sh-list`, không đẩy trang. Đầu bảng dính khi cuộn dọc.
   Widget TỰ KHAI đủ kiểu chữ và xuống dòng cho `th`/`td`: trang chủ có luật trần
   `th,td{white-space:nowrap}` + `th{text-transform:uppercase}` (màn Cuộc gọi rớt), luật ấy
   rò vào đây và đẩy hai cột cuối ra ngoài khung (đo trên hub local 23/09/2026) — kể cả
   luật kéo giãn cột cuối `th:last-child,td:last-child{width:100%}`. Không viết `font:` gộp — `inherit`
   không được làm một thành phần của cách viết gộp (Luật 11 trong src/core/ui-shell.test.ts). */
.sh-card.hp-ch-card { width: 1200px; }
/* Biến thể KHAY PHẢI của lớp phủ (CRM-363): thẻ dán mép phải, cao cả màn, lớp phủ nhẹ
   (`--scrim-soft`) để danh sách phía sau còn đọc được — cùng luật với ngăn kéo sự vụ. */
.sh-overlay.sh-right { justify-content: flex-end; align-items: stretch; padding: 0; background: var(--scrim-soft); }
.sh-overlay.sh-right > .sh-card { height: 100%; max-height: none; width: min(960px, 94vw); border-radius: var(--r3) 0 0 var(--r3); }
/* Thân khay chiếm phần còn lại, chân khay (nút Lưu/Đóng) dính ĐÁY — không lơ lửng giữa khay khi nội
   dung ngắn. Áp cho mọi khay phải, kể cả khay hồ sơ `#rec-ov`. */
.sh-overlay.sh-right > .sh-card > .sh-body, #rec-ov .sh-card > .sh-body { flex: 1 1 auto; min-height: 0; }
/* NGĂN KÉO PHẢI KHÔNG CHẶN (CRM-366) — cùng HÌNH HỌC ngăn kéo sự vụ (`.cd-drawer`): dán mép phải,
   cao trọn khung nhìn, rộng tối đa nửa màn và luôn chừa 340px danh sách sau dải menu. KHÔNG có
   lớp phủ chặn: danh sách bên trái vẫn đọc và bấm được, bấm dòng khác là đổi nội dung ngăn kéo —
   đúng nết của Sự vụ. Khác `.sh-overlay.sh-right` ở trên (lớp phủ + hộp thoại): khay ấy dành cho
   việc phải làm xong mới quay lại danh sách. Hai màn CRM cũ từng có HAI kiểu chi tiết riêng (khoang
   chia đôi 300px · khay phủ 580px) cạnh ngăn kéo 50vw của Sự vụ — ba kiểu cho một việc "xem một
   dòng". Phần tử phải đứng NGOÀI `.hp-page` — `.hp-page` là ngữ cảnh xếp lớp riêng, đặt trong
   đó là ngăn kéo nằm DƯỚI thanh trên (xem khối khung trang). */
.hp-drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: var(--z-drawer);
  width: min(760px, 50vw, calc(100vw - var(--side-w) - 340px)); min-width: min(440px, 100vw);
  display: flex; flex-direction: column; background: var(--surface); color: var(--ink);
  border-left: 1px solid var(--line); box-shadow: var(--sh3); animation: hp-drawer-in var(--dur-3) var(--ease-out); }
.hp-drawer[hidden] { display: none; }
@keyframes hp-drawer-in { from { transform: translateX(24px); opacity: 0; } }
@media (max-width: 700px) { .hp-drawer { width: 100vw; min-width: 0; } }
@media (prefers-reduced-motion: reduce) { .hp-drawer { animation: none; } }
.hp-ch-tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-2); }
.hp-ch-tbl th { position: sticky; top: 0; z-index: 1; background: var(--surface-2); color: var(--ink); font-family: var(--f-sans); font-size: var(--fs-2); line-height: var(--lh-2); font-weight: 600; letter-spacing: normal; text-transform: none; white-space: normal; text-align: left; vertical-align: bottom; padding: 8px 10px; border-bottom: 1px solid var(--line); }
.hp-ch-tbl td { padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; white-space: nowrap; text-align: left; }
.hp-ch-tbl th:last-child, .hp-ch-tbl td:last-child { width: auto; }
.hp-ch-tbl td.hp-ch-pb { white-space: normal; min-width: 120px; max-width: 220px; }
.hp-ch-tbl tbody tr:last-child td { border-bottom: 0; }
.hp-ch-tbl tbody tr { cursor: pointer; }
.hp-ch-tbl tbody tr:hover { background: var(--in-bubble); }
.hp-ch-tbl td.num { text-align: right; white-space: nowrap; }
.hp-ch-tbl td.mono { white-space: nowrap; }
.hp-ch-tbl .muted { color: var(--muted); }
.hp-ch-sub { color: var(--muted); font-size: var(--fs-1); }
.hp-ch-sub.warn { color: var(--warn); }
.hp-ch-row { border: 0; background: transparent; color: var(--teal); font-family: inherit; font-size: var(--fs-2); padding: 0; cursor: pointer; white-space: nowrap; }
.hp-ch-row:hover { text-decoration: underline; }
.hp-ch-row:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
/* Trần 210px của `.sh-list` hợp với danh sách vài dòng; popup này hiện tới 50 cuộc nên nó
   cần cao hơn, mà vẫn không được đẩy thẻ vượt `max-height: 86vh` của `.sh-card`. */
.sh-list.hp-ch-list { max-height: min(60vh, 560px); }
.sh-label { display: block; font-size: var(--fs-1); font-weight: 600; margin-bottom: 5px; }
.sh-select { width: 100%; padding: 8px 9px; border: 1px solid var(--line); border-radius: 8px; font-size: var(--fs-2); background: var(--panel); color: var(--ink); }
.sh-warn { margin-top: 11px; background: var(--warn-soft); color: var(--warn); border-radius: 8px; padding: 9px 11px; font-size: var(--fs-1); }
.sh-err { margin-top: 11px; background: var(--bad-soft); color: var(--bad); border-radius: 8px; padding: 9px 11px; font-size: var(--fs-1); }
.sh-ok { margin-top: 11px; background: var(--ok-soft); color: var(--ok); border-radius: 8px; padding: 9px 11px; font-size: var(--fs-1); }
/* Popup chi tiết một phiên chat (CRM-254) — hàng thẻ số + danh sách lượt.
   Dựng TRÊN `.sh-card`/`.sh-list` có sẵn, chỉ thêm đúng phần mà khay cuộc gọi không có:
   một hàng thẻ số co giãn, cột giờ đơn cách (để các mốc thẳng hàng khi đọc dọc), và một
   dòng phân cách ngày — phiên trải hai ngày mà cột chỉ có giờ phút thì một lượt chờ qua
   đêm trông như chờ vài phút. */
.hp-cs-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(98px, 1fr)); gap: 8px; margin-bottom: 12px; }
.hp-cs-the { border: 1px solid var(--line); border-radius: 9px; padding: 8px 10px; }
.hp-cs-the .l { font-size: var(--fs-1); color: var(--muted); }
.hp-cs-the .v { font-size: var(--fs-3); font-weight: 600; }
.sh-list.hp-cs-list { max-height: min(46vh, 380px); }
.hp-cs-gio { font-family: var(--f-mono); font-size: var(--fs-1); color: var(--muted); white-space: nowrap; }
.hp-cs-ngay { background: var(--in-bubble); }
.hp-cs-ngay b { font-size: var(--fs-1); color: var(--muted); font-weight: 600; }
/* Hang bang mo popup chi tiet phien. Nhan dang bang THUOC TINH `data-conv` — dung thu ma
   widget uy quyen doc — chu khong bang mot ten lop: mot lop mang nghia hanh vi la nut thu
   hai deo cung lop se cuop hanh vi, va hai man se dat hai ten khac nhau cho cung mot viec. */
tr[data-conv] { cursor: pointer; }
tr[data-conv]:hover { background: var(--in-bubble); --hp-ghim-nen: var(--in-bubble); }
.hp-cs-open { border: 0; background: transparent; color: var(--muted); cursor: pointer; font-family: inherit; font-size: var(--fs-2); padding: 0 4px; }
.hp-cs-open:hover { color: var(--ink); }
.hp-cs-open:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.sh-btn { border: 1px solid var(--line); background: var(--panel); color: var(--ink); border-radius: 8px; padding: 8px 13px; font-size: var(--fs-2); font-weight: 600; cursor: pointer; }
.sh-btn:hover { background: var(--in-bubble); }
.sh-btn.primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.sh-btn.primary:hover { background: var(--brand-dark); }
.sh-btn:disabled { opacity: .55; cursor: default; }

/* ============================================================================
   KHAY HỒ SƠ BẢN GHI (public/admin/records.js)

   Khối này trước nằm trong <style> của public/admin/index.html, tức chỉ màn Quản trị mới
   có. Từ 18/09/2026 thì khu Phân quyền (`/app/auth/`) cũng mở khay ấy — ba danh sách
   Người dùng · Phòng ban đã chuyển về đó — nên CSS phải về đây, đúng chỗ tệp
   `records.js` đã về chỗ dùng chung. Một bản sao thứ hai trong `auth/index.html` là hai
   bản phải giữ đồng bộ bằng tay, và lệch thì lệch IM LẶNG: khay vẫn mở, chỉ trông sai.

   MƯỢN `.sh-overlay`/`.sh-card`/`.sh-btn` ngay trên — ở đây chỉ khai phần THÂN. Mọi bộ
   chọn neo vào `#rec-ov` hoặc mang tiền tố `rec-`.
   ============================================================================ */
/* NGĂN KÉO PHẢI (CRM-368), không phải hộp thoại giữa màn: sửa một hồ sơ mà vẫn thấy dòng của
   nó trong bảng phía sau — cùng khuôn `.sh-overlay.sh-right` của khay lịch sử cuộc gọi. */
#rec-ov.sh-overlay { justify-content: flex-end; align-items: stretch; padding: 0; background: var(--scrim-soft); }
#rec-ov .sh-card { width: min(620px, 94vw); height: 100%; max-height: none; border-radius: var(--r3) 0 0 var(--r3); }
#rec-ov .rec-f { margin-bottom: 14px; }
#rec-ov .rec-ro { padding: 8px 10px; border: 1px dashed var(--line); border-radius: var(--r1);
  background: var(--surface-2); color: var(--ink2); font-size: var(--fs-2); }
#rec-ov .rec-inp { width: 100%; padding: 8px 10px; border: 1px solid var(--line-2); border-radius: var(--r1);
  background: var(--surface); color: var(--ink); font-family: inherit; font-size: var(--fs-2); }
#rec-ov .rec-inp:focus, #rec-ov .sh-select:focus { outline: 0; border-color: var(--brand); box-shadow: var(--focus); }
#rec-ov .rec-inp:disabled, #rec-ov .sh-select:disabled { opacity: .6; }
#rec-ov .rec-sw { display: flex; align-items: center; gap: 8px; font-size: var(--fs-2); }
#rec-ov .rec-sw input { width: auto; margin: 0; }
#rec-ov .rec-hint { margin: 5px 0 0; color: var(--muted); font-size: var(--fs-1); line-height: var(--lh-1); }
#rec-ov .rec-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
#rec-ov .rec-chip { display: inline-flex; align-items: center; gap: 6px; padding: 3px 5px 3px 10px;
  border: 1px solid var(--brand-line); border-radius: var(--rp); background: var(--brand-soft);
  color: var(--brand-2); font-size: var(--fs-1); font-weight: 600; }
#rec-ov .rec-chip button { border: 0; background: transparent; color: inherit; cursor: pointer;
  padding: 0 3px; font-size: var(--fs-1); }
#rec-ov .rec-none { color: var(--muted); font-size: var(--fs-1); }
#rec-ov .rec-msg { align-self: center; font-size: var(--fs-1); color: var(--muted); }
#rec-ov .rec-msg.err { color: var(--bad); }
#rec-ov .rec-badge { padding: 2px 9px; border-radius: var(--rp); background: var(--ok-soft); color: var(--ok);
  font-size: var(--fs-1); font-weight: 600; }
#rec-ov .rec-badge.arch { background: var(--warn-soft); color: var(--warn); }
#rec-ov .rec-badge.mya { background: var(--mya-soft); color: var(--mya); }
#rec-ov #rec-badge { display: inline-flex; flex-wrap: wrap; gap: 6px; }
/* Khung xương của lượt đọc — không để khay trống một nhịp rồi nhảy cỡ (docs/CHUYEN-TRANG-MUOT.md). */
#rec-ov .rec-sk { height: 38px; margin-bottom: 14px; border-radius: var(--r1); background: var(--surface-2); }
/* Dải TAB của khay (18/09/2026): một bản ghi, nhiều mặt, ba chủ vẽ — xem khối `tabs` trong
   records.js. Chỉ hiện khi loại bản ghi ấy có khai tab phụ. */
#rec-ov .rec-tabs { display: flex; gap: 2px; margin: -4px -4px 14px; border-bottom: 1px solid var(--line); }
#rec-ov .rec-tabs button { padding: 8px 12px; border: 0; border-bottom: 2px solid transparent;
  background: transparent; color: var(--muted); font-family: inherit; font-size: var(--fs-2);
  font-weight: 600; cursor: pointer; }
#rec-ov .rec-tabs button:hover { color: var(--ink); }
#rec-ov .rec-tabs button[aria-selected="true"] { color: var(--brand-2); border-bottom-color: var(--brand); }
#rec-ov .rec-tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-2); }
#rec-ov .rec-tbl th { text-align: left; padding: 5px 6px; border-bottom: 1px solid var(--line);
  color: var(--ink); font-weight: 500; }
#rec-ov .rec-tbl td { padding: 6px; border-bottom: 1px solid var(--line); vertical-align: top; }
#rec-ov .rec-tbl tr.dim td { opacity: .55; }
/* ĐẦU KHAY (CRM-414): tiêu đề + nhãn · chữ phụ · nút ‹ n / N › · hàng thao tác dưới đầu khay.
   `.rec-head` chồng lên `.sh-head`: căn trên để chữ phụ không kéo nút ✕ xuống giữa. */
#rec-ov .rec-head { align-items: flex-start; gap: 10px; }
#rec-ov .rec-head-main { flex: 1 1 auto; min-width: 0; }
#rec-ov .rec-head-l { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
#rec-ov .rec-sub { margin-top: 3px; color: var(--muted); font-size: var(--fs-1); }
#rec-ov .rec-sub[hidden] { display: none; }
#rec-ov .rec-head .sh-x { margin-left: 0; }
#rec-ov .rec-nav { display: flex; align-items: center; gap: 4px; flex: none; color: var(--muted);
  font-size: var(--fs-1); white-space: nowrap; }
#rec-ov .rec-nav:empty { display: none; }
#rec-ov .rec-nb { display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; }
#rec-ov .rec-acts { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 18px; border-bottom: 1px solid var(--line); }
#rec-ov .rec-acts[hidden] { display: none; }
#rec-ov .sh-btn.sm { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; font-size: var(--fs-1); }
#rec-ov .sh-btn.danger { color: var(--bad); border-color: var(--bad-line); }
#rec-ov .sh-btn.danger.solid { background: var(--bad); border-color: var(--bad); color: var(--bad-ink); }
/* Nhóm mục của tab Hồ sơ — lưới hai cột, ô dài (`.full`) chiếm trọn hàng. */
#rec-ov .rec-sec { margin: 0 0 6px; }
#rec-ov .rec-sec + .rec-sec { padding-top: 12px; border-top: 1px solid var(--line); }
#rec-ov .rec-sec-h { margin: 0 0 10px; font-size: var(--fs-2); font-weight: 600; color: var(--ink); }
#rec-ov .rec-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 14px; }
#rec-ov .rec-grid > .rec-f.full { grid-column: 1 / -1; }
@media (max-width: 640px) { #rec-ov .rec-grid { grid-template-columns: minmax(0, 1fr); } }
#rec-ov .rec-ck { display: block; }
/* Chân khay: dòng "N thay đổi chưa lưu", và chân ĐANG HỎI (rời khi chưa lưu, xác nhận lưu trữ). */
#rec-ov .rec-dirty { align-self: center; color: var(--muted); font-size: var(--fs-1); }
#rec-ov .rec-dirty.on { color: var(--warn); font-weight: 600; }
#rec-ov .sh-foot.rec-ask { background: var(--warn-soft); border-top-color: var(--warn-line); }
#rec-ov .rec-hoi { flex: 1 1 260px; align-self: center; color: var(--warn); font-size: var(--fs-1); font-weight: 600; }
#rec-ov .rec-tab-n { margin-left: 6px; padding: 0 6px; border-radius: var(--rp); background: var(--surface-3);
  color: var(--ink-2); font-size: var(--fs-1); }
#rec-ov .rec-tab-n.warn { background: var(--warn-soft); color: var(--warn); }
#rec-ov .rec-tab-n[hidden] { display: none; }

/* Dòng ĐÃ LƯU TRỮ trong các bảng quản trị: mờ đi, không ẩn — ẩn cứng thì không ai khôi
   phục được. Trước đây neo vào `#panel` (khung của riêng màn Quản trị); nay khu Phân quyền
   dựng cùng những dòng ấy trong `#pq-main`, nên luật neo vào chính THUỘC TÍNH DỮ LIỆU của
   dòng — thứ cả hai trang đều đặt. */
tr.rec-arch-row > td { opacity: .55; }
tr[data-rec-id] { cursor: pointer; }
tr[data-rec-id]:hover > td { background: var(--surface-2); }
.rec-arch-toggle { display: inline-flex; align-items: center; gap: 6px; margin-left: 10px;
  color: var(--muted); font-size: var(--fs-1); cursor: pointer; }
.rec-arch-toggle input { width: auto; margin: 0; }
/* Khay "Chia sẻ chờ tôi nhận" (docs/specs/2026-09-10-chia-se-hoi-thoai.md): mỗi dòng có
   HAI dòng chữ (tên khách + phụ đề), khác `.sh-row` mặc định (một dòng, `align-items:
   baseline`) nên cần `flex-start` để phụ đề không kéo lệch cụm nút. */
.sh-row-share { align-items: flex-start; flex-wrap: wrap; gap: 10px; }
.sh-row-acts { margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap; }
/* ===== CÙNG CHĂM SÓC (CRM-180) — chế độ thứ hai của hộp thoại Chuyển tiếp =====
   Dải chọn chế độ nằm TRÊN CÙNG thân hộp thoại: hai việc gần nhau (giao hội thoại này
   cho ai) nhưng hệ quả khác hẳn, nên người dùng phải thấy mình đang ở chế độ nào TRƯỚC
   khi đọc tới ô chọn người. */
.sh-modes { display: flex; gap: 6px; margin-bottom: 12px; }
.sh-modes .sh-btn { flex: 1 1 0; padding: 7px 10px; }
/* Danh sách người đang cùng chăm sóc — mỗi dòng một người kèm nút gỡ. */
.sh-people { list-style: none; margin: 0 0 13px; padding: 0; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; }
.sh-people li { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-bottom: 1px solid var(--line); font-size: var(--fs-2); }
.sh-people li:last-child { border-bottom: 0; }
.sh-people li > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh-people .sh-mini { margin-left: auto; padding: 4px 9px; font-size: var(--fs-1); }
.sh-hint { margin-top: 6px; font-size: var(--fs-1); color: var(--muted); }

@media (max-width: 560px) { .sh-card { width: 100%; } }

/* ===== Ô chọn có tìm kiếm (HPUI.enhanceSelect) — bọc <select> gốc, xem hp-ui.js ===== */
.hp-cb { position: relative; display: block; min-width: 0; width: 100%; }
.hp-cb > select.hp-cb-native { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px; border: 0 !important; opacity: 0; pointer-events: none; overflow: hidden; }
.hp-cb-btn { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; min-height: 40px; padding: 0 12px; border: 1px solid var(--line); border-radius: var(--r1); background: var(--surface); color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
.f > .hp-cb > .hp-cb-btn { height: 42px; padding: 0 12px; font-size: var(--fs-2); }
@media (max-width: 760px) { .f > .hp-cb > .hp-cb-btn { height: 48px; font-size: var(--fs-3); } }
.hp-cb.empty > .hp-cb-btn { color: var(--placeholder); }
.hp-cb-btn:focus-visible, .hp-cb-btn[aria-expanded="true"] { outline: 0; border-color: var(--brand); box-shadow: var(--focus); }
.hp-cb-btn:disabled { opacity: .6; cursor: default; }
.hp-cb-btn .chev { flex: none; opacity: .7; transition: transform var(--dur-1) var(--ease-out); }
.hp-cb-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
.hp-cb-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* Hộp nổi neo theo khung nhìn (hp-ui.js đặt left/width/top|bottom lúc mở và gắn nó vào <body>),
   nên không bị `overflow` của khối cha hay hộp thoại cắt; luôn sổ xuống dưới. */
.hp-cb-pop { position: fixed; z-index: var(--z-pop); box-sizing: border-box; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r2); box-shadow: var(--sh2); padding: 6px; font: var(--fs-2)/1.4 var(--f-sans); color: var(--ink); }
.hp-cb-pop[hidden] { display: none; }
.hp-cb-q { width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: var(--r1); padding: 8px 10px; font: inherit; font-size: var(--fs-2); background: var(--surface-2); color: var(--ink); margin-bottom: 6px; }
.hp-cb-q:focus { outline: 0; border-color: var(--brand); box-shadow: var(--focus); }
.hp-cb-pop.no-search .hp-cb-q { display: none; }
/* `position: relative` để `offsetTop` của mục tính từ DANH SÁCH (keepVisible cuộn theo nó), không từ hộp nổi. */
.hp-cb-list { position: relative; max-height: 280px; overflow: auto; outline: 0; }
.hp-cb-item { padding: 9px 10px; border-radius: var(--r1); cursor: pointer; font-size: var(--fs-2); color: var(--ink); white-space: normal; }
.hp-cb-item:hover, .hp-cb-item.act { background: var(--surface-2); }
.hp-cb-item.on { font-weight: 600; color: var(--brand-2); }
.hp-cb-empty { padding: 10px; color: var(--muted); font-size: var(--fs-2); }

/* ===== Chip chọn nhiều (HPUI.multiPick) ===== */
/* TRẠNG THÁI TRONG Ô BẢNG = CHẤM MÀU + CHỮ (CRM-366, dùng chung) — trang gắn `.hp-dotpills` lên
   khung chứa bảng thì mọi `.pill` kiểu THƯỜNG trong `td` mất nền tô, còn chấm màu dẫn đầu. Cột
   trạng thái tô nền là một dải màu dọc bảng; chấm đủ để mắt tìm dòng "Chờ" giữa dòng "Đang xử lý".
   Hai kiểu `solid`/`line` (bảng PROGRESS của `HPUI.statusTone`) giữ hình vì mang nghĩa riêng. */
.hp-dotpills td .pill:not(.solid):not(.line) { display: inline-flex; align-items: center; gap: 6px; padding: 0; border-radius: 0; background: none; color: var(--ink-2); font-weight: 500; }
.hp-dotpills td .pill:not(.solid):not(.line)::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--line-2); }
.hp-dotpills td .pill.ok:not(.solid):not(.line)::before { background: var(--ok); }
.hp-dotpills td .pill.warn:not(.solid):not(.line)::before { background: var(--warn); }
.hp-dotpills td .pill.info:not(.solid):not(.line)::before { background: var(--info); }
.hp-dotpills td .pill.bad:not(.solid):not(.line) { color: var(--bad); }
.hp-dotpills td .pill.bad:not(.solid):not(.line)::before { background: var(--bad); }

/* BIỂU ĐỒ SVG (CRM-367) — xem public/hp-chart.js. Màu đi qua `color` của lớp `.hpc-sN` rồi
   `currentColor`, nên đổi nền sáng/tối là biểu đồ đổi theo, không phải vẽ lại. Tím KHÔNG có
   trong dải màu: tím chỉ dành cho Mya. */
.hpc-svg { display: block; width: 100%; height: auto; overflow: visible; }
.hpc-grid { stroke: var(--line); stroke-width: 1; }
.hpc-ax { fill: var(--muted); font-family: var(--f-sans); font-size: var(--fs-1); }
.hpc-line { fill: none; stroke: currentColor; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.hpc-area { fill: currentColor; opacity: .12; }
.hpc-bar { fill: currentColor; }
.hpc-slice { fill: none; stroke: currentColor; stroke-width: 26; }
.hpc-total { fill: var(--ink); font-family: var(--f-sans); font-size: var(--fs-4); font-weight: 600; }
.hpc-s0 { color: var(--brand); } .hpc-s1 { color: var(--info); } .hpc-s2 { color: var(--warn); }
.hpc-s3 { color: var(--ok); } .hpc-s4 { color: var(--ink-2); } .hpc-s5 { color: var(--line-2); }
.hpc-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 10px 0 0; padding: 0; list-style: none; font-size: var(--fs-1); color: var(--ink-2); }
.hpc-legend li { display: inline-flex; align-items: center; gap: 6px; }
.hpc-legend b { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.hpc-sw { width: 10px; height: 10px; border-radius: 3px; background: currentColor; flex: none; }
.hpc-donut { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
.hpc-donut > .hpc-svg { width: 160px; flex: none; }
.hpc-donut .hpc-legend { flex-direction: column; margin: 0; }
.hpc-empty { padding: 28px 12px; text-align: center; color: var(--muted); font-size: var(--fs-1); }

/* TOAST DÙNG CHUNG (CRM-366) — `HPUI.toast(chữ, {tone, ms})`. Góc trên-phải, tự tắt, bấm là
   tắt; một hộp cho cả trang, câu sau thay câu trước. Thay cho `alert()` khi một lượt ghi tại chỗ
   hỏng: hộp thoại của trình duyệt chặn cả trang và mang tên miền lên đầu câu, còn ô vừa sửa đã
   tự lùi về giá trị cũ — chỉ cần NÓI ra, không cần bắt bấm OK. `tone:'bad'` có vạch đỏ mép trái. */
.hp-toast { position: fixed; top: 16px; right: 16px; z-index: var(--z-toast); transform: translateY(-8px);
  max-width: min(420px, calc(100vw - 32px)); padding: 10px 14px; border-radius: var(--r2);
  background: var(--ink); color: var(--surface); font-size: var(--fs-2); line-height: 1.45;
  box-shadow: var(--sh3); opacity: 0; pointer-events: none;
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out); }
.hp-toast.on { opacity: 1; transform: translateY(0); pointer-events: auto; cursor: pointer; }
.hp-toast.bad { box-shadow: inset 3px 0 0 var(--bad), var(--sh3); }

/* DẢI SỐ LIỆU (CRM-363) — một khung, các ô ngăn bằng vạch mảnh, thay cho N thẻ rời có viền
   và bóng. Tám thẻ rời là tám khung, mắt đếm khung trước khi đọc số. Vạch vẽ bằng
   `box-shadow` phía phải + phía dưới của TỪNG ô và khung `overflow:hidden` cắt vạch sát mép —
   nên xuống hàng ở bề rộng nào cũng không có vạch thừa hay ô trống đổ màu vạch. Ô dùng
   khuôn `.v` (con số) + `.l` (nhãn) có sẵn của các màn cũ. */
.hp-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr)); background: var(--surface); border: 1px solid var(--line); border-radius: var(--r3); overflow: hidden; }
.hp-kpis > * { min-width: 0; padding: 12px 16px; box-shadow: 1px 0 0 var(--line), 0 1px 0 var(--line); }
.hp-kpis .v { font-size: var(--fs-4); line-height: var(--lh-4); font-weight: 500; font-variant-numeric: tabular-nums; color: var(--ink); }
.hp-kpis .l { font-size: var(--fs-2); line-height: var(--lh-2); color: var(--ink); }

/* SỐ ĐẾM TRONG BẢNG (CRM-363): phông sans + chữ số cùng bề rộng, KHÔNG mono. Mono dành cho
   thứ phải đọc từng ký tự (mã, serial, số che) — một cột "0 · 12 · 3" in mono là cột có số
   0 gạch chéo nổi hơn cả tên người bên cạnh. */
.num { font-variant-numeric: tabular-nums; }

/* CÔNG TẮC ĐOẠN (CRM-362) — hai đến bốn lựa chọn LOẠI TRỪ NHAU nằm cạnh nhau ("Của tôi ·
   Tất cả", "Bảng · Biểu đồ"). Đoạn đang chọn là tấm nổi trên rãnh nhạt, không phải nút có
   viền màu: đọc được bằng chữ, và cả hàng chỉ có MỘT khung. Trạng thái đọc từ `.on` hoặc từ
   `aria-pressed`/`aria-selected` — màn hình giữ trạng thái ở đâu thì CSS nghe ở đó. */
.hp-seg { display: inline-flex; align-items: center; gap: 2px; padding: 2px; border-radius: var(--r2); background: var(--surface-3); flex: none; }
.hp-seg > button { height: 28px; padding: 0 10px; border: 0; border-radius: 8px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: var(--fs-2); font-weight: 500; cursor: pointer; white-space: nowrap; }
.hp-seg > button:hover { color: var(--ink); }
.hp-seg > button.on,
.hp-seg > button[aria-pressed="true"],
.hp-seg > button[aria-selected="true"] { background: var(--surface); color: var(--ink); box-shadow: var(--sh1); }
.hp-seg > button:focus-visible { outline: 0; box-shadow: var(--focus); }
.hp-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.hp-chips[hidden] { display: none; }
.hp-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 6px 3px 10px; border-radius: var(--rp); background: var(--brand-soft); border: 1px solid var(--brand-line); color: var(--brand-2); font-size: var(--fs-1); font-weight: 600; }
.hp-chip-x { border: 0; background: transparent; color: inherit; font-size: var(--fs-3); line-height: 1; cursor: pointer; padding: 0 2px; border-radius: 50%; }
.hp-chip-x:hover { background: var(--surface-3); }
/* Chip nằm trong nút combobox: nhãn thành hàng chip cuộn xuống được, nút cao theo nội dung. */
.hp-cb-multi > .hp-cb-btn { height: auto; min-height: 42px; padding-top: 5px; padding-bottom: 5px; }
.hp-cb-lbl.in-btn { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 0; white-space: normal; overflow: visible; }
.hp-cb-lbl.in-btn .hp-chips-ph { color: var(--muted); }
.hp-cb.empty .hp-cb-lbl.in-btn .hp-chips-ph { color: var(--placeholder); }

/* ===== Chọn nhiều có tìm + ô tích (HPUI.checkPick) — bọc <select multiple> =====
   Nút mở là <div role="combobox"> (chip mang nút × bên trong), nên tự khai con trỏ và trạng thái
   khoá thay cho `:disabled` của <button>. Hộp nổi KHÔNG đóng sau mỗi lượt tích. */
.hp-ck > .hp-cb-btn { cursor: pointer; }
.hp-ck.is-disabled > .hp-cb-btn { opacity: .6; cursor: default; background: var(--surface-2); }
.hp-ck-item { display: flex; align-items: flex-start; gap: 9px; }
.hp-cb-item.hp-ck-item.on { font-weight: 400; color: var(--ink); }
.hp-ck-box { flex: none; display: grid; place-items: center; width: 16px; height: 16px; margin-top: 2px;
  border: 1.5px solid var(--line-2); border-radius: var(--r1); background: var(--surface); color: var(--brand-ink); }
.hp-ck-item.on .hp-ck-box { background: var(--brand); border-color: var(--brand); }
.hp-ck-item:not(.on) .hp-ck-box svg { visibility: hidden; }
.hp-cb-item.hp-ck-item.dis { color: var(--muted); cursor: default; }
.hp-cb-item.hp-ck-item.dis .hp-ck-box { background: var(--surface-2); }
.hp-ck-foot { display: flex; align-items: center; gap: 6px; margin-top: 6px; padding-top: 6px;
  border-top: 1px solid var(--line); color: var(--muted); font-size: var(--fs-1); }
.hp-ck-foot .sp { flex: 1; }
.hp-ck-foot button { padding: 5px 8px; border: 0; border-radius: var(--r1); background: transparent;
  color: var(--brand); font: inherit; font-weight: 600; cursor: pointer; }
.hp-ck-foot button:hover { background: var(--surface-2); }
.hp-ck .hp-chip-x { display: inline-grid; place-items: center; }

/* Ô chọn có tìm kiếm bên trong .f: viền lỗi/nháy chuyển sang NÚT, vì <select> gốc đã bị bọc và ẩn. */
.f > .hp-cb > select.bad + .hp-cb-btn { border-color: var(--bad); }
.f.flash > .hp-cb > .hp-cb-btn { border-color: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }

  /* ===== WIDGET TUỲ BIẾN DANH SÁCH (CRM-202) — dùng chung cho MỌI màn danh sách =====
   Hai menu "Cột" và "Bộ lọc yêu thích" do `public/hp-listprefs.js` dựng. Khai ở đây chứ không
   ở trang nào cả: sáu màn dùng chung một widget thì cũng dùng chung một bộ dáng, và trang thứ
   bảy gắn widget vào là có sẵn dáng, không phải chép CSS lần nữa.
   Đứng trên thanh tìm kiếm nên mang dáng nút viền liền của thanh đó, không phải dáng menu chìm. */
  .usermenu[hidden]{display:none}
  .usermenu>.hp-menu-btn{border:1px solid var(--line);background:var(--surface);color:var(--ink);
    font-size:var(--fs-2);padding:8px 12px;min-height:36px;white-space:nowrap}
  .usermenu>.hp-menu-btn:hover{border-color:var(--line-2)}
  .usermenu .hp-menu-list{min-width:264px;max-width:calc(100cqw - 24px);max-height:min(60vh,420px);
    overflow:auto;padding:10px}
  .usermenu .mhead{font-size:var(--fs-1);font-weight:500;color:var(--muted);padding:0 2px 6px}
  .usermenu .mfoot{display:flex;gap:8px;align-items:center;justify-content:space-between;
    border-top:1px solid var(--line);margin-top:8px;padding-top:8px}
  .usermenu .merr{font-size:var(--fs-1);color:var(--bad);padding:4px 2px 0}
  .usermenu .mnone{font-size:var(--fs-1);color:var(--muted);padding:4px 2px}

  /* Một dòng cột: ô tick — tên — hai nút dời. Vùng bấm cao 44px cho ngón tay, và cả ba điều
     khiển đều tới được bằng Tab (không dùng kéo–thả: chuột mới kéo được, bàn phím thì không). */
  .colrow{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:8px;
    padding:4px 2px;min-height:44px}
  .colrow>label{display:flex;align-items:center;gap:8px;font-size:var(--fs-2);color:var(--ink);
    cursor:pointer;min-height:44px}
  .colrow input[type=checkbox]{width:18px;height:18px;accent-color:var(--brand);cursor:pointer}
  .colrow input[type=checkbox]:disabled{cursor:not-allowed}
  .colrow .fixed{color:var(--muted)}
  .colmove{border:1px solid var(--line);background:var(--surface);color:var(--ink-2);border-radius:8px;
    width:34px;height:34px;line-height:1;cursor:pointer;font-size:var(--fs-1)}
  .colmove:hover:not(:disabled){border-color:var(--line-2);color:var(--ink)}
  .colmove:focus-visible{outline:0;box-shadow:var(--focus)}
  .colmove:disabled{opacity:.35;cursor:default}

  /* Một bộ lọc đã lưu: tên bấm để áp, hai nút nhỏ để đặt mặc định và xoá. */
  /* Bốn ô: tên (co giãn) · đặt mặc định · đổi tên · xoá. Thiếu một cột là nút cuối rơi
     xuống hàng hai và hàng bộ lọc cao gấp đôi. */
  .favrow{display:grid;grid-template-columns:1fr auto auto auto;align-items:center;gap:6px;min-height:44px}
  .favrow .favgo{flex:1;text-align:left;border:0;background:transparent;color:var(--ink);font:inherit;
    font-size:var(--fs-2);cursor:pointer;padding:8px;border-radius:8px;min-height:44px}
  .favrow .favgo:hover{background:var(--surface-2)}
  .favrow .favgo:focus-visible{outline:0;box-shadow:var(--focus)}
  .favrow .favgo .star{color:var(--warn)}
  .favact{border:0;background:transparent;color:var(--muted);cursor:pointer;border-radius:8px;
    width:34px;height:34px;font-size:var(--fs-1)}
  .favact:hover{background:var(--surface-2);color:var(--ink)}
  .favact:focus-visible{outline:0;box-shadow:var(--focus)}
  .favact.on{color:var(--warn)}
  .favsave{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:4px}
  /* Dòng tóm tắt điều kiện (`filters.describe`) dưới tên bộ, và "thứ sẽ lưu" ngay trên ô đặt tên. */
  .favrow .favgo .favsum{display:block;margin-top:2px;color:var(--muted);font-weight:400}
  .favrow.on .favgo{background:var(--brand-soft);color:var(--brand-2)}
  .favsave .favwhat{flex:1 1 100%;padding:6px 8px;border-radius:8px;background:var(--surface-2);
    color:var(--ink-2);font-size:var(--fs-1)}
  .favsave input:disabled,.favsave .btn:disabled{opacity:.5;cursor:default}
  .favupd{padding:4px 2px 0}
  .usermenu .lp-fav-cur{color:var(--brand-2);font-weight:600}
  .usermenu .lp-fav-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--warn);
    vertical-align:middle}
  /* Ô đặt tên một hàng riêng, nút Lưu xuống dưới bên phải: menu rộng 264px, ô và nút chung một hàng
     thì ô chỉ còn ~100px và câu gợi ý bị cắt ngang. */
  .favsave>.btn{margin-left:auto}
  .favsave input{flex:1 1 100%;min-width:0;border:1px solid var(--line);border-radius:8px;padding:8px 10px;
    font:inherit;font-size:var(--fs-2);background:var(--surface);color:var(--ink)}
  .favsave input:focus{outline:0;border-color:var(--brand);box-shadow:var(--focus)}
  /* Ô đổi tên ngay trên dòng — cùng dáng ô đặt tên ở chân menu, chiếm đúng chỗ nút tên. */
  .favrename{flex:1;min-width:0;width:100%;border:1px solid var(--brand);border-radius:8px;
    padding:8px;font:inherit;font-size:var(--fs-2);background:var(--surface);color:var(--ink)}
  .favrename:focus{outline:0;box-shadow:var(--focus)}

/* ============================================================================
   TRUNG TÂM THÔNG BÁO — chuông, khay, hộp thoại Gửi
   docs/specs/2026-09-16-trung-tam-thong-bao.md

   Tái dùng NGUYÊN `.hp-menu` / `.hp-menu-btn` của khung chung: chuông là một menu thả
   xuống như mọi menu khác, chỉ khác phần thân. Khai lại một khung riêng là khai lại cả
   phép đóng-khi-bấm-ra-ngoài, tiêu điểm bàn phím và nết ở màn hẹp.
   ============================================================================ */
.noti-btn { position: relative; }
/* Badge NEO VÀO GÓC icon, không đứng cạnh: nút chuông chỉ có icon nên một badge đứng cạnh
   sẽ nong nút ra và đẩy cả cụm phải của thanh trên sang trái mỗi khi có việc mới. */
.noti-badge { position: absolute; top: 2px; right: 2px; margin: 0; padding: 1px 4px; min-width: 16px; text-align: center; }

.noti-panel { position: absolute; top: calc(100% + 6px); right: 0; width: 380px; max-width: calc(100vw - 32px);
  max-height: min(70vh, 560px); background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r2); box-shadow: var(--sh2); z-index: var(--z-menu); display: none; flex-direction: column; overflow: hidden; }
.hp-menu.open .noti-panel { display: flex; }
.noti-head { display: flex; align-items: center; gap: 6px; padding: 10px 12px; border-bottom: 1px solid var(--line); }
.noti-head h3 { margin: 0; font: 600 var(--fs-3)/var(--lh-3) var(--f-sans); color: var(--ink); }
.noti-head .spacer { margin-left: auto; }
.noti-count { border-radius: var(--rp); padding: 1px 7px; background: var(--bad); color: var(--brand-ink);
  font: 600 var(--fs-1)/1.4 var(--f-sans); }
.noti-count[hidden], .noti-readall[hidden], .noti-compose[hidden] { display: none; }
.noti-readall, .noti-compose { border: 0; background: transparent; color: var(--brand-2);
  font: 600 var(--fs-1)/1 var(--f-sans); cursor: pointer; padding: 6px 5px; border-radius: var(--r1); white-space: nowrap; }
.noti-readall:hover, .noti-compose:hover { background: var(--surface-2); }
.noti-readall:focus-visible, .noti-compose:focus-visible { outline: 0; box-shadow: var(--focus); }
.noti-tabs { display: flex; gap: 6px; padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--surface-2); }
.noti-tab { border: 1px solid var(--line-2); background: var(--surface); color: var(--ink-2);
  border-radius: var(--rp); padding: 4px 12px; font: 600 var(--fs-1)/1.4 var(--f-sans); cursor: pointer; }
.noti-tab[aria-selected="true"] { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.noti-tab:focus-visible { outline: 0; box-shadow: var(--focus); }
/* `min-height` giữ chỗ cho ba dòng skeleton, để khay KHÔNG đổi chiều cao giữa lượt tải và
   lượt vẽ thật — docs/CHUYEN-TRANG-MUOT.md. */
.noti-body { overflow-y: auto; flex: 1; min-height: 168px; }
.noti-list { list-style: none; margin: 0; padding: 0; }

.noti-row { display: flex; gap: 10px; align-items: flex-start; width: 100%; padding: 10px 12px;
  border: 0; border-bottom: 1px solid var(--line); background: transparent; color: inherit;
  text-decoration: none; text-align: left; cursor: pointer; font-family: inherit; font-size: var(--fs-2); }
.noti-list li:last-child .noti-row { border-bottom: 0; }
.noti-row:hover { background: var(--surface-3); }
.noti-row:focus-visible { outline: 0; box-shadow: var(--focus); }
/* CHƯA ĐỌC có HAI dấu hiệu, không một: nền + chấm. Chỉ dựa vào nền thì người mù màu và màn
   hình chói không phân biệt được. */
.noti-row.unread { background: var(--surface-2); }
.noti-row.unread:hover { background: var(--surface-3); }
.noti-ic { flex: none; width: 28px; height: 28px; border-radius: var(--rp); display: grid; place-items: center;
  background: var(--surface-3); color: var(--ink-2); }
.noti-ic.chat { background: var(--brand-soft); color: var(--brand-2); }
.noti-ic.case { background: var(--info-soft); color: var(--info); }
.noti-ic.due { background: var(--warn-soft); color: var(--warn); }
.noti-ic.dmx { background: var(--mya-soft); color: var(--mya); }
/* Thông báo DO NGƯỜI GỬI — màu khác hẳn bốn loại máy phát, để mắt tách được ngay "người
   nói" với "máy nói". */
.noti-ic.notice { background: var(--ok-soft); color: var(--ok); }
.noti-txt { min-width: 0; flex: 1; }
.noti-t { margin: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  font: 400 var(--fs-2)/var(--lh-2) var(--f-sans); color: var(--ink); }
.noti-row.unread .noti-t { font-weight: 600; }
.noti-row.open .noti-t { -webkit-line-clamp: unset; }
.noti-s { margin: 2px 0 0; font: 400 var(--fs-1)/var(--lh-1) var(--f-sans); color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Thân thông báo của người gửi: `pre-wrap` giữ xuống dòng mà KHÔNG phải dựng thẻ ngắt dòng từ
   chuỗi — dựng thẻ nghĩa là innerHTML, tức chạy mã của người gửi trên máy người nhận. */
.noti-full { margin: 6px 0 0; white-space: pre-wrap; font: 400 var(--fs-2)/var(--lh-2) var(--f-sans); color: var(--ink-2); }
.noti-dot { flex: none; width: 8px; height: 8px; border-radius: var(--rp); background: var(--brand); margin-top: 8px; }
.noti-row:not(.unread) .noti-dot { background: transparent; }

.noti-skel { padding: 10px 12px; border-bottom: 1px solid var(--line); display: flex; gap: 10px; }
.noti-skel i { flex: none; width: 28px; height: 28px; border-radius: var(--rp); background: var(--surface-3); }
.noti-skel div { flex: 1; }
.noti-skel span { display: block; height: 10px; border-radius: var(--r1); background: var(--surface-3); }
.noti-skel span + span { margin-top: 8px; width: 55%; }
.noti-empty, .noti-err { padding: 28px 20px; text-align: center; color: var(--muted); font-size: var(--fs-1); line-height: var(--lh-1); }
.noti-empty b, .noti-err b { display: block; color: var(--ink); font: 600 var(--fs-2)/var(--lh-2) var(--f-sans); margin-bottom: 4px; }
.noti-empty .ghost { color: var(--line-2); display: block; width: 34px; margin: 0 auto 10px; }
.noti-err { color: var(--bad); }
.noti-retry { margin-top: 10px; border: 1px solid var(--line-2); background: var(--surface); color: var(--ink);
  border-radius: var(--r1); padding: 6px 14px; font: 600 var(--fs-1)/1.4 var(--f-sans); cursor: pointer; }
.noti-retry:focus-visible { outline: 0; box-shadow: var(--focus); }

/* --- hộp thoại Gửi: mượn `.sh-overlay`/`.sh-card` của hộp thoại Kết ca, chỉ khai phần thân --- */
.nc-blk { margin-bottom: 16px; }
.nc-lbl { display: block; margin: 0 0 6px; color: var(--ink-2);
  font: 500 var(--fs-1)/var(--lh-1) var(--f-sans); }
.nc-cnt { float: right; color: var(--muted); text-transform: none; letter-spacing: 0;
  font: 400 var(--fs-1)/var(--lh-1) var(--f-sans); }
.nc-targets { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.nc-target { display: flex; gap: 9px; align-items: flex-start; border: 1px solid var(--line-2);
  background: var(--surface); border-radius: var(--r2); padding: 10px 11px; cursor: pointer; }
.nc-target:hover { border-color: var(--brand-line); background: var(--surface-2); }
.nc-target:focus-visible { outline: 0; box-shadow: var(--focus); }
.nc-target[aria-checked="true"] { border-color: var(--brand); background: var(--brand-soft); }
.nc-target.wide { grid-column: 1 / -1; }
.nc-target.danger[aria-checked="true"] { border-color: var(--warn-line); background: var(--warn-soft); }
.nc-target b { display: block; font: 600 var(--fs-2)/var(--lh-2) var(--f-sans); }
.nc-target small { display: block; color: var(--muted); font: 400 var(--fs-1)/var(--lh-1) var(--f-sans); }
.nc-target i { flex: none; width: 16px; height: 16px; margin-top: 3px; border-radius: var(--rp); border: 2px solid var(--line-2); }
.nc-target[aria-checked="true"] i { border-color: var(--brand); box-shadow: inset 0 0 0 3px var(--brand); }
.nc-sub { margin-top: 10px; padding: 10px 11px; border: 1px dashed var(--line-2); border-radius: var(--r2); background: var(--surface-2); }
.nc-check { display: flex; gap: 7px; align-items: center; margin-top: 9px; font-size: var(--fs-1); }
.nc-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.nc-chip { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--brand-line);
  background: var(--brand-soft); color: var(--brand-2); border-radius: var(--rp); padding: 3px 5px 3px 10px;
  font: 600 var(--fs-1)/1.5 var(--f-sans); }
.nc-chip button { border: 0; background: transparent; color: inherit; cursor: pointer; padding: 0 3px; font-size: var(--fs-1); }
.nc-prev { display: flex; gap: 8px; align-items: flex-start; border: 1px solid var(--line);
  background: var(--surface-2); border-radius: var(--r2); padding: 10px 12px; font-size: var(--fs-1); line-height: var(--lh-1); }
.nc-prev b { font: 600 var(--fs-2)/var(--lh-2) var(--f-sans); }
.nc-prev p { margin: 2px 0 0; color: var(--muted); font: 400 var(--fs-1)/var(--lh-1) var(--f-sans); }
.nc-prev.warn { border-color: var(--warn-line); background: var(--warn-soft); color: var(--warn); }
.nc-prev.bad { border-color: var(--bad-line); background: var(--bad-soft); color: var(--bad); }
.nc-prev.warn p, .nc-prev.bad p { color: inherit; }
#noti-compose-overlay .sh-card { width: 560px; }
#noti-compose-overlay input[type="text"], #noti-compose-overlay textarea, #noti-compose-overlay select {
  width: 100%; border: 1px solid var(--line-2); background: var(--surface); color: var(--ink);
  border-radius: var(--r1); padding: 8px 10px; font-family: inherit; font-size: var(--fs-2); line-height: var(--lh-2); }
#noti-compose-overlay textarea { resize: vertical; min-height: 84px; }
#noti-compose-overlay input[type="text"]:focus, #noti-compose-overlay textarea:focus, #noti-compose-overlay select:focus {
  outline: 0; border-color: var(--brand); box-shadow: var(--focus); }

/* Màn hẹp: khay bung hết bề ngang và neo dưới thanh trên, thay vì treo lơ lửng bên phải một
   nút chỉ rộng 34px. */
@media (max-width: 700px) {
  .noti-panel { position: fixed; left: 8px; right: 8px; top: calc(var(--top-h) + 6px); width: auto; max-width: none; }
  .nc-targets { grid-template-columns: 1fr; }
}

/* Chân khay thông báo — lối vào trang "Tất cả thông báo" (đợt 2). */
/* Chân DANH SÁCH (khác `.noti-foot` — cái đó là chân KHAY, dính đáy): dòng "đang tải thêm"
   hoặc nút thử lại của lượt phân trang, cuộn theo danh sách. */
.noti-more { padding: 10px 14px; text-align: center; color: var(--muted);
  font: 400 var(--fs-1)/var(--lh-1) var(--f-sans); }
.noti-foot { flex: none; border-top: 1px solid var(--line); background: var(--surface-2); }
.noti-all { display: block; padding: 9px 12px; text-align: center; text-decoration: none;
  color: var(--brand-2); font: 600 var(--fs-1)/1.4 var(--f-sans); }
.noti-all:hover { background: var(--surface-3); }
.noti-all:focus-visible { outline: 0; box-shadow: var(--focus); }

/* Hai tab của hộp thoại Gửi (Soạn · Đã gửi) — tab thứ hai là đường DUY NHẤT để thu hồi. */
.nc-tabs { display: flex; gap: 4px; padding: 0 16px; border-bottom: 1px solid var(--line); background: var(--surface-2); }
.nc-tab { border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--muted);
  padding: 10px 12px; cursor: pointer; font: 600 var(--fs-2)/1.2 var(--f-sans); }
.nc-tab[aria-selected="true"] { color: var(--brand-2); border-bottom-color: var(--brand); }
.nc-tab:focus-visible { outline: 0; box-shadow: var(--focus); }
.nc-sent { list-style: none; margin: 0; padding: 0; }
.nc-sent li { display: flex; gap: 10px; align-items: flex-start; padding: 11px 0; border-bottom: 1px solid var(--line); }
.nc-sent li:last-child { border-bottom: 0; }
.nc-sent .pn { flex: 1; min-width: 0; }
.nc-sent .pn b { display: block; font: 600 var(--fs-2)/var(--lh-2) var(--f-sans); }
.nc-sent .pn small { display: block; color: var(--muted); font: 400 var(--fs-1)/var(--lh-1) var(--f-sans); }
.nc-sent .acts { margin-left: auto; flex: none; }
.nc-tag { display: inline-block; border-radius: var(--rp); padding: 2px 9px; border: 1px solid var(--line-2);
  color: var(--muted); font: 600 var(--fs-1)/1.5 var(--f-sans); }
.nc-tag.off { border-color: var(--bad-line); background: var(--bad-soft); color: var(--bad); }
/* Dòng chữ phụ TRONG hộp thoại Gửi. Không dùng `.muted`: lớp ấy do TỪNG TRANG tự khai trong
   <style> của nó, không có ở tokens.css — mà hp-noti.js chạy ở mọi trang, nên dựa vào nó là
   dựa vào một thứ có thể không tồn tại. Hàng rào `notify/catalog.test.ts` bắt đúng chuyện này. */
.nc-note { color: var(--muted); font: 400 var(--fs-2)/var(--lh-2) var(--f-sans); padding: 8px 0; }
   SỐ ĐIỆN THOẠI VÀ CỬA LỘ SỐ — CRM-238 (public/hp-phone.js)

   Máy chủ đã che số ở mọi cửa `/api/`, nên ở đây không có lớp nào "che bằng CSS":
   thứ CSS này dựng là cái nút bên cạnh con số và hộp thoại hỏi lý do.

   Ô số dùng chữ MONO và `tabular-nums`: một cột số mà mỗi dòng lệch một pixel thì
   mắt không so được hai số với nhau — đúng việc mà bốn chữ số CUỐI sinh ra để làm
   (CRM-271; trước 22/09/2026 phần đọc được là bốn chữ số đầu).
   ============================================================================ */
.hp-phone { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.hp-phone-v { font-family: var(--f-mono); font-size: var(--fs-2); letter-spacing: .02em;
  font-variant-numeric: tabular-nums; }
.hp-phone.mo .hp-phone-v { color: var(--ok); font-weight: 600; }
.hp-phone-eye { display: inline-grid; place-items: center; width: 24px; height: 24px; flex: 0 0 auto;
  padding: 0; border: 1px solid var(--line); border-radius: var(--r1); background: var(--surface);
  color: var(--muted); cursor: pointer; }
.hp-phone-eye:hover { color: var(--brand); border-color: var(--brand); background: var(--brand-soft); }
.hp-phone-eye:focus-visible { outline: 0; box-shadow: var(--focus); }
/* NÚT GỌI — cùng hình học với con mắt (24×24, `flex:0 0 auto`) để một cột số có hai nút vẫn
   xếp thẳng hàng dọc. Khác ở MÀU: con mắt là hành động XEM (trung tính, phải hỏi lý do và
   phải ghi sổ), nút này là hành động LÀM (gọi khách) nên nó mang màu thương hiệu ngay ở trạng
   thái nghỉ. Hai việc khác hẳn nhau đứng cạnh nhau trong một ô thì phải phân biệt được mà
   không cần rê chuột đọc tooltip.
   KHÔNG dùng màu tím: tím là màu DÀNH RIÊNG cho Mya (docs/HE-THONG-THIET-KE.md). */
.hp-phone-call { display: inline-grid; place-items: center; width: 24px; height: 24px; flex: 0 0 auto;
  padding: 0; border: 1px solid var(--brand-line); border-radius: var(--r1);
  background: var(--brand-soft); color: var(--brand); cursor: pointer; }
.hp-phone-call:hover { background: var(--brand); color: var(--brand-ink); border-color: var(--brand); }
.hp-phone-call:focus-visible { outline: 0; box-shadow: var(--focus); }
.hp-phone-call[disabled] { opacity: .5; cursor: progress; }
/* Đồng hồ đếm ngược của lượt lộ. Nó nói "còn bao lâu", KHÔNG phải một phép chặn —
   số đã ra tới trình duyệt rồi; xem chú thích đầu public/hp-phone.js. */
.hp-phone-cd { font: 500 var(--fs-1)/1 var(--f-mono); font-variant-numeric: tabular-nums;
  color: var(--ok); background: var(--ok-soft); border: 1px solid var(--ok-line);
  border-radius: var(--rp); padding: 3px 7px; }

/* ============================================================================
   HỘP THOẠI HỎI LÝ DO của cửa lộ số (public/hp-phone.js).

   Ô địa chỉ + con mắt xin địa chỉ đầy đủ (`.hp-addr*`, public/hp-address.js) đã gỡ ngày
   30/09/2026 — địa chỉ chi tiết không còn che, mọi màn vẽ nó như chữ thường.
   ============================================================================ */
/* Lớp phủ PHẢI gắn vào <body> (hp-phone.js làm đúng vậy): `.hp-page` là ngữ cảnh xếp
   lớp riêng (`isolation: isolate`), nên lớp phủ đặt trong đó nằm DƯỚI thanh trên —
   xem cảnh báo ở phần khung trang bên trên. */
.hp-scrim { position: fixed; inset: 0; z-index: var(--z-pop); display: grid; place-items: center;
  padding: 16px; background: var(--scrim); }
.hp-modal { width: min(520px, 100%); background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r3); box-shadow: var(--sh2); overflow: hidden; }
.hp-modal > header { padding: 14px 16px; border-bottom: 1px solid var(--line); }
.hp-modal > header h3 { margin: 0; font: 500 var(--fs-5)/var(--lh-5) var(--f-sans); }
.hp-modal-body { padding: 16px; display: grid; gap: 10px; }
.hp-modal > footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px;
  border-top: 1px solid var(--line); background: var(--surface-2); }
.hp-lo-who { padding: 10px 12px; border-radius: var(--r2); background: var(--surface-2);
  border: 1px solid var(--line); }
.hp-lo-lb { font-size: var(--fs-1); color: var(--muted); }
.hp-lo-lb .req { color: var(--bad); }
.hp-modal-body textarea { width: 100%; resize: vertical; font: inherit; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r1); padding: 8px; }
.hp-modal-body textarea:focus { outline: 0; border-color: var(--brand); box-shadow: var(--focus); }
.hp-lo-cnt { font-size: var(--fs-1); color: var(--muted); }
.hp-lo-cnt .err { color: var(--bad); }
.hp-lo-note { margin: 0; font-size: var(--fs-1); color: var(--warn); background: var(--warn-soft);
  border: 1px solid var(--warn-line); border-radius: var(--r2); padding: 8px 10px; }

/* ===========================================================================
   HỘP THOẠI "NHẬP TỪ TỆP" (hp-import.js) — MỘT khuôn cho MỌI lượt nhập bảng.

   Khai ở đây chứ không trong từng trang: widget sống ở tầng dùng chung, mà màn Quản trị
   và màn Dữ liệu nguồn là hai trang riêng — chép khối này vào cả hai là hai bản CSS phải
   giữ đồng bộ bằng tay, đúng thứ việc rút widget ra đã làm để tránh.
   =========================================================================== */
.hp-imp { position: fixed; inset: 0; background: var(--scrim);
  display: none; place-items: center; z-index: var(--z-modal); padding: 16px; }
.hp-imp.open { display: grid; }
.hp-imp-box { background: var(--card); border-radius: var(--r3); box-shadow: var(--sh2);
  width: 100%; max-width: 760px; max-height: 88vh; display: flex; flex-direction: column;
  overflow: hidden; }
.hp-imp-head { display: flex; align-items: center; gap: 9px; padding: 13px 16px;
  border-bottom: 1px solid var(--line); font: 600 var(--fs-3)/1.3 var(--f-sans); }
.hp-imp-xbtn { margin-left: auto; border: 0; background: transparent; cursor: pointer;
  color: var(--muted); font-size: var(--fs-4); line-height: 1; padding: 2px 4px; }
.hp-imp-xbtn:hover { color: var(--ink); }
.hp-imp-body { padding: 15px 16px; overflow: auto; display: flex; flex-direction: column; gap: 11px; }
.hp-imp-hint { margin: 0; color: var(--muted); font-size: var(--fs-1); line-height: 1.65; }
.hp-imp-pick { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.hp-imp-fname { font-size: var(--fs-1); color: var(--muted); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.hp-imp-btn { font: 600 var(--fs-1)/1 var(--f-sans); border: 1px solid var(--line);
  background: var(--surface); color: var(--ink); border-radius: var(--r2);
  padding: 8px 13px; cursor: pointer; }
.hp-imp-btn:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
.hp-imp-btn:disabled { opacity: .5; cursor: default; }
.hp-imp-btn.pri { background: var(--teal); border-color: var(--teal); color: var(--brand-ink); }
.hp-imp-btn.pri:hover:not(:disabled) { color: var(--brand-ink); }
.hp-imp-sheet { display: flex; align-items: center; gap: 8px; }
.hp-imp-sheet label { font-size: var(--fs-1); color: var(--muted); }
.hp-imp-sheet select { flex: 1; min-width: 0; font: inherit; font-size: var(--fs-1);
  border: 1px solid var(--line); border-radius: var(--r2); padding: 7px 9px;
  background: var(--surface); color: var(--ink); }
.hp-imp-more summary { font-size: var(--fs-1); color: var(--teal); cursor: pointer; width: fit-content; }
.hp-imp-more[open] summary { margin-bottom: 8px; }
.hp-imp-textarea { width: 100%; box-sizing: border-box; font-family: var(--f-mono);
  font-size: var(--fs-1); line-height: 1.55; padding: 9px 11px; border: 1px solid var(--line);
  border-radius: var(--r2); background: var(--surface); color: var(--ink); resize: vertical; }
.hp-imp-textarea:focus { outline: 0; border-color: var(--brand); box-shadow: var(--focus); }
/* Bảng xem trước tự cuộn TRONG hộp: một tệp 300 dòng không được đẩy hai nút ở chân ra
   khỏi màn hình — đó là hai nút duy nhất kết thúc được việc này. */
.hp-imp-out { max-height: 300px; overflow: auto; border: 1px solid var(--line);
  border-radius: var(--r2); }
.hp-imp-out table { width: 100%; border-collapse: collapse; font-size: var(--fs-2); }
.hp-imp-out th { position: sticky; top: 0; z-index: 1; background: var(--surface-3);
  color: var(--ink); text-align: left; font: 500 var(--fs-2)/1.4 var(--f-sans);
  padding: 7px 9px; border-bottom: 1px solid var(--line); white-space: nowrap; }
.hp-imp-out td { padding: 6px 9px; border-bottom: 1px solid var(--line); vertical-align: top; }
.hp-imp-out tr:last-child td { border-bottom: 0; }
.hp-imp-out td.n { font-family: var(--f-mono); color: var(--muted); white-space: nowrap; }
.hp-imp-dash { color: var(--muted); }
.hp-imp-why { display: block; margin-top: 3px; color: var(--bad); }
.hp-imp-chg { display: block; margin-top: 3px; color: var(--muted); }
.hp-imp-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 9px;
  background: var(--surface-2); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 2; }
.hp-imp-chips:empty { display: none; }
.hp-imp-pill { display: inline-flex; align-items: center; font: 600 var(--fs-1)/1 var(--f-sans);
  border-radius: var(--rp); padding: 4px 9px; border: 1px solid var(--line);
  background: var(--surface-3); color: var(--muted); }
.hp-imp-pill.ok { background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok); }
.hp-imp-pill.warn { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }
.hp-imp-pill.bad { background: var(--bad-soft); border-color: var(--bad-line); color: var(--bad); }
.hp-imp-prog { display: flex; flex-direction: column; gap: 6px; }
.hp-imp-progtop { display: flex; justify-content: space-between; gap: 10px;
  font: 600 var(--fs-1)/1.4 var(--f-sans); }
.hp-imp-rail { height: 8px; border-radius: var(--rp); background: var(--surface-3); overflow: hidden; }
.hp-imp-fill { height: 100%; width: 0; border-radius: var(--rp); background: var(--teal);
  transition: width var(--dur-2) var(--ease-out); }
.hp-imp-skel { display: flex; flex-direction: column; gap: 7px; padding: 4px 0; }
.hp-imp-skel i { display: block; height: 11px; border-radius: var(--r1); background: var(--surface-3); }
.hp-imp-skel i:nth-child(2) { width: 82%; }
.hp-imp-skel i:nth-child(3) { width: 64%; }
.hp-imp-msg { font-size: var(--fs-1); line-height: 1.5; color: var(--muted); }
.hp-imp-msg:empty { display: none; }
.hp-imp-msg.ok { color: var(--ok); }
.hp-imp-msg.warn { color: var(--warn); }
.hp-imp-msg.err { color: var(--bad); }
.hp-imp-foot { display: flex; align-items: center; gap: 8px; padding: 12px 16px;
  border-top: 1px solid var(--line); background: var(--surface-2); }
.hp-imp-foot .sp { flex: 1; }
/* `.hp-imp-prog`/`.hp-imp-skel`/`.hp-imp-sheet` khai `display:flex`, mà luật của tệp CSS
   luôn THẮNG luật `[hidden]{display:none}` của trình duyệt bất kể độ đặc hiệu — thiếu
   dòng ép này thì `el.hidden = true` không giấu được gì và cả tám trạng thái của hộp hiện
   chồng lên nhau. Cùng cái bẫy `.md-f[hidden]` bên public/master/index.html đã phải vá. */
.hp-imp [hidden] { display: none !important; }

/* Nút "+ Sự vụ mới" của MỌI màn (danh sách, tổng quan, ngăn kéo, Danh sách yêu cầu, Job card
   ĐMX, Tra cứu, thẻ cuộc gọi) — ẩn khi người dùng không có quyền TẠO sự vụ (`case:c`, mức
   "Chỉ xem"). Dấu do rolebar.js đặt sau lượt `whoami`; máy chủ vẫn là cửa gác thật. */
html[data-case-create='0'] .hp-case-new { display: none !important; }

/* ---- CASE STATUS TAG — one look on every screen ----
   Drawn by `HPUI.caseStatusTag(row)` (hp-ui.js): colour = stage of the Master-data status, style =
   order of that status among the statuses of the same stage in its case type (soft → solid → line).
   Always a tag, tables included, so a status reads the same everywhere. */
.hp-status { display: inline-flex; align-items: center; font-size: var(--fs-1); font-weight: 600;
  line-height: var(--lh-1); padding: 2px 9px; border-radius: 999px; white-space: nowrap;
  background: var(--line); color: var(--muted); }
.hp-status.ok { background: var(--ok-soft); color: var(--ok); }
.hp-status.warn { background: var(--warn-soft); color: var(--warn); }
.hp-status.bad { background: var(--bad-soft); color: var(--bad); }
.hp-status.info { background: var(--info-soft); color: var(--info); }
.hp-status.neu { background: var(--line); color: var(--muted); }
.hp-status.solid { color: var(--surface); }
.hp-status.solid.ok { background: var(--ok); }
.hp-status.solid.warn { background: var(--warn); }
.hp-status.solid.bad { background: var(--bad); }
.hp-status.solid.info { background: var(--info); }
.hp-status.solid.neu { background: var(--muted); }
.hp-status.line { background: transparent; box-shadow: inset 0 0 0 1px currentColor; }
