/* ============================================================================
   DESIGN SYSTEM DÙNG CHUNG — Livestream Analytics
   ============================================================================
   Bảng màu/spacing kiểu dashboard ecommerce hiện đại (card nổi bằng shadow thay
   vì chỉ viền mỏng, số liệu KPI làm nổi bật, accent màu thương hiệu).
   Dùng biến CSS (:root) để đổi màu 1 chỗ áp dụng toàn bộ trang.
   ============================================================================ */
:root {
  --brand: #4f46e5;         /* tím-xanh chủ đạo (indigo) — thay bằng màu brand thật nếu có */
  --brand-dark: #4338ca;
  --brand-soft: #eef2ff;
  --accent: #ee4d2d;        /* cam-đỏ Shopee thật (đổi từ #f97316 chung chung, 2026-07-10) — dùng cho series phụ trong chart/table */
  --hince: #ff6b4a;         /* cam san hô HINCE — dùng cho điểm nhấn ở các trang landing/login (2026-07-10) */
  --shopee: #ee4d2d;        /* cam-đỏ Shopee thật — alias cùng giá trị --accent, đặt tên rõ nghĩa hơn khi dùng cho các khối liên quan Shopee */
  /* 2026-08-01 (design-critique) — cặp soft/dark của --hince, cùng công thức với
     --brand-soft/--brand-dark, để dùng cho callout/badge cần tông ẤM (khen thưởng,
     nổi bật) thay vì lặp lại --brand chàm ở mọi nơi — bản thân đúng giá trị đã
     dùng ở .chip.hince (index.php), gộp về đây làm biến dùng chung. 4.70:1 trên
     nền trắng, đạt AA. */
  --hince-soft: #fff1ec;
  --hince-dark: #c2410c;
  /* ⚠️ 2026-08-01 (audit accessibility) — success/warning/text-faint gốc KHÔNG đạt
     WCAG AA (4.5:1) cho chữ cỡ thường: success #16a34a=3.30:1, warning #d97706=
     3.19:1, text-faint #94a3b8=2.56:1 trên nền trắng — quá mờ để đọc, đặc biệt
     .hint (dùng ở ~mọi form/trang) và ↑/↓ delta trên KPI card. Đổi sang sắc độ
     ĐẬM HƠN cùng tông màu, đều ≥4.5:1 (đo bằng công thức luminance WCAG), KHÔNG
     đổi layout/kích thước — chỉ đổi đúng 3 giá trị hex này, áp dụng lại toàn app. */
  --success: #15803d;       /* 5.02:1 trên nền trắng (trước: #16a34a = 3.30:1, KHÔNG đạt AA) */
  --danger: #dc2626;
  --warning: #a35a04;       /* 5.22:1 trên nền trắng (trước: #d97706 = 3.19:1, KHÔNG đạt AA) */

  /* ⚠️ 2026-08-06 — 2 màu chữ dùng KHI NẰM TRÊN NỀN ĐÃ PHA CÙNG TÔNG.
     Vấn đề tự tạo ra: các khối cảnh báo mới (.range-hint.danger, .mode-warn.danger)
     pha nền bằng CHÍNH màu chữ ở 10% — nền tối đi nên tương phản tụt xuống 4.2:1 và
     4.13:1, dưới chuẩn AA 4.5:1. Đo thử: hạ độ pha xuống 4% mới vừa đạt, nhưng lúc đó
     nền gần như vô hình, mất luôn tác dụng phân biệt.
     Cách đúng là làm CHỮ đậm hơn chứ không làm nền nhạt đi — cùng cách đã xử lý
     --success/--warning ở trên. Chỉ dùng cho chữ trên nền pha, KHÔNG thay thế
     --danger/--warning ở các chỗ khác (trên nền trắng 2 màu gốc đã đạt). */
  --warning-dark: #8f4e03;  /* 5.19:1 trên nền vàng pha 10% · 6.44:1 trên trắng */
  --danger-dark:  #b91c1c;  /* 5.54:1 trên nền đỏ pha 10%  · 6.47:1 trên trắng */
  /* 2026-08-01 (redesign — tinh chỉnh màu/token, giữ nguyên --brand/--shopee) —
     đổi neutrals từ xám trung tính sang xám ÁM TÍM NHẸ, nhất quán với tông chàm
     của sidebar/--brand thay vì "xám mượn" không chủ đích. Đã đo lại contrast
     TRƯỚC khi đổi — không được thấp hơn 3 giá trị vừa sửa accessibility ở trên
     (4.5:1 trên CẢ nền trắng lẫn --bg mới, không chỉ nền trắng). */
  --text: #16213d;          /* 15.9:1 trên trắng — đậm hơn #1e293b cũ, "ink" rõ hơn */
  --text-muted: #5b6485;    /* 5.8:1 trên trắng / 5.3:1 trên --bg mới */
  --text-faint: #666f90;    /* 4.95:1 trên trắng / 4.53:1 trên --bg mới — vẫn đạt AA ở CẢ 2 nền, không lặp lại lỗi cũ */
  --border: #e6e4f2;
  --bg: #f5f4fb;
  --card-bg: #ffffff;
  /* Màu DỮ LIỆU theo sàn (khác --brand/--shopee dùng cho UI chrome) — dùng cho
     series TikTok trong chart/bảng, đi cặp với --shopee cho series Shopee. Đã
     validate bằng scripts/validate_palette.js (dataviz skill): CVD ΔE 18.4
     (deutan)/39.4 (tritan), normal-vision ΔE 33.5 — tách bạch rõ kể cả mù màu. */
  --tiktok: #0891a8;
  /* Cặp *-dark cho chữ TRÊN NỀN PHA (2026-08-08, badge chọn kênh upload) — cùng
     công thức --warning-dark/--danger-dark: --tiktok/--shopee gốc không đủ đậm
     để làm CHỮ (3.7:1 / 3.7:1 trên trắng, KHÔNG đạt AA 4.5:1), giữ nguyên 2 màu
     gốc cho khối DỮ LIỆU (fill biểu đồ...), chỉ thêm bản đậm hơn riêng cho chữ.
     067083: 5.75:1 trên trắng · a8371e: 6.50:1 trên trắng — đều đạt AA. */
  --tiktok-dark: #067083;
  --shopee-dark: #a8371e;
  /* Màu DỮ LIỆU theo BRAND (khác --tiktok/--shopee vốn theo SÀN) — 2026-08-16,
     theo yêu cầu user "quy ước tiktok màu gì, brand màu gì thì nên đồng nhất giữa
     các biểu đồ, đừng để mỗi biểu đồ 1 màu khác nhau". --bbia là màu ĐẶT TẠM (chưa
     có màu brand thật, cùng tinh thần ghi chú ở --brand) — đổi lại đây nếu có màu
     brand chính thức, mọi biểu đồ dùng token này sẽ tự đồng bộ theo, không cần sửa
     JS. --hince TÁI DÙNG token cam san hô đã có sẵn (trước chỉ dùng cho landing/
     login) làm luôn màu series HINCE trong chart — 1 màu, 2 vai trò nhất quán.
     Cặp *-dark dùng cho ĐƯỜNG (line) khi cần phân biệt với CỘT (bar) cùng brand
     trên cùng biểu đồ mà vẫn giữ chung tông màu (đậm hơn nền, không phải màu khác). */
  --bbia: #2563eb;
  --bbia-dark: #1d4ed8;
  /* 2026-08-30 (host_stats.php, "nền card BBIA/HINCE có thể dùng tone màu nhạt
     của brand thay vì giống nhau") — --bbia CHƯA có bản "-soft" như --hince đã
     có (dòng ~20) khi cần nền pha nhạt cùng tông. Cùng công thức: gần trắng,
     ngả nhẹ sang màu brand, chỉ dùng làm NỀN (chữ vẫn --text đậm, không đặt
     chữ thường lên đây bằng --bbia trực tiếp — không đạt AA). */
  --bbia-soft: #eaf2ff;
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, .06), 0 1px 3px rgba(15, 23, 42, .08);
  --shadow-md: 0 4px 12px rgba(15, 23, 42, .08);
  --radius: 12px;
  --radius-sm: 8px;
  --shell-dark-1: #1e1b4b;  /* nền tối sidebar app-shell (2026-07-11) — CÙNG tông với hero login.php, để nhận diện thương hiệu xuyên suốt từ lúc đăng nhập */
  --shell-dark-2: #322a6b;
}

* { box-sizing: border-box; }

/* ⚠️ 2026-08-06 — BẮT BUỘC, đừng xoá.
   Thuộc tính hidden của HTML chỉ hoạt động nhờ quy tắc mặc định [hidden]{display:none}
   của trình duyệt — mà BẤT KỲ quy tắc display nào mình tự viết cũng có độ ưu tiên cao
   hơn và đè mất. Hậu quả rất khó đoán ra: JS đặt hidden=true, không có lỗi nào báo,
   phần tử vẫn nằm đó với nội dung cũ.
   Đã dính ĐÚNG lỗi này 2 lần trong 1 ngày: .range-hint (display:flex — dòng nhắc
   khoảng ngày không tắt được khi giảm ngày) rồi .btn (display:inline-block — nút tải
   bản offline hiện ngay cả khi chưa có dữ liệu). Vá từng class thì lần sau vẫn sót,
   nên chặn 1 lần ở đây cho MỌI phần tử.
   !important là cần thiết: nó phải thắng mọi quy tắc display khác trong file này. */
[hidden] { display: none !important; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
  margin: 0;
  padding: 0;
  line-height: 1.5;
}

.page {
  /* Fluid, co theo màn hình thực tế (đa số giờ đã 1920px) nhưng chặn trần để
     không bị kéo quá rộng trên màn hình siêu to (2560px, 3440px...). */
  width: 70%;
  max-width: 1800px;
  margin: 0 auto;
  padding: 24px 20px 60px;
}
@media (max-width: 900px) { .page { width: 100%; } }

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ── Topbar ── */
.topbar {
  display: flex; justify-content: space-between; align-items: center;
  background: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow-sm);
  padding: 14px 20px; margin-bottom: 20px; font-size: 13px; color: var(--text-muted);
}
.topbar b { color: var(--text); }
.topbar .role-badge {
  background: var(--brand-soft); color: var(--brand-dark); font-weight: 600;
  padding: 2px 10px; border-radius: 999px; font-size: 12px; margin-left: 6px;
}

h2.page-title { margin: 0 0 18px; font-size: 22px; font-weight: 700; display: flex; align-items: center; gap: 8px; }
/* 2026-08-21 — kích thước cho icon SVG (app_icon(), lib/icons.php) khi trang dùng
   <span class="ic">...</span> thay vì emoji trực tiếp trong .page-title. */
h2.page-title .ic { width: 22px; height: 22px; color: var(--brand); flex-shrink: 0; display: inline-flex; }
h2.page-title .ic svg { width: 100%; height: 100%; }
h3.section-title { margin: 0 0 14px; font-size: 15px; font-weight: 700; color: var(--text); }

/* Nút chuyển VI/EN (2026-07-19) — xem lib/i18n.php::t_switcher_html() */
.lang-switch {
  display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px;
}
.lang-switch a { color: var(--text-faint); text-decoration: none; padding: 2px 4px; }
.lang-switch a.lang-switch-active { color: var(--brand); }
.lang-switch a:hover { color: var(--brand-dark); }
.lang-switch-sep { color: var(--border); }

/* Hàng tiêu đề bảng + nút Copy (2026-07-11, theo yêu cầu user "xuất dữ liệu bảng
   dễ copy vào báo cáo") — bọc h3 + nút vào 1 hàng flex, giữ NGUYÊN margin-bottom
   14px cũ của h3/.section-title (đẩy margin ra ngoài div thay vì h3 để không lệch
   khoảng cách với bảng bên dưới so với các h3 không có nút). */
.table-heading-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
.table-heading-row h3, .table-heading-row h3.section-title { margin: 0; }
.copy-table-btn { flex-shrink: 0; }
/* 2026-09-01 (session_metrics.php — 2 vòng chỉnh liên tiếp cho đúng ý, xem
   [[project-quick-filter-chips-and-period-options]] để biết đủ lịch sử):
   "N session" + cụm chip + nút Copy đứng CHUNG 1 hàng .table-heading-row (đã
   gộp từ vòng trước) nhưng KHÔNG cần class riêng nào áp justify-content nữa
   — hành vi MẶC ĐỊNH space-between của .table-heading-row (dòng ~159) đã tự
   đẩy "N session" sát trái + nút Copy sát phải + cụm chip (phần tử giữa) tự
   nằm khoảng giữa, ĐÚNG ý user "phải đồng bộ với các chỗ khác" — vòng trước
   lỡ thêm .sm-toolbar-row ép center làm nút Copy trôi vào giữa, ĐÃ BỎ. */
.sm-chip-cluster { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Dấu "|" phân cách 2 cụm chip Brand/Platform (user: "giữa 2 cụm chip cần có
   1 khoảng cách nhỏ và | để chia ra cho dễ nhìn") — màu nhạt, không phải chữ
   thật (aria-hidden, chỉ trang trí — 2 nhóm chip đã có aria-label riêng nên
   không mất thông tin gì với screen reader khi ẩn dấu này). */
.sm-chip-divider { color: var(--text-faint); font-size: 16px; line-height: 1; }
/* 2026-09-01 (user: "tôi muốn cố định title khi cuộn được không") — cố định
   HÀNG TIÊU ĐỀ của #smTable khi cuộn (bảng có thể dài hàng trăm dòng, tiêu
   đề cột cuộn mất thì không biết đang xem cột nào). CHỦ Ý CHỈ áp dụng
   #smTable/.sm-table-scroll (riêng session_metrics.php), KHÔNG sửa
   `.report-table-wrap`/`table.report th` dùng chung — đổi thẳng lớp chung sẽ
   kéo theo MỌI bảng report khác trong app (~30 bảng) cùng đổi hành vi cuộn
   dù không ai yêu cầu, đúng bài học đã rút ra từ vụ chip Brand/Platform
   trước đó (đổi lan ra ngoài phạm vi hỏi thì phải sửa lại).
   ⚠️ Bản ĐẦU chỉ thêm `position:sticky` cho `<th>`, giữ nguyên
   `.report-table-wrap{overflow-x:auto}` — tự kiểm chứng bằng harness THẤY
   TIÊU ĐỀ TRÔI MẤT khi cuộn, KHÔNG dính. Nguyên nhân: theo spec CSS, hễ
   overflow-x khác 'visible' thì overflow-y bị trình duyệt TỰ suy ra 'auto'
   (không thể ép lại 'visible' dù không viết gì) — .report-table-wrap vô
   tình trở thành 1 vùng cuộn riêng NGOÀI Ý MUỐN dù chỉ khai overflow-x, và
   sticky bên trong nó bám theo cuộn NỘI BỘ của chính div đó (mà div lại
   không tự cuộn dọc vì không giới hạn chiều cao) chứ KHÔNG bám theo cuộn
   trang -> tiêu đề không cố định được thật.
   **Giải pháp ĐÚNG**: cho hẳn khung bảng 1 chiều cao giới hạn
   (`max-height`) + `overflow: auto` (cuộn CẢ 2 CHIỀU TRONG khung đó, không
   cuộn theo trang nữa) — sticky lúc này có scroll-container THẬT để bám
   vào, đã re-verify bằng harness: tiêu đề đứng yên khi cuộn nội dung bên
   trong khung. `65vh` (không phải px cố định) để tự co theo chiều cao màn
   hình người dùng. Viền `--border` giúp người dùng nhận ra đây là 1 khung
   cuộn riêng (khác các bảng khác trong app không có viền/không cuộn dọc). */
.sm-table-scroll { max-height: 65vh; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); }
#smTable th { position: sticky; top: 0; z-index: 5; }
/* 2026-09-01 (user: "cho tôi hàng total, avg (tuỳ chỉ số) cố định cuối
   bảng") — hàng <tfoot> (id smTotalRow, xem ajax_session_metrics.php) dính
   ĐÁY khung .sm-table-scroll, ĐỐI XỨNG với tiêu đề dính ĐỈNH ở trên — CÙNG cơ
   chế sticky (khung đã có max-height+overflow:auto thật từ trước nên
   `bottom: 0` hoạt động ngay, không lặp lại bẫy overflow-x đã gặp ở tiêu đề).
   Nền `--brand-soft`/viền trên `--brand` ĐÃ có sẵn từ `.report-total-row`
   dùng chung (đặc, không xuyên thấu — không cần khai riêng). */
#smTable tfoot td { position: sticky; bottom: 0; z-index: 5; }
/* 2026-09-01 (user: "bỏ cột id session, rờ vào tiêu đề thì hiện id session,
   có thể copy được") — bỏ hẳn cột riêng, thay bằng tooltip hiện khi hover
   NGAY TRÊN ô Tiêu đề. Cố ý KHÔNG dùng attribute `title` (tooltip gốc trình
   duyệt) vì text trong đó KHÔNG chọn/copy được — dùng 1 <span> THẬT nằm
   trong DOM (ẩn bằng opacity, không phải display:none — display:none cũng
   không chọn được), hiện ra khi hover/focus thì user bôi đen copy bình
   thường như mọi text khác trên trang. */
.sm-title-cell { position: relative; cursor: help; }
.sm-title-cell .sm-session-tooltip {
  position: absolute; left: 0; top: 100%; margin-top: 4px; z-index: 30;
  background: var(--text); color: #fff; border-radius: var(--radius-sm);
  padding: 5px 9px; font-size: 12px; white-space: nowrap; line-height: 1.4;
  box-shadow: var(--shadow-md);
  opacity: 0; pointer-events: none; transition: opacity .12s ease;
}
.sm-title-cell:hover .sm-session-tooltip,
.sm-title-cell:focus-within .sm-session-tooltip { opacity: 1; pointer-events: auto; }
.sm-title-cell .sm-session-tooltip::before {
  content: ""; position: absolute; bottom: 100%; left: 12px;
  border: 5px solid transparent; border-bottom-color: var(--text);
}

/* ── Cards (chung cho card, chart-box, form) ── */
.card {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); padding: 18px;
}

/* ── Filter bar ── */
.filters {
  display: flex; gap: 14px; flex-wrap: wrap; align-items: end;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); padding: 18px; margin-bottom: 22px;
}
.filters label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 5px; font-weight: 600; }
.filters select, .filters input[type=date] {
  height: 38px; box-sizing: border-box;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: #fff; font-size: 13px; color: var(--text);
}
.filters select:focus, .filters input:focus { outline: 2px solid var(--brand-soft); border-color: var(--brand); }
/* 2026-08-31 (dấu phân chia trong <select name="period">, giữa "Tháng này" và
   "Tuần trước") — 3 vòng chỉnh liên tiếp, xem [[project-quick-filter-chips-
   and-period-options]] để biết đủ lịch sử. Vòng cuối (user: "đường quá dài,
   và khoảng trống giữa phần trên và phần dưới vẫn còn xa nhau") — bản trước
   (16 ký tự "┄") vừa DÀI vừa vẫn chừa nhiều khoảng trống trên/dưới vì chỉ đổi
   font-size, KHÔNG đổi padding/line-height — option vẫn giữ nguyên chiều cao
   mặc định của trình duyệt (padding dọc mặc định không nhỏ). Sửa 2 việc CÙNG
   LÚC: (1) rút ngắn ký tự (8, không phải 16 — HTML/PHP tự rút gọn ở nơi gọi,
   không sửa gì thêm ở đây); (2) ép RIÊNG `padding`/`line-height` của dòng này
   nhỏ hết mức trình duyệt cho phép — khác mọi `<option>` khác (vẫn giữ
   `padding: 8px 10px` mặc định của `.filters select`), CHỈ dòng phân chia
   ĐƯỢC PHÉP nhỏ hơn vì nó không phải lựa chọn thật, không cần đủ diện tích
   chạm (touch target) như các option khác. */
.filters select option:disabled {
  color: var(--text-faint);
  font-size: 10px;
  padding: 0 10px;
  line-height: 1.2;
}
/* 2026-08-25 (user: "nút áp dụng, bỏ lọc to hơn đúng không?") — .btn mặc định
   (padding 9px 20px, font 14px) cao hơn hẳn select/input trong .filters (38px
   cố định ở trên) vì 2 nơi tính chiều cao theo 2 công thức khác nhau (padding+
   line-height so với height cố định). Ép cùng height:38px + box-sizing:border-box
   thay vì chỉnh tay padding/font — khớp CHÍNH XÁC, không phải áng chừng. */
.filters .btn { height: 38px; box-sizing: border-box; display: inline-flex; align-items: center; }

/* Nút chip "phân loại trực tiếp" (2026-08-31, session_metrics.php, user: "dropdown
   để chọn khi xem, xem xong sẽ có các nút chip để sort phân loại trực tiếp") —
   KHÁC hẳn <select> Brand/Platform ở đầu trang (đó là bộ lọc THẬT, quyết định
   dữ liệu tải khi bấm "Xem") — nhóm chip này nằm TRONG kết quả trả về
   (ajax_session_metrics.php, ngay trên bảng), chỉ xuất hiện SAU khi đã có dữ
   liệu, lọc/ẩn-hiện dòng NGAY trên bảng đang xem, không gọi mạng lại. */
.quick-filter { display: flex; gap: 6px; flex-wrap: wrap; }
.qf-btn {
  height: 32px; box-sizing: border-box; padding: 0 12px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: #fff; color: var(--text-muted);
  font-size: 12px; font-weight: 600; cursor: pointer; transition: background .15s, border-color .15s, color .15s;
}
.qf-btn:hover { border-color: var(--brand); color: var(--text); }
.qf-btn:focus-visible { outline: 2px solid var(--brand-soft); outline-offset: 1px; border-color: var(--brand); }
.qf-btn.active { background: var(--brand); border-color: var(--brand); color: #fff; }
/* Đổi màu active theo ĐÚNG brand/platform đang chọn — khớp màu badge/chart đã
   dùng cho cùng ý nghĩa đó ở nơi khác (.platform-badge, series chart theo
   brand/sàn). Dùng bản "-dark" (không phải màu gốc) làm NỀN ĐẶC — chữ trắng
   trên màu gốc --tiktok/--shopee/--hince không đạt AA 4.5:1 (xem ghi chú ở
   :root, dòng ~58-75), bản "-dark" đã được validate đủ tương phản cho chữ. */
.qf-btn.qf-hince.active { background: var(--hince-dark); border-color: var(--hince-dark); }
.qf-btn.qf-bbia.active { background: var(--bbia-dark); border-color: var(--bbia-dark); }
.qf-btn.qf-tiktok.active { background: var(--tiktok-dark); border-color: var(--tiktok-dark); }
.qf-btn.qf-shopee.active { background: var(--shopee-dark); border-color: var(--shopee-dark); }

/* Ô "Chọn kênh" upload (2026-08-08, user: "dễ đọc nhầm" TikTok/Shopee) — nền
   từng dòng trong danh sách xổ xuống tô theo platform (chỉ có tác dụng lúc
   danh sách ĐANG MỞ, độ hỗ trợ tuỳ trình duyệt) + badge màu bên cạnh LUÔN
   HIỆN sau khi chọn — badge là chỗ chống nhầm chính vì không phụ thuộc trạng
   thái mở/đóng của select. */
/* justify-content:center — #channelSelect rộng 100% nên luôn chiếm trọn 1 "dòng"
   flex-wrap riêng, dòng badge (dòng sau) do đó tự căn giữa độc lập, không ảnh hưởng
   vị trí select (2026-08-10, user: "chữ Shopee ... căn giữa cho dễ tập trung"). */
/* 2026-08-15 — mặc định căn TRÁI để khớp các field khác của form upload file
   (product/trend/detail xếp label-trên-input theo khối, căn giữa sẽ lệch hàng).
   Trang upload ảnh giữ căn giữa qua .center — xem render_channel_select(). */
.channel-select-row { display: flex; align-items: center; justify-content: flex-start; gap: 10px; flex-wrap: wrap; }
.channel-select-row.center { justify-content: center; }
.channel-select-row select { flex: 1 1 auto; min-width: 0; }
.channel-select-row.center select { flex: 0 1 auto; }
/* ⚠️ 2026-08-15 — admin.php đặt ô chọn kênh này BÊN TRONG .form-grid (khối "Gửi
   Zalo theo ca có sẵn"), nơi có rule .form-grid select { width:100% } (dòng ~654).
   Rule đó và rule ngay trên CÙNG độ đặc hiệu (1 lớp + 1 thẻ) — ai đứng SAU trong
   FILE thắng bất kể HTML lồng nhau thế nào, và .form-grid select đứng sau nên nó
   thắng: select bị ép full-width, hết chỗ cho badge trên cùng hàng, badge rớt xuống
   dòng dưới và đẩy lệch với ô "Ngày" cạnh nó (form-grid ở đó dùng align-items:end).
   Đổi width:auto NGAY TRONG rule gốc ở trên không đủ — vẫn thua vì cùng specificity
   mà đứng trước. Phải LỒNG THÊM .form-grid để tăng độ đặc hiệu lên 2 lớp, thắng bất
   kể thứ tự file. */
.form-grid .channel-select-row select { width: auto; }
/* Bỏ #channelSelect, đổi sang class chung — 4 trang upload đều dùng ô này, mà id
   thì mỗi trang chỉ được có 1. */
.channel-select-row option[data-platform="tiktok"] { background: #e8f4f6; color: var(--tiktok-dark); }
.channel-select-row option[data-platform="shopee"] { background: #fceae6; color: var(--shopee-dark); }
.platform-badge {
  display: inline-flex; align-items: center; font-size: 14px; font-weight: 700;
  padding: 6px 13px; border-radius: 999px; white-space: nowrap;
}
.platform-badge.tiktok { background: rgba(8, 145, 168, .12); color: var(--tiktok-dark); }
.platform-badge.shopee { background: rgba(238, 77, 45, .12); color: var(--shopee-dark); }

.btn {
  display: inline-block; padding: 9px 20px; background: var(--brand); color: #fff; border: none;
  border-radius: var(--radius-sm); cursor: pointer; font-size: 14px; font-weight: 600;
  box-shadow: var(--shadow-sm); transition: background .15s; line-height: 1.4;
  text-decoration: none; vertical-align: middle; box-sizing: border-box;
}
.btn:hover { background: var(--brand-dark); }
.btn:disabled { opacity: .55; cursor: not-allowed; pointer-events: none; }

/* Vòng xoay "đang chờ" — DÙNG CHUNG cho nút, băng đang tải, và màn hình chờ
   (2026-08-06). Ban đầu chỉ làm cho nút (tên cũ .btn-spin) rồi mới thấy các chỗ khác
   vẫn còn ký tự ⏳ đứng yên — user hỏi "đồng hồ cát này nó không xoay hả".
   Dùng viền chứ không dùng ký tự/emoji: emoji mỗi hệ điều hành một kiểu và không
   xoay mượt, còn vòng viền thì luôn hiện đúng và không đổi bề rộng nút. */
.spinner {
  display: inline-block; width: 13px; height: 13px; margin-right: 8px;
  /* margin-right dung cho nut + man hinh cho; .dash-loading la flex nen dat gap:0 */
  vertical-align: -2px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: spinner .7s linear infinite;
}
@keyframes spinner { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  /* Tắt hiệu ứng quay nhưng GIỮ vòng tròn — vẫn thấy nút đang bận. */
  .spinner { animation: none; opacity: .6; }
}
.btn:disabled:hover { background: var(--brand); }
/* 2026-08-25 (theo yêu cầu user "bấm chuyển trang phải đợi lâu mà không có
   thông báo gì, dễ tưởng bị treo") — overlay CHE TOÀN MÀN HÌNH khi bấm 1 link
   điều hướng sang trang khác, DÙNG CHUNG toàn app (gắn từ render_app_shell_script()
   ở lib/sidebar.php, không phải sửa từng trang). App này KHÔNG phải SPA — mỗi
   link là 1 lượt điều hướng THẬT, có trang gọi Apps Script trước khi trả HTML
   nên có thể mất vài giây (cold start, xem CLAUDE.md) mà trình duyệt chỉ có icon
   xoay nhỏ ở tab, dễ bị bỏ qua trên điện thoại. */
/* ⚠️ 2026-09-11 (vòng 2 — user: "Chrome, Samsung Internet đều có overlay, CHỈ Safari
   là không") — xác nhận đây là hành vi riêng của WebKit: Safari đóng băng/chụp ảnh
   trang cũ NGAY khi bắt đầu điều hướng, nên MỌI thứ chạy sau cú chạm đều không lên
   được màn hình — kể cả `setTimeout` (bản đầu) LẪN animation trên compositor (bản
   sửa lần 1). Kết luận: thứ gì cần thấy thì phải được vẽ NGAY trong khung hình của
   chính cú chạm.
   Giải pháp 2 TẦNG:
     • .bar   — thanh mảnh 3px trên cùng, `opacity:1` NGAY, không delay/animation
                nào cả -> Safari chắc chắn vẽ kịp. Loé 1 nhịp ở trang nhanh cũng
                gần như không ai để ý (kiểu thanh tiến trình của YouTube/GitHub).
     • .scrim — lớp phủ tối + spinner + chữ, vẫn trễ 200ms như cũ để trang nhanh
                không bị chớp cả màn hình. Trên Safari tầng này có thể không kịp
                hiện, nhưng KHÔNG SAO — tầng .bar đã làm xong việc báo hiệu. */
.app-nav-loading { display: none; }
.app-nav-loading.show { display: block; }

/* Thanh mảnh trên cùng — nằm TRÊN cả lớp phủ (z-index cao hơn) để vẫn thấy rõ. */
.app-nav-loading .bar {
  position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 10000;
  background: var(--brand);
  box-shadow: 0 0 8px rgba(79, 70, 229, .7);
}

/* ⚠️ 2026-09-11 (vòng 4, user chọn "hiện luôn lớp phủ đầy đủ trên Safari") —
   KHÔNG còn animation/opacity/delay nào ở đây. Lý do: sau khi đổi sang cơ chế
   "chặn điều hướng -> vẽ -> rồi mới đi" (xem lib/sidebar.php), ta chỉ đợi ĐÚNG
   2 khung hình (~32ms) trước khi rời trang. Mọi hiệu ứng cần lâu hơn thế đều
   KHÔNG kịp vẽ trên Safari — nó đóng băng trang ngay sau đó. Muốn chắc chắn
   thấy thì phải vẽ TỨC THÌ, không qua bất kỳ transition/animation nào.
   Đánh đổi đã thống nhất với user: trang tải nhanh sẽ loé lớp phủ một nhịp. */
.app-nav-loading .scrim {
  position: fixed; inset: 0; z-index: 9999;
  /* 2026-09-11 (user: "vẫn còn trong suốt và xuyên thấy nhiều quá", rồi chỉnh lại
     "blur 5px, opacity 70") — .6 -> .7, blur 8px -> 5px. Cùng kỹ thuật
     .lightbox-overlay đã dùng sẵn trong file này (dòng ~863).
     ⚠️ Nền CỐ Ý đặt đủ đục để nếu backdrop-filter không kịp vẽ/không được hỗ trợ
     thì lớp phủ VẪN che kín — blur chỉ là phần tô điểm, không phải thứ gánh việc
     che. Quan trọng vì ta chỉ đợi 2 khung hình rồi rời trang (xem lib/sidebar.php). */
  background: rgba(22, 33, 61, .7);
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
  display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 14px;
}
.app-nav-loading .spinner { width: 30px; height: 30px; border-width: 3px; color: #fff; margin-right: 0; }
.app-nav-loading .msg { color: #fff; font-size: 14px; font-weight: 600; }
/* ── Chặn trang cần màn hình lớn khi mở trên điện thoại (2026-08-05) ──────────
   Khối này do render_app_topbar($user, false) sinh ra và đứng TRƯỚC .app-topbar /
   .app-shell, nên dùng bộ chọn anh-em (~) để giấu toàn bộ phần còn lại của trang.
   Trên màn hình rộng khối này ẩn, không ảnh hưởng gì.                          */
.desktop-only-block { display: none; }
@media (max-width: 860px) {
  .desktop-only-block {
    display: flex; flex-direction: column; align-items: center; text-align: center;
    min-height: 100vh; justify-content: center; padding: 40px 24px; gap: 10px;
  }
  .desktop-only-block ~ .app-topbar,
  .desktop-only-block ~ .app-overlay,
  .desktop-only-block ~ .app-shell { display: none; }

  .desktop-only-block .ic {
    width: 56px; height: 56px; border-radius: 15px; background: var(--brand-soft);
    display: flex; align-items: center; justify-content: center; color: var(--brand-dark);
  }
  .desktop-only-block .ic svg { width: 28px; height: 28px; }
  .desktop-only-block h2 { font-size: 17px; margin: 6px 0 0; color: var(--text); font-weight: 800; }
  .desktop-only-block p { font-size: 13.5px; line-height: 1.6; color: var(--text-muted); margin: 0; max-width: 300px; }
  .desktop-only-block .btn { margin-top: 12px; }
  .desktop-only-block p.sub { font-size: 12px; color: var(--text-faint); }
}

/* ⚠️ 2026-08-06 (user báo: chọn 1 brand thì các bảng dính vào nhau, không có khoảng cách)
   — .card KHÔNG có margin nào; từ trước tới nay khoảng cách giữa các thẻ trên dashboard
   do TỪNG khối tự thêm margin-top. Thẻ "So sánh kỳ trước ↔ kỳ này" thì quên hẳn, nên
   nó luôn dính vào thẻ phía trên. Lâu nay không ai thấy vì .ai-panel (margin-bottom
   24px) thường chen vào giữa và che mất; bỏ phân tích AI, hoặc lọc 1 brand làm bảng
   so sánh brand biến mất, là lộ ra ngay.
   Sửa ở GỐC: hai thẻ nằm liền nhau thì LUÔN có khoảng cách, khỏi phụ thuộc việc người
   viết thẻ mới có nhớ thêm margin hay không.

   ⚠️ 2026-08-15 — BỎ giới hạn #dashboardResults (user báo 2 thẻ dính nhau ở tab
   "Gửi Zalo" của admin). Giới hạn cũ đặt ra vì SỢ đụng bố cục trang khác, nhưng đó
   chính là chỗ hỏng: mọi trang NGOÀI dashboard đều không có khoảng cách nào giữa 2
   thẻ .card, vì .card tự nó KHÔNG có margin. Đã kiểm tra toàn app trước khi mở rộng:
   .card không bao giờ là con của .charts hay lưới nào khác (.form-grid/.kpis nằm BÊN
   TRONG thẻ, không chứa thẻ), nên không có chỗ nào bị cộng dồn margin với gap. */
.card + .card { margin-top: 16px; }

/* ── Trạng thái "đang tải" GIỮ NGUYÊN nội dung cũ (2026-08-06) ────────────────
   Trước đây mọi lần tải đều xoá sạch bảng biểu để thay bằng một dòng chờ. Rất tệ
   với nút "Phân tích AI": chờ 30-60 giây chỉ để thêm mấy đoạn văn, mà toàn bộ số
   liệu đang đọc bị cất đi. Nay chỉ làm mờ và khoá thao tác, số cũ vẫn đọc được. */
#dashboardResults.is-loading {
  opacity: .45;
  pointer-events: none;    /* tránh bấm nhầm vào bảng cũ tưởng là số mới */
  transition: opacity .15s;
}
.dash-loading {
  position: sticky; top: 10px; z-index: 20;   /* dính lại khi cuộn — luôn biết là đang chạy */
  display: flex; align-items: center;
  margin-bottom: 14px; padding: 11px 15px;
  border-radius: var(--radius-sm);
  background: var(--brand-soft); border: 1px solid var(--brand);
  color: var(--brand-dark); font-size: 13px; font-weight: 700;
  box-shadow: var(--shadow-md);
}
@media (prefers-reduced-motion: reduce) {
  /* Tôn trọng người tắt hiệu ứng chuyển động — băng vẫn hiện, chỉ không quay. */
}

/* Thẻ so sánh brand có thể đứng ngay sau một khối KHÔNG phải .card (dòng gợi ý
   "Chọn Cả 2 ở bộ lọc Kênh") — lúc đó quy tắc anh-em ở trên không khớp, nên vẫn cần
   margin riêng. */
.brand-compare { margin-top: 16px; }

/* Bảng bên trái + biểu đồ tròn bên phải (2026-08-06) — bản DÙNG CHUNG, tên không
   gắn với nội dung cụ thể nào. Đặt riêng thay vì tái dùng .platform-compare-* vì
   tên đó đã gắn với bảng 2 sàn; dùng chéo sẽ khó lần khi cần sửa 1 bên.
   Bảng co giãn, biểu đồ giữ kích thước cố định — biểu đồ tròn nhỏ quá thì không
   đọc được lát nào ra lát nào. */
.table-donut-body { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; margin-top: 4px; }
.table-donut-main { flex: 1 1 520px; min-width: 320px; margin: 0; }
.table-donut-side { flex: 0 0 190px; width: 190px; height: 190px; }
@media (max-width: 900px) {
  /* Màn hình hẹp: xếp dọc, biểu đồ xuống dưới bảng thay vì bị bóp lại */
  .table-donut-body { flex-direction: column; align-items: stretch; }
  /* ⚠️ min-width/max-width BẮT BUỘC — bẫy flexbox kinh điển: ô flex mặc định KHÔNG
     co nhỏ hơn nội dung bên trong. Bảng 11 cột rộng ~850px sẽ ĐẨY khung ra rộng theo
     nó (đo được: khung 851px trong khi cha chỉ 694px), làm overflow-x:auto của
     .report-table-wrap thành vô dụng và CẢ TRANG bị cuộn ngang. Ép co lại thì bảng
     mới cuộn BÊN TRONG khung đúng như thiết kế. */
  .table-donut-main { flex: 1 1 auto; min-width: 0; max-width: 100%; }
  .table-donut-side { align-self: center; width: 170px; height: 170px; }
}

/* Dòng tổng của MỘT NHÓM trong bảng (VD tổng mỗi brand) — 2026-08-06.
   Nhạt hơn .report-total-row (dòng TỔNG cuối bảng) để phân biệt được hai cấp:
   nhóm và toàn bảng. Cùng nền brand-soft nhưng không viền đậm. */
.report tr.report-subtotal-row td {
  background: var(--bg); font-weight: 700; color: var(--text);
  border-top: 1px solid var(--border);
}

/* ── Xem lại nội dung tin Zalo vừa gửi, có nút copy (2026-08-06) ────────────── */
.zalo-preview {
  margin: 16px 0 0; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--card-bg); overflow: hidden;
}
.zalo-preview-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 12px; background: var(--bg); border-bottom: 1px solid var(--border);
  font-size: 13px; font-weight: 700; color: var(--text);
}
.zalo-preview pre {
  margin: 0; padding: 13px 15px; font-size: 13px; line-height: 1.65; color: var(--text);
  /* Tin Zalo có emoji + dòng kẻ ━━━: cần font giữ nguyên bề rộng ký tự thì dòng kẻ
     mới thẳng, nhưng KHÔNG dùng monospace thuần vì emoji sẽ vỡ khung. */
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  white-space: pre-wrap;    /* giữ xuống dòng, vẫn tự ngắt khi hẹp */
  word-break: break-word;
  max-height: 340px; overflow-y: auto;
}
/* Nhánh "chưa gửi được": nội dung ẩn sau 1 nút bấm mới mở ra copy (2026-08-10) */
.zalo-preview-toggle { padding: 0 15px 13px; }
.zalo-preview-toggle summary {
  cursor: pointer; list-style: none; padding: 10px 0; font-size: 13px; font-weight: 700;
  color: var(--brand); user-select: none;
}
.zalo-preview-toggle summary::-webkit-details-marker { display: none; }
.zalo-preview-toggle[open] summary { padding-bottom: 4px; }
.zalo-preview-toggle pre { padding: 0; max-height: 300px; }

.customrange { display: none; gap: 14px; flex-wrap: wrap; }
.customrange.show { display: flex; }

/* ── Khối "So sánh với" trong bộ lọc dashboard.php (2026-08-23) ──────────
   User báo thanh bộ lọc "không logic và hơi rối mắt" khi thêm kỳ so sánh
   thứ 2 (Tháng trước) — 2 cặp checkbox+ngày trộn thẳng vào hàng bộ lọc
   chính, không có gì phân biệt ngày nào thuộc "Kỳ trước" hay "Tháng trước".
   Khối này CHỈ dùng ở dashboard.php (data_check/host_manage/host_stats chỉ
   có 1 .customrange đơn, không có nhóm so sánh) — không đụng .filters/
   .customrange gốc để tránh ảnh hưởng 3 trang kia. */
.compare-panel {
  flex-basis: 100%; box-sizing: border-box;
  display: flex; flex-direction: column; gap: 8px;
  margin-top: 2px; padding-top: 12px; border-top: 1px dashed var(--border);
}
/* 2026-08-23 (theo yêu cầu user "chung 1 hàng") — nhãn khối + dòng giải
   thích mặc định giờ nằm CHUNG 1 hàng (baseline), không còn 2 dòng riêng. */
.compare-panel-header { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.compare-panel-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint); white-space: nowrap;
}
.compare-panel-hint { font-size: 12px; color: var(--text-muted); }
.compare-rows { display: flex; flex-wrap: wrap; gap: 10px; }
.compare-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 9px 12px; border-radius: var(--radius-sm);
  background: var(--bg); border: 1px solid var(--border);
}
/* 2026-08-23 (theo yêu cầu user "(tick) Kỳ 1..." thay vì "Kỳ 1 (tick)...") —
   checkbox đứng TRƯỚC, "Kỳ 1"/"Kỳ 2" là nhãn của chính checkbox đó (không
   còn span tiêu đề riêng + chữ "Tuỳ chỉnh ngày" thừa bên cạnh).
   ⚠️ 2026-08-23 (theo yêu cầu user "lệch lệch trên dưới, không giữa cùng
   nhau") — PHẢI viết ".compare-panel .compare-toggle" (2 lớp), KHÔNG được
   viết trần ".compare-toggle" (1 lớp): .compare-toggle LÀ MỘT THẺ <label>,
   mà ".filters label" (1 lớp + 1 thẻ, xem dòng ~169) có độ đặc hiệu CAO HƠN
   1 lớp trần — nó âm thầm đè display:flex thành display:block + cộng thêm
   margin-bottom:5px, làm checkbox/"Kỳ 1" lệch khỏi tâm hàng so với khối ngày
   bên cạnh. Đây CHÍNH LÀ nguyên nhân lỗi lệch dòng đã gặp — xác nhận qua
   getComputedStyle() trong preview harness (toggleDisplay trả về "block"
   chứ không phải "flex" trước khi sửa). */
.compare-panel .compare-toggle {
  display: flex; align-items: center; gap: 7px; white-space: nowrap;
  font-size: 13px; font-weight: 700; color: var(--text); cursor: pointer;
  margin: 0;
}
.compare-toggle input[type=checkbox] { width: auto; margin: 0; }
.compare-dates {
  display: none; gap: 14px; flex-wrap: wrap; align-items: center;
  padding-left: 12px; border-left: 1px solid var(--border);
}
.compare-dates.show { display: flex; }
/* 2026-08-23 (theo yêu cầu user "rút thành 1 hàng, VD Từ: ... Đến: ...") —
   nhãn đứng NGANG hàng với ô ngày thay vì nằm TRÊN nó, để mỗi kỳ chỉ chiếm
   1 dòng thay vì 2. */
.compare-date-field { display: flex; align-items: center; gap: 6px; }
.compare-date-field label { font-size: 12px; color: var(--text-muted); font-weight: 600; white-space: nowrap; margin: 0; }
@media (max-width: 640px) {
  .compare-dates { padding-left: 0; border-left: none; width: 100%; }
}

/* Nhắc thời gian chờ khi khoảng ngày rộng (2026-08-05, dời vị trí 2026-08-06).
   Là ô CON TRỰC TIẾP của .filters và đứng CUỐI -> flex-basis:100% cho nó tự xuống
   HÀNG RIÊNG bên dưới toàn bộ ô lọc. Trước đây nằm trong .customrange nên kéo phình
   chính khối đó và đẩy lệch cả thanh bộ lọc.
   Nền pha rgba từ token màu — cùng cách với .kpi .delta, không thêm hex rời. */
.range-hint {
  flex-basis: 100%; box-sizing: border-box;
  margin-top: 2px;                       /* .filters đã có gap 14px, chỉ cần nhích nhẹ */
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; border-radius: var(--radius-sm);
  font-size: 12.5px; line-height: 1.55; font-weight: 600;
  color: var(--text-muted); background: var(--bg); border: 1px solid var(--border);
}
/* (Việc ẩn do quy tắc [hidden] dùng chung ở đầu file lo — xem giải thích ở đó.) */
.range-hint::before { content: '⏱'; font-size: 14px; flex-shrink: 0; }
.range-hint.danger {
  /* chu dung --warning-dark (khong phai --warning): nen da pha cung tong nen phai dam hon */
  color: var(--warning-dark); background: rgba(163, 90, 4, .10); border-color: var(--warning);
}
.range-hint.danger::before { content: '⚠️'; }

/* ── KPI cards ── */
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
@media (max-width: 700px) { .kpis { grid-template-columns: repeat(2, 1fr); } }
.kpi {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); padding: 18px;
  transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease;
  /* ⚠️ 2026-08-28 (user gửi ảnh: 6 card KPI trên di động "chạy lung tung", tràn
     ngang cả khối) — BUG chung của .kpis/.kpi (dùng ở CẢ 3 nơi: dashboard.php,
     data_check.php, host_stats.php), không riêng trang nào. Nguyên nhân: item
     lưới CSS Grid mặc định có min-width:auto = min-content của nội dung bên
     trong — số tiền dạng "108,799,590đ" không có chỗ ngắt dòng nào (dấu phẩy
     ngăn cách nghìn KHÔNG được coi là điểm ngắt hợp lệ giữa 2 chữ số, theo quy
     tắc ngắt dòng chuẩn — đúng ý giữ số liền mạch), nên trình duyệt buộc cột
     lưới phải rộng ÍT NHẤT bằng bề rộng của CẢ CHUỖI SỐ đó, dù cột được khai
     "1fr". Trên điện thoại, GMV lớn (9-10 chữ số) rộng hơn nửa màn hình -> đẩy
     card kế bên tràn hẳn ra ngoài, cả khối phải cuộn ngang trông như "vỡ hàng".
     min-width:0 cho phép item co lại ĐÚNG theo track 1fr thay vì bị nội dung ép
     rộng ra — số quá dài sẽ tự NGẮT xuống dòng (nhờ overflow-wrap:anywhere ở
     .kpi .val bên dưới) thay vì đẩy tràn cả khối. */
  min-width: 0;
}
.kpi:hover { box-shadow: var(--shadow-md); transform: translateY(-3px); }
/* Card GMV = "hero" — nổi bật hơn 7 card còn lại (khớp mockup redesign: gradient
   nhạt nghiêng sang --hince-soft + viền --hince, thay vì y hệt các card khác).
   GMV vốn là chỉ số quan trọng nhất kỳ, xứng đáng có điểm nhấn thị giác riêng —
   không phải áp cho tất cả card, tránh loãng ý nghĩa "nổi bật". */
.kpi.hero { background: linear-gradient(160deg, var(--card-bg) 60%, var(--hince-soft)); border-color: var(--hince); }
/* Card có hover-popup (xem info thêm) — viền sáng lên chút để gợi ý "rờ vào có
   thêm thông tin", tinh tế hơn cursor:help (2026-07-12, theo yêu cầu user). */
.kpi-hoverable:hover { border-color: var(--brand); }
.kpi .kpi-top { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.kpi .icon {
  width: 32px; height: 32px; border-radius: 8px; background: var(--brand-soft); color: var(--brand-dark);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  transition: transform .2s ease;
}
/* 2026-08-21 — icon giờ là SVG (app_icon(), lib/icons.php) thay emoji, cần định
   kích thước riêng cho <svg> (font-size không có tác dụng gì với SVG con). */
.kpi .icon svg { width: 17px; height: 17px; }
.kpi.hero .icon { background: var(--hince-soft); color: var(--hince-dark); }
.kpi:hover .icon { transform: scale(1.1); }
.kpi .lbl { font-size: 12px; color: var(--text-muted); font-weight: 600; }
/* 2026-08-01 (design-critique) — căn TRÁI thay vì giữa, khớp hàng icon+label phía
   trên (cũng căn trái) — trước đây 2 kiểu căn lề trộn trong cùng 1 card nhỏ. */
/* 2026-08-10 (design-critique, chuẩn báo cáo TMĐT) — thiếu tabular-nums trong khi
   bảng .report bên dưới ĐÃ có; số tiền lớn trên thẻ KPI co giãn không đều hàng,
   không nhất quán với phần còn lại của trang cho 1 tài liệu tài chính. */
.kpi .val {
  font-size: 24px; font-weight: 700; letter-spacing: -.3px; text-align: left; font-variant-numeric: tabular-nums;
  /* Đi kèm .kpi{min-width:0} ở trên — cho phép số DÀI (VD "108,799,590đ") tự
     ngắt xuống dòng khi card bị co hẹp trên di động, thay vì tràn ra khỏi card.
     (Chỉ còn là lưới AN TOÀN cuối cùng — bình thường trên di động cỡ chữ đã tự
     thu nhỏ đủ để KHÔNG cần ngắt, xem @media bên dưới.) */
  overflow-wrap: anywhere;
}
/* ⚠️ 2026-08-28 (user: sau khi sửa tràn ngang bằng overflow-wrap:anywhere ở
   trên, số GMV dài lại bị NGẮT GIỮA CHỮ SỐ — "109,251,3" xuống dòng "40đ" —
   "font chữ không phù hợp, xuống dòng không đúng mục đích"). overflow-wrap:
   anywhere chỉ nên là lưới an toàn cuối cùng, không phải cách chính để xử lý số
   dài — cách ĐÚNG là thu nhỏ cỡ chữ theo bề rộng màn hình để số vừa TRỌN 1
   dòng (số tiền không có điểm ngắt đẹp giữa các chữ số nên xuống dòng luôn khó
   đọc hơn, phải tránh nếu có thể). Công thức 5.2vw suy từ bề rộng card thật ở
   layout 2 cột mobile (đã đo bằng preview thật 375-430px) — vừa đủ cho số dạng
   "999,999,999đ" trên 1 dòng; trần 21px = không vượt cỡ chữ gốc 24px (tránh
   nhảy cỡ đột ngột lúc đúng breakpoint 700px); sàn 15px đủ đọc trên máy rất
   hẹp. ⚠️ PHẢI đặt SAU khối .kpi .val gốc ở trên (không phải gộp vào bên trong
   @media của .kpis phía đầu file) — CSS ưu tiên theo THỨ TỰ NGUỒN khi độ đặc
   hiệu bằng nhau, đặt trước sẽ bị khối 24px không-điều-kiện ở dưới đè mất dù
   media query đúng (lỗi thật đã tự bắt được qua ảnh chụp preview, ban đầu đặt
   nhầm chỗ này). Số vượt ngưỡng cực đại hiếm gặp (>1 tỷ, gần như không xảy ra
   với GMV 1 host/1 kỳ) vẫn có lưới an toàn overflow-wrap:anywhere ở trên, không
   mất dữ liệu, chỉ xấu hơn chút ở trường hợp cực hiếm đó. */
@media (max-width: 700px) {
  /* ⚠️ 2026-08-28 (user gửi ảnh máy THẬT: số "266,290,934đ" VẪN rớt "đ" xuống
     dòng dù đã có công thức 5.2vw ở trên) — 5.2vw/21px TÍNH TỪ preview dựng ở
     môi trường này (Windows/Chromium, font "-apple-system" KHÔNG resolve được
     ra San Francisco thật — tự rơi về font hệ thống khác hẹp hơn số liệu thật
     trên iPhone), nên số đo "vừa khít" lúc build KHÔNG phản ánh đúng bề rộng
     chữ số Bold + tabular-nums thật của San Francisco trên máy user — môi
     trường này KHÔNG có cách nào tự kiểm tra chính xác font Apple thật. Hạ hẳn
     công thức xuống AN TOÀN HƠN NHIỀU (15-19px thay vì 15-21px, hệ số 4.2vw
     thay vì 5.2vw) — thà chữ nhỏ hơn 1 chút còn hơn tiếp tục đoán sai lần nữa. */
  .kpi .val { font-size: clamp(15px, 4.2vw, 19px); }
}
.up { color: var(--success); } .down { color: var(--danger); } .flat { color: var(--text-faint); }
/* 2026-08-01 (redesign — chip trạng thái, theo yêu cầu user) — delta trên KPI
   card đổi từ chữ màu trần (chỉ .up/.down/.flat ở trên, vẫn dùng nguyên cho
   bảng số/target-pct) sang "chip" nền pill để trạng thái đọc được ngay bằng
   HÌNH DẠNG chứ không chỉ màu chữ (đỡ phụ thuộc hoàn toàn vào phân biệt màu).
   CHỈ scope trong .kpi .delta — không đụng .up/.down/.flat dùng ở chỗ khác. */
.kpi .delta {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px; font-weight: 700; margin-top: 8px;
  padding: 3px 10px; border-radius: 999px;
}
.kpi .delta.up { background: rgba(21, 128, 61, .12); color: var(--success); }
.kpi .delta.down { background: rgba(220, 38, 38, .12); color: var(--danger); }
.kpi .delta.flat { background: var(--bg); color: var(--text-faint); }
/* 2026-08-01 — sparkline mini xu hướng theo ngày, khớp ĐÚNG kích thước/vị trí
   bản mockup redesign trước đó (user báo bản đầu — dải to tô nền dưới cùng card
   — không giống mockup): icon nhỏ 46×20px nằm NGAY CẠNH label (trong .kpi-top,
   đẩy sát phải bằng margin-left:auto), không tô nền vùng dưới đường (chỉ nét
   mảnh, giống mockup dùng SVG <path fill="none">). Ẩn hẳn khi kỳ chỉ có 1 ngày
   (PHP không render canvas, xem ajax_dashboard.php) nên không cần xử lý rỗng ở đây. */
/* Chart.js responsive:true đo kích thước theo THẺ CHA của canvas, không theo
   CSS width/height đặt thẳng trên chính canvas — phải bọc trong khung CỐ ĐỊNH
   kích thước thật (span display:inline-block có width/height rõ ràng) thì
   Chart.js mới co đúng 46x20px thay vì phình to theo khoảng trống flex còn lại
   của .kpi-top (bug đã gặp: canvas rộng cả nghìn px do đo nhầm parent). */
.kpi-spark-box { display: inline-block; width: 46px; height: 20px; margin-left: auto; flex-shrink: 0; }
.kpi-spark { width: 100%; height: 100%; }

/* ── Thẻ mục tiêu GMV (tab KPI_TARGET, đối chiếu vs GMV đạt được) ── */
.target-card { margin-top: 16px; }
.target-card-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.target-card-top .lbl { font-size: 13px; font-weight: 700; color: var(--text); }
.target-pct { font-size: 20px; font-weight: 700; }
.target-bar-track { background: var(--brand-soft); border-radius: 999px; height: 14px; overflow: hidden; }
.target-bar-fill { background: var(--brand); height: 100%; border-radius: 999px; transition: width .2s; }
.target-bar-note { font-size: 12px; color: var(--text-muted); margin-top: 8px; }

/* ── Bảng So sánh 2 sàn + donut (Bước 3, 2026-07-10) ── */
.platform-compare { margin-top: 16px; }
.platform-compare-body { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; margin-top: 4px; }
.platform-compare-table { flex: 1 1 420px; min-width: 320px; margin: 0; }
.platform-compare-donut { flex: 0 0 180px; width: 180px; height: 180px; }
@media (max-width: 700px) {
  .platform-compare-body { flex-direction: column; }
  .platform-compare-donut { width: 160px; height: 160px; }
}

/* ── Callout "Ca xuất sắc nhất kỳ" (Bước 4, bảng Host) ──
   2026-08-01 (design-critique) — đổi từ tông --brand (chàm, dùng lặp lại ở gần
   như mọi nơi khác trong app) sang tông --hince (cam san hô) — đúng ngữ cảnh
   "khen thưởng/nổi bật" hơn, đồng thời nối lại bản sắc màu thương hiệu vốn chỉ
   xuất hiện ở login.php + sidebar, không có mặt ở nội dung chính. */
.callout-best {
  background: var(--hince-soft); color: var(--hince-dark); border-radius: var(--radius-sm);
  padding: 10px 14px; margin-bottom: 14px; font-size: 13px; line-height: 1.5;
}

.empty {
  color: var(--text-muted); padding: 28px; text-align: center; background: var(--card-bg);
  border: 1px dashed var(--border); border-radius: var(--radius); margin-top: 20px; font-size: 14px;
}
/* 2026-08-01 — phân biệt "gọi Apps Script lỗi" (đáng lo, cần thử lại/báo admin)
   với "thật sự không có dữ liệu" (bình thường, chỉ cần đổi bộ lọc) — trước đây
   dùng CHUNG 1 thông báo .empty màu trung tính cho cả 2, không ai phân biệt
   được. Màu cảnh báo (--warning) khớp nguyên tắc status color đã dùng ở
   .msg-banner.warn, không phải màu mới. */
.empty.err { color: var(--warning); border-color: var(--warning); border-style: solid; }
.hint { color: var(--text-faint); font-size: 13px; }

/* 2026-09-11 (host_slot_registration.php — "không cần đợi mở đăng ký", theo yêu cầu
   user "làm đẹp lại ... không có in đậm, màu sắc hay gì cả, nội dung k tập trung")
   — thẻ thông báo "chưa mở đăng ký" RIÊNG cho trang này, KHÔNG sửa .empty dùng
   chung (đang dùng ở ~20+ trang khác cho trạng thái rỗng đơn giản, sửa ở đó sẽ
   ảnh hưởng toàn app). Icon-trong-khung-tròn tái dùng đúng công thức
   .desktop-only-block .ic đã có sẵn (56px bo góc 15px, nền --brand-soft). */
.hsr-schedule-card {
  margin-top: 20px; padding: 34px 24px; text-align: center; background: var(--card-bg);
  border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-sm);
}
.hsr-schedule-ic {
  width: 56px; height: 56px; margin: 0 auto 14px; border-radius: 15px;
  background: var(--brand-soft); color: var(--brand-dark);
  display: flex; align-items: center; justify-content: center;
}
.hsr-schedule-ic svg { width: 28px; height: 28px; }
.hsr-schedule-card h2 { font-size: 18px; font-weight: 800; color: var(--text); margin: 0 0 8px; }
.hsr-schedule-card p { font-size: 14px; color: var(--text-muted); line-height: 1.6; margin: 0 auto; max-width: 420px; }
.hsr-schedule-card p b { color: var(--text); font-weight: 700; }
.hsr-schedule-countdown {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 18px; padding: 10px 22px;
  background: var(--brand-soft); color: var(--brand-dark); border-radius: 999px;
  font-size: 17px; font-weight: 800; font-variant-numeric: tabular-nums; min-height: 20px;
}
/* Chưa hẹn giờ gì cả (admin chưa điền open_at) — tông trung tính, không phải cảnh báo. */
.hsr-schedule-card.is-muted .hsr-schedule-ic { background: rgba(91,100,133,.1); color: var(--text-faint); }

/* ── Charts grid ── */
.charts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 28px; }
@media (max-width: 800px) { .charts { grid-template-columns: 1fr; } }
.chart-box {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); padding: 18px;
}
.chart-box.full { grid-column: 1 / -1; }
.chart-box h3 { margin: 0 0 14px; font-size: 15px; font-weight: 700; }

/* ── Heatmap table ── */
.heatmap-wrap { overflow-x: auto; border-radius: var(--radius-sm); }
.heatmap { border-collapse: collapse; font-size: 12px; width: 100%; }
.heatmap th, .heatmap td { border: 1px solid var(--border); padding: 8px 10px; text-align: center; white-space: nowrap; }
.heatmap th { background: var(--brand-soft); color: var(--brand-dark); font-weight: 700; }
.heatmap td.hourlbl { background: #f8fafc; font-weight: 700; text-align: center; }
.heatmap tr.heatmap-total td { font-weight: 700; background: var(--brand-soft); border-top: 2px solid var(--brand); }

/* ── Funnel (CSS thuần, không Chart.js) ──
   Hình thang cột TRÁI + nhãn (tên bước + số + % chuyển đổi) cột PHẢI, không chồng
   lấn lên nhau. % là tỷ lệ so với BƯỚC NGAY TRƯỚC (không phải so với Impressions)
   — có chú thích "vs {bước trước}" để khỏi đọc nhầm.
   ⚠️ SỬA (2026-07-12, user báo "không phủ đủ ô") — .chart-box mặc định KHÔNG co
   giãn theo chiều cao ô lưới (.charts là CSS grid, item tự stretch theo hàng cao
   nhất nhưng NỘI DUNG bên trong không tự lớn theo) — .funnel-box đổi sang flex
   column để .funnel + từng .funnel-row GIÃN LẤP ĐỦ chiều cao thật của khung, thay
   vì đứng im ở chiều cao cố định rồi để trống khoảng lớn bên dưới khi khung cao
   hơn (VD khung 2 cao bằng khung 1 có canvas cao).
   ⚠️ SỬA TIẾP (2026-07-12, user báo "vẫn dư bên phải quá") — cột nhãn cũ
   flex:1 chiếm HẾT phần rộng còn lại dù chữ chỉ cần một khoảng nhỏ, để trống hẳn
   dải trắng dài bên phải chữ. Đổi ngược lại: NHÃN có bề rộng CỐ ĐỊNH (vừa đủ chữ
   dài nhất, không bị bó), SHAPE lấy flex:1 (nuốt hết phần thừa còn lại) — hình
   thang giờ giãn full theo đúng khoảng trống thật, không còn dư thừa. */
.chart-box.funnel-box { display: flex; flex-direction: column; }
.funnel { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.funnel-row { flex: 1; display: flex; align-items: center; gap: 16px; min-height: 40px; }
.funnel-shape-wrap { position: relative; height: 100%; flex: 1; min-width: 0; }
.funnel-shape { position: absolute; inset: 0; }
.funnel-label { flex: 0 0 auto; width: 168px; }
.funnel-label .fname { font-weight: 700; font-size: 12px; color: var(--text); }
.funnel-label .fval { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
.funnel-label .fconv { color: var(--brand); font-weight: 700; }
.funnel-label .fconv-note { color: var(--text-faint); font-size: 11px; margin-left: 4px; }
@media (max-width: 700px) {
  .funnel-label { width: 130px; }
  .funnel-row { gap: 10px; }
}

/* ── Bảng Top SP (chart 7 — HTML table thuần, không Chart.js) ──
   Canvas Chart.js căn PHẢI nhãn trục Y sát biểu đồ -> tên SP dài bị cắt mất phần
   ĐẦU chữ khi chart hẹp. Dùng table.report có sẵn (header sort + zebra-stripe),
   thêm cột ảnh (từ PRODUCT_IMG_RAW) + cột mini bar GMV. Chiều cao dòng THẤP hơn
   bar-list cũ (không co giãn theo tên dài — tên chỉ 1 dòng, cắt "..." + xem đầy đủ
   qua tooltip title) để đỡ tốn diện tích. */
/* Bảng hiện ĐỦ mọi SP (không giới hạn 10 nữa, 2026-07-12) — khung cao ~10 dòng,
   cuộn dọc bên trong để xem tiếp thay vì kéo dài cả trang; header dính lại khi
   cuộn (sticky) để vẫn thấy tên cột. */
.table-topsp-scroll { max-height: 600px; overflow-y: auto; }
.table-topsp-scroll thead th { position: sticky; top: 0; z-index: 1; }
table.table-topsp td, table.table-topsp th { vertical-align: middle; }
table.table-topsp .sp-name {
  max-width: 260px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
table.table-topsp .sp-bar { width: 160px; }
table.table-topsp .sp-bar .bar-track { background: var(--brand-soft); border-radius: 6px; height: 10px; overflow: hidden; }
table.table-topsp .sp-bar .bar-fill { background: var(--brand); height: 100%; border-radius: 6px; min-width: 2px; }
.sp-thumb {
  width: 36px; height: 36px; border-radius: 6px; object-fit: cover;
  border: 1px solid var(--border); display: block; cursor: zoom-in;
  transition: transform .15s ease, box-shadow .15s ease;
}
.sp-thumb:hover { transform: scale(1.15); box-shadow: 0 4px 12px rgba(0,0,0,.18); }
.sp-thumb:active { transform: scale(.92); }
.sp-thumb-empty {
  display: flex; align-items: center; justify-content: center;
  background: var(--brand-soft); color: var(--text-faint); font-size: 12px;
  cursor: default;
}
/* Cột "Link" bảng Top SP (2026-07-15, theo yêu cầu user — link trang SP tự điền
   vào PRODUCT_IMG_RAW theo product_id) — icon nhỏ, gọn, không chiếm nhiều chỗ. */
.sp-link { font-size: 16px; text-decoration: none; display: inline-block; transition: transform .15s ease; }
.sp-link:hover { transform: scale(1.2); }
.sp-link-empty { color: var(--text-faint); }

/* ── Popup xem ảnh SP to (click thumbnail chart 7) — fade + zoom-in khi mở
   (2026-07-11, theo yêu cầu user "thêm animation khi bấm vào hình sản phẩm").
   Dùng opacity/visibility thay vì display để có transition mượt; visibility
   delay khi ẩn (delay .2s = đúng thời gian transition) để overlay hết hẳn
   click được mới biến mất khỏi layout, tránh chặn click phía dưới lúc đang mờ. ── */
.lightbox-overlay {
  display: flex; position: fixed; inset: 0; z-index: 1000;
  background: rgba(15, 23, 42, .72);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  align-items: center; justify-content: center; cursor: zoom-out;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .2s ease, visibility 0s linear .2s;
}
.lightbox-overlay.show {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity .2s ease, visibility 0s linear 0s;
}
.lightbox-overlay img {
  max-height: 70vh; max-width: 90vw; border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, .5); cursor: default;
  transform: scale(.85); transition: transform .2s ease;
}
.lightbox-overlay.show img { transform: scale(1); }
.lightbox-close {
  position: absolute; top: 20px; right: 28px; color: #fff; font-size: 32px;
  line-height: 1; font-weight: 300; cursor: pointer; opacity: .85;
}
.lightbox-close:hover { opacity: 1; }

/* ── Report tables (8d) ── */
.report-table-wrap { overflow-x: auto; }
table.report {
  width: 100%; border-collapse: collapse; font-size: 13px;
}
table.report th {
  text-align: left; padding: 10px 12px; background: var(--brand-soft); color: var(--brand-dark);
  font-weight: 700; font-size: 12px; text-transform: uppercase; letter-spacing: .3px;
}
table.report td { padding: 9px 12px; border-bottom: 1px solid var(--border); }
table.report tbody tr:nth-child(even) { background: #fafbff; }
table.report tbody tr:hover { background: var(--brand-soft); }
table.report td.num, table.report th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.report tr.report-total-row td { font-weight: 700; background: var(--brand-soft); border-top: 2px solid var(--brand); }
table.report td.rank {
  display: inline-flex; width: 22px; height: 22px; border-radius: 50%; background: var(--brand-soft);
  color: var(--brand-dark); align-items: center; justify-content: center; font-weight: 700; font-size: 11px;
}
table.report th.sortable { cursor: pointer; user-select: none; }
table.report th.sortable:hover { color: var(--brand); }
/* ⚠️ SỬA (2026-07-10): trước đây width:12px CỐ ĐỊNH dù rỗng — với cột số
   (text-align:right), span rỗng này chiếm chỗ BÊN PHẢI chữ tiêu đề, đẩy cả cụm
   "chữ+span" lệch trái 12px so với số liệu bên dưới (số liệu không có span nên
   sát phải hoàn toàn) -> tiêu đề nhìn "lệch" so với cột. Bỏ width cố định, để
   span tự co theo nội dung (rỗng thì gần như 0px, không đẩy lệch nữa) — chỉ khi
   ĐANG sort (có chữ ▾/▴) mới chiếm chỗ, chấp nhận cột hơi nhích nhẹ lúc đó. */
table.report th.sortable .sort-ind { display: inline-block; }

/* Popup hover danh sách ca chi tiết (bảng 10 Host + bảng 12 khung giờ, 2026-07-10) */
.table-hover-popup {
  display: none;
  position: fixed;
  z-index: 10000;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  padding: 6px;
  max-height: 240px;
  max-width: 320px;
  overflow-y: auto;
  font-size: 12px;
  pointer-events: none;   /* popup không chặn thao tác chuột, tránh nhấp nháy khi rê qua popup */
}
/* Popup KPI hover (2026-07-12, theo yêu cầu user "muốn copy được thông tin") —
   BẬT LẠI pointer-events riêng popup này (JS có độ trễ ẩn + lắng nghe hover trên
   chính popup, xem script cuối trang, nên không bị nhấp nháy như 2 popup còn lại
   vốn dựa vào pointer-events:none để tránh phải xử lý việc đó).
   2026-07-19: user báo bảng 10/12 dài phải CUỘN mà "di chuột vào thì mất" — 2
   popup này (hostSessionPopup/hourSessionPopup) giờ cũng dùng CÙNG cơ chế độ trễ
   ẩn + lắng nghe hover trên popup (xem ajax_dashboard.php), nên cũng cần bật
   pointer-events để nhận được thao tác cuộn/hover. */
#kpiHoverPopup, #hostSessionPopup, #hourSessionPopup, #crossLiveHoverPopup { pointer-events: auto; user-select: text; }
.table-hover-popup table { border-collapse: collapse; width: 100%; }
.table-hover-popup th, .table-hover-popup td { padding: 4px 8px; text-align: left; white-space: nowrap; }
.table-hover-popup td.num, .table-hover-popup th.num { text-align: right; font-variant-numeric: tabular-nums; }
.table-hover-popup th { color: var(--text-muted); font-weight: 600; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--card-bg); }
.table-hover-popup tbody tr:nth-child(even) td { background: #fafbff; }

/* ── Admin (Phase 10, 2026-07-10): form user, tabs, banner thông báo ── */
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; margin-bottom: 14px; }
@media (max-width: 700px) { .form-grid { grid-template-columns: 1fr; } }
.form-grid label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 5px; font-weight: 600; }
/* 2026-08-13 (user: "ô chọn ngày không giống với chọn ngày ngoài trang upload ảnh")
   — danh sách type= ở đây thiếu date/time/file, nên input[type=date] trong bất kỳ
   .form-grid nào (VD admin.php tab "Gửi Zalo") rơi về giao diện MẶC ĐỊNH của trình
   duyệt (không viền/bo góc/theme giống các ô khác) — không phải cố ý, chỉ là danh
   sách cũ viết trước khi có ai dùng type=date/time/file bên trong .form-grid.
   Thêm đủ 3 type còn thiếu để MỌI input trong .form-grid luôn đồng nhất 1 kiểu,
   không phải patch riêng lẻ từng trang. */
.form-grid input[type=text], .form-grid input[type=password], .form-grid input[type=date],
.form-grid input[type=time], .form-grid input[type=file], .form-grid select {
  width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-size: 13px; box-sizing: border-box; background: #fff; color: var(--text);
}
.form-grid input:focus, .form-grid select:focus { outline: 2px solid var(--brand-soft); border-color: var(--brand); }
.chk-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.chk-row label { display: flex; align-items: center; gap: 6px; font-weight: 500; color: var(--text); margin: 0; font-size: 13px; }
/* Bọc nhóm checkbox bằng <fieldset>/<legend> thay vì <label> rời (2026-08-01,
   audit accessibility — <label> không gắn với input NÀO cụ thể thì screen reader
   không đọc được là nhãn của nhóm nào; fieldset/legend là đúng ngữ nghĩa cho
   "1 nhãn cho NHIỀU checkbox"). Reset viền/padding mặc định của trình duyệt để
   giữ nguyên hình thức cũ (trước đây chỉ là dòng chữ thường, không khung viền). */
.chk-fieldset { border: none; padding: 0; margin: 0; }
.chk-fieldset legend { padding: 0; font-size: 12px; color: var(--text-muted); font-weight: 600; margin-bottom: 5px; }

.msg-banner { padding: 10px 14px; border-radius: var(--radius-sm); margin-bottom: 16px; font-size: 13px; }
.msg-banner.ok { background: #e6f7e9; color: #1a7d33; }
.msg-banner.err { background: #fdeceb; color: #c0392b; }
.msg-banner.warn { background: #fff4d6; color: #8a6600; }   /* 4.81:1 (trước: #a67c00 = 3.48:1, KHÔNG đạt AA) */

.tabs { display: flex; gap: 4px; margin-bottom: 18px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.tabs a {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 16px; color: var(--text-muted); text-decoration: none; font-size: 13px; font-weight: 600;
  border-bottom: 2px solid transparent;
}
.tabs a.active { color: var(--brand); border-bottom-color: var(--brand); }
.tabs a:hover { color: var(--brand-dark); }
/* 2026-08-29 (tối ưu giao diện Admin) — icon SVG thay emoji trong nhãn tab,
   CÙNG mẫu size cố định + currentColor như .app-nav-item .ic (sidebar) để tự đổi
   màu theo state active/hover mà không cần override riêng. */
.tabs a .ic { width: 15px; height: 15px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.tabs a .ic svg { width: 100%; height: 100%; }

/* 2026-08-29 (tối ưu giao diện Admin) — thay cho việc lặp lại
   <hr style="border:none;border-top:1px solid var(--border);margin:20px 0"> ở
   MỖI ranh giới giữa các khối cấu hình trong 1 .card (7 lần y hệt nhau ở tab cũ
   "Thiết lập quyền") — 1 class dùng chung, dễ chỉnh khoảng cách đồng loạt sau
   này thay vì sửa từng dòng inline. */
.settings-section { padding-top: 20px; margin-top: 20px; border-top: 1px solid var(--border); }
.settings-section:first-child { padding-top: 0; margin-top: 0; border-top: none; }
/* Tiêu đề nhóm lớn (VD "Host & Lịch làm việc") — phân biệt với .section-title
   của TỪNG khối con bên trong, không lẫn cấp bậc. */
.settings-group-title {
  display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 800;
  color: var(--text-faint); text-transform: uppercase; letter-spacing: .5px; margin: 0 0 14px;
}
.settings-group-title .ic { width: 15px; height: 15px; display: flex; flex-shrink: 0; }
.settings-group-title .ic svg { width: 100%; height: 100%; }

/* 2026-08-30 (user: "các nút, dòng này nọ nó cứ thụt thò... không thuận mắt") —
   TRƯỚC ĐÂY input/span trạng thái/nút của MỖI khối cấu hình trôi tự do ngay sau
   đoạn .hint, không có khung nào phân biệt "đây là vùng bấm được" khỏi đoạn chữ
   mô tả — mắt không có điểm neo, mỗi khối lại xếp control theo 1 kiểu khác nhau
   (có khối nút nằm ngay cạnh input, có khối nút đứng riêng 1 dòng bên dưới nhiều
   dòng khác) nên nhìn "thụt thò" dù khoảng cách dọc đã đều. Bọc TOÀN BỘ vùng
   điều khiển (input/select/checkbox/nút/badge trạng thái) của MỖI khối trong 1
   khung nền nhạt CÙNG 1 KHUÔN — bất kể bên trong có 1 hàng hay nhiều hàng, mắt
   luôn thấy "đây là 1 cụm thao tác" tách bạch khỏi phần mô tả phía trên nó. */
.settings-control-panel {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 12px 14px; margin-top: 10px;
}
/* 2026-08-30 (user: "chỗ chọn ngày sao nhìn nó cứ phèn phèn") — GỐC RỄ THẬT:
   class cũ (.settings-toggle-form, đã bỏ) chỉ khai display:flex, CHƯA TỪNG style
   riêng input/select nào — input[type=date]/number/select bên trong hiện nguyên
   hình dạng MẶC ĐỊNH của trình duyệt (khung xám thô, không bo góc, không khớp
   font/kích thước phần còn lại của trang) — khác hẳn input CÙNG LOẠI ở nơi khác
   trong app (VD .filters, dashboard.php) vốn đã được style riêng từ trước. Style
   THEO ĐÚNG khuôn đó, chỉ hạ nhẹ kích thước (32px thay 38px) cho khớp .btn-sm
   dùng dày đặc trong khối này. */
.settings-control-panel input[type="date"],
.settings-control-panel input[type="number"],
.settings-control-panel input[type="text"],
.settings-control-panel select {
  height: 32px; box-sizing: border-box; padding: 5px 10px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: #fff; font-size: 13px; color: var(--text);
}
.settings-control-panel input:focus, .settings-control-panel select:focus {
  outline: 2px solid var(--brand-soft); border-color: var(--brand);
}
/* Khối nhiều hàng (VD "Nhắc lịch host mỗi ngày": bật/tắt + giờ/phút + chọn kênh +
   nút Lưu) — xếp DỌC nhưng vẫn CÙNG 1 khung nền, để nút Lưu neo NGAY DƯỚI cụm
   input của chính nó thay vì đứng cô độc rời khỏi khung. */
.settings-control-panel.stack { flex-direction: column; align-items: stretch; gap: 12px; }
.settings-control-panel .control-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Ghi chú PHỤ (VD "Đồng bộ xuôi gần nhất lúc...") đứng NGAY TRONG khung, không
   còn là .hint rời rạc trôi giữa 2 cụm nút — nhạt hơn chữ thường nhưng vẫn cùng
   khung, không tạo thêm 1 "tầng" mới cho mắt phải xử lý. */
.settings-control-panel .control-note { font-size: 12.5px; color: var(--text-muted); line-height: 1.6; flex-basis: 100%; }

/* Badge trạng thái BẬT/TẮT — thay chữ màu trần nổi cạnh nút (mỗi chỗ từng có 1
   kiểu emoji khác nhau: ⚡/🔄 vs 🟢/🔴) bằng 1 khuôn PILL duy nhất, chấm tròn
   currentColor thay emoji — nhất quán màu --success/--danger đã dùng toàn app. */
.status-pill {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px;
  border-radius: 999px; font-size: 12.5px; font-weight: 700; white-space: nowrap;
}
.status-pill::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.status-pill.on { background: rgba(21,128,61,.12); color: var(--success); }
.status-pill.off { background: rgba(220,38,38,.12); color: var(--danger); }
/* 2026-09-11 — trạng thái "đã hẹn giờ, chưa tới lúc" (đăng ký lịch host) — tông
   --warning, KHÁC hẳn .on/.off để không lẫn với 2 trạng thái dứt khoát đó. */
.status-pill.pending { background: rgba(163,90,4,.12); color: var(--warning-dark); }

.btn-sm { padding: 5px 12px; font-size: 12px; box-shadow: none; }
.btn-danger { background: var(--danger); }
.btn-danger:hover { background: #b91c1c; }
.btn-outline { background: transparent; color: var(--brand); border: 1px solid var(--brand); box-shadow: none; }
.btn-outline:hover { background: var(--brand-soft); color: var(--brand-dark); }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn[disabled]:hover { background: var(--brand); }

/* ── AI Analysis nhúng theo cụm chart/bảng (2026-07-10, thay trang analyze.php riêng) ── */
.ai-panel {
  background: var(--brand-soft); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 16px 18px; margin: 14px 0 24px; font-size: 13px;
}
.ai-panel-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px;
  color: var(--brand-dark); margin-bottom: 8px; display: flex; align-items: center; gap: 6px;
}
.ai-panel h4 { margin: 10px 0 4px; font-size: 14px; color: var(--brand-dark); }
.ai-panel h4:first-child { margin-top: 0; }
.ai-panel p { margin: 6px 0; line-height: 1.6; }
.ai-panel ul, .ai-panel ol { margin: 6px 0 6px 4px; padding-left: 20px; }
.ai-panel li { margin: 3px 0; line-height: 1.55; }
.ai-panel code { background: #fff; padding: 1px 5px; border-radius: 4px; font-size: 12px; }

/* ============================================================================
   APP SHELL — sidebar trái + topbar mobile (2026-07-11) — dùng chung qua
   lib/sidebar.php (render_app_topbar/render_app_sidebar/render_app_shell_script).
   Bắt đầu ở index.php, sẽ tái dùng khi redesign dashboard/admin/upload/... sau.
   ============================================================================ */
.app-shell { display: flex; min-height: 100vh; }
.app-main { flex: 1; padding: 36px 44px; }
/* Bề rộng CHUẨN cho các trang form 1 cột (index/product/trend/detail/upload/
   host_report) — 2026-07-11, sửa bug: mỗi trang trước đó tự đặt max-width khác
   nhau (760px vs 640px) khiến form nhìn to nhỏ lệch nhau. Đặt DÙNG CHUNG 1 chỗ,
   trang nào cần hẹp thì thêm class này vào <main>, KHÔNG tự ý đặt số khác nữa. */
.app-main-narrow { max-width: 760px; }
@media (max-width: 900px) { .app-main-narrow { max-width: 100%; } }

/* ── Sidebar ── */
.app-sidebar {
  flex: 0 0 240px; background: linear-gradient(180deg, var(--shell-dark-1) 0%, var(--shell-dark-2) 100%);
  color: #fff; display: flex; flex-direction: column; padding: 22px 16px;
  /* Ghim sidebar theo cửa sổ khi trang dài (2026-07-11, theo yêu cầu user) — trang
     dashboard/report có thể cao hơn nhiều màn hình, sidebar cần "chạy theo" khi
     cuộn thay vì cuộn mất luôn cùng nội dung. height:100vh + overflow-y:auto phòng
     hờ menu dài hơn màn hình thấp (hiếm nhưng an toàn, tự cuộn riêng bên trong). */
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.app-logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 15px; padding: 0 8px 6px; }
.app-logo .ic {
  width: 32px; height: 32px; border-radius: 9px; background: rgba(255,255,255,.14); color: #fff;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.app-logo .ic svg { width: 18px; height: 18px; }
/* Footer app (APP_FOOTER_TEXT) — nằm NGAY DƯỚI logo/title, TRÊN nhóm menu
   (2026-07-11, theo yêu cầu user — đổi từ vị trí đầu "trên hàng user ở đáy"
   sang đây). Canh trái khớp logo phía trên, viền dưới nhẹ làm ranh giới trước
   khi vào menu. */
.app-sidebar-footer-text {
  font-size: 10px; color: rgba(255,255,255,.35); text-align: left; padding: 0 8px 14px;
  border-bottom: 1px solid rgba(255,255,255,.1);
}
/* Nhóm menu tách hẳn khỏi title/footer phía trên 1 khoảng (theo yêu cầu user
   "dời cụm menu xuống dưới 1 chút tách ra khỏi title"). */
.app-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; margin-top: 14px; }
.app-nav-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 8px;
  color: rgba(255,255,255,.78); font-size: 13px; font-weight: 600; text-decoration: none;
  border-left: 3px solid transparent; transition: background .12s, color .12s, border-color .12s;
}
/* 2026-08-21 — icon giờ là SVG (app_icon(), lib/icons.php) thay emoji: co giãn
   theo currentColor nên tự đổi màu THEO ĐÚNG state của .app-nav-item (thường/
   hover/active) mà không cần override riêng — chỉ cần định kích thước cố định. */
.app-nav-item .ic { width: 17px; height: 17px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.app-nav-item .ic svg { width: 100%; height: 100%; }
.app-nav-item:hover { background: rgba(255,255,255,.07); color: #fff; text-decoration: none; }
.app-nav-item.active { background: rgba(255,255,255,.1); color: #fff; border-left-color: var(--hince); }
/* Focus rõ ràng khi điều hướng bằng bàn phím (Tab) — trước đây không có gì riêng,
   dựa vào outline mặc định của trình duyệt vốn dễ bị nền tối/viền bo góc nuốt mất. */
.app-nav-item:focus-visible, .app-nav-subitem:focus-visible, .app-user:focus-visible, .app-logout:focus-visible,
.app-nav-toggle:focus-visible {
  outline: 2px solid var(--hince); outline-offset: 2px;
}

/* Nhóm "Upload dữ liệu" thu gọn/mở — tối giản menu, chỉ hiện mục con khi mở. */
.app-nav-toggle { width: 100%; background: none; border: none; cursor: pointer; font-family: inherit; text-align: left; }
.app-nav-toggle .chev { margin-left: auto; width: 13px; height: 13px; display: flex; transition: transform .18s; opacity: .6; }
.app-nav-toggle .chev svg { width: 100%; height: 100%; }
.app-nav-toggle[aria-expanded="true"] { color: #fff; }
.app-nav-toggle[aria-expanded="true"] .chev { transform: rotate(180deg); }

.app-nav-sub {
  max-height: 0; overflow: hidden; transition: max-height .2s ease;
  display: flex; flex-direction: column; gap: 1px; padding-left: 30px;
}
/* 2026-09-25 — nhóm Upload dữ liệu giờ tới 8 mục (~32px/mục) -> 160px cũ cắt
   mất mục cuối; 420px đủ dư cho mọi nhóm hiện có. */
.app-nav-sub.open { max-height: 420px; }
/* Nhóm chứa trang đang đứng mà user bấm thu gọn -> vẫn phải thấy "mình đang ở
   trong nhóm này" (vạch cam như .app-nav-item.active), không mất dấu vị trí. */
.app-nav-toggle.contains-active[aria-expanded="false"] { color: #fff; border-left-color: var(--hince); }
.app-nav-subitem {
  padding: 7px 10px; border-radius: 7px; color: rgba(255,255,255,.6); font-size: 12.5px; font-weight: 600;
  text-decoration: none; position: relative;
}
.app-nav-subitem::before { content: "•"; position: absolute; left: -13px; color: rgba(255,255,255,.28); }
.app-nav-subitem:hover { background: rgba(255,255,255,.06); color: #fff; text-decoration: none; }
.app-nav-subitem.active { background: rgba(255,255,255,.1); color: #fff; }

.app-sidebar-foot { border-top: 1px solid rgba(255,255,255,.12); padding-top: 14px; margin-top: 14px; }
/* Nút VI/EN trong sidebar (2026-07-19) — nền sidebar tối, .lang-switch mặc định
   làm cho nền sáng (dùng ở dashboard.php trước đây) nên cần override riêng. */
.sidebar-lang-row { display: flex; justify-content: center; margin-bottom: 10px; }
.sidebar-lang-row .lang-switch {
  background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.16);
}
.sidebar-lang-row .lang-switch a { color: rgba(255,255,255,.55); }
.sidebar-lang-row .lang-switch a.lang-switch-active { color: #fff; }
.sidebar-lang-row .lang-switch a:hover { color: #fff; }
.sidebar-lang-row .lang-switch-sep { color: rgba(255,255,255,.2); }
/* .app-user giờ là link tới account.php (đổi mật khẩu) — thêm hover/radius cho
   giống 1 mục bấm được, không còn là khối tĩnh (2026-08-10). */
.app-user {
  display: flex; align-items: center; gap: 10px; padding: 6px 8px; margin: 0 -8px 4px;
  border-radius: 8px; text-decoration: none; transition: background .15s;
}
.app-user:hover { background: rgba(255,255,255,.08); text-decoration: none; }
.app-user-avatar {
  width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, var(--hince), #ff8f70);
  display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex-shrink: 0;
}
.app-user-name { font-size: 13px; font-weight: 700; color: #fff; line-height: 1.3; }
.app-user-role { font-size: 11px; color: rgba(255,255,255,.55); text-transform: capitalize; }
.app-logout {
  display: block; text-align: center; font-size: 12px; font-weight: 600; color: rgba(255,255,255,.7);
  padding: 8px; border-radius: 8px; border: 1px solid rgba(255,255,255,.16); text-decoration: none;
}
.app-logout:hover { background: rgba(255,255,255,.08); color: #fff; text-decoration: none; }

/* ── Topbar (chỉ hiện dưới 900px) — "☰" mở sidebar dạng ngăn kéo trượt từ trái,
   phía trên vẫn thấy thông tin đang đăng nhập. ── */
.app-topbar { display: none; }
.app-overlay { display: none; }

@media (max-width: 900px) {
  .app-shell { display: block; }

  .app-topbar {
    display: flex; align-items: center; justify-content: space-between;
    background: var(--shell-dark-1); color: #fff; padding: 10px 16px;
    position: sticky; top: 0; z-index: 60;
    box-shadow: 0 2px 10px rgba(0,0,0,.15);
  }
  .app-hamburger {
    width: 36px; height: 36px; flex-shrink: 0; border-radius: 8px; cursor: pointer;
    background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); color: #fff; font-size: 17px;
  }
  .app-hamburger:focus-visible {
    outline: 2px solid var(--hince); outline-offset: 2px;
  }
  .app-topbar-user { display: flex; align-items: center; gap: 8px; }
  .app-topbar-user .app-user-name, .app-topbar-user .app-user-role { color: #fff; }
  .app-topbar-user .app-user-role { color: rgba(255,255,255,.55); }

  /* Sidebar biến thành ngăn kéo cố định, mặc định ẩn ngoài màn hình bên trái */
  .app-sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 100; flex: 0 0 240px;
    transform: translateX(-100%); transition: transform .22s ease; box-shadow: 0 0 0 rgba(0,0,0,0);
  }
  .app-sidebar.open { transform: translateX(0); box-shadow: 0 0 40px rgba(0,0,0,.35); }

  .app-overlay.show { display: block; position: fixed; inset: 0; background: rgba(15,23,42,.5); z-index: 90; }

  .app-main { padding: 20px 18px; }

  /* ⚠️ 2026-09-11 (user: "trang cứng đờ rất lâu, không hiển thị giao diện đang tải
     đó trên mobile") — BUG THẬT, chỉ xảy ra ở đây: .dash-loading dính ở `top:10px`
     với `z-index:20`, mà trên mobile .app-topbar dính ở `top:0`, CAO ĐÚNG 56px
     (nút 36px + padding 10+10) và `z-index:60`. Thanh "Đang tải dữ liệu" vì thế
     nằm TRỌN trong vùng 0-56px của topbar VÀ ở dưới về thứ tự xếp chồng -> bị
     che kín, người dùng tưởng trang treo. Desktop không lộ vì trên 900px topbar
     `display:none` (dùng sidebar cột cố định thay thế).
     Sửa: đẩy xuống DƯỚI topbar (56 + 10 = 66px, giữ nguyên khoảng thở 10px như
     bản desktop). z-index 55: trên nội dung trang, nhưng vẫn DƯỚI topbar (60) và
     ngăn kéo sidebar (100) — nếu có chồng lấn thì topbar phải nằm trên. */
  .dash-loading { top: 66px; z-index: 55; }
}

/* ============================================================================
   TRANG UPLOAD (index.php + upload dữ liệu: product/trend/detail/
   traffic_source/redistribute.php) — 2026-08-21, gộp về đây phần CSS gần như Y
   HỆT từng bị lặp lại riêng ở mỗi trang (đã đối chiếu từng file trước khi gộp,
   không phần nào là duy nhất của 1 trang). Trang nào cần khác biệt nhỏ vẫn giữ
   override RIÊNG trong <style> của chính trang đó — chỉ gộp phần THẬT SỰ chung.
   ============================================================================ */
.app-main-head { margin-bottom: 20px; }
.app-main-head h1 { display: flex; align-items: center; gap: 8px; font-size: 20px; margin: 0 0 6px; color: var(--text); font-weight: 800; }
.app-main-head p { font-size: 13px; color: var(--text-muted); margin: 0; line-height: 1.6; }
.app-main-head code { background: var(--brand-soft); padding: 1px 5px; border-radius: 4px; font-size: 12px; }
/* Icon SVG (app_icon(), lib/icons.php) đứng đầu tiêu đề trang/card — thay cho
   emoji trước đây. Dùng chung 1 class cho cả h1 (5 trang upload dữ liệu) lẫn h3
   trong .upload-card (index.php, tiêu đề card khác cấu trúc trang kia). */
.app-main-head h1 .ic, .upload-card h3 .ic, .results-card h3 .ic {
  width: 20px; height: 20px; color: var(--brand); flex-shrink: 0; display: inline-flex;
}
.app-main-head h1 .ic svg, .upload-card h3 .ic svg, .results-card h3 .ic svg { width: 100%; height: 100%; }

.upload-card h3 { display: flex; align-items: center; gap: 8px; margin: 0 0 4px; font-size: 15px; }
.upload-card .hint-top { font-size: 12px; color: var(--text-muted); margin: 0 0 18px; }
.upload-card label { display: block; font-size: 12px; color: var(--text-muted); margin: 14px 0 6px; font-weight: 600; }
.upload-card select, .upload-card input[type=file], .upload-card input[type=text],
.upload-card input[type=time], .upload-card input[type=date] {
  width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-size: 13px; box-sizing: border-box; background: #fff; color: var(--text);
}
.upload-card select:focus, .upload-card input:focus { outline: 2px solid var(--brand-soft); border-color: var(--brand); }
.upload-card select:focus-visible, .upload-card input:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.upload-card .frow { display: flex; gap: 16px; } .upload-card .frow > div { flex: 1; }
.upload-card .hint { font-size: 12px; color: var(--text-faint); margin-top: 6px; }
.upload-card .btn { margin-top: 20px; }

.results-card { margin-top: 20px; }
.results-card h3 { display: flex; align-items: center; gap: 8px; margin: 0 0 14px; font-size: 15px; }

/* Ô chọn/dán ảnh gộp 1 khung (paste-zone) — dùng chung index.php + redistribute.php,
   2 trang duy nhất nhận ảnh trực tiếp qua clipboard/file picker theo kiểu này. */
.paste-row { display: flex; gap: 8px; align-items: stretch; margin-top: 8px; }
/* .upload-card .btn có margin-top:20px cho nút submit cuối form — rò rỉ sang mọi
   .btn khác trong card kể cả 2 nút bấm ảnh -> reset riêng cho cả hai. */
#pasteBtn, #pasteChangeBtn { margin-top: 0; }
.paste-row #pasteBtn {
  flex-shrink: 0; align-self: stretch; width: 76px;
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; white-space: normal; text-align: center; line-height: 1.25;
}
.paste-row #pasteBtn .paste-btn-icon { width: 16px; height: 16px; display: inline-flex; }
.paste-row #pasteBtn .paste-btn-icon svg { width: 100%; height: 100%; }
.paste-zone {
  border: 2px dashed var(--border); border-radius: var(--radius-sm);
  padding: 12px; text-align: center; font-size: 12px; color: var(--text-muted); cursor: pointer;
  transition: border-color .15s, background .15s; min-height: 20px;
  display: flex; align-items: center; justify-content: center; flex: 1; min-width: 0;
}
.paste-zone:hover, .paste-zone:focus { border-color: var(--brand); background: var(--brand-soft); outline: none; }
.paste-zone:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.paste-zone.has-image { border-style: solid; border-color: var(--success); justify-content: flex-start; cursor: default; }
.paste-zone.has-image #pasteZoneEmptyText { display: none; }
.paste-zone-empty-icon { width: 15px; height: 15px; display: inline-flex; vertical-align: -3px; margin-right: 2px; }
.paste-zone-empty-icon svg { width: 100%; height: 100%; }
.paste-zone-filled { display: none; align-items: center; gap: 10px; width: 100%; text-align: left; }
.paste-zone.has-image .paste-zone-filled { display: flex; }
.paste-zone-filled img {
  height: 44px; width: auto; max-width: 100px; object-fit: contain; border-radius: var(--radius-sm);
  border: 1px solid var(--border); cursor: zoom-in; flex-shrink: 0; transition: transform .15s ease;
}
.paste-zone-filled img:hover { transform: scale(1.08); }
.paste-zone-filled span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
