/* ============================================================
   Be Vietnam Pro — tự host, subset vietnamese + latin, weight 400/500/600/700/800
   ============================================================ */
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-400-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-500-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-600-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-700-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-800-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Be Vietnam Pro';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('/fonts/be-vietnam-pro-800-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================
   DESIGN TOKENS — 04_Design_System.md
   Lớp 1 (primitive) → lớp 2 (semantic). Component CHỈ dùng semantic.
   ============================================================ */
:root {
  /* --- Primitive: Slate --- */
  --slate-50:  #F8FAFC; --slate-100: #F1F5F9; --slate-200: #E2E8F0;
  --slate-300: #CBD5E1; --slate-400: #94A3B8; --slate-500: #64748B;
  --slate-600: #475569; --slate-700: #334155; --slate-800: #1E293B;
  --slate-900: #0F172A;
  /* --- Primitive: Green (hành động + thương hiệu) --- */
  --green-50: #E9F9EF; --green-100: #D9F2E3; --green-200: #A6E0BC;
  --green-400: #22C55E; --green-500: #16A34A; --green-600: #15803D; --green-700: #0F6B30;
  /* --- Primitive: Court (XANH THẢM SÂN CẦU LÔNG — primary hành động + thương hiệu) ---
     Xanh lá ngả lam, trầm — màu mặt thảm thi đấu BWF/Yonex. Đây là "xanh lá điềm đạm làm neo
     thương hiệu (không phải xanh 'success' chói)" mà 02_Brand_Identity.md § 7.3/§ 8 đã chốt; code
     trước đây dùng --blue-500 nên lệch với brand doc, đợt này đưa về đúng hướng.

     🔴 Ngả LAM là điều kiện, không phải sở thích: brand doc buộc primary phải "tách khỏi
     màu-ngữ-nghĩa-tiền". Chọn một xanh lá thuần (vd #157A47) sẽ gần như trùng --money-positive
     (#15803D) ⇒ nút "Lưu" và con số tiền dương cùng màu trên cùng màn hình.

     Tương phản đã tính theo công thức WCAG, không ước lượng:
       court-500 trên trắng / trắng trên court-500 = 5,29:1 (blue-500 cũ: 5,17:1 — nhỉnh hơn một chút)
       court-700 trên court-100                    = 7,63:1  (chữ mục nav đang chọn; cặp blue cũ: 7,15:1)
       court-400 trên --surface tối (#1E293B)      = 6,68:1  (blue-400 cũ: 5,75:1)
       court-300 trên --surface tối                = 8,74:1  (hover, phải sáng hơn base)
       trắng trên court-600                        = 7,11:1  (nền đặc ở theme tối; blue-600 cũ: 6,70:1)
     court-650 (#0B5A48) + court-800 (#0A493B) thêm 2026-08-27 khi làm NHẠT nền sidebar — chỉ
     phục vụ --nav-bg, số đo tương phản của chúng nằm ở khối --nav-* bên dưới. */
  --court-50: #ECF8F4; --court-100: #D3EDE4; --court-200: #A5DBC8; --court-300: #7FD9BC;
  --court-400: #3EC49E; --court-500: #0E7A5F; --court-600: #0A6450; --court-650: #0B5A48;
  --court-700: #08503F; --court-800: #0A493B; --court-900: #093A2F; --court-950: #06281F;
  /* --- Primitive: Blue (KHÔNG còn là màu thương hiệu — chỉ là một Ô MÀU PHÂN LOẠI) ---
     🔴 Đừng đổi ramp này sang xanh thảm cho "thống nhất": nó đang phục vụ bánh xe màu avatar
     (--c0), .badge-soft.is-blue, .stat-card__icon.is-blue, --status-info và .app-alert.is-info —
     những chỗ mà xanh dương mang NGHĨA (thông tin / một ô trong dải màu phân biệt người), không
     mang thương hiệu. Đổi ở đây là đổi nghĩa, và làm tên biến nói dối. */
  --blue-50: #EFF6FF; --blue-100: #DBEAFE; --blue-200: #BFDBFE; --blue-300: #93C5FD;
  --blue-400: #60A5FA; --blue-500: #2563EB; --blue-600: #1D4ED8; --blue-700: #1E40AF;
  /* --- Primitive: Violet (accent icon tile) --- */
  --violet-500: #7C3AED; --violet-300: #C4B5FD;
  /* --- Primitive: Red (danger / money âm) --- */
  --red-50: #FEF2F2; --red-100: #FBDADA; --red-200: #F3A8A8;
  --red-400: #F87171; --red-500: #DC2626; --red-600: #B91C1C; --red-700: #8A1919;
  /* --- Primitive: Amber (accent ấm) --- */
  --amber-50: #FFF7ED; --amber-100: #FDE5D1; --amber-200: #FBC496;
  --amber-400: #FB923C; --amber-500: #F97316; --amber-600: #EA580C; --amber-700: #7C380B;
  /* --- Primitive: Pink (icon giới tính Nữ trên trang chia sẻ công khai — KHÔNG dùng cho tiền) --- */
  --pink-300: #F9A8D4; --pink-700: #BE185D;

  /* --- Spacing (4px) --- */
  --space-2xs: .25rem; --space-xs: .5rem; --space-sm: .75rem; --space-md: 1rem;
  --space-lg: 1.5rem; --space-xl: 2rem; --space-2xl: 3rem; --space-3xl: 4rem;

  /* --- Radius --- */
  --radius-sm: 8px; --radius-md: 10px; --radius-lg: 16px;
  --radius-btn: 14px; --radius-card: 20px; --radius-pill: 999px;

  /* --- Content width --- */
  --w-narrow: 560px; --w-content: 720px; --w-wide: 960px;

  /* --- Motion --- */
  --motion-instant: 100ms; --motion-fast: 150ms; --motion-base: 250ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);

  /* --- Kích thước khung --- */
  --sidebar-width: 248px; --bottom-nav-height: 73px; --topbar-height: 56px;
  /* Khe hở giữa viên thuốc điều hướng và hai mép NGANG của màn hình (trái · phải). Một nguồn cho
     `padding-inline` của khung nav. */
  --bottom-nav-gap: 12px;
  /* Khe hở DƯỚI, tách khỏi khe ngang (22/09/2026 — người dùng báo nav trông xa mép dưới quá).
     Hai khe không cùng vai trò nên không dùng chung một con số: khe ngang 12px là để nội dung
     cuộn qua thấy được hai bên viên thuốc, còn khe dưới chỉ cần đủ cho bóng đổ + bo góc 20px
     đọc ra là "nổi" chứ không phải "dán mép". 4px làm được việc đó mà nav vẫn sát đáy.
     🔴 Đổi con số này phải đổi luôn --bottom-nav-height ở trên (= khe dưới + thân 69 đo được)
     và giá trị AdSenseAdviceTest chốt — token là CẢ DẢI nav chiếm, không phải chiều cao thân. */
  --bottom-nav-gap-bottom: 4px;
  /* Trần bề ngang viên thuốc lúc THU NHỎ: 5 ô × 56px + 2 × 8px đệm = 296. Là `max-width` chứ không
     phải `width` để nội suy mượt được từ trạng thái giãn (`100%`) — và vì nav chỉ có 4 ô khi người
     dùng không thấy Sổ quỹ, lúc đó 4 ô chia nhau 296px, mỗi ô rộng hơn chứ không hụt vùng chạm. */
  --bottom-nav-compact-max: 296px;
  /* Khe dưới 4px + viên thuốc thu gọn 50px (ô chạm 48px và viền 2px).
     Chỉ các lớp nổi dùng mốc này; phần đệm nội dung vẫn giữ chiều cao nav đầy đủ. */
  --bottom-nav-compact-height: 54px;
  /* Chiều cao nút nổi Góp ý (.bmh-fab) — một nguồn cho cả nút lẫn phần `.page-content` chừa ra
     để dòng cuối trang cuộn lên được khỏi nút. */
  --fab-height: 48px;
  /* 🔴 --bottom-nav-height là cả DẢI CHIỀU DỌC mà .app-bottom-nav chiếm ở đáy màn hình, không phải
     con số thiết kế: khe hở dưới (--bottom-nav-gap-bottom 4) + chiều cao thật đo được của viên
     thuốc (69) = 73. Nav không lấy chiều cao từ token này (nó tự cao theo nội dung: viền 1 + ô
     `min-height: 56px` nhưng thực tế 67,9 vì icon 20px mang line-height của bootstrap-icons)
     — token chỉ dùng để CÁC LỚP KHÁC né nó. Nên token nhỏ hơn dải thật = mọi lớp chồng
     lên trên đều tụt xuống đè vào nav.
     🔴 Token KHÔNG co lại khi nav thu nhỏ (`is-compact`) dù lúc đó viên thuốc chỉ cao 48px. Cố ý:
     nếu `padding-bottom` của .page-content co theo thì toàn trang nhảy mỗi lần người dùng đổi
     hướng vuốt — đúng thứ khó chịu nhất khi đang đọc. Đổi lại là ~21px trống thừa dưới đáy lúc
     compact, mà chỗ đó đang có nội dung cuộn qua nên không ai thấy.
     Trước 2026-08-28 token là 60px trong khi đo được 68,9px ở 390×844 (tools/mobile-audit).
     Chênh 9px đó vô hại với `.page-content`/`.bmh-fab` vì hai chỗ đó còn cộng thêm --space-md
     (16px) nên vẫn dư — nhưng thanh quảng cáo dán đáy KHÔNG cộng gì: nó ngồi đúng
     `bottom: var(--bottom-nav-height)` nên đè lên 9px trên cùng của nav, tức che mất một dải
     của chính vùng chạm điều hướng (đo lại sau khi sửa: nav 775,1–844, quảng cáo 710–784 ⇒
     chồng 8,9px). Sửa ở TOKEN chứ không cộng bù ở từng chỗ: mọi lớp né nav đều đúng một lần. */
  /* Quảng cáo dán đáy màn hình (fragments/ads :: oQuangCaoDinh). Đặt cạnh --bottom-nav-height vì
     cùng loại: kích thước của một lớp cố định mà .page-content và nút Góp ý đều phải né.
     🔴 --sticky-ad-band là chiều cao THẬT của cả thanh và là HỢP ĐỒNG — nút Góp ý và dòng cuối
     trang cộng đúng nó để né. Giá trị init ở đây là bản DESKTOP/TABLET (leaderboard 728×90:
     4 padding + 13 nhãn + 4 + 90 + 1 viền = 112, làm tròn 114); tầng mobile hạ xuống banner 320×50
     bằng cách override đúng ba token này, không sửa luật nào khác.
     🔴 Nhưng con số init chỉ là DỰ ĐOÁN: khổ cuối cùng do AdSense chọn, nên chiều cao thật được
     script trong fragments/ads.html ĐO lại và ghi đè --sticky-ad-band. Đổi khổ quảng cáo giờ không
     còn bắt buộc phải nhớ sửa số ở đây — nhưng vẫn nên sửa cho lần vẽ đầu khỏi giật. */
  --sticky-ad-height: 90px; --sticky-ad-band-init: 114px; --sticky-ad-max: 728px;
  /* 🔴 HAI token cho một chiều cao, vì có hai nguồn sự thật khác nhau:
       --sticky-ad-band-init = chiều cao CSS đoán trước, dùng cho lần vẽ đầu khi quảng cáo
         chưa về. Chỉ CSS đặt, không ai ghi đè.
       --sticky-ad-band      = chiều cao THẬT của thanh, thứ .page-content / nút Góp ý / cột
         dọc cộng vào để né. Khởi đầu bằng init, rồi script trong fragments/ads.html ĐO thanh
         sau khi quảng cáo về và ghi đè inline lên <html>.
     Tách ra vì thanh tự cao theo quảng cáo (min-height: init) — nếu chính nó cũng đọc
     --sticky-ad-band thì sau một quảng cáo 100px thanh không bao giờ co lại được cho quảng
     cáo 50px kế tiếp: min-height đã bị kéo lên 122px và tự giữ mình ở đó. */
  --sticky-ad-band: var(--sticky-ad-band-init);

  /* Hai cột quảng cáo dọc hai bên nội dung (fragments/ads :: oQuangCaoDoc). Chỉ sống ở màn đủ rộng
     — ngưỡng tính ở khối .bmh-ad-side, không phải con số chọn cho đẹp. */
  --side-ad-width: 120px; --side-ad-height: 600px;

  /* --- Sidebar TỐI — độc lập theme sáng/tối, luôn xanh thảm ---
     🔴 Bộ CHỮ phải đổi cùng nền, không được giữ lại bộ xám-lam cũ (#C7D2E4 / #8394B0 / #64789B):
     xám ngả lam đứng trên nền xanh lá cho ra sắc tím bẩn. Bộ dưới đây ngả lục, chọn theo SỐ ĐO chứ
     không theo mắt — tính bằng công thức WCAG, KHÔNG ước lượng bằng mắt.

     🟢 2026-08-27 — nền được LÀM NHẠT theo yêu cầu người dùng, HAI đợt trong cùng một ngày: đợt 1
     court-900 → court-950 thành court-800 → court-900 (+~6 L*, người dùng đo bằng mắt thấy "gần như
     vẫn vậy"), đợt 2 thành court-650 → court-800 (L* 33,7 → 27,1). Tổng cộng đỉnh gradient sáng lên
     12,5 điểm L* so với bản gốc #093A2F. Đỉnh (court-650 #0B5A48) là chỗ nền SÁNG NHẤT nên mọi con
     số dưới đây đo trên nó (worst case):
       --nav-text        6,63:1  (#C6DCD3 → #DCEBE4; giữ nguyên #C6DCD3 chỉ còn 5,67:1)
       --nav-text-muted  5,01:1  (#8FB3A8 → #9CBFB4 → #B7D0C8; giữ #9CBFB4 chỉ còn 4,09:1 — DƯỚI AA)
       --nav-group-label 3,70:1  (#6E9287 → #81A197 → #9AB4AC — bám đúng mức 3,69:1 lịch sử)
       --nav-text-strong 7,81:1  (#F8FAFC giữ nguyên)
     🔴 --nav-text TỤT khỏi AAA (nền gốc 8,79 → 7,19 đợt 1 → 6,63 bây giờ). Vẫn trên AA (4,5) và đây
     là CÓ CHỦ Ý: kéo lại ≥7 phải dùng #E6F0EC (L* 94) — gần trắng hẳn, vừa làm nav mất sắc vừa nuốt
     luôn phân cấp với --nav-text-strong (#F8FAFC, L* 98) khiến hai bậc chữ không còn phân biệt được.
     Nhãn nhóm vốn đã dưới AA từ trước khi đổi màu; ở đây giữ NGUYÊN mức đó, không làm tệ hơn —
     nâng nó lên là việc của một task riêng về tương phản, không nhân tiện làm ở đợt đổi màu.

     🔴 MỤC ĐANG CHỌN ĐỔI TỪ PILL XANH ĐẶC SANG PILL SÁNG — đây là hệ quả BẮT BUỘC của nền nhạt, không
     phải dọn thẩm mỹ tiện tay: nền cũ court-500 chỉ còn nổi 1,54:1 trên nền mới (bản gốc 2,39:1), tức
     gần như tan vào nền. Pill sáng court-100 nổi 6,62:1 trên đỉnh gradient và 8,38:1 ở đáy, chữ+icon
     court-700 trên court-100 đạt 7,63:1 — cao hơn cả cặp trắng/court-500 cũ (5,29:1). Vai trò này
     04_Design_System.md vốn đã ghi sẵn cho court-100 ("nền mục nav đang chọn (sáng)").
     🔴 Đừng "tiết kiệm" bằng cách đẩy pill lên court-400 để giữ nền xanh: trắng trên court-400 chỉ
     2,19:1 (xem ghi chú ở --action-primary-solid).
     Ô dấu thương hiệu (--nav-brand-mark-bg) VẪN là court-500 — BrandMarkTest gác nền ô đúng màu đó,
     và con cầu trắng bên trong vẫn giữ 5,29:1 dù ô bớt nổi trên nền nhạt.
     Đối chiếu bộ chữ xám-lam cũ trên nền navy trước đợt xanh thảm: #C7D2E4 10,81:1 · #8394B0
     5,36:1 · #64789B 3,69:1. */
  --nav-bg:           linear-gradient(180deg, var(--court-650) 0%, var(--court-800) 100%);
  --nav-text:         #DCEBE4;
  --nav-text-strong:  #F8FAFC;
  --nav-text-muted:   #B7D0C8;
  --nav-group-label:  #9AB4AC;
  --nav-border:       rgba(148,163,184,.14);
  --nav-hover-bg:     rgba(148,163,184,.10);
  --nav-active-bg:    var(--court-100);
  --nav-active-text:  var(--court-700);
  --nav-brand-mark-bg: var(--court-500);
}

/* ============================================================
   SEMANTIC — Light (mặc định) + [data-bs-theme="light"]
   ============================================================ */
:root,
[data-bs-theme="light"] {
  --surface:        #FFFFFF;
  --surface-sunken: var(--slate-50);
  --surface-muted:  var(--slate-100);
  --border:         var(--slate-200);
  --border-strong:  var(--slate-300);
  --text:           var(--slate-800);
  --text-strong:    var(--slate-900);
  --text-muted:     var(--slate-600);
  --text-disabled:  var(--slate-400);

  --action-primary:       var(--court-500);
  --action-primary-hover: var(--court-600);
  --action-secondary:     var(--green-500);

  --money-positive: var(--green-600);
  --money-negative: var(--red-500);
  --money-neutral:  var(--slate-800);

  --status-danger:  var(--red-500);
  --status-warning: var(--amber-500);
  --status-info:    var(--blue-500);
  /* 🔴 `--status-warning` là màu VIỀN/NỀN, KHÔNG dùng được làm màu CHỮ ở theme sáng: amber-500
     (#F97316) trên nền trắng chỉ ra 2,8:1 — trượt cả 4,5:1 của chữ thường lẫn 3:1 của chữ ≥20px
     (MB-7, đo ở task 4.6 trên màn Báo cáo công nợ: "CLB nợ lại thành viên" + "CLB nợ họ …₫"). Đây là
     bậc CHỮ của cùng ngữ nghĩa cảnh báo, tách riêng thay vì đổi `--status-warning` — token kia đang
     làm đúng việc của nó ở `border-color` và không có vấn đề tương phản nào ở đó. amber-700 trên
     trắng = 8,65:1. Theme tối có bản đối ứng ngay dưới (khối [data-bs-theme="dark"]). */
  --status-warning-text: var(--amber-700);

  --focus-ring: rgba(14, 122, 95, .38);

  /* Hộp thư thông báo — dòng CHƯA ĐỌC (ADR-22, `TB-4`). Nền phải nhấn đủ để thấy ngay trong
     `.modal-body` của chuông: bản đầu dùng `--bs-tertiary-bg`, gần như trùng nền modal nên hai
     trạng thái trông y hệt nhau (người dùng báo 09/09/2026). court-50 là bậc nhạt nhất của màu
     thương hiệu — nhấn được mà không biến danh sách thành một mảng màu. */
  --tb-unread-bg:       var(--court-50);
  --tb-unread-bg-hover: var(--court-100);
  --tb-unread-bar:      var(--action-primary);

  --sidebar-active-bg:   var(--court-100);
  --sidebar-active-text: var(--court-700);

  --elevation-1: 0 1px 2px rgba(15,23,42,.04), 0 2px 6px rgba(15,23,42,.06);
  --elevation-2: 0 2px 4px rgba(15,23,42,.04), 0 8px 24px rgba(15,23,42,.08);
  --elevation-3: 0 8px 16px rgba(15,23,42,.08), 0 24px 48px rgba(15,23,42,.14);

  /* Badge soft — foreground đổi theo theme, background dùng rgba (chung 2 theme) */
  --badge-green-fg: #0F6B30; --badge-blue-fg: #1E40AF; --badge-red-fg: #8A1919;
  --badge-amber-fg: #7C380B; --badge-slate-fg: #475569;
  /* --badge-pink-fg: var(--pink-700) — icon "Nữ" trên /chia-se/{token}. Trên trắng = 6,03:1 (AA). */
  --badge-pink-fg: var(--pink-700);

  /* --- Ghi đè biến Bootstrap --- */
  --bs-primary: var(--action-primary);
  --bs-primary-rgb: 14, 122, 95;
  --bs-primary-text-emphasis: var(--court-700);
  --bs-primary-bg-subtle: var(--court-100);
  --bs-primary-border-subtle: var(--court-200);
  /* Bootstrap :root bake --bs-link-color: #0d6efd. `a{}` bên dưới đã tự đặt màu, nhưng .btn-link
     và .link-primary đọc BIẾN này chứ không đọc quy tắc `a` ⇒ vẫn ra xanh dương nếu bỏ qua. */
  --bs-link-color: var(--action-primary);
  --bs-link-color-rgb: 14, 122, 95;
  --bs-link-hover-color: var(--action-primary-hover);
  /* 🔴 `<code>` — Bootstrap bake --bs-code-color: #d63384 (hồng). Trên nền TRANG (--surface-sunken
     #F8FAFC, vì app override --bs-body-bg thành nền chìm) ra ĐÚNG 4,30:1 — con số đã gặp 5 lần ở họ
     nút, nay lặp lại ở một element HTML thuần. Đo ở task 6.1 tại `/clb/{id}/cai-dat`, nơi `<code>`
     in MÃ THAM GIA CLB (BR-MEM-13) — chuỗi ngắn, cỡ .fs-5, là thứ Owner đọc để đọc/gõ cho người
     khác, nên đọc sai một ký tự là hỏng việc thật. Trỏ về token app đã có thay vì bịa hex mới:
     light court-500 trên nền chìm = 5,06:1. Theme tối KHÔNG trượt (Bootstrap tự đổi sang #e685b5 =
     7,13:1 trên #0F172A) nhưng vẫn có bản đối ứng ngay dưới (court-400 = 8,16:1) — luật P5: một màu
     chỉ đúng ở một theme thì phải có bản đối ứng ở theme kia, không để rơi về mặc định Bootstrap. */
  --bs-code-color: var(--action-primary);
  --bs-success: var(--green-500);
  --bs-success-rgb: 22, 163, 74;
  --bs-danger: var(--red-500);
  --bs-danger-rgb: 220, 38, 38;
  --bs-warning: var(--amber-500);
  --bs-warning-rgb: 249, 115, 22;
  --bs-info: var(--blue-500);
  --bs-info-rgb: 37, 99, 235;
  --bs-body-bg: var(--surface-sunken);
  --bs-body-color: var(--text);
  --bs-secondary-color: var(--text-muted);
  --bs-border-color: var(--border);
  --bs-border-radius: var(--radius-md);
  --bs-border-radius-sm: var(--radius-sm);
  --bs-border-radius-lg: var(--radius-lg);
  --bs-body-font-family: 'Be Vietnam Pro', system-ui, -apple-system, 'Segoe UI', sans-serif;
}

/* ============================================================
   SEMANTIC — Dark
   ============================================================ */
[data-bs-theme="dark"] {
  --surface:        #1E293B;
  --surface-sunken: #0F172A;
  --surface-muted:  #334155;
  /* P5: #334155 trùng luôn --surface-muted ⇒ viền .data-card gần như biến mất trên nền tối (~1,5:1,
     dưới ngưỡng 3:1 khuyến nghị cho viền mang nghĩa — WCAG 1.4.11). Nới sáng hơn để ranh giới rõ. */
  --border:         #3E4C63;
  --border-strong:  #475569;
  --text:           #E2E8F0;
  --text-strong:    #F8FAFC;
  --text-muted:     #94A3B8;
  --text-disabled:  #64748B;

  --action-primary:       var(--court-400);
  --action-primary-hover: var(--court-300);
  --action-secondary:     var(--green-400);

  --money-positive: #4ADE80;
  --money-negative: #F87171;
  --money-neutral:  #E2E8F0;

  --status-danger:  #F87171;
  --status-warning: #FB923C;
  --status-info:    #60A5FA;
  /* Theme tối KHÔNG có vấn đề: #FB923C trên nền thẻ #1E293B ra 6,46:1 (đo được `tươngphản = 0` ở
     dark trước khi sửa). Vẫn khai tường minh vì spec § 3.5 cấm để một token rơi về giá trị mặc định
     ở một theme — đọc được ngay rằng "ở dark, màu cảnh báo dùng làm chữ được luôn". */
  --status-warning-text: var(--status-warning);

  /* Bản đối ứng theme tối: court-50 (#ECF8F4) trên nền #1E293B là một vệt trắng chói, ngược hẳn ý
     đồ "nhấn nhẹ". Dùng chính màu thương hiệu của theme tối pha loãng — nền sáng hơn `--surface`
     vừa đủ để tách khỏi dòng đã đọc mà không tranh chấp với chữ. */
  --tb-unread-bg:       rgba(62, 196, 158, .13);
  --tb-unread-bg-hover: rgba(62, 196, 158, .20);
  --tb-unread-bar:      var(--court-400);

  --focus-ring: rgba(62, 196, 158, .45);

  --sidebar-active-bg:   rgba(62, 196, 158, .20);
  --sidebar-active-text: var(--court-300);

  --elevation-1: 0 1px 3px rgba(0,0,0,.40), 0 1px 2px rgba(0,0,0,.30);
  --elevation-2: 0 4px 12px rgba(0,0,0,.45), 0 2px 4px rgba(0,0,0,.35);
  --elevation-3: 0 16px 40px rgba(0,0,0,.55), 0 4px 12px rgba(0,0,0,.40);

  --badge-green-fg: #4ADE80; --badge-blue-fg: #93C5FD; --badge-red-fg: #FCA5A5;
  --badge-amber-fg: #FDBA74; --badge-slate-fg: #CBD5E1;
  /* --badge-pink-fg: var(--pink-300) — icon "Nữ" trên nền thẻ tối (#1E293B) = 8,07:1 (AAA). */
  --badge-pink-fg: var(--pink-300);

  /* P5 — nền "mềm" (badge-soft/avatar/app-alert) khai báo dùng chung 2 theme bằng rgba() (xem
     comment ở nơi khai báo .badge-soft, .avatar--cN, .app-alert). Công cụ audit (audit.js `bgOf`) đọc
     RAW kênh r/g/b của rgba() và BỎ QUA alpha (coi như nền ĐẶC nguyên bản), nên báo trượt tương phản
     dù mắt nhìn thật vẫn ổn (nền đã hoà rgba lên --surface trong thực tế). Ở đây viết lại chính giá
     trị ĐÃ HOÀ đó thành số đặc — cho công cụ đo đúng, mắt nhìn không đổi. Tính tay bằng công thức
     alpha*fg + (1-alpha)*30,41,59 (=#1E293B=--surface) cho từng cặp màu+alpha gốc, đã đo lại xác
     nhận contrast 6,9–8,3:1 với --badge-*-fg / --violet-300 hiện có (không cần đổi fg). LIGHT không
     định nghĩa các biến này ⇒ var(...,fallback) ở nơi dùng tự trả về đúng rgba() gốc, không đổi gì. */
  --badge-green-bg:  #1D3A3D;
  --badge-blue-bg:   #1F3154;
  --badge-red-bg:    #392929;
  --badge-amber-bg:  #413535;
  --badge-slate-bg:  #293548;
  --badge-violet-bg: #2B2B54;
  --alert-court-bg:   #1C313F;   /* rgba(14,122,95,.10) đã hoà lên --surface — xem .app-alert.is-court */
  --alert-info-bg:    #1F2F4D;
  --alert-success-bg: #1D353D;
  --alert-warning-bg: #383237;
  --alert-danger-bg:  #312929;

  /* --action-primary (court-400) được chọn để làm CHỮ/ICON nổi trên nền tối (6,68:1) — nhưng vài nơi
     tái dùng nó làm NỀN ĐẶC với chữ trắng đè lên (.nav-pills .nav-link.active, .pagination-bar__btn
     .is-active, .step-flow__item .num): court-400 quá sáng để làm nền cho chữ trắng (trắng/court-400
     chỉ 2,19:1 — blue-400 cũ 2,54:1, cùng một cái bẫy). Token riêng cho vai trò "nền đặc" — dark dùng --court-600 (đã có sẵn trong ramp,
     không phải màu mới) đạt 7,11:1 với chữ trắng; LIGHT không định nghĩa ⇒ fallback về
     --action-primary y hệt trước (light vốn đã dùng court-500 làm cả 2 vai trò ổn, không cần tách).
     🔴 KHÔNG áp dụng cho `.action-bar__primary .btn` (P4, task 0.5) — ngoài phạm vi task này. */
  --action-primary-solid: var(--court-600);

  --bs-primary: var(--action-primary);
  --bs-primary-rgb: 62, 196, 158;
  --bs-primary-text-emphasis: var(--court-300);
  --bs-primary-bg-subtle: rgba(62,196,158,.18);
  --bs-primary-border-subtle: rgba(62,196,158,.32);
  --bs-link-color: var(--action-primary);
  --bs-link-color-rgb: 62, 196, 158;
  --bs-link-hover-color: var(--action-primary-hover);
  /* Bản đối ứng của --bs-code-color ở khối :root (task 6.1): court-400 trên nền chìm tối = 8,16:1. */
  --bs-code-color: var(--action-primary);
  --bs-success: var(--green-400);
  --bs-success-rgb: 34, 197, 94;
  --bs-danger: #F87171;
  --bs-danger-rgb: 248, 113, 113;
  --bs-warning: #FB923C;
  --bs-warning-rgb: 251, 146, 60;
  --bs-info: #60A5FA;
  --bs-info-rgb: 96, 165, 250;
  --bs-body-bg: var(--surface-sunken);
  --bs-body-color: var(--text);
  --bs-secondary-color: var(--text-muted);
  --bs-border-color: var(--border);
  --bs-emphasis-color: var(--text-strong);
}

/* ============================================================
   🔴 GHI ĐÈ COMPONENT BOOTSTRAP BAKE CỨNG HEX PRIMARY (#0d6efd)
   ------------------------------------------------------------
   Đổi `--bs-primary` là KHÔNG ĐỦ. Bootstrap 5.3.3 sinh các component dưới đây bằng mixin, và mixin
   ghi thẳng giá trị hex vào selector chứ KHÔNG tham chiếu `--bs-primary`:

       .btn-primary { --bs-btn-bg: #0d6efd; ... }   ← 11 giá trị hex, không có var() nào

   Hệ quả đo được (2026-08-20, đọc thẳng webjar bootstrap-5.3.3.jar): nút primary của app **chưa bao
   giờ** mang màu token. App khai `--action-primary: #2563EB` nhưng nút thật vẫn là #0d6efd của
   Bootstrap — hai màu xanh dương khác nhau sống song song mà không ai thấy, vì chúng gần giống nhau.
   Chỉ tới khi primary đổi sang XANH LÁ thì sai lệch mới lộ ra: sidebar đổi màu, nút thì không.

   🔴 Đây là lần thứ SÁU của cùng một cái bẫy #0d6efd trong file này (2.1 · 3.3 · 5.6 · nav-tabs ·
   nút Messenger — xem các comment bên dưới). Năm lần trước đều vá theo kiểu SCOPE HẸP cho đúng một
   nút đang trượt AA. Khối này vá tận gốc: liệt kê ĐỦ mọi selector Bootstrap bake hex primary, tìm
   bằng cách grep chính file bootstrap.css chứ không dựa vào trí nhớ.

   ⚠️ Ai nâng cấp Bootstrap: grep lại `#0d6efd` trong bootstrap.css của bản mới rồi đối chiếu danh
   sách này. Selector mới xuất hiện sẽ âm thầm mang màu xanh dương, không có test nào bắt được.
   ============================================================ */

/* --- Nút --- */
.btn-primary {
  --bs-btn-bg: var(--action-primary);
  --bs-btn-border-color: var(--action-primary);
  --bs-btn-hover-bg: var(--action-primary-hover);
  --bs-btn-hover-border-color: var(--action-primary-hover);
  --bs-btn-active-bg: var(--action-primary-hover);
  --bs-btn-active-border-color: var(--action-primary-hover);
  --bs-btn-disabled-bg: var(--action-primary);
  --bs-btn-disabled-border-color: var(--action-primary);
  --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
/* 🔴 Theme tối: --action-primary (court-400) là màu để làm CHỮ trên nền tối, làm NỀN cho chữ trắng
   chỉ được 2,19:1. Nút nền đặc phải dùng --action-primary-solid (court-600, 7,11:1) — cùng lý do đã
   ghi ở chỗ khai báo token đó. Không có dòng này thì nút primary ở dark trượt AA. */
[data-bs-theme="dark"] .btn-primary {
  --bs-btn-bg: var(--action-primary-solid);
  --bs-btn-border-color: var(--action-primary-solid);
  --bs-btn-hover-bg: var(--court-700);
  --bs-btn-hover-border-color: var(--court-700);
  --bs-btn-active-bg: var(--court-700);
  --bs-btn-active-border-color: var(--court-700);
  --bs-btn-disabled-bg: var(--action-primary-solid);
  --bs-btn-disabled-border-color: var(--action-primary-solid);
}
.btn-outline-primary {
  --bs-btn-color: var(--action-primary);
  --bs-btn-border-color: var(--action-primary);
  --bs-btn-hover-bg: var(--action-primary);
  --bs-btn-hover-border-color: var(--action-primary);
  --bs-btn-active-bg: var(--action-primary);
  --bs-btn-active-border-color: var(--action-primary);
  --bs-btn-disabled-color: var(--action-primary);
  --bs-btn-disabled-border-color: var(--action-primary);
  --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
[data-bs-theme="dark"] .btn-outline-primary {
  --bs-btn-hover-bg: var(--action-primary-solid);
  --bs-btn-hover-border-color: var(--action-primary-solid);
  --bs-btn-active-bg: var(--action-primary-solid);
  --bs-btn-active-border-color: var(--action-primary-solid);
}

/* --- Ô nhập: viền focus #86b7fe + ô tích #0d6efd --- */
.form-control:focus,
.form-select:focus,
.form-check-input:focus { border-color: var(--action-primary); }
.form-check-input:checked {
  background-color: var(--action-primary);
  border-color: var(--action-primary);
}
.form-check-input[type=checkbox]:indeterminate {
  background-color: var(--action-primary);
  border-color: var(--action-primary);
}
.form-range::-webkit-slider-thumb { background-color: var(--action-primary); }
.form-range::-moz-range-thumb     { background-color: var(--action-primary); }
/* Trạng thái :active của nút kéo dùng một hex KHÁC (#b6d4fe — bậc nhạt), không phải #0d6efd. Hai
   dòng này do BootstrapPrimaryOverrideTest bắt được; đọc mắt thường đã bỏ sót vì chỉ dò #0d6efd. */
.form-range::-webkit-slider-thumb:active { background-color: var(--court-200); }
.form-range::-moz-range-thumb:active     { background-color: var(--court-200); }

/* --- Component khác đặt hex qua biến RIÊNG của chúng (không phải --bs-primary) --- */
.dropdown-menu { --bs-dropdown-link-active-bg: var(--action-primary-solid, var(--action-primary)); }
.nav-pills     { --bs-nav-pills-link-active-bg: var(--action-primary-solid, var(--action-primary)); }
.pagination    { --bs-pagination-active-bg: var(--action-primary-solid, var(--action-primary));
                 --bs-pagination-active-border-color: var(--action-primary-solid, var(--action-primary)); }
.list-group    { --bs-list-group-active-bg: var(--action-primary-solid, var(--action-primary));
                 --bs-list-group-active-border-color: var(--action-primary-solid, var(--action-primary)); }
.progress, .progress-stacked { --bs-progress-bar-bg: var(--action-primary); }

/* ============================================================
   P5 — Dark: fix riêng cho vài component KHÔNG dùng token màu theo cách bình thường
   (Bootstrap bake literal hex ở .btn-outline-secondary; --nav-group-label cố định 2 theme).
   Chỉ áp trong [data-bs-theme="dark"] — LIGHT không đụng tới (MB-8).
   ============================================================ */
/* .btn-outline-secondary của Bootstrap bake CỨNG #6c757d vào --bs-btn-color/--bs-btn-border-color
   ngay trên chính selector (không tham chiếu --bs-secondary) — override --bs-primary/--bs-danger...
   ở :root KHÔNG áp dụng vào đây (đã lộ y hệt ở task 0.5 với .btn-outline-info/.btn-outline-primary).
   #6c757d trên nền tối (--surface/--surface-sunken) chỉ ra 3,12–3,81:1. Dùng --text-muted (đã có,
   đạt 5,7–7:1 với 2 nền đó) thay vì bịa màu mới. */
[data-bs-theme="dark"] .btn-outline-secondary {
  --bs-btn-color: var(--text-muted);
  --bs-btn-border-color: var(--text-muted);
  --bs-btn-disabled-color: var(--text-disabled);
  --bs-btn-disabled-border-color: var(--text-disabled);
}
/* .btn-outline-danger cũng bake CỨNG #dc3545 (--bs-danger của Bootstrap, KHÁC --red-500 #DC2626 của
   app) vào --bs-btn-color/--bs-btn-border-color — cùng cơ chế .btn-outline-secondary ở trên, override
   --bs-danger ở :root KHÔNG tới được. #dc3545 trên nền tối (--surface #1E293B) chỉ 3,23:1 — đo ở nút
   "Vô hiệu hoá tài khoản của tôi" (/toi = auth/ho-so.html) theme dark, task 1.7. Dùng --red-400
   (#F87171) = chính màu danger dark app đã chọn cho --money-negative/--status-danger ⇒ 5,29:1. Hover
   nền đặc --red-600 để chữ trắng đạt ~6,5:1. Chỉ dark; light giữ nguyên #dc3545 (đạt AA ⇒ MB-8 an toàn). */
/* 🔴 Task 1.7 kết luận "light giữ nguyên #dc3545 (đạt AA)" — đúng với NGỮ CẢNH nó đo (nút nằm trong
   .app-card nền trắng: #dc3545/#FFFFFF = 4,53:1, vừa đủ). Đo ở task 5.2 tại `member/sua.html` cho ngữ
   cảnh KHÁC: nút "Cho rời CLB" nằm trong .page-header, tức đứng TRỰC TIẾP trên nền trang
   (--surface-sunken = --slate-50 #F8FAFC) ⇒ chỉ còn **4,33:1**, dưới AA. Cùng cơ chế đã gặp ở task 3.3
   với .btn-outline-primary: một màu "vừa đủ đạt" trên nền trắng sẽ trượt ngay khi đổi sang nền chìm.
   Dùng --red-500 (#DC2626 — chính màu danger của app, không bịa màu mới): **4,62:1**; hover nền đặc
   --red-600 với chữ trắng ~7,6:1. Scope theo 2 ngữ cảnh có nền CHÌM — .page-header và .modal-footer
   (nút "Cho rời CLB" xuất hiện ở CẢ HAI: một nút mở modal trên header, một nút submit trong chân
   modal; đo lần đầu chỉ sửa .page-header thì nút trong modal vẫn 4,33 — bắt được nhờ đo lại với
   BMH_OPEN). Trong card trắng nút này vẫn đạt AA nên không đổi hàng loạt. Đặt TRƯỚC luật dark ngay
   dưới để dark vẫn thắng theo thứ tự nguồn (cùng độ đặc hiệu) và giữ --red-400 của task 1.7.
   🔴 Đây là thay đổi thấy được cả trên DESKTOP: AA không phụ thuộc bề rộng màn hình nên không thể sửa
   riêng cho mobile — cùng lập luận đã ghi ở toast (task 1.2) và .btn-outline-primary (task 3.3). */
.page-header .btn-outline-danger,
.modal-footer .btn-outline-danger {
  --bs-btn-color: var(--red-500);
  --bs-btn-border-color: var(--red-500);
  --bs-btn-hover-bg: var(--red-600);
  --bs-btn-hover-border-color: var(--red-600);
  --bs-btn-active-bg: var(--red-600);
  --bs-btn-active-border-color: var(--red-600);
}
/* Cùng CHÍNH XÁC cơ chế đó cho .btn-outline-secondary, chỉ khác con số: #6c757d đạt 4,69:1 trên nền
   trắng của card nhưng chỉ **4,48:1** trên nền chìm --slate-50 (#F8FAFC) — nền của .page-header và của
   .modal-footer. Mục này đã bị ghi nhận rồi bỏ qua ở 0.6 · 3.4 · 3.5 · 4.1 · 5.1 với lý do "marginal
   Bootstrap default"; đến task 5.2 thì nó rơi đúng vào nút "Hủy" của modal đang đo nên xử luôn, bằng
   ĐÚNG cách theme tối đã xử ở task 0.6: dùng --text-muted (light = --slate-600 #475569) ⇒ **7,24:1**.
   Vẫn scope theo ngữ cảnh nền chìm — trong card trắng nút này đạt AA, không có lý do đổi hàng loạt.
   🔴 Task 4.5 (lần ghi nhận thứ 7 của đúng lỗi này): nút "Tháng trước"/"Tháng sau"/"Xem sổ giao dịch"
   ở báo cáo tài chính đứng TRỰC TIẾP trên nền trang — cũng là nền chìm, cũng 4,48:1, nhưng không nằm
   trong `.page-header` lẫn `.modal-footer` nên 2 selector trên không với tới. Thêm `is-on-sunken` làm
   móc dùng chung cho đúng tình huống đó, thay vì đẻ thêm một selector theo tên trang mỗi lần gặp lại
   (đã gặp ở 0.6 · 3.4 · 3.5 · 4.1 · 5.1 · 5.2 · 4.5). Màn sau gặp nút outline-secondary nằm ngoài
   card thì gắn class này, KHÔNG viết luật mới. */
.page-header .btn-outline-secondary,
.modal-footer .btn-outline-secondary,
.btn-outline-secondary.is-on-sunken {
  --bs-btn-color: var(--text-muted);
  --bs-btn-border-color: var(--text-muted);
}
/* 🔴 `.btn-outline-info` — cùng CƠ CHẾ bake hex, và là ca NẶNG NHẤT của cả họ: đã bị ghi nhận rồi bỏ
   qua từ task 0.5. Bootstrap bake `#0dcaf0` (cyan) thẳng vào chữ/viền, nên `--bs-info:
   var(--blue-500)` mà app khai ở `:root` (dòng ~239) CHƯA BAO GIỜ tới được một nút nào — y hệt chuyện
   `--action-primary` với `#0d6efd` ở khối trên. Đo ở task 5.12: chữ `#0dcaf0` trên nền trắng chỉ
   **1,96:1** — thấp nhất trong mọi mục `MB-7` của Đợt 0-5 (kỷ lục cũ là 2,80:1 của `--status-warning`
   bị dùng làm màu chữ). Sửa = trỏ về đúng token app ĐÃ khai, không bịa hex mới:
     • light: chữ `--status-info` (= `--blue-500` #2563EB) trên trắng = **5,17:1**, trên nền chìm
       #F8FAFC = 4,94:1 — đạt AA ở cả hai nền
     • dark:  chữ `--status-info` #60A5FA trên `--surface` #1E293B = **5,75:1**
   Còn dùng ở: "Vote tham gia" · "Chia sẻ buổi tập" · "Chia sẻ bảng chia tiền" · các LỐI VÀO của luồng
   đăng ký ở `session/buoi.html` + `dashboard/cua-toi.html` — blue ở đó vẫn đúng nghĩa "thông tin
   trung tính" (xem ghi chú ở ramp Blue, dòng ~115).
   🔴 Thay đổi này THẤY ĐƯỢC TRÊN DESKTOP — đúng tiền lệ toast 1.2 / CTA 2.1 / nav-tabs G2: AA không
   phụ thuộc bề rộng màn hình nên không nhét được vào tầng mobile.
   🔴 `.btn-info` (nền đặc) đã từng được sửa ở đây rồi GỠ ĐI ở vòng 2 của task 5.12: người dùng chốt
   luồng "Đăng ký đi tập" chuyển hẳn sang XANH THẢM (xem `.btn-court-soft` ngay dưới), nên không
   template nào còn dùng `.btn-info` — giữ lại chỉ là code chết. Bài học của lần đó vẫn đáng nhớ:
   **nhóm nút NỀN ĐẶC có HAI màu ràng buộc nhau** — Bootstrap để `--bs-btn-color: #000` cho `.btn-info`
   vì cyan rất sáng, nên đổi nền sang một màu đậm mà quên chữ thì chữ đen trên nền đó ra 4,06:1, tức
   vừa sửa xong một vi phạm AA đã đẻ ngay một vi phạm khác ở đúng nút đó.
   `.action-bar__primary .btn` (tầng mobile) tự ép màu riêng nên không bị ảnh hưởng. */
.btn-outline-info {
  --bs-btn-color: var(--status-info);
  --bs-btn-border-color: var(--status-info);
  --bs-btn-hover-bg: var(--blue-500);
  --bs-btn-hover-border-color: var(--blue-500);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--blue-500);
  --bs-btn-active-border-color: var(--blue-500);
  --bs-btn-active-color: #fff;
  --bs-btn-disabled-color: var(--status-info);
  --bs-btn-disabled-border-color: var(--status-info);
}
[data-bs-theme="dark"] .btn-outline-info {
  --bs-btn-hover-bg: var(--blue-600);
  --bs-btn-hover-border-color: var(--blue-600);
  --bs-btn-active-bg: var(--blue-600);
  --bs-btn-active-border-color: var(--blue-600);
}
/* 🔴 `.btn-court-soft` — nút của luồng "Đăng ký đi tập" (`OQ-54`), thêm ở vòng 2 task 5.12 theo quyết
   định của người dùng: luồng này mang XANH THẢM như phần còn lại của app, không còn xanh dương.
   Vì sao là bậc SOFT (nền tint) chứ không phải `.btn-primary` nền đặc — đây là ràng buộc, không phải
   thẩm mỹ: `AC-SESS-03.6` (`20_Requirements.md`) buộc giao diện phải phân biệt **rõ ràng bằng mắt**
   "Đã đăng ký" với "Đã điểm danh", **không cùng icon, không cùng màu** — và đó là một AC về giao diện
   nhưng nó BẢO VỆ TIỀN (nhầm hai danh sách = tính tiền cho người không đi). Cho "Tôi sẽ đi" nền đặc
   `--action-primary` là biến nó thành đúng cái nút mà mọi màn khác dùng cho hành động chính, kể cả
   luồng điểm danh ⇒ mất luôn tín hiệu mà AC này đòi. Bậc soft giữ được ba việc cùng lúc:
     • cùng HỆ MÀU thương hiệu (court) — thứ người dùng yêu cầu;
     • khác hẳn nút "Có mặt" của màn Điểm danh (nút đó là `.btn-success`, ramp GREEN `#16A34A` nền đặc)
       và khác nút hành động chính `--action-primary` nền đặc — thoả `AC-SESS-03.6`;
     • nói đúng nghĩa "DỰ KIẾN, chưa tính tiền": trọng lượng thị giác nhẹ hơn một hành động đã chốt.
   Tương phản tính theo WCAG, không ước lượng:
     light  court-700 trên court-100 = 7,63:1 · hover court-700 trên court-200 = 6,08:1
     dark   court-300 trên court-900 = 7,57:1 · hover court-300 trên court-700 = 5,63:1 */
.btn-court-soft {
  --bs-btn-bg: var(--court-100);
  --bs-btn-border-color: var(--court-200);
  --bs-btn-color: var(--court-700);
  --bs-btn-hover-bg: var(--court-200);
  --bs-btn-hover-border-color: var(--court-300);
  --bs-btn-hover-color: var(--court-700);
  --bs-btn-active-bg: var(--court-200);
  --bs-btn-active-border-color: var(--court-300);
  --bs-btn-active-color: var(--court-700);
  --bs-btn-disabled-bg: var(--court-100);
  --bs-btn-disabled-border-color: var(--court-200);
  --bs-btn-disabled-color: var(--court-700);
}
[data-bs-theme="dark"] .btn-court-soft {
  --bs-btn-bg: var(--court-900);
  --bs-btn-border-color: var(--court-700);
  --bs-btn-color: var(--court-300);
  --bs-btn-hover-bg: var(--court-700);
  --bs-btn-hover-border-color: var(--court-600);
  --bs-btn-hover-color: var(--court-300);
  --bs-btn-active-bg: var(--court-700);
  --bs-btn-active-border-color: var(--court-600);
  --bs-btn-active-color: var(--court-300);
  --bs-btn-disabled-bg: var(--court-900);
  --bs-btn-disabled-border-color: var(--court-700);
  --bs-btn-disabled-color: var(--court-300);
}
[data-bs-theme="dark"] .btn-outline-danger {
  --bs-btn-color: var(--red-400);
  --bs-btn-border-color: var(--red-400);
  --bs-btn-hover-bg: var(--red-600);
  --bs-btn-hover-border-color: var(--red-600);
  --bs-btn-active-bg: var(--red-600);
  --bs-btn-active-border-color: var(--red-600);
}
/* --nav-group-label (#64789B) là hằng số DÙNG CHUNG 2 theme, định cho nền navy cố định của sidebar
   desktop (--nav-bg) — nhưng cùng class còn được dùng lại trong offcanvas menu mobile, nơi nền theo
   theme (.offcanvas{background:var(--surface)}, dòng ~879). Trên --surface tối, #64789B chỉ ra
   3,28:1. Dark-only: đổi sang --text-muted (đã qua theme, đạt AA trên --surface tối). Light giữ
   nguyên --nav-group-label (không thuộc phạm vi task này). */
[data-bs-theme="dark"] .app-sidebar-group-label {
  color: var(--text-muted);
}
/* Task 2.1 — nút CTA "Xem cách hoạt động" (.auth-cta-landing) là .btn-outline-primary. Bootstrap bake
   CỨNG #0d6efd vào --bs-btn-color/border (không lấy --bs-primary app đã override ở :root — cùng cơ chế
   .btn-outline-secondary/danger ở trên). #0d6efd trên nền vỏ auth --surface-sunken chỉ **4,3:1 (light)**
   / **3,97:1 (dark)** — DƯỚI AA ở CẢ hai theme (đo tĩnh, task 2.1). Đổi sang --action-primary (đã theo
   theme: light --court-500 #0E7A5F ⇒ 5,29:1; dark --court-400 #3EC49E ⇒ 6,68:1 — trước đợt đổi sang
   xanh thảm là --blue-500/--blue-400, cùng mức tương phản nên kết luận AA của task 2.1 không đổi);
   hover nền đặc --action-primary-solid (dark = --court-600) cho chữ trắng đạt. Đây là fix AA THẬT nên áp mọi viewport
   (AA độc lập bề rộng — như ca toast task 1.2), CÓ đổi desktop; scope theo .auth-cta-landing để chỉ
   chạm đúng nút này, KHÔNG lan ra .btn-outline-primary trên nền trắng (ở đó #0d6efd vẫn đạt AA). */
.auth-cta-landing .btn-outline-primary {
  --bs-btn-color: var(--action-primary);
  --bs-btn-border-color: var(--action-primary);
  --bs-btn-hover-bg: var(--action-primary-solid, var(--action-primary));
  --bs-btn-hover-border-color: var(--action-primary-solid, var(--action-primary));
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--action-primary-solid, var(--action-primary));
  --bs-btn-active-border-color: var(--action-primary-solid, var(--action-primary));
  --bs-btn-active-color: #fff;
}
/* Đợt 3 task 3.3 — "Thêm người khác"/"Thêm khách vãng lai" (attendance/fragments.html) là
   .btn-outline-primary đứng TRỰC TIẾP trên nền trang (--surface-sunken, không phải trong .app-card
   trắng) — CÙNG hoàn cảnh và CÙNG số đo với .auth-cta-landing ở trên (#0d6efd bake cứng: 4,3:1 light /
   3,97:1 dark, cả hai DƯỚI AA — đã đo ở task 0.5, để dành "đợt sau" xử, nay đúng lúc). Fix giống hệt,
   scope riêng để KHÔNG lan ra .btn-outline-primary ở chỗ khác (trong .app-card trắng #0d6efd vẫn ổn,
   9 template khác dùng class này chưa đo, không tự ý đổi hàng loạt). */
.attendance-quick-add .btn-outline-primary {
  --bs-btn-color: var(--action-primary);
  --bs-btn-border-color: var(--action-primary);
  --bs-btn-hover-bg: var(--action-primary-solid, var(--action-primary));
  --bs-btn-hover-border-color: var(--action-primary-solid, var(--action-primary));
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--action-primary-solid, var(--action-primary));
  --bs-btn-active-border-color: var(--action-primary-solid, var(--action-primary));
  --bs-btn-active-color: #fff;
}
/* Task 5.6 — nút "Thêm sân" của fragment dùng chung `court-slots :: editor` cũng là
   .btn-outline-primary, và nó nằm trên `.modal-body` (nền `--surface-sunken` #F8FAFC) ⇒ **cùng con số
   4,3:1** đã đo hai lần trước (2.1 và 3.3). Đây là lần thứ BA của đúng một lỗi, nên fix y hệt và scope
   bằng attribute sẵn có `[data-court-slots]` — không phải thêm class vào template, và một lần sửa ăn
   cả **4 form** dùng fragment này (thêm/sửa Lịch tập · thêm/sửa Buổi tập). Là fix AA THẬT nên áp mọi
   viewport (AA không phụ thuộc bề rộng); `MB-8` của màn Lịch tập vẫn byte-identical vì nút này nằm
   trong modal đang đóng (`display: none`, không vẽ pixel nào ở ảnh desktop). */
[data-court-slots] .btn-outline-primary,
/* Lần thứ NĂM của đúng lỗi #0d6efd-trên-nền-chìm (2.1 · 3.3 · 5.6 · nav-tabs). Nút "Nhắn tin trực
   tiếp qua Messenger" trong modal Góp ý cũng nằm trên `.modal-body` (--surface-sunken #F8FAFC) ⇒
   ĐÚNG 4,30:1 như ba lần trước, đo bằng tools/mobile-audit ở 390×844 (2026-08-20).
   Gộp vào cùng khối thay vì viết luật thứ năm y hệt. 🔴 Ghi chú cho lần sau: `.btn-outline-secondary`
   đã có móc dùng chung `is-on-sunken` sau 7 lần lặp; `.btn-outline-primary` nay cũng tới lần thứ 5 —
   đáng làm móc tương tự trong một task dọn dẹp riêng, thay vì cứ thêm selector theo tên component. */
#modalGopY .btn-outline-primary {
  --bs-btn-color: var(--action-primary);
  --bs-btn-border-color: var(--action-primary);
  --bs-btn-hover-bg: var(--action-primary-solid, var(--action-primary));
  --bs-btn-hover-border-color: var(--action-primary-solid, var(--action-primary));
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--action-primary-solid, var(--action-primary));
  --bs-btn-active-border-color: var(--action-primary-solid, var(--action-primary));
  --bs-btn-active-color: #fff;
}
/* Lần thứ TƯ của đúng một lỗi #0d6efd-trên-nền-chìm (2.1 · 3.3 · 5.6 ở trên), lần này qua
   --bs-nav-link-color chứ không phải --bs-btn-color: tab CHƯA chọn của .nav-tabs đo được **4,30:1**
   (#0d6efd trên --surface-sunken #F8FAFC) ở màn Chi tiết buổi tập — dưới AA 4,5:1. Task 3.3 đã thấy
   đúng con số này ("phát hiện phụ, KHÔNG sửa — để lại cho lần sau chạm tới CSS thanh tab"); đo lại khi
   đóng khoảng hở G2 (2026-08-20) xác nhận nó vẫn còn, nên nay xử.
   Scope `.nav-tabs` là hẹp CÓ ĐO: chỉ 2 template dùng class này (`session/buoi.html` +
   `expense/fragments.html`, đều là thanh tab của chính màn buổi tập). KHÔNG lan sang `.nav-pills`
   (`fragments/tabs.html`, màn Cài đặt) — kiểu active của nó khác hẳn (nền đặc) và CHƯA ĐO, đổi mù là
   đúng thứ CLAUDE.md cấm. Tab ĐANG chọn không bị chạm (nó dùng --bs-nav-tabs-link-active-color).
   Là fix AA THẬT ⇒ áp mọi viewport, CÓ đổi desktop — giống ca toast 1.2 / CTA auth 2.1.
   🔴 Đổi lần hai 12/10/2026 — bản vá trên chữa đúng tương phản nhưng để lại THỨ BẬC NGƯỢC: tab chưa
   chọn mang màu thương hiệu, còn tab ĐANG chọn giữ màu mặc định của Bootstrap (đen trên nền xám), nên
   mắt bị kéo về chỗ mình không đứng. Nay đảo lại: tab đang chọn mới là thứ mang màu thương hiệu, tab
   chưa chọn lùi về --text-muted. Vẫn qua AA ở cả hai theme (slate-600 trên --surface-sunken sáng
   ≈ 7,2:1; #94A3B8 trên #0F172A tối ≈ 7:1), nên không mở lại lỗi 4,30:1 ở trên. */
.nav-tabs {
  --bs-nav-link-color: var(--text-muted);
  --bs-nav-link-hover-color: var(--text-strong);
  --bs-nav-tabs-link-active-color: var(--action-primary);
}
/* Hệ quả của việc đảo màu ở trên: màu tab KHOÁ của Bootstrap (--bs-secondary-color) trong app này
   trùng đúng --text-muted, nên tab chưa mở được ("Chi phí & chốt sổ" khi buổi chưa điểm danh) nhìn y
   hệt tab bấm được. Làm mờ đi để còn phân biệt — trạng thái khoá không thuộc diện phải qua AA. */
.nav-tabs .nav-link.disabled { opacity: .5; }

/* ============================================================
   BASE
   ============================================================ */
/* Luôn chừa chỗ cho thanh cuộn dọc — thiếu dòng này, trang có nội dung ngắn (không cần cuộn) sẽ
   nhích ngang vài px so với trang có nội dung dài (có thanh cuộn) vì layout mất/còn khác nhau bề
   rộng viewport khả dụng. Rõ nhất khi chuyển qua lại giữa các tab cùng một nhóm (VD: Cài đặt). */
html {
  scrollbar-gutter: stable;
}
/* scrollbar-gutter:stable chỉ áp dụng khi overflow ở trạng thái auto/scroll — Bootstrap Modal khoá
   cuộn nền bằng cách set body.style.overflow = 'hidden' (bootstrap.bundle.js ScrollBarHelper), làm
   "gutter" vừa chừa sẵn ở trên BIẾN MẤT đúng lúc đó, ĐỒNG THỜI Bootstrap còn tự cộng thêm
   padding-right = bề rộng thanh cuộn vào <body> để "bù" cho thanh cuộn biến mất — cộng dồn hai hiệu
   ứng này khiến bề rộng nội dung hụt hẳn một khoảng bằng bề rộng thanh cuộn (~15-17px) trong lúc
   modal mở, nội dung (căn giữa bằng margin auto trong .page-content) nhích ngang, rồi trả lại đúng vị
   trí cũ khi đóng modal — đã đo trực tiếp bằng CDP (BEFORE/AFTER getBoundingClientRect lệch, dừng
   lệch khi bỏ dòng padding-right này). Phần bù của Bootstrap thành thừa vì gutter đã tự lo chuyện
   này rồi — vô hiệu hoá nó bằng !important (thắng inline style Bootstrap tự set qua JS). */
body.modal-open {
  padding-right: 0 !important;
}
/* Các hộp thoại Bootstrap thường dùng .modal-dialog trống; căn giữa chúng theo cùng
   cơ chế của .modal-dialog-centered. Sheet và hộp toàn màn hình giữ vị trí riêng. */
.modal:not(.modal-sheet) > .modal-dialog:not(.modal-dialog-centered):not(.modal-sheet):not(.modal-fullscreen) {
  display: flex;
  align-items: center;
  min-height: calc(100% - var(--bs-modal-margin) * 2);
}
body {
  background: var(--surface-sunken);
  color: var(--text);
}
h1, h2, h3, h4, h5, h6 { font-weight: 700; color: var(--text-strong); }
h1 { font-size: 1.5rem; line-height: 2rem; letter-spacing: -.01em; }
h2 { font-size: 1.25rem; line-height: 1.75rem; }
h3 { font-size: 1.125rem; line-height: 1.625rem; font-weight: 600; }
a { color: var(--action-primary); }
a:hover { color: var(--action-primary-hover); }

/* Focus ring thống nhất — không bao giờ mất dấu focus */
a:focus-visible,
button:focus-visible,
.btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible,
.stat-card:focus-visible,
.app-sidebar-nav .nav-link:focus-visible,
.app-bottom-nav-item:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--focus-ring);
}

/* Input ≥16px trên mobile — chống Safari iOS auto-zoom */
.form-control, .form-select { font-size: 1rem; background: var(--surface); color: var(--text); }

/* ============================================================
   TYPOGRAPHY UTILITIES
   ============================================================ */
.text-app-muted { color: var(--text-muted) !important; }
.text-caption {
  font-size: .75rem; line-height: 1rem; font-weight: 600;
  letter-spacing: .02em; text-transform: uppercase; color: var(--text-muted);
}

/* Số tiền — luôn tabular-nums, và không bao giờ bẻ dòng: "1.575.000" rớt chữ "₫"
   xuống dòng dưới đọc như hai con số khác nhau. */
.money { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* white-space cùng lý do với .money ở trên: chuỗi chữ số không có chỗ ngắt, nên chỗ DUY NHẤT bẻ
   được là khoảng trắng trước "₫" — bẻ ở đó làm "1.575.000" và "₫" nằm hai dòng, đọc như hai con số. */
.money-hero {
  font-size: 1.75rem; line-height: 2.25rem; font-weight: 700;
  font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--money-neutral);
}
.money-lg {
  font-size: 1.25rem; line-height: 1.75rem; font-weight: 700;
  font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--money-neutral);
}
.money-pos { color: var(--money-positive) !important; }
.money-neg { color: var(--money-negative) !important; }
.money-neutral { color: var(--money-neutral) !important; }

/* ------------------------------------------------------------
   Dòng "tên sân + khoảng giờ" — court-slot.
   🔴 Cạm bẫy đứng ngay cạnh `.money` ở trên, nên để luật ngay đây: `nowrap` là ĐÚNG cho
   con số, nhưng một giá trị TRỘN chữ tự do với số thì không được thừa hưởng nó. Tên sân
   do người dùng tự đặt, tới 100 ký tự (`CourtSlotForm`); gộp "tên: 19:00–21:00" thành
   một chuỗi rồi gắn `.money` là ép cả cụm nằm lì một dòng.
   Đã lộ thật ở 3 chỗ cùng render dữ liệu này (2026-08-25), đo với tên 100 ký tự:
     • `session/buoi.html` (Thông tin buổi tập) — 1440×900: chữ vọt khỏi cột lưới 800px,
       đẩy CẢ TRANG cuộn ngang 644px. Nặng nhất vì cột `row-cols-md-4` rộng cố định 208px.
     • `fragments/buoi-hero.html` (chip trang chia sẻ) — 390×844: chip vọt khỏi thẻ 236px.
     • `schedule/lich-tap.html` (ô Giờ) — 390×844: ô cắt cụt 201px chữ.
   Cách sửa nằm ở TEMPLATE, không phải ở đây: tách tên và giờ thành hai <span>, `.money`
   chỉ bọc khoảng giờ. Class này lo phần còn lại — cho bẻ giữa từ khi tên không có dấu
   cách nào (`anywhere` chứ không `break-word`, cùng lý do đã ghi ở `can-wrap-note`).
   ------------------------------------------------------------ */
.court-slot { overflow-wrap: anywhere; }
/* Chip mang `nowrap` từ CHÍNH `.badge-soft` (không chỉ từ `.money`) nên phải gỡ riêng, và nó là
   inline-flex ⇒ cần `flex-wrap` thì hai mảnh bên trong mới xuống hàng được thay vì phình ngang. */
.badge-soft.court-slot { white-space: normal; flex-wrap: wrap; max-width: 100%; }

/* ============================================================
   BADGE — soft (thay bg-* đặc mặc định Bootstrap)
   ============================================================ */
.badge-soft {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .2rem .55rem; border-radius: var(--radius-pill);
  font-size: .75rem; font-weight: 600; line-height: 1.2; white-space: nowrap;
}
/* P5: nền dùng var(--badge-*-bg, rgba(...)) — chỉ định nghĩa ở [data-bs-theme="dark"] (xem chỗ khai
   báo), nên LIGHT vẫn ra đúng rgba() y hệt trước (fallback), không đổi 1 pixel nào. */
.badge-soft.is-green { background: var(--badge-green-bg, rgba(22,163,74,.14));  color: var(--badge-green-fg); }
.badge-soft.is-blue  { background: var(--badge-blue-bg, rgba(37,99,235,.14));  color: var(--badge-blue-fg); }
.badge-soft.is-red   { background: var(--badge-red-bg, rgba(220,38,38,.14));  color: var(--badge-red-fg); }
.badge-soft.is-amber { background: var(--badge-amber-bg, rgba(249,115,22,.16)); color: var(--badge-amber-fg); }
.badge-soft.is-slate { background: var(--badge-slate-bg, rgba(100,116,139,.16)); color: var(--badge-slate-fg); }

/* Bộ đếm của tab Điểm danh ("N người tham gia" / "N đã điểm danh" / "N nam · N nữ"). Trước đây cỡ
   chữ + padding nằm ở `style=` ngay trong template; đưa ra đây vì tầng mobile cần bóp padding lại mà
   inline style thì @media không đè được. Cỡ chữ .95rem là CỐ Ý to hơn badge thường (.75rem) và cố ý
   giữ nguyên ở mobile — đây là con số Admin đọc từ xa khi đang đứng ở sân (CT-6), không phải chip
   phụ; nó đè luật `.badge-soft { font-size: var(--fs-caption) }` của tầng mobile nhờ specificity. */
.attendance-counter .badge-soft { font-size: .95rem; padding: .35rem .7rem; }
/* Khoảng cách của hai hàng đầu tab Điểm danh (bộ đếm và cụm nút "Thêm …"). Trước đây là utility
   `gap-1 gap-sm-2` / `gap-2` ngay trong template; chuyển ra đây vì utility của Bootstrap là
   `!important` nên tầng mobile KHÔNG đè nổi để đặt `column-gap: 0` — cùng cái bẫy đã ghi ở
   `.lich-buoi` và `.lich-header-actions` phía trên. `.5rem` là ĐÚNG giá trị cũ ở mọi bề rộng mà
   luật này còn hiệu lực: `gap-sm-2` (≥576px) và `gap-2`. Bậc 4px của `gap-1` (<576px) không cần
   chép lại — dải đó đã nằm trọn trong tầng ≤767,98px, nơi `column-gap` bị đặt về 0 để `space-evenly`
   chia đều. Nhờ vậy desktop ≥768px không đổi một pixel (`MB-8`). */
.attendance-counter, .attendance-quick-add { gap: .5rem; }

/* Khối "Hoạt động đăng ký gần đây" là đích anchor của thông báo (BR-NOTI-16): trình duyệt cuộn tới
   nó khi bấm thông báo. Chừa chỗ cho thanh trên cùng dính của điện thoại để tiêu đề không bị che. */
#hoatDongDangKy { scroll-margin-top: 4.5rem; }

/* Chấm trạng thái bên trong chip (VD "Đang sinh hoạt"). Màu kế thừa currentColor của chip. */
.badge-soft .dot { width: .5rem; height: .5rem; border-radius: 50%; background: currentColor; flex: none; }

/* Chữ màu theo status (VD giờ buổi tập ở màn Lịch) — dùng chung biến màu với .badge-soft cho nhất quán. */
.text-status-green { color: var(--badge-green-fg); }
.text-status-amber { color: var(--badge-amber-fg); }
.text-status-slate { color: var(--badge-slate-fg); }
/* `blue` = buổi ĐÃ CHỐT SỔ, tách khỏi nhóm xanh lá "đang mở" từ 2026-09-06 (TrainingSessionStatus
   #badgeTone). Bốn nhóm màu của màn Lịch phải có đủ bốn lớp ở CẢ BA họ (.badge-soft / .text-status
   / .legend-dot) — thiếu một lớp thì chữ giờ trong ô lịch rơi về màu mặc định và trông như lỗi. */
.text-status-blue  { color: var(--badge-blue-fg); }
/* Cố ý KHÔNG dùng .text-danger của Bootstrap cho việc này: nó ghim cứng #dc3545 ở CẢ hai theme
   (chỉ nền mới đổi theo data-bs-theme), nên trên nền tối chữ tụt tương phản. Biến ở đây có sẵn
   bản sáng/tối. */
.text-status-red   { color: var(--badge-red-fg); }

/* Chấm tròn chú thích màu status (VD legend phía trên bảng Lịch). */
.legend-dot { display: inline-block; width: .6rem; height: .6rem; border-radius: 50%; }
.legend-dot.is-green { background: var(--badge-green-fg); }
.legend-dot.is-amber { background: var(--badge-amber-fg); }
.legend-dot.is-blue  { background: var(--badge-blue-fg); }
.legend-dot.is-slate { background: var(--badge-slate-fg); }

/* Ô ngày bấm-được (Owner/Admin) — bấm vùng trống mở modal tạo buổi đột xuất. Dấu "+" mờ hiện khi
   rê chuột (desktop có hover); màn hình cảm ứng không hiện nhưng cả ô vẫn chạm được. `position:
   relative` để neo dấu "+" tuyệt đối vào góc phải-trên ô. */
.lich-day--clickable { position: relative; cursor: pointer; }
.lich-day--clickable::after {
    content: "\002B"; /* dấu + */
    position: absolute; top: 2px; right: 6px;
    font-size: .95rem; line-height: 1; font-weight: 700;
    color: var(--bs-primary); opacity: 0; pointer-events: none;
    transition: opacity .12s ease-in-out;
}
@media (hover: hover) {
    .lich-day--clickable:hover::after { opacity: .55; }
}

/* Lưới lịch tháng — chuyển từ style inline sang class ở task 5.11, GIỮ NGUYÊN từng giá trị cũ.
   Lý do phải chuyển: tầng mobile cần nới chiều cao ô để mỗi buổi tập đạt vùng chạm 44px (`MB-3`),
   mà style inline thì chỉ `!important` mới đè được — và `!important` trong tầng mobile là thứ luật
   này cấm (nó khoá luôn mọi lần chỉnh sau). `height` trên `<td>` là chiều cao TỐI THIỂU theo cách
   trình duyệt tính bảng; `.lich-day` là hộp cuộn bên trong để một ngày nhiều buổi không đội cả hàng
   tuần cao lên. */
/* Hộp cuộn ngang bao quanh lưới tháng. Trước 20/09/2026 là `style="overflow-x: auto"` inline trên
   thẻ `.app-card`; tách ra thành class vì JS cần một móc để TÌM đúng hộp này (`bmhCuonLichToiHomNay()`
   ở `fragments/layout :: scripts` canh cột hôm nay vào giữa nó). Giá trị y hệt style cũ ⇒ không đổi
   một pixel nào ở mọi bề rộng; việc cuộn ngang vẫn là ngoại lệ có chủ đích của `MB-1` (xem tầng
   mobile bên dưới). */
.lich-cuon { overflow-x: auto; }
.lich-grid > tbody > tr > td { width: 14.28%; height: 110px; overflow: hidden; }
.lich-grid .lich-day { max-height: 94px; overflow-y: auto; }
/* Nút "một buổi tập" trong ô ngày. Trước đây dùng `.d-block w-100` của Bootstrap, nhưng utility đó là
   `!important` nên tầng mobile KHÔNG đè được — luật `display: flex` viết cho nó ở tầng mobile (task
   5.11) thực ra chưa bao giờ chạy, và chữ giờ nằm dính mép TRÊN của vùng chạm 44px thay vì căn giữa.
   Một class thường (không !important) trả lại quyền đè cho tầng mobile. Desktop: y hệt `.d-block
   w-100` cũ.
   🔴 `padding`/`margin-top` cũng phải khai ở đây, KHÔNG để lại utility `p-0 mt-1` trên thẻ như trước
   (2026-08-30, lúc tách nút này ra fragment dùng chung với lưới TUẦN): `p-0` là `!important` nên
   biến thể `.lich-buoi--day-du` — chip lưới tuần, cần đệm quanh chữ — không thể có padding nào khác
   0 nếu utility còn nằm trên thẻ. Hai giá trị dưới đây bằng ĐÚNG `p-0` (0) và `mt-1` (.25rem) cũ,
   nên lưới tháng không đổi một pixel. */
.lich-grid .lich-buoi { display: block; width: 100%; padding: 0; margin-top: .25rem; }
/* Hàng nút của header lịch. `flex-wrap`/`gap` khai ở đây thay vì bằng utility `.flex-wrap`/`.gap-2`
   (đúng giá trị cũ: wrap + .5rem) — utility của Bootstrap là `!important`, tầng mobile sẽ không đè
   nổi để ép bốn nút về một dòng. */
.lich-header-actions, .lich-header-actions__nav { flex-wrap: wrap; gap: var(--space-xs, .5rem); }

/* Nút "tạo buổi" TƯỜNG MINH (chế độ Ngày) — dùng chung móc JS `.lich-day--clickable[data-day]` với
   ô ngày trên lưới, nhưng nó đã có nhãn chữ nên dấu "+" mờ (gợi ý cho vùng trống) là thừa và còn
   đè lên chữ. */
.lich-day--clickable.lich-day--nut::after { content: none; }

/* Hàng nút cuối màn Lịch: "Tạo buổi ngày dd/MM" (chỉ chế độ Ngày, chỉ Owner/Admin) + "Xem dạng danh
   sách". Hai nút phải nằm trên CÙNG MỘT dòng ở mọi bề rộng — `nowrap` là phần cưỡng chế, không có nó
   chỉ cần một nhãn dài hơn dự tính là hàng vỡ đôi. `space-between` đẩy mỗi nút dính một mép nội dung
   (trái/phải), khoảng trống dồn hết vào giữa; `gap` vì thế chỉ còn là SÀN tối thiểu, giữ để hai nút
   không chạm nhau nếu nhãn dài ra. Ở chế độ Tuần/Tháng hàng chỉ còn một nút — `space-between` với một
   phần tử để nó dính mép trái, đúng chỗ cũ trước khi gộp hàng.
   `gap`/`flex-wrap` khai bằng class thường thay vì utility `gap-2`/`flex-nowrap` của Bootstrap
   (utility là `!important`, tầng mobile sẽ không đè nổi để hạ cỡ chữ) — cùng lý do với
   `.lich-header-actions` ở trên. */
.lich-footer-actions { flex-wrap: nowrap; gap: var(--space-xs, .5rem); justify-content: space-between; }

/* Hàng công cụ của màn Lịch: bộ chọn chế độ (Ngày · Tuần · Tháng) bên trái, chú thích màu status bên
   phải. Cùng lý do với `.lich-header-actions` ở trên — `gap`/`flex-wrap`/`display` khai bằng class
   thường thay vì utility `d-flex`/`gap-*` của Bootstrap, để tầng mobile còn đè được (ở đó bộ chọn
   chiếm trọn bề ngang). Giá trị `gap` của `.lich-legend` bằng đúng `gap-3` cũ của hàng chú thích. */
.lich-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-xs, .5rem); }
.lich-legend { display: flex; flex-wrap: wrap; gap: var(--space-md, 1rem); }
/* Nút chế độ đang chọn: `.active` của Bootstrap trên `.btn-outline-secondary` đổi nền thành xám đặc.
   Đây là bộ lọc CHÍNH của màn, nên dùng màu thương hiệu để nó đọc được ngay là "đang xem cái nào" —
   cùng cách trạng thái chọn được đánh dấu ở nav. */
.lich-mode .btn.active { background: var(--bs-primary); border-color: var(--bs-primary); color: #fff; }

/* ------------------------------------------------------------
   Lưới TUẦN (chế độ `?cheDo=tuan`, 2026-08-30). Cố ý KHÔNG dùng `<table>` như lưới tháng: tuần chỉ
   có một hàng, nên grid CSS cho phép tầng mobile đổi thẳng `grid-template-columns` về một cột — 7
   ngày xếp chồng thành một lịch trình đọc dọc — mà không phải phá cấu trúc bảng (thứ đã buộc lưới
   THÁNG phải chấp nhận cuộn ngang, xem ghi chú ở tầng mobile).
   `minmax(0, 1fr)`: thiếu `minmax` thì cột grid lấy `min-width: auto`, và một tên sân dài sẽ phình
   cột đó ra, đẩy cả lưới rộng hơn thẻ chứa.
   ------------------------------------------------------------ */
.lich-tuan { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--space-2xs, .25rem); }
.lich-tuan__cot { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
/* Đầu cột XẾP DỌC (thứ ở trên, ngày ở dưới) chứ không nằm ngang: cột tuần hẹp nhất đo được ~93px
   (máy tính bảng dọc 768px, đúng mép breakpoint), mà "Hôm nay 31/08" cần ~95px ⇒ riêng cột hôm nay
   tự xuống hai dòng và đầu cột đó cao hơn 6 cột còn lại, làm nội dung bên trong lệch hàng. Xếp dọc
   là hai dòng ở MỌI cột, mọi bề rộng — cao bằng nhau một cách tất định, và cũng đúng khuôn quen
   thuộc của lịch tuần (tên thứ ở trên, ngày ở dưới). Ở điện thoại cột trải hết bề ngang nên tầng
   mobile lật lại thành một hàng. */
.lich-tuan__dau {
  display: flex; flex-direction: column; gap: 0;
  padding: .3rem .45rem; background: var(--surface-sunken);
  border-bottom: 1px solid var(--border); font-size: var(--fs-caption, .8125rem);
}
.lich-tuan__thu { font-weight: 700; }
.lich-tuan__than { flex: 1 1 auto; padding: .3rem; min-height: 5rem; }
/* Cột hôm nay: viền màu thương hiệu, cùng tín hiệu với ô "hôm nay" của lưới tháng (`border-primary`
   `border-2` ở template). `box-shadow` inset thay vì tăng `border-width` để không làm lệch bề rộng
   cột so với 6 cột còn lại. */
.lich-tuan__cot.is-today { border-color: var(--bs-primary); box-shadow: inset 0 0 0 1px var(--bs-primary); }
.lich-tuan__cot.is-today .lich-tuan__dau { background: var(--badge-green-bg, rgba(22,163,74,.14)); color: var(--badge-green-fg); }
/* Chip buổi tập ở lưới tuần — ba dòng (giờ · sân · trạng thái) thay vì một dòng giờ như lưới tháng:
   cột tuần rộng gấp đôi ô lưới tháng nên đủ chỗ, và đó chính là lý do người dùng chọn chế độ Tuần. */
.lich-tuan .lich-buoi--day-du {
  display: flex; flex-direction: column; align-items: flex-start; gap: .1rem;
  width: 100%; padding: .3rem .4rem; margin-top: .25rem;
  border-radius: var(--radius-sm); background: var(--surface-sunken);
}
.lich-tuan .lich-buoi--day-du:hover { background: var(--surface-muted); }
/* Tên sân trong chip: kẹp 2 dòng. Tên do người dùng tự đặt tới 100 ký tự — ở cột tuần rộng ~127px
   nó ăn 5-6 dòng và đẩy chip cao gấp ba những chip khác, làm hàng tuần so le. Bản đầy đủ vẫn còn
   nguyên trong modal xem-nhanh (bấm chính chip đó), nên không mất thông tin — chỉ là chip tóm tắt,
   giống việc lưới THÁNG không hiện tên sân chút nào. */
.lich-tuan .lich-buoi__san {
  line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ============================================================
   CARD & COMPONENT
   ============================================================ */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-1);
}
.app-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  box-shadow: var(--elevation-1);
}

/* Thẻ vừa được tạo ở request ngay trước (màn "Lời mời") — chỉ đổi VIỀN chứ không đổi nền: thẻ chứa
   một ô <input readonly> trắng, tô nền thẻ sẽ làm ô input nổi lên trông như lỗi hiển thị. Dùng token
   --action-primary (đã có bản đối ứng ở [data-bs-theme="dark"]), không hardcode màu. */
.app-card.is-highlight {
  border-color: var(--action-primary);
  box-shadow: 0 0 0 1px var(--action-primary), var(--elevation-1);
}

/* Radius mới theo DESIGN_RULES — card mềm hơn, nút/ô nhập bo 14px */
.card, .app-card, .stat-card, .empty-state, .auth-card { border-radius: var(--radius-card); }
.btn { border-radius: var(--radius-btn); }

/* ============================================================
   PHẢN HỒI NHẤN — dùng chung cho mọi thứ bấm được
   ============================================================ */
/* Bootstrap tắt -webkit-tap-highlight-color ở <html>, và :active của .btn chỉ đổi màu nền đi một
   bậc — trên điện thoại gần như không thấy. Hệ quả đo 12/10/2026: chạm vào nút, mục bottom nav hay
   thẻ bấm được thì KHÔNG có gì xảy ra cho tới khi trang đổi; cả file chỉ có 8 luật :active, đều cục
   bộ. Một cú lún nhỏ ngay lúc ngón tay CHẠM XUỐNG (không đợi nhấc lên) lấp đúng khoảng lặng đó —
   cùng ý với việc fast-nav.js tô sáng mục menu ở pointerdown.
   🔴 Chỉ `transform`: không đổi bố cục, không đẩy phần tử bên cạnh, và chạy trên compositor.
   🔴 `.btn` phải khai LẠI đủ bốn thuộc tính màu của Bootstrap: `transition` không cộng dồn giữa các
   luật, viết mỗi `transform` là mất hết chuyển màu khi hover. */
.btn {
  transition: color var(--motion-fast) ease-in-out,
              background-color var(--motion-fast) ease-in-out,
              border-color var(--motion-fast) ease-in-out,
              box-shadow var(--motion-fast) ease-in-out,
              transform var(--motion-instant) ease-out;
}
.btn:active { transform: scale(.97); }
/* Nút dính liền nhau (btn-group, nút gắn vào ô nhập) mà lún riêng từng mảnh thì hở khe ở chỗ nối. */
.btn-group > .btn:active,
.input-group > .btn:active { transform: none; }
/* Bộ chọn có thẻ `a` vừa để loại bản khoá (`div.stat-card--locked`, không bấm được), vừa để thắng
   translateY(-1px) của .stat-card:hover — trên máy tính con trỏ luôn hover lẫn active cùng lúc. */
a.stat-card:active { transform: scale(.985); }
.app-bottom-nav-item:active { transform: scale(.94); }
.bmh-fab:active { transform: scale(.95); }
/* Người bật "giảm chuyển động": bỏ cú lún, phản hồi còn lại là đổi màu nền sẵn có của Bootstrap. */
@media (prefers-reduced-motion: reduce) {
  .btn:active,
  a.stat-card:active,
  .app-bottom-nav-item:active,
  .bmh-fab:active { transform: none; }
}
.form-control, .form-select { border-radius: var(--radius-btn); padding: .625rem .85rem; }

/* Tom Select — ô tìm kiếm cho mọi select.form-select (CLAUDE.md § 2). tom-select.bootstrap5.css tự
   đọc --bs-body-bg/--bs-border-color, nhưng --bs-body-bg ở app này trỏ --surface-sunken (nền TRANG,
   dòng 196/257) chứ không phải --surface (nền Ô NHẬP mà .form-control/.form-select dùng ở dòng 310)
   — không override thì ô Tom Select lệch màu nền so với mọi input khác trên cùng form. */
.ts-wrapper.form-select .ts-control {
  background: var(--surface);
  color: var(--text);
  border-color: var(--border);
  border-radius: var(--radius-btn);
  padding: .625rem .85rem;
}
/* Trạng thái tải nằm cạnh nhãn ô chọn, không đẩy các trường trong popup.
   Thông báo lỗi hoặc danh sách trống vẫn hiện dưới ô chọn khi cần. */
#modalDieuChinh .fund-correction__status:empty { display: none; }
#modalDieuChinh .ts-wrapper.disabled .ts-control,
#modalDieuChinh .form-select:disabled {
  background: var(--surface);
  opacity: 1;
}
/* 🔴 Ô tìm kiếm của Tom Select không được chiếm chỗ trong .ts-control khi danh sách ĐANG ĐÓNG.
   Mặc định thư viện chỉ đưa input ra khỏi luồng (class input-hidden) khi ô MẤT focus — inputState()
   gate bằng `!isFocused`. Nhưng ngay sau khi người dùng chọn một dòng, dropdown đã đóng mà ô VẪN đang
   focus, nên input còn nằm trong luồng flex với `margin: 0 4px !important`
   (.has-items .ts-control > input) và `min-width: 7rem` + `flex-wrap: wrap` (.ts-control) ⇒
     (a) luôn thừa một dấu cách 4px ngay sau chữ,
     (b) nhãn dài thì input WRAP xuống dòng 2, đội ô cao thêm đúng một dòng.
   Đo bằng Chrome headless/CDP ở viewport 390px (2026-08-09): .ts-control cao 44px → 68px ngay sau khi
   chọn "Chị Hằng (Quản trị viên CLB Cầu Giấy)", và cũng 68px khi mở lại ô đã có giá trị. Lòng ô ở
   390px chỉ còn 314px (padding-right 36px do --ts-pr-caret ép !important, cộng padding .85rem ở trên)
   nên nhãn dài hơn ~198px là wrap — nhãn thật trong app vượt ngưỡng này dễ dàng (vd option sổ quỹ
   "dd/MM — mô tả — 500.000 ₫").
   Điều kiện đúng là "danh sách đang đóng" (:not(.dropdown-active)), KHÔNG phải "mất focus".
   ⚠️ KHÔNG sửa bằng plugin 'dropdown_input' của thư viện: nó dời ô tìm kiếm ra ngoài .modal và bị
   _enforceFocus của Bootstrap chặn — xem ghi chú ở fragments/layout.html. */
.ts-wrapper.single.has-items:not(.dropdown-active) .ts-control > input {
  position: absolute;
  left: -10000px;
  opacity: 0;
}
/* Danh sách ĐANG MỞ thì input phải gõ được thật: nhường trọn dòng cho nó bằng cách ẩn nhãn đã chọn.
   Không làm vậy thì nhãn + input(min 7rem) vẫn wrap thành 2 dòng như cũ, còn ép min-width: 0 thì input
   chỉ còn ~6px, không nhìn thấy mình đang gõ gì. Giá trị hiện tại vẫn tra được: nó được tô sáng trong
   danh sách ngay bên dưới. */
.ts-wrapper.single.has-items.dropdown-active .ts-control > .item {
  display: none;
}
/* 🔴 Nguyên nhân THỨ HAI, độc lập với ô tìm kiếm ở trên: nhãn đã chọn tự wrap vì thư viện để
   `white-space: normal`. Đo trong modal ở 390px (2026-08-09): lòng ô chỉ còn 288.4px mà nhãn "Chị Hằng
   (Quản trị viên CLB Cầu Giấy)" cần 300.5px ⇒ 2 line box, .item cao 24px → 48px ⇒ ô 68px. Ở 1280px thì
   lòng ô 414.4px nên vừa 1 dòng, đó là lý do lỗi chỉ lộ trên máy nhỏ.
   Cắt bằng ellipsis thay vì để wrap: ô nhập luôn cao đúng một dòng như mọi .form-control khác trên cùng
   form, giống hành vi của <select> gốc. Giá trị đầy đủ vẫn đọc được khi mở danh sách (dòng đang chọn
   được tô sáng).
   `display: block` là BẮT BUỘC: thư viện đặt `.ts-control .item { display: flex }`, mà text-overflow
   không áp dụng cho chữ trực tiếp trong flex container. `min-width: 0` để item (là flex item của
   .ts-control) co được xuống dưới bề rộng nội dung — thiếu nó thì flex item không chịu co và vẫn tràn. */
.ts-wrapper.single .ts-control > .item {
  display: block;
  min-width: 0;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ts-wrapper.form-select.is-invalid .ts-control {
  border-color: var(--bs-danger);
}
.ts-dropdown {
  background: var(--surface);
  color: var(--text);
  border-color: var(--border);
  /* dropdownParent: 'body' (layout.html) append .ts-dropdown làm con trực tiếp của <body> — CSS mặc
     định của tom-select.bootstrap5.css chỉ đặt z-index: 10 cho selector này (bản có class
     ".form-select" đi kèm mới lên 1000, nhưng Tom Select KHÔNG tự copy class đó sang dropdown trừ khi
     bật copyClassesToDropdown, mặc định false). Bootstrap modal ở app này dùng --bs-modal-zindex:
     1055 ⇒ thiếu dòng này, danh sách tìm kiếm bị modal đè lên, thấy list nhưng bấm chọn không ăn
     (bug thật đã gặp: mở modal "Tạo buổi tập đột xuất", không chọn được Sân). +5 để chắc chắn nổi
     trên modal dù giá trị --bs-modal-zindex có đổi. */
  z-index: calc(var(--bs-modal-zindex, 1055) + 5);
}
.ts-dropdown .option.active {
  background: var(--surface-muted);
}

/* Danh sách được LẬT LÊN trên ô vì không đủ chỗ bên dưới (bmhDatViTriDropdown() ở layout.html).
   `margin: 0.25rem 0 0` mặc định của tom-select là khoảng hở cho trường hợp thả xuống dưới; giữ
   nguyên nó khi lật lên thì danh sách bị đẩy xuống đúng 4px và chạm vào ô. Đổi khoảng hở sang cạnh
   dưới — JS đã trừ sẵn 4px khi tính `top`, hai thứ phải khớp nhau. */
.ts-dropdown.bmh-dropdown-lat-len {
  margin-top: 0;
  margin-bottom: 0.25rem;
}

/* ══ Tom Select — chế độ NHIỀU lựa chọn (<select multiple>) ═══════════════════════════════════
   Dùng ở "Thêm người khác" (Điểm danh) và "Đăng ký hộ" (Đăng ký đi tập). Mọi luật dưới đây là để
   ghi đè CSS mặc định của thư viện, không phải trang trí:

   (1) MÀU CHIP BỊ HARDCODE. tom-select.bootstrap5.css đặt thẳng
       `.ts-wrapper.multi .ts-control > div { background: #efefef; color: #343a40 }` và
       `.remove { border-left: 1px solid #dee2e6 }` — số hex cứng, KHÔNG đọc biến theme. Ở theme tối
       chip thành mảng xám sáng chữ đen giữa ô nền tối, và `MB-7` (tương phản AA cả hai theme) hỏng.
       Selector ở đây dùng `.item` (0,4,0) nên thắng `> div` (0,3,1) của thư viện.

   (2) PADDING CỦA Ô. `.ts-wrapper.multi.has-items .ts-control` (0,3,0) của thư viện thắng
       `.ts-wrapper.form-select .ts-control` (0,2,0) ở khối trên ⇒ ô multi khi ĐÃ có chip lại lệch
       padding so với mọi .form-control khác cùng form. Phải khai riêng ở đúng độ đặc hiệu đó.

   (3) `padding-right: 0 !important` trên .item là của plugin remove_button — đừng chống lại nó:
       nút × tự mang padding của mình và nằm sát mép phải chip, đó là bố cục thư viện thiết kế sẵn.

   (4) TRÀN NGANG (`MB-1`). Tên thật dài ("Chị Hằng (Quản trị viên CLB Cầu Giấy)") vượt lòng ô
       288px ở 390px. KHÔNG cắt bằng ellipsis như select đơn: ở đây người dùng cần đọc đủ tên để
       biết mình đã chọn đúng ai. Cho chữ xuống dòng TRONG chip (`white-space: normal` +
       `overflow-wrap: anywhere`) và chặn `max-width: 100%` — chip cao thêm chứ không đẩy ngang.

   (5) CHIỀU CAO Ô. Chọn 10-20 người thì ô dài bằng cả màn hình, nút "Thêm" bị đẩy khỏi tầm nhìn và
       không ai biết là còn nút. Cap chiều cao rồi cho cuộn DỌC bên trong (cuộn dọc có chủ ý, không
       vi phạm MB-1 vốn chỉ nói về chiều ngang). */
.ts-wrapper.multi.form-select .ts-control,
.ts-wrapper.multi.form-select.has-items .ts-control {
  padding: .45rem .5rem;
  gap: .3rem;
  max-height: 11rem;
  overflow-y: auto;
}
.ts-wrapper.multi .ts-control > .item {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  min-width: 0;
  margin: 0;                       /* khoảng cách do `gap` ở .ts-control lo, tránh cộng dồn hai lớp */
  padding: .2rem 0 .2rem .6rem;    /* padding-right bị plugin ép 0 !important — xem (3) */
  background: var(--surface-muted);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  font-size: .9375rem;             /* 15px — trên ngưỡng 13px của MB-2 */
  line-height: 1.35;
  white-space: normal;
  overflow-wrap: anywhere;
}
.ts-wrapper.multi .ts-control > .item.active {
  background: var(--surface-muted);
  color: var(--text);
  border-color: var(--border);
}
.ts-wrapper.multi .ts-control > .item > .remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  align-self: stretch;
  min-width: 2rem;
  margin-left: .4rem;
  padding: 0 .35rem;
  border-left: 1px solid var(--border);
  border-radius: 0 var(--radius-btn) var(--radius-btn) 0;
  color: var(--text-muted);
  font-size: 1.1rem;
  line-height: 1;
  text-decoration: none;
}
.ts-wrapper.multi .ts-control > .item > .remove:hover {
  background: var(--surface);
  color: var(--bs-danger);
}
/* Ô tìm kiếm ở chế độ multi phải LUÔN nhìn thấy (khác select đơn — nơi nó bị đẩy ra khỏi luồng khi
   danh sách đóng): nó chính là chỗ gõ để chọn thêm người kế tiếp. */
.ts-wrapper.multi .ts-control > input {
  min-width: 6rem;
  font-size: .9375rem;
}

/* 🔴 KHÔNG có biến thể riêng cho điện thoại — chip ở mọi bề ngang trông GIỐNG HỆT nhau (người dùng
   chốt 2026-08-19). Bản đầu tiên cho mỗi chip chiếm trọn một dòng với nút × 44×44 để qua `MB-3`,
   nhưng tên ngắn ("Chi Lan") để lại một khoảng trống dài tới tận nút × ở mép phải — nhìn rời rạc,
   không còn ra hình cái chip. Đã đảo lại theo yêu cầu.
   ⚠️ Đánh đổi ĐÃ BIẾT, đừng "sửa" lại mà không hỏi: nút × giờ ~32×30px ⇒ `tools/mobile-audit` sẽ
   đếm nó vào `smallTargets` và `MB-3` (vùng chạm ≥ 44×44) BÁO ĐỎ ở màn Điểm danh và Đăng ký đi tập.
   Đây là ngoại lệ có chủ ý, không phải sót. Bỏ nhầm một người thì chọn lại được ngay ở cùng ô nên
   hậu quả nhẹ; muốn quay lại bố cục cũ thì khôi phục khối @media trong lịch sử git của file này. */

/* Statistic Card — cả card là link về nguồn (PP-3) */
.stat-card {
  display: block; height: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  box-shadow: var(--elevation-1);
  text-decoration: none; color: inherit;
  transition: box-shadow var(--motion-fast) var(--ease-standard),
              border-color var(--motion-fast) var(--ease-standard),
              transform var(--motion-fast) var(--ease-standard);
}
/* Hover chỉ dành cho thiết bị CÓ con trỏ. Trên cảm ứng, trình duyệt giữ :hover ở phần tử vừa chạm
   cho tới khi chạm chỗ khác ⇒ thẻ/nút "dính" ở trạng thái nhấc lên sau khi đã buông tay. */
@media (hover: hover) {
  .stat-card:hover {
    border-color: var(--border-strong);
    box-shadow: var(--elevation-2);
    transform: translateY(-1px);
    color: inherit;
  }
}
.stat-card__label {
  display: flex; align-items: center; gap: .4rem;
  font-size: .75rem; font-weight: 600; letter-spacing: .02em;
  text-transform: uppercase; color: var(--text-muted);
  margin-bottom: var(--space-xs);
}
.stat-card__label i { font-size: 1rem; }
.stat-card__value { margin-bottom: var(--space-2xs); }
.stat-card__meta { font-size: .875rem; color: var(--text-muted); }
.stat-card__meta .arrow { color: var(--action-primary); }

/* Thẻ CLB đang chờ duyệt (BR-MEM-13, Đ8) — không click được, không giống link thường. */
.stat-card--locked {
  opacity: .6;
  cursor: default;
}
.stat-card--locked:hover {
  border-color: var(--border);
  box-shadow: var(--elevation-1);
  transform: none;
}

/* Biểu đồ doanh thu 12 tháng (`/admin/doanh-thu`) — cột dựng bằng div, KHÔNG nạp thư viện chart.
   Lý do: <script> ở cấp trang không chạy khi điều hướng bằng hx-boost (xem templates/CLAUDE.md),
   nên thư viện phải nạp động từ footer chung như /js/report-charts.js — cái giá đó không đáng cho
   12 cột không tương tác. Chiều cao cột do server tính (RevenueMonthPoint.tyLeCot), template chỉ
   bơm vào `height: N%`: Thymeleaf không phải chia, và không có phép chia cho 0 khi cả 12 tháng rỗng.
   Bề rộng cột theo flex chứ không px ⇒ tự vừa từ 360px tới màn rộng, không cần @media riêng. */
.revenue-chart {
  display: flex; align-items: stretch; gap: var(--space-2xs);
  height: 11rem; margin-bottom: var(--space-sm);
}
.revenue-chart__col {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: var(--space-2xs);
}
.revenue-chart__track { flex: 1 1 auto; width: 100%; display: flex; align-items: flex-end; }
.revenue-chart__bar {
  width: 100%; min-height: 2px;
  background: var(--action-primary);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
/* Tháng không thu được đồng nào vẫn phải có vạch — cột cao 0 trông y hệt "thiếu dữ liệu". */
.revenue-chart__bar.is-empty { background: var(--border); }
.revenue-chart__label { font-size: .8125rem; color: var(--text-muted); }

/* Đầu card có ô icon bo góc màu (kiểu FastClub) */
.stat-card__head { display: flex; align-items: center; gap: .75rem; margin-bottom: var(--space-sm); }
.stat-card__icon {
  width: 2.75rem; height: 2.75rem; border-radius: 12px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center; font-size: 1.3rem;
}
/* Biến thể nhỏ cho hàng chỉ số THAM KHẢO (Báo cáo tài chính): giữ nguyên bộ màu is-* bên dưới,
   chỉ hạ kích thước để cụm nhãn không lấn chỗ của số tiền ở khổ máy tính bảng. */
.stat-card__icon--sm { width: 2rem; height: 2rem; border-radius: 9px; font-size: 1rem; }
.stat-card__icon.is-blue   { background: rgba(37,99,235,.12);  color: var(--blue-600); }
.stat-card__icon.is-green  { background: rgba(22,163,74,.12);  color: var(--green-600); }
.stat-card__icon.is-amber  { background: rgba(249,115,22,.14); color: var(--amber-600); }
.stat-card__icon.is-violet { background: rgba(124,58,237,.12); color: var(--violet-500); }
.stat-card__icon.is-red    { background: rgba(220,38,38,.12);  color: var(--red-500); }
[data-bs-theme="dark"] .stat-card__icon.is-blue   { color: #93C5FD; }
[data-bs-theme="dark"] .stat-card__icon.is-green  { color: #4ADE80; }
[data-bs-theme="dark"] .stat-card__icon.is-amber  { color: #FDBA74; }
[data-bs-theme="dark"] .stat-card__icon.is-violet { color: var(--violet-300); }
[data-bs-theme="dark"] .stat-card__icon.is-red    { color: #FCA5A5; }

/* Club pill — bộ chọn CLB góc phải header (kiểu FastClub) */
.page-header__aside { display: flex; align-items: center; gap: .6rem; }
.club-pill {
  display: inline-flex; align-items: center; gap: .55rem;
  padding: .5rem .85rem; border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface); color: var(--text); text-decoration: none;
  font-weight: 600; font-size: .9rem; box-shadow: var(--elevation-1);
}
.club-pill:hover { border-color: var(--border-strong); color: var(--text); }
.club-pill .lead-ic { color: var(--action-primary); }
.club-pill .chev { color: var(--text-muted); font-size: .8rem; }
.club-pill__logo { width: 1.1rem; height: 1.1rem; border-radius: 50%; object-fit: cover; }
.club-list__logo { width: 1.5rem; height: 1.5rem; border-radius: 50%; object-fit: cover; flex: 0 0 auto; }
/* Kết quả tra mã ở modal "Tham gia CLB" (BR-MEM-22, /js/tra-cuu-clb.js). Logo 48px — đủ to để nhận
   ra đúng CLB trên điện thoại, nhỏ hơn thế thì logo chữ không đọc được. Tên CLB dài xuống dòng chứ
   không cắt: đây là chỗ người dùng xác nhận "đúng CLB chưa", cắt mất chữ là mất đúng thứ cần đọc. */
.join-preview { font-size: .875rem; }
.join-preview.is-muted { color: var(--text-muted); }
.join-preview.is-error, .join-preview__note.is-error { color: var(--status-danger); }
.join-preview__card { display: flex; align-items: center; gap: .75rem; padding: .625rem .75rem;
  border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-sunken); }
.join-preview__logo { width: 3rem; height: 3rem; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
  background: var(--surface); border: 1px solid var(--border); }
.join-preview__logo--icon { display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.375rem; color: var(--text-muted); }
.join-preview__text { min-width: 0; }
.join-preview__name { font-weight: 600; color: var(--text-strong); overflow-wrap: anywhere; }
.join-preview__note { color: var(--text-muted); font-size: .8125rem; }
/* Biến thể chỉ-hiển-thị: định danh CLB trong header trang, không bấm được (đổi CLB dùng switcher navbar). */
.club-pill--static { cursor: default; }
.club-pill--static:hover { border-color: var(--border); color: var(--text); }

/* Page Header — thay link "← Về…" đặt lệch */
.page-header { margin-bottom: var(--space-xl); }
/* Hàng trên header: breadcrumb (trái) + club-pill định danh CLB (phải) — dùng CHUNG cho MỌI trang
   trong CLB qua fragment `page :: pageHeader`, để tên CLB đang xem luôn hiện, không chỉ Dashboard.
   flex-wrap: pill rớt xuống dòng dưới trên màn hẹp thay vì chèn ép breadcrumb. */
.page-header__top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-sm); flex-wrap: wrap; margin-bottom: var(--space-2xs);
}
.page-header__top .page-header__crumb { margin-bottom: 0; }
.page-header__top .club-pill { margin-left: auto; }
/* Breadcrumb — "thanh menu" đường dẫn: mọi cấp cha bấm được để quay lại, cấp hiện tại là chữ mờ.
   Dùng flex + wrap để chuỗi dài (VD Buổi tập › Chi tiết buổi › Điểm danh) tự xuống dòng trên mobile. */
.page-header__crumb {
  display: flex; flex-wrap: wrap; align-items: center; gap: .375rem;
  font-size: .8125rem; color: var(--text-muted); margin-bottom: var(--space-2xs);
}
.page-header__crumb a { color: var(--action-primary); text-decoration: none; font-weight: 500; }
.page-header__crumb a:hover { text-decoration: underline; }
.page-header__crumb .sep { color: var(--text-muted); opacity: .55; }
.page-header__crumb .current { color: var(--text-muted); }
.page-header__row {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--space-md); flex-wrap: wrap; margin-top: var(--space-2xs);
}
/* Tiêu đề trang to & đậm hơn — nhịp thị giác giống mockup FastClub */
.page-header__title { margin: 0; font-size: 1.75rem; line-height: 2.25rem; font-weight: 800; letter-spacing: -.02em; }
.page-header__subtitle { font-size: .9rem; color: var(--text-muted); margin-top: var(--space-2xs); max-width: 60ch; }
/* --reserve: chừa cứng chiều cao 2 dòng cho subtitle. Dùng ở nhóm 4 tab Cài đặt (Chung / Cách chia
   tiền / Sân / Loại cầu): mô tả mỗi tab dài ngắn khác nhau (1 hay 2 dòng) làm page-header cao thấp
   khác nhau, đẩy thanh tab con bên dưới lệch vị trí khi chuyển tab. Ghim cao 2 dòng ⇒ tab luôn cùng
   một chỗ; tab nào mô tả 1 dòng thì dưới nó dư một dòng trắng (chấp nhận được, đổi lấy vị trí ổn định).
   line-height khai rõ để min-height khớp đúng 2 dòng. Mobile ẩn subtitle (display:none ở tầng mobile
   bên dưới) nên min-height này không tác động ở điện thoại. */
.page-header__subtitle--reserve { line-height: 1.4; min-height: calc(1.4em * 2); }

/* ============================================================
   NAV-PILLS — tab dạng segmented (Báo cáo, Sân, Cài đặt…)
   Active = viên xanh đặc (đã map --bs-primary). Track nền chìm.
   ============================================================ */
.nav-pills {
  display: inline-flex; flex-wrap: wrap; gap: .25rem;
  background: var(--surface-muted); padding: .3rem;
  border-radius: var(--radius-md); border: 1px solid var(--border);
}
/* P5: chữ tab chưa active (--text-muted) trên --surface-muted chỉ đạt 4,04:1 ở dark (--surface-muted
   dark #334155 sáng hơn light tương ứng, không đủ tối để bù cho --text-muted). Dark-only: hạ nền pill
   xuống --surface-sunken (đã có, dùng cho nền trang) — đạt ~7:1, không cần đổi --text-muted (dùng ở
   rất nhiều nơi khác, đổi token đó rủi ro lan rộng ngoài phạm vi task này). */
[data-bs-theme="dark"] .nav-pills { background: var(--surface-sunken); }
.nav-pills .nav-link {
  color: var(--text-muted); font-weight: 600; font-size: .9rem;
  padding: .45rem 1rem; border-radius: var(--radius-sm);
}
.nav-pills .nav-link:hover:not(.active) { color: var(--text); background: var(--surface); }
.nav-pills .nav-link.active {
  background: var(--action-primary-solid, var(--action-primary)); color: #fff;
  box-shadow: 0 4px 10px rgba(14,122,95,.28);
}

/* Sổ quỹ: cùng cỡ tab ở cả Giao dịch và Tiền ứng, trên mọi bề rộng. */
.fund-tabs .nav-link { padding-inline: var(--space-xs); }

/* Số dư là bối cảnh của hai tab: giữ thẻ và số tiền gọn, kể cả trên desktop. */
.fund-balance {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  column-gap: var(--space-sm); row-gap: var(--space-2xs);
  padding: var(--space-sm) var(--space-md);
}
.fund-balance .stat-card__head { gap: var(--space-xs); margin-bottom: 0; }
.fund-balance .stat-card__icon { width: 1.75rem; height: 1.75rem; border-radius: var(--radius-sm); }
.fund-balance .stat-card__icon > .bi { font-size: var(--icon-sm, 1rem); }
.fund-balance .stat-card__label { margin-bottom: 0; }
.page-content .stat-card.fund-balance .money-hero {
  margin-bottom: 0; margin-left: auto; text-align: right;
  font-size: 1.25rem; line-height: 1.4;
}
.fund-balance .stat-card__meta { flex-basis: 100%; font-size: var(--fs-caption, .8125rem); }

/* Hai tab Sổ quỹ: hành động lấp khoảng trống ở mobile, canh phải vùng nội dung trên desktop. */
.fund-ledger-nav { display: flex; align-items: center; gap: var(--space-xs); }
.fund-ledger-nav__tabs { min-width: 0; }
.fund-ledger-nav .nav-pills { margin-bottom: 0 !important; }
.fund-ledger-nav__title { margin: 0; font-size: var(--fs-strong, 1.0625rem); font-weight: 700; }
.fund-ledger-nav__action { flex: 1 1 auto; min-width: 0; max-width: 14rem; }
.fund-ledger-nav__button { position: relative; width: 100%; }
/* Bộ lọc "Loại" ở popup Lọc sổ quỹ: chip tự xuống dòng, vùng chạm >= 44px trên điện thoại (CT-6). */
.fund-filter-chips { display: flex; flex-wrap: wrap; gap: var(--space-xs, .375rem); }
.fund-filter-chips .btn { min-height: 2.75rem; display: inline-flex; align-items: center; }
.fund-ledger-nav__action .input-group .form-control { min-width: 0; }
@media (min-width: 768px) {
  .fund-ledger-nav__action { flex: 0 0 14rem; margin-left: auto; }
}
.fund-ledger-nav__button .badge-soft { display: none; }
.fund-ledger-nav__button.is-filtered::after {
  content: ''; position: absolute; top: .25rem; right: .25rem;
  width: .4rem; height: .4rem; border-radius: 50%; background: var(--action-primary);
}

/* App Alert — soft, kèm hành động bên phải */
.app-alert {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-sm); flex-wrap: wrap;
  padding: var(--space-md); border-radius: var(--radius-md);
  border: 1px solid transparent; font-size: .95rem;
  margin-bottom: var(--space-md);
}
.app-alert__body { display: flex; align-items: center; gap: .5rem; }
/* P5: nền dùng var(--alert-*-bg, rgba(...)) — chỉ định nghĩa ở [data-bs-theme="dark"], LIGHT rơi về
   fallback rgba() gốc, không đổi. */
.app-alert.is-info    { background: var(--alert-info-bg, rgba(37,99,235,.10));  border-color: rgba(37,99,235,.22);  color: var(--badge-blue-fg); }
/* `is-court` — banner của luồng "Đăng ký đi tập" (vòng 2 task 5.12). Cùng lý do với `.btn-court-soft`
   ở khối nút: người dùng chốt cả MÀN đó mang xanh thảm, không còn xanh dương. Chỉ dùng ở màn này —
   `is-info` vẫn là ô màu "thông tin trung tính" dùng chung cho toàn app.
   Tương phản: light chữ --court-700 trên nền đã hoà rgb(231,242,239) = 8,23:1 · dark --court-300 trên
   --alert-court-bg = 8,04:1. Dark khai nền ĐẶC (đúng cơ chế P5 của 4 alert kia) để công cụ audit đọc
   được nền thật thay vì kênh RGB thô của rgba. */
.app-alert.is-court   { background: var(--alert-court-bg, rgba(14,122,95,.10)); border-color: rgba(14,122,95,.22); color: var(--court-700); }
[data-bs-theme="dark"] .app-alert.is-court { color: var(--court-300); }
.app-alert.is-success { background: var(--alert-success-bg, rgba(22,163,74,.10));  border-color: rgba(22,163,74,.22);  color: var(--badge-green-fg); }
.app-alert.is-warning { background: var(--alert-warning-bg, rgba(249,115,22,.12)); border-color: rgba(249,115,22,.26); color: var(--badge-amber-fg); }
.app-alert.is-danger  { background: var(--alert-danger-bg, rgba(220,38,38,.10));  border-color: rgba(220,38,38,.24);  color: var(--badge-red-fg); }

/* Toast thông báo dùng chung (BmhToast, notification.js) — góc trên-phải ở MỌI cỡ màn hình (kể cả
   mobile). z-index trên cả modal vì lỗi có thể xảy ra khi đang mở modal.
   🔴 SỬA COMMENT LỖI THỜI (task 1.2): đoạn trước đây ở đây nói `top` chỉ là giá trị mặc định vì
   notification.js "đo chiều cao THẬT của .app-topbar-mobile/.auth-topbar/.page-header__top rồi ghi đè
   top bằng inline style mỗi lần show()". Cơ chế đó ĐÃ BỊ BỎ (`notification.js` dòng 14–17: toast nay
   ghim cứng ở góc trên vì nền đậm/đặc, đè lên navbar vẫn đọc rõ) — comment cũ khiến người đọc đi tìm
   một hàm không còn tồn tại. `top: var(--space-md)` bây giờ là vị trí THẬT và duy nhất.
   Hệ quả trên mobile, đo được ở 390×844 (ghi lại chứ KHÔNG tự sửa — đây là quyết định thiết kế của
   spec toast 2026-08-17, đảo lại phải hỏi người dùng): toast rộng 350/390px nên nó che gần trọn
   topbar, gồm cả `.app-theme-toggle` — phần tử bấm được DUY NHẤT của topbar mobile — trong 4 giây
   (7 giây với toast `danger`). Trên desktop góc trên-phải là chỗ trống nên không ai mất gì. */
#bmh-toast-container {
  position: fixed; top: var(--space-md); right: var(--space-md);
  z-index: calc(var(--bs-modal-zindex, 1055) + 10);
  display: flex; flex-direction: column; gap: var(--space-sm);
  max-width: min(24rem, calc(100vw - 2rem));
}
#bmh-toast-container .app-alert { margin-bottom: 0; box-shadow: var(--elevation-2); flex-wrap: nowrap; }
/* Nền đậm/đặc thay vì màu nhạt (tint) của .app-alert dùng ở nội dung trang — toast cần nổi bật, tự
   ẩn nhanh nên không cần giữ tông nhẹ nhàng như banner tĩnh. Chữ/icon trắng để đủ tương phản trên nền
   đặc; .btn-close-white (Bootstrap) đổi màu nút đóng cho khớp.
   🔴 Task 1.2 — ĐO ĐƯỢC hai nền TRƯỢT AA với chữ trắng, ở CẢ hai theme và CẢ desktop (probe riêng:
   `audit.js` không bao giờ thấy toast vì toast chỉ tồn tại sau khi JS gọi `BmhToast.show()`):
     • is-success `#16A34A` → **3,30:1** (cần 4,5 cho chữ 15,2px)  ⇒ `--green-500` một bậc quá sáng
     • is-warning `#D97706` → **3,19:1**
   Sửa bằng token có sẵn, không nghĩ mã hex mới:
     • is-success → `--green-600` (#15803D) với chữ trắng: **5,02:1** đo được. Chỉ tối thêm một bậc,
       vẫn đọc ra "thành công".
     • is-warning → GIỮ nền amber, đổi CHỮ sang màu tối (`--slate-900`): **6,37:1**. 🔴 Bản sửa đầu
       tối nền lên `--amber-700` (#7C380B) đạt 8,65 nhưng chụp ảnh ra màu **nâu bùn**, mất luôn nghĩa
       "cảnh báo" và đá nhau với banner amber nhạt phía sau. Bảng màu không có bậc nào giữa
       `--amber-600` (#EA580C — vẫn chỉ 3,56, vẫn trượt) và `--amber-700`, nên đường đúng là đảo
       chiều tương phản chứ không phải tối nền: đây CHÍNH LÀ cách app đã làm cho `.btn-warning`
       (chữ đen trên #ffc107, đo được 12,9:1 ở task 0.6). Kéo theo: phải bỏ `filter` của
       `.btn-close-white` cho riêng toast này, nếu không dấu X trắng biến mất trên nền sáng.
   `is-info`/`is-danger` giữ nguyên GIÁ TRỊ (5,17 và 4,83 — đều đạt), chỉ đổi cách viết từ mã hex sang
   token tương ứng cho khớp luật "mọi màu đến từ token" (`--blue-500` = #2563EB, `--red-500` = #DC2626,
   đo lại xác nhận tương phản không đổi).
   🔴 Đây là thay đổi nhìn thấy được TRÊN CẢ DESKTOP. Khác với ca nút "Chốt sổ ngay" bị hoàn tác ở task
   0.6, ca này KHÔNG phải dọn thẩm mỹ: hai nền trên trượt tiêu chí `MB-7` bằng số, và AA không phụ
   thuộc bề rộng màn hình nên không thể sửa riêng cho mobile. `MB-8` (ảnh 1440×900) vẫn byte-identical
   vì ảnh chụp trang tĩnh không có toast nào. */
#bmh-toast-container .app-alert.is-success { background: var(--green-600); border-color: var(--green-600); color: #fff; }
#bmh-toast-container .app-alert.is-info    { background: var(--blue-500);  border-color: var(--blue-500);  color: #fff; }
#bmh-toast-container .app-alert.is-warning { background: var(--amber-500); border-color: var(--amber-500); color: var(--slate-900); }
#bmh-toast-container .app-alert.is-danger  { background: var(--red-500);   border-color: var(--red-500);   color: #fff; }
/* Dấu X của Bootstrap là ảnh nền; `.btn-close-white` (notification.js gắn cho MỌI toast) đảo nó thành
   trắng — trên nền amber sáng thì gần như mất hẳn. Trả về dấu X tối cho đúng một kiểu toast này.
   Dùng primitive `--slate-900` chứ không phải `--text-strong`: nền toast là màu CỐ ĐỊNH ở cả hai
   theme, nên chữ trên nó cũng phải cố định — `--text-strong` ở theme tối là màu SÁNG, sẽ mất chữ. */
#bmh-toast-container .app-alert.is-warning .btn-close { filter: none; }

/* Empty State — biến màn trống thành điểm khởi đầu */
.empty-state {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--elevation-1);
  padding: var(--space-2xl) var(--space-xl); text-align: center;
}
.empty-state__mark {
  display: flex; align-items: center; justify-content: center;
  width: 3.5rem; height: 3.5rem; margin: 0 auto var(--space-md);
  border-radius: 50%; font-size: 1.6rem; line-height: 1;
  background: rgba(14,122,95,.10); color: var(--action-primary);
}
.empty-state__title { margin-bottom: var(--space-xs); }
.empty-state__desc { color: var(--text-muted); margin-bottom: var(--space-lg); }

/* ============================================================
   Bọc nội dung trang — đẩy phải khi có sidebar (>=992px),
   chừa chỗ bottom-nav (<992px)
   ============================================================ */
.page-content { padding-top: var(--space-lg); }
/* Bề rộng nội dung — QUY ƯỚC: MỌI màn trong CLB dùng content-wide (960) cho đồng nhất bề rộng.
   (Trước đây các màn *sua* dùng content-default(720) nên trông "bé hơn" màn danh sách — theo yêu
    cầu đồng bộ, đã gộp về cùng một size 960.)
     • content-wide  (960): MỌI màn trong CLB — list, bảng tiền, dashboard, VÀ các màn *sua*.
     • content-default(720): còn định nghĩa để dùng lại nếu cần trang một cột hẹp (hiện không màn nào dùng).
     • content-narrow (560): dành cho auth/shell ngoài CLB.
   🔴 Bảng dữ liệu KHÔNG được dùng content-default — nó co bảng lại hẹp hơn các màn khác (bug đã từng gặp ở
      bang-chia-tien). Thêm màn mới ⇒ content-wide. */
.page-content .content-narrow  { max-width: var(--w-narrow); }
.page-content .content-default { max-width: var(--w-content); }
.page-content .content-wide    { max-width: var(--w-wide); }

@media (min-width: 992px) {
  .page-content { margin-left: var(--sidebar-width); padding-top: var(--space-xl); }
}
@media (max-width: 991.98px) {
  .page-content {
    padding-bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom) + var(--space-md));
  }
}

/* ============================================================
   Topbar tối giản (<992px)
   ============================================================ */
.app-topbar-mobile {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: .5rem 1rem;
  min-height: var(--topbar-height);
  /* Dính ở mép trên; cuộn XUỐNG thì trượt ẩn (trả chiều cao cho nội dung), vuốt LÊN thì hiện lại
     ngay — cùng hành vi `.lp-nav` của bề mặt công khai. Class `is-hidden` do khối "Topbar mobile ẩn
     khi cuộn" ở `fragments/layout :: scripts` gắn/gỡ. z-index 1020 = `.sticky-top` của Bootstrap:
     trên nội dung trang, dưới offcanvas (1045)/modal (1055) và dưới `.ts-dropdown` (1060) — danh sách
     tìm kiếm lật lên sát mép trên vẫn nổi trên topbar, không bị che.
     🔴 Sticky chỉ chạy khi (1) thẻ cha cao bằng cả trang — thẻ bọc fragment nav vì thế mang
     `.app-nav-shell` (display: contents) ngay dưới đây; (2) không tổ tiên nào là scroll container —
     `overflow-x: hidden` trên body/html sẽ lặng lẽ giết nó (bẫy đã ghi ở landing.css, dùng `clip`).
     `TopbarMobileAnKhiCuonTest` gác cả hai. */
  position: sticky; top: 0; z-index: 1020;
  transition: transform .25s cubic-bezier(.4, 0, .2, 1);
}
.app-topbar-mobile.is-hidden { transform: translateY(-100%); }
@media (prefers-reduced-motion: reduce) {
  .app-topbar-mobile { transition: none; }
}
.app-nav-shell { display: contents; }

/* Tên CLB ở topbar: dài tới đâu cũng KHÔNG BAO GIỜ wrap — cắt bằng ellipsis (P3, task 0.4).
   🔴 Vì sao luật này nằm ở ĐÂY chứ không trong khối tầng mobile ở cuối tệp (task 1.1) — và tại sao
   đừng viết lại nguyên văn chuỗi mốc của khối đó trong bất kỳ comment nào: `MobileScaleTest` đếm
   số lần chuỗi mốc xuất hiện để bắt lỗi "luật mobile rải rác nhiều khối", nên một comment nhắc
   đúng chuỗi đó cũng bị tính là mốc thứ hai và ba test của lớp này đỏ cùng lúc (đã xảy ra thật khi
   viết task 1.1). Lý do đặt ở đây: topbar mang
   `d-lg-none` nên nó SỐNG TỚI 991,98px, rộng hơn tầng mobile 767,98px — dải 768–991px (tablet
   dọc) vì thế nhận chrome điện thoại nhưng KHÔNG có luật chống wrap. Đo bằng probe task 1.1:
   ở 768px và 991px, tên CLB 95 ký tự đội topbar từ 56px lên **103,4px** (2 dòng), trong khi ở
   390px cùng tên đó chỉ chiếm 234px (=60vw) và topbar giữ 60,2px. Đặt ngoài mọi @media là an
   toàn cho MB-8 theo đúng lý lẽ của tầng mobile — chỉ khác là bảo vệ bằng CẤU TRÚC COMPONENT
   (>=992px không render topbar này) thay vì bằng breakpoint.
   🔴 --fs-strong/--tap-min chỉ tồn tại TRONG tầng mobile (<768px), nên mọi chỗ dùng token ở
   ngoài đó phải viết kèm giá trị dự phòng CÙNG con số — thiếu nó, 768–991px rơi về giá trị
   thừa hưởng chứ không phải bậc của thang chữ. */
.app-topbar-mobile__club {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 60vw;
  font-size: var(--fs-strong, 1.0625rem); font-weight: 700;
}
.app-topbar-mobile .app-brand { min-width: 0; }

/* 🔴 Chống wrap cho CẢ HÀNG, không chỉ cho chuỗi tên CLB (đo 09/09/2026, task 3.6 của module Thông
   báo). Luật `.app-topbar-mobile__club` ở trên chỉ hứa rằng CHỮ không xuống dòng — nó không giữ
   được HÀNG. Bootstrap khai `.navbar { flex-wrap: wrap }` và `.navbar > .container-fluid
   { flex-wrap: inherit }`, nên hàng vẫn wrap được; `d-flex` viết trong template chỉ đặt
   `display: flex`, không đụng tới `flex-wrap`.

   Số đo ở 390×844 trước khi vá: tên CLB **28 ký tự trở lên** làm cụm nút bên phải (chuông 44px +
   đổi giao diện 44px) rơi xuống hàng thứ hai — topbar đội từ **61px lên 96,2px**, chuông trượt từ
   x=276,8 về x=28. Ngưỡng 28 ký tự là tên CLB đời thường ("CLB Cầu Lông Cầu Giấy Hà Nội"), không
   phải trường hợp biên bịa ra. Lỗi chỉ lộ ra sau khi module Thông báo thêm nút chuông vào topbar:
   trước đó chỉ có một nút nên hàng vừa đủ.

   `*:last-child` là cụm nút: nó KHÔNG được co lại, vì co nút là hạ vùng chạm 44×44 (`MB-3`). Phần
   phải nhường chỗ là tên CLB, và nó đã có sẵn `text-overflow: ellipsis`. Sau khi vá, topbar giữ
   61px với tên 49 ký tự ở cả `390×844` lẫn `360×800`, không trang nào tràn ngang.
   `TopbarMobileNoWrapTest` gác. */
.app-topbar-mobile > .container-fluid { flex-wrap: nowrap; }
.app-topbar-mobile > .container-fluid > *:last-child { flex-shrink: 0; }
/* Icon topbar về bậc 20px của thang icon (MB-4). `.app-theme-toggle` có `font-size: .85rem` nên
   icon con thừa hưởng 13,6px — đo thật ở 768×1024 (ở <768px tầng mobile đã ép 20px). */
.app-topbar-mobile .bi { font-size: var(--icon-md, 1.25rem); }

/* Vùng chạm topbar (MB-3): logo+tên CLB đo 98×35 và nút đổi theme 35×34 ở 768px. GIỮ kích
   thước nhìn thấy — phóng to chúng đội cao topbar — mở vùng chạm bằng pseudo-element trong
   suốt như P1 làm với nút icon-only. Tầng mobile đã có luật này cho `.app-brand` trần (áp cả
   trang auth/entry ở <768px); cụm dưới đây chỉ mở rộng nó ra dải tablet và CHỈ trong topbar,
   không nới cho `.app-brand` ở nơi khác — chồng nhau ở <768px là vô hại vì cùng kích thước.
   `.app-theme-toggle` phải scope theo topbar: cùng class còn dùng ở footer sidebar desktop,
   nơi một lớp phủ 44px vô hình sẽ giành cú bấm của nút Đăng xuất ngay cạnh. */
.app-topbar-mobile .app-brand,
.app-topbar-mobile .app-theme-toggle { position: relative; }
.app-topbar-mobile .app-brand::after,
.app-topbar-mobile .app-theme-toggle::after {
  content: ''; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap-min, 44px); height: var(--tap-min, 44px);
}

/* ============================================================
   Sidebar trái cố định (>=992px)
   ============================================================ */
.app-sidebar {
  position: fixed; top: 0; left: 0; bottom: 0;
  width: var(--sidebar-width);
  background: var(--nav-bg);
  border-right: 1px solid var(--nav-border);
  padding: var(--space-md) 0 0;
  z-index: 1030;
}
.app-sidebar-brand { padding: 0 var(--space-md) var(--space-md); }
.app-brand {
  display: flex; align-items: center; gap: .6rem;
  font-weight: 800; font-size: 1.1rem; color: var(--text-strong);
  text-decoration: none;
}
/* Logo mark: ô bo góc màu xanh — dùng chung sidebar + topbar mobile */
.app-brand .app-brand-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.2rem; height: 2.2rem; border-radius: 11px;
  background: var(--nav-brand-mark-bg); font-size: 1.15rem; flex: 0 0 auto;
  box-shadow: 0 4px 10px rgba(14,122,95,.35);
}
/* Cỡ mặc định logo SVG (auth/entry — không có ô tile): xanh thương hiệu */
.brand-svg { width: 1.4rem; height: 1.4rem; color: var(--action-primary); flex: 0 0 auto; vertical-align: -.2em; }
/* Trong ô tile sidebar/topbar/entry: trắng, khớp tile */
.app-brand .app-brand-mark .brand-svg { width: 1.35rem; height: 1.35rem; color: #fff; }
/* Biến thể logo CLB (topbar <992px): ảnh lấp KÍN ô tile ⇒ bỏ nền xanh + đổ bóng thương hiệu, nếu
   không ảnh sẽ có viền xanh thừa quanh mép. Giữ nguyên 2,2rem và bo góc 11px của .app-brand-mark ⇒
   chiều cao topbar không đổi (MB-8 an toàn). Ngoài mọi @media: topbar sống tới 991,98px trong khi
   tầng mobile dừng ở 767,98px, và bảo vệ ở đây là bằng CẤU TRÚC (>=992px không render topbar). */
.app-brand .app-brand-mark--logo { background: none; box-shadow: none; overflow: hidden; }
.app-brand .app-brand-mark__logo { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; display: block; }
.app-brand-text { display: flex; flex-direction: column; line-height: 1.12; }
.app-brand-name { font-weight: 800; font-size: 1.05rem; }
.app-brand-sub  { font-size: .7rem; font-weight: 500; }
/* Trong sidebar tối: chữ sáng */
.app-sidebar .app-brand-name { color: var(--nav-text-strong); }
.app-sidebar .app-brand-sub  { color: var(--nav-text-muted); }

/* Club switcher — base SÁNG (dùng trong offcanvas menu mobile) */
.app-club-switch {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  margin: 0 var(--space-sm) var(--space-sm);
  padding: .55rem .7rem;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface-sunken); color: var(--text);
  text-decoration: none; font-size: .9rem;
}
.app-club-switch:hover { border-color: var(--border-strong); color: var(--text); }
/* 🔴 `text-overflow` KHÔNG được đặt ở đây. `.label` là flex container (nó bọc logo + tên), mà
   text-overflow chỉ áp cho BLOCK container — trên flex nó là luật chết: `overflow: hidden` vẫn
   xén, dấu `…` không bao giờ được vẽ. Lỗi đã lộ thật 16/09/2026: thanh bên in "BVPM Bảo Việ",
   chuỗi tên tràn 8,4px (kết ở x=153,1 trong khi .label cắt ở x=144,7) nên chữ `t` bị cắt phẳng
   giữa thân — trông như tên CLB lưu thiếu ký tự chứ không như tên bị rút gọn.
   Ellipsis nay nằm ở `.app-club-switch__ten` bên dưới, tức chính FLEX ITEM chứa chuỗi.
   `ClubSwitchTenEllipsisTest` gác. */
.app-club-switch .label { display: inline-flex; align-items: center; min-width: 0; overflow: hidden; white-space: nowrap; }
.app-club-switch__ten { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.app-club-switch i { color: var(--text-muted); }
.app-club-switch__logo { width: 1.1rem; height: 1.1rem; border-radius: 50%; object-fit: cover; margin-right: .4rem; flex: 0 0 auto; }
/* Trong sidebar TỐI: đổi sang nền/chữ sáng cho hợp navy.
   🔴 Padding ngang + gap SIẾT lại chỉ ở đây (16/09/2026). Sidebar rộng cố định 248px ⇒ nút còn
   223px, và sau khi trừ logo (17,6 + 6,4 lề), huy hiệu FREE (48), mũi tên (16), 2 gap (16) và
   padding ngang (22,4) thì ô tên chỉ còn **97px** — trong khi chính CLB ví dụ của dự án,
   "CLB Cầu Giấy", cần **98px**. Tức tên đời thường nào cũng rơi vào ellipsis vì thiếu đúng 1px.
   `.7rem → .55rem` (padding, +6,4px) và `.5rem → .375rem` (gap, +4px) nâng ô tên lên **105px**:
   "CLB Cầu Giấy" 98/105 và "BVPM Bảo Việt" 105/105 hiện đủ, tên 28 ký tự vẫn rút gọn bằng `…`
   đúng như mong đợi. Chỉ khoanh trong sidebar — ngăn kéo điện thoại rộng 375px, không chịu sức ép
   này, siết ở đó chỉ làm nút trông chật mà không đổi được gì. */
.app-sidebar .app-club-switch {
  border-color: var(--nav-border); background: var(--nav-hover-bg); color: var(--nav-text);
  padding: .55rem; gap: .375rem;
}
.app-sidebar .app-club-switch:hover { border-color: var(--border-strong); color: var(--nav-text-strong); background: rgba(148,163,184,.16); }
.app-sidebar .app-club-switch i { color: var(--nav-text-muted); }

/* Huy hiệu gói cạnh tên CLB trong club-switch (BR-TIER-12).
   `flex: 0 0 auto` để huy hiệu không bị bóp — thứ phải co khi tên CLB dài là .label (nó đã có
   overflow:hidden nên min-width tự về 0), không phải huy hiệu. 🔴 Câu này TRƯỚC ĐÂY còn ghi thêm
   "và ellipsis chạy đúng" — sai, và chính giả định đó đã để lọt lỗi xén trụi chữ cuối ngày
   16/09/2026; ellipsis nằm ở `.app-club-switch__ten`, không ở `.label`.
   `:empty` vì ngoài phạm vi CLB fragment trả về thẻ bọc rỗng — flex gap vẫn chừa chỗ cho một thẻ
   rỗng, thành ra khoảng hở lạ trước dấu mũi tên. */
.app-club-switch__tier { flex: 0 0 auto; display: inline-flex; }
.app-club-switch__tier:empty { display: none; }
/* 🔴 `.app-club-switch i` (và bản trong sidebar) tô MỌI icon con thành màu muted — kể cả dấu tick
   trong huy hiệu PRO, làm nó xám xịt giữa nền thương hiệu. Trả lại `inherit` cho đúng icon đó. */
.app-club-switch .bmh-pro-badge i,
.app-sidebar .app-club-switch .bmh-pro-badge i { color: inherit; }
/* Sidebar là nền navy tối: token slate của .bmh-free-badge là chữ sẫm trên nền sáng, dán nguyên
   vào đây thì gần như đọc không ra. Dùng đúng cặp màu sidebar đang dùng cho trạng thái hover. */
.app-sidebar .app-club-switch .bmh-free-badge {
  background: rgba(148,163,184,.22);
  color: var(--nav-text-strong);
}

/* 🔴 flex-wrap: nowrap là BẮT BUỘC, không phải mặc định thừa. Thẻ này là <ul class="nav
   flex-column">, mà Bootstrap .nav khai flex-wrap: wrap — với flex-direction: column, "wrap" nghĩa
   là mục nào không đủ chỗ theo CHIỀU DỌC sẽ nhảy sang một CỘT MỚI bên phải, chứ không kích hoạt
   overflow-y: auto ở ngay dòng này. Lỗi đã lộ thật 09/09/2026: thêm mục "Thông báo" (ADR-22) làm
   sidebar của CLB gói FREE có đủ 14 mục, vượt 652px khả dụng ở 1440×900, nên mục cuối "Nâng cấp"
   vẽ đè lên nhãn "VẬN HÀNH" và tràn ra ngoài bề rộng sidebar. Không có lỗi, không có log — chỉ
   nhìn mới thấy, và chỉ thấy ở CLB gói FREE (mục "Nâng cấp" chỉ hiện khi showUpsell).
   SidebarNavNoWrapTest gác. */
.app-sidebar-nav { flex: 1 1 auto; flex-wrap: nowrap; overflow-y: auto; padding: 0 var(--space-xs); }
/* Khi lưới an toàn overflow-y ở trên thật sự phải cuộn, thanh cuộn mặc định của Windows hiện ra là
   một dải TRẮNG có hai mũi tên trên nền xanh đậm (thấy được 12/10/2026 ở 1440×900 khi bật thêm mục
   Giải đấu). Vẽ lại thành một vạch mảnh cùng tông với chữ menu.
   Chỉ áp cho thanh bên máy tính (`.app-sidebar`): offcanvas điện thoại nền --surface, vạch sáng sẽ
   chìm mất. Hai nhánh tách nhau vì Chrome ≥121 hễ thấy scrollbar-color là BỎ QUA toàn bộ
   ::-webkit-scrollbar — mà chỉ ::-webkit-scrollbar mới giấu được mũi tên. */
.app-sidebar .app-sidebar-nav::-webkit-scrollbar { width: .375rem; }
.app-sidebar .app-sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.app-sidebar .app-sidebar-nav::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--nav-text-muted) 40%, transparent);
  border-radius: var(--radius-pill);
}
.app-sidebar .app-sidebar-nav::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--nav-text-muted) 70%, transparent);
}
@supports not selector(::-webkit-scrollbar) {
  .app-sidebar .app-sidebar-nav {
    scrollbar-width: thin;
    scrollbar-color: color-mix(in srgb, var(--nav-text-muted) 40%, transparent) transparent;
  }
}
.app-sidebar-group-label {
  padding: var(--space-md) var(--space-sm) var(--space-2xs);
  font-size: .6875rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--nav-group-label);
}
/* 🔴 Nhãn nhóm hẹp lại CHỈ trong sidebar desktop. Cùng class này còn được dùng trong offcanvas menu
   trên điện thoại (xem `.offcanvas .app-sidebar-group-label`), nơi màn hình cuộn được và khoảng thở
   16px là đúng — siết chung một chỗ là sửa desktop bằng cách làm chật mobile. */
.app-sidebar .app-sidebar-group-label { padding-top: var(--space-xs); padding-bottom: 0; }
/* 🔴 Mật độ dọc của sidebar là một HỢP ĐỒNG, không phải thẩm mỹ: cả danh sách phải vừa MỘT màn,
   không cuộn (người dùng yêu cầu 16/09/2026). `overflow-y: auto` ở `.app-sidebar-nav` là lưới an
   toàn cho màn thấp bất thường, KHÔNG phải cách vận hành bình thường — điều hướng mà phải cuộn mới
   thấy "Cài đặt" thì mục đó coi như không tồn tại với người dùng.

   Bản cũ (padding .6rem · margin 2px · nhãn nhóm padding .6rem/.25rem) cần 704px. Đo thật bằng
   Chrome ở cấu hình NHIỀU mục nhất — CLB gói FREE + Owner: 12 mục + 3 nhãn nhóm (gói PRO ẩn mục
   "Nâng cấp" nên luôn dư một mục, đúng cách lỗi 09/09/2026 lọt qua, xem SidebarNavNoWrapTest):

     khung nhìn   phần cố định   còn cho danh sách   bản cũ cần   bản này cần
     1366×768     248px          520px               704 ✗        510,2 (dư 9,8)
     1440×900     248px          652px               704 ✗        586,7 (dư 65,3)

   🔴 Padding dọc TRÔI theo chiều cao khung nhìn thay vì chia bậc bằng @media: một mốc cứng làm
   sidebar nhảy một nấc 100px khi kéo cửa sổ qua ngưỡng, và mốc đó luôn sai ở đâu đó vì chiều cao
   khung nhìn THẬT không bằng chiều cao màn hình (1440×900 trừ thanh tiêu đề + thanh tác vụ chỉ còn
   ~780px). Công thức: 4px ở khung nhìn ≤ 800px, nới dần tới 7,2px từ 880px trở lên — chặn hai đầu
   bằng clamp() nên không bao giờ mỏng hơn 4px (vùng bấm) hay dày hơn bản thiết kế gốc.
   Đổi con số ở đây thì phải ĐO LẠI ở cả hai khung nhìn trong bảng trên. */
.app-sidebar-nav .nav-link {
  color: var(--nav-text); padding: clamp(.25rem, (100vh - 700px) / 25, .45rem) .75rem; margin: 1px 0;
  border-radius: var(--radius-md); display: flex; align-items: center; gap: .7rem;
  font-size: .9375rem; font-weight: 500;
  transition: background var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard);
}
.app-sidebar-nav .nav-link i { font-size: 1.1rem; color: var(--nav-text-muted); width: 1.2rem; text-align: center; }
.app-sidebar-nav .nav-link:hover { background: var(--nav-hover-bg); color: var(--nav-text-strong); }
.app-sidebar-nav .nav-link:hover i { color: var(--nav-text-strong); }
/* Bóng đổ theo pill: pill giờ là nền SÁNG trên nền tối (xem khối --nav-*), nên bóng xanh
   rgba(14,122,95,.35) cũ không còn nghĩa — bóng tối trung tính mới tách được pill khỏi nền. */
.app-sidebar-nav .nav-link.active {
  background: var(--nav-active-bg);
  color: var(--nav-active-text);
  font-weight: 600;
  box-shadow: 0 4px 12px rgba(0,0,0,.22);
}
.app-sidebar-nav .nav-link.active i { color: var(--nav-active-text); }


.app-sidebar-footer {
  padding: var(--space-sm);
  margin-top: var(--space-xs);
  border-top: 1px solid var(--nav-border);
  display: flex; flex-direction: column; gap: var(--space-xs);
}

/* Hồ sơ người dùng cuối sidebar */
.app-user {
  display: flex; align-items: center; gap: .6rem;
  padding: .5rem; border-radius: var(--radius-md);
  text-decoration: none; color: var(--nav-text);
}
.app-user:hover { background: var(--nav-hover-bg); color: var(--nav-text-strong); }
.app-user__avatar {
  width: 2.2rem; height: 2.2rem; border-radius: 50%; flex: 0 0 auto;
  background: rgba(148,163,184,.18); color: var(--nav-text-strong);
  display: inline-flex; align-items: center; justify-content: center; font-size: 1.05rem;
  overflow: hidden;
}
.app-user__avatar img { width: 100%; height: 100%; object-fit: cover; }
.app-user__meta { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.app-user__name { font-weight: 600; font-size: .85rem; color: var(--nav-text-strong);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 130px; }
.app-user__role { font-size: .72rem; color: var(--nav-text-muted); }
.app-user__chev { margin-left: auto; color: var(--nav-text-muted); font-size: .8rem; }

/* 🔴 Hai nút [Giao diện] [Đăng xuất] phải rộng BẰNG NHAU, nhưng hai flex item lại khác
   loại: một bên là chính <button>, bên kia là <form> bọc nút (form bắt buộc — đăng xuất
   là POST có CSRF). Với flex:1 1 0 Chrome vẫn chia lệch 117.3px/99.3px (đo thật), mà nút
   hẹp hơn lại là nút có chữ dài hơn (cần 102.4px) ⇒ "Đăng xuất" gãy 2 dòng còn "Giao diện"
   1 dòng, hai nút cao bằng nhau nhưng trông lệch hẳn. Grid 2 cột 1fr chia đúng bằng nhau
   bất kể item là thẻ gì; minmax(0,1fr) để cột không bị nội dung đẩy phình ra. */
.app-sidebar-footer-actions {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .3rem;
}
.app-sidebar-footer .app-theme-toggle,
.app-sidebar-footer .btn-logout {
  display: inline-flex; align-items: center; justify-content: center; gap: .3rem;
  background: rgba(148,163,184,.08); border: 1px solid var(--nav-border);
  color: var(--nav-text); border-radius: var(--radius-md);
  padding: .45rem .35rem; font-size: .8rem; width: 100%; cursor: pointer;
  /* 🔴 CHỮ PHẢI NẰM 1 DÒNG. Hai lớp bảo vệ:
     1. nowrap — chặn gãy dòng, kể cả khi ai đó đổi nhãn dài hơn (lúc đó lỗi lộ ra bằng
        cách tràn viền, thấy ngay, chứ không âm thầm làm hai nút cao lệch nhau).
     2. Nới chỗ: gap .4→.3rem, padding ngang .5→.35rem ⇒ nhãn dài nhất ("Đăng xuất" +
        icon) chỉ cần 93.6px trong cột 108.8px — dư 15.2px thay vì 5.9px. Nhờ vậy vẫn
        vừa 1 dòng cả khi cỡ chữ gốc trình duyệt lớn hơn 16px (zoom, hệ điều hành đặt
        font to) — trường hợp trước đây sẽ tràn. */
  white-space: nowrap;
}
.app-sidebar-footer .app-theme-toggle:hover,
.app-sidebar-footer .btn-logout:hover { background: var(--nav-hover-bg); color: var(--nav-text-strong); border-color: var(--border-strong); }

/* ============================================================
   Sidebar collapse (desktop >=992px). Trạng thái ở <html class="sidebar-collapsed">,
   áp trước paint từ localStorage (layout.html) để không nháy.
   ============================================================ */
.app-sidebar-brand { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.app-sidebar-collapse-btn {
  flex: none; background: transparent; border: none; color: var(--nav-text-muted);
  padding: .3rem; border-radius: var(--radius-md); cursor: pointer; line-height: 0;
}
.app-sidebar-collapse-btn:hover { background: var(--nav-hover-bg); color: var(--nav-text-strong); }
.collapse-ic { display: inline-flex; }
.collapse-ic--open { display: none; }
html.sidebar-collapsed .collapse-ic--close { display: none; }
html.sidebar-collapsed .collapse-ic--open  { display: inline-flex; }

@media (min-width: 992px) {
  html.sidebar-collapsed { --sidebar-width: 76px; }
  html.sidebar-collapsed .app-sidebar { width: 76px; }
  html.sidebar-collapsed .page-content { margin-left: 76px; }

  /* Ẩn nhãn/khối chữ; chừa nút collapse (để mở lại) + icon. */
  html.sidebar-collapsed .app-brand,
  html.sidebar-collapsed .app-club-switch,
  html.sidebar-collapsed .app-sidebar-group-label,
  html.sidebar-collapsed .app-user__meta,
  html.sidebar-collapsed .app-user__chev,
  html.sidebar-collapsed .app-sidebar-nav .nav-link > span,
  html.sidebar-collapsed .app-theme-toggle > span,
  html.sidebar-collapsed .btn-logout > span { display: none; }

  /* Căn giữa các phần tử còn lại trong rail hẹp. */
  html.sidebar-collapsed .app-sidebar-brand { justify-content: center; }
  html.sidebar-collapsed .app-sidebar-nav .nav-link { justify-content: center; padding: clamp(.25rem, (100vh - 700px) / 25, .45rem); }
  html.sidebar-collapsed .app-user { justify-content: center; }
  /* Rail hẹp: xếp dọc — grid 1 cột (trước là flex-direction: column). */
  html.sidebar-collapsed .app-sidebar-footer-actions { grid-template-columns: 1fr; }
}

/* Nút ghost — nền trong suốt, hover nền chìm (dùng cho trigger menu "…") */
.btn-ghost { background: transparent; border: 1px solid transparent; color: var(--text-muted); }
.btn-ghost:hover { background: var(--surface-muted); color: var(--text); }
.min-w-0 { min-width: 0; }

/* Nút đổi theme sáng/tối (mặc định — dùng trên topbar mobile sáng) */
.app-theme-toggle {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-muted); border-radius: var(--radius-md);
  padding: .35rem .6rem; font-size: .85rem; cursor: pointer;
}
.app-theme-toggle:hover { border-color: var(--border-strong); color: var(--text); }

/* ============================================================
   Bottom-nav cố định đáy (<992px)
   ============================================================ */
/* 🔴 `env(safe-area-inset-bottom)` ở đây (và trong `padding-bottom` của `.page-content`) hiện **trơ
   trên máy thật**: `env()` chỉ khác 0 khi thẻ meta viewport có `viewport-fit=cover`, mà
   `fragments/layout.html` cố tình KHÔNG có. Đã kiểm ở task 1.2 và kết luận: **vô hại, đừng "sửa"**.
   Không có `viewport-fit=cover`, trình duyệt tự thu layout viewport về vùng an toàn, nên thanh nav ở
   `bottom: 0` vẫn nằm TRÊN home indicator — đúng kết quả mong muốn, chỉ là do trình duyệt lo thay vì
   do CSS. Thêm `viewport-fit=cover` để tràn viền (edge-to-edge) là một QUYẾT ĐỊNH THIẾT KẾ, không
   phải sửa lỗi, và khi đó phải xử lý cả 4 chiều: đo bằng `Emulation.setSafeAreaInsetsOverride`
   (inset 47/34 của iPhone 14) cho thấy topbar hiện KHÔNG có inset trên — nội dung chỉ cách mép trên
   8px trong khi notch cao 47px ⇒ bật cover mà không thêm `padding-top` cho topbar là đẩy tên CLB
   xuống dưới thanh trạng thái. Giữ `env()` ở đây như một lớp phòng thân: đúng ngay nếu sau này bật. */
/* Khung ngoài: trải hết bề ngang chỉ để CANH GIỮA viên thuốc, bản thân nó vô hình và không nhận
   chuột/chạm — nếu không có `pointer-events: none` thì dải trống hai bên viên thuốc lúc thu nhỏ
   sẽ nuốt mọi cú chạm vào nội dung nằm dưới. */
.app-bottom-nav {
  position: fixed; left: 0; right: 0;
  bottom: calc(var(--bottom-nav-gap-bottom) + env(safe-area-inset-bottom));
  display: flex; justify-content: center;
  padding-inline: var(--bottom-nav-gap);
  pointer-events: none;
  z-index: 1030;
}
/* Viên thuốc: thứ người dùng THẤY. Bo 4 góc + bóng đổ vì nó nổi trên nội dung đang cuộn, không
   còn dán mép dưới như trước (bản cũ dùng `border-top` thay bóng — viền trên giờ vô nghĩa). */
.app-bottom-nav__pill {
  display: flex; width: 100%; max-width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  box-shadow: 0 6px 20px rgb(0 0 0 / .14);
  pointer-events: auto;
  transition: max-width var(--motion-base) var(--ease-out);
}
/* Thu nhỏ khi vuốt xuống (class gắn bởi vòng lặp scroll ở fragments/layout.html, cùng nhịp với
   topbar mobile trượt lên). Nhãn biến mất, viên thuốc thấp xuống và co về giữa màn hình.
   🔴 Ô chạm lúc này đo 56×48px — vẫn qua MB-3 (≥44). Đừng hạ `min-height` xuống nữa: 48 là biên
   an toàn cuối cùng, mà đây lại là thanh điều hướng bấm-khi-đang-cầm-vợt (CT-6). */
.app-bottom-nav.is-compact .app-bottom-nav__pill {
  max-width: var(--bottom-nav-compact-max);
}
.app-bottom-nav.is-compact .app-bottom-nav-item {
  min-height: 48px; padding: .5rem 0; gap: 0;
}
/* Nhãn tắt bằng `max-height` + `opacity` chứ không `display: none` — cần nội suy được để chữ tan
   dần thay vì biến mất giật cục giữa lúc người dùng đang cuộn. */
.app-bottom-nav.is-compact .app-bottom-nav-item > span {
  opacity: 0; max-height: 0; overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .app-bottom-nav__pill,
  .app-bottom-nav-item,
  .app-bottom-nav-item > span { transition: none; }
}
/* Nhãn 13px = bậc nhỏ nhất của thang chữ (MB-2); icon 20px = --icon-md (MB-4); mỗi ô đã đủ
   44×44 nhờ `min-height: 56px` + `flex: 1 1 0` (5 ô trên 360px = 72px mỗi ô).
   🔴 Trước task 1.1 nhãn là `.7rem` (11,2px) và chỉ được tầng mobile nâng lên 13px ở <768px —
   nhưng bottom-nav mang `d-lg-none` nên nó sống tới 991,98px: đo ở 768×1024 nhãn "Trang chủ"
   vẫn 11,2px. Đặt bậc chữ ngay tại component (kèm giá trị dự phòng vì token chỉ tồn tại
   trong tầng mobile) thay vì thêm một breakpoint nữa — component không render ở >=992px nên
   desktop không thể bị chạm. Cùng lý lẽ với `.app-topbar-mobile__club` ở trên. */
.app-bottom-nav-item {
  flex: 1 1 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: .15rem;
  min-height: 56px; padding: .5rem 0;
  color: var(--text-muted); background: none; border: none;
  font-size: var(--fs-caption, .8125rem); text-decoration: none;
  border-radius: 19px; /* 20 của viên thuốc trừ 1 viền — focus ring không thò ra ngoài bo góc */
  transition: min-height var(--motion-base) var(--ease-out),
              padding var(--motion-base) var(--ease-out),
              transform var(--motion-instant) ease-out;
}
.app-bottom-nav-item > span {
  max-height: 1.4em; opacity: 1;
  transition: max-height var(--motion-base) var(--ease-out),
              opacity var(--motion-fast) var(--ease-out);
}
.app-bottom-nav-item i { font-size: var(--icon-md, 1.25rem); }
.app-bottom-nav-item.active { color: var(--action-primary); }

/* ============================================================
   VẬT LIỆU — thanh trên di động và viên thuốc bottom nav là lớp MỜ nổi trên nội dung
   ============================================================ */
/* Hai thanh này vốn nền đặc: mỗi cái che hẳn một dải nội dung đang cuộn bên dưới. Nền bán trong
   suốt + làm nhoè phía sau cho thấy trang vẫn tiếp tục dưới chúng, mà chữ trên thanh vẫn đọc được
   (12/10/2026). `saturate` bù lại phần màu bị nhoè làm xỉn đi.
   🔴 Đứng SAU hai luật gốc (`.app-topbar-mobile`, `.app-bottom-nav__pill`) vì cùng độ ưu tiên —
   chuyển lên trên là nền đặc thắng lại.
   🔴 `backdrop-filter` biến phần tử thành containing block của mọi con `position: fixed`. Lúc viết
   hai thanh này KHÔNG chứa modal/offcanvas/dropdown nào (modal Thông báo nằm ở `.app-nav-shell`,
   anh em của topbar) — đặt một cái vào trong là nó bị nhốt trong bề rộng của thanh.
   Bọc trong @supports: trình duyệt không có backdrop-filter mà nhận nền 78% thì chữ nằm thẳng lên
   nội dung đang cuộn, không có gì làm nhoè ⇒ ở đó giữ nền đặc cũ. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .app-topbar-mobile,
  .app-bottom-nav__pill {
    background: color-mix(in srgb, var(--surface) 78%, transparent);
    -webkit-backdrop-filter: blur(18px) saturate(180%);
    backdrop-filter: blur(18px) saturate(180%);
  }
}
/* Người bật "Giảm độ trong suốt" hoặc "Tăng độ tương phản" ở hệ điều hành: trả lại nền đặc. Hai
   thiết lập độc lập nhau; bản tương phản cao thêm viền đậm để mép thanh không tan vào trang. */
@media (prefers-reduced-transparency: reduce) {
  .app-topbar-mobile,
  .app-bottom-nav__pill {
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
@media (prefers-contrast: more) {
  .app-topbar-mobile,
  .app-bottom-nav__pill {
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-color: var(--border-strong);
  }
}

/* Offcanvas menu (mobile) dùng token */
.offcanvas { background: var(--surface); color: var(--text); }
.offcanvas .offcanvas-header { border-bottom: 1px solid var(--border); }

/* ------------------------------------------------------------
   Vùng chạm + thang chữ trong offcanvas menu (task 1.1, MB-2/MB-3/MB-7).
   🔴 Trạng thái ĐÓNG của offcanvas có `visibility: hidden`, nên `audit.js` (mục 3) bỏ qua toàn
   bộ phần tử bên trong: mọi vùng chạm nhỏ ở đây KHÔNG BAO GIỜ bị đếm nếu chỉ đo trang tĩnh —
   đó là lý do task 1.1 thêm `BMH_OPEN` vào công cụ đo. Đo với menu MỞ (390×844 light /
   360×800 dark / 768×1024 light) trước khi sửa: mục menu 40px · club switcher 41–44px ·
   `.btn-close` 31–32px · "Đăng xuất" 31px (ở 768px, nơi tầng mobile không với tới).
   Toàn bộ cụm này scope theo `.offcanvas` — app chỉ có ĐÚNG MỘT offcanvas (`#menuOffcanvas`,
   `fragments/nav.html`) và nó mang `d-lg-none` ⇒ desktop 1440px không render, MB-8 an toàn về
   mặt cấu trúc.
   ------------------------------------------------------------ */
/* Mục menu: trước task 1.1 là tổ hợp utility viết tay trong template
   (`d-flex align-items-center gap-2 py-2 text-decoration-none` → cao đúng 40px). Đặt tên class
   thật để chiều cao vùng chạm là HỢP ĐỒNG của component, không phải hệ quả tình cờ của một
   chuỗi utility mà lần sửa sau dễ gõ thiếu. */
.app-offcanvas-link {
  display: flex; align-items: center; gap: .5rem;
  min-height: var(--tap-min, 44px);
  margin: 0 -.5rem; padding: 0 .5rem;   /* lùi lề âm: chữ vẫn thẳng cột với nhãn nhóm */
  border-radius: var(--radius-md);
  color: var(--text); text-decoration: none;
}
.app-offcanvas-link:hover,
.app-offcanvas-link:focus-visible { background: var(--surface-muted); color: var(--text-strong); }
/* Icon hộp quà cùng hàng chữ "Menu" (BR-GIFT-08) — mở màn Mã quà tặng. Vùng chạm vuông 44×44 là
   hợp đồng của chính class này (MB-3), không trông vào padding của tiêu đề offcanvas. Màu thương
   hiệu để nổi lên giữa hàng tiêu đề vốn toàn chữ xám. */
.app-offcanvas-gift {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap-min, 44px); height: var(--tap-min, 44px);
  border-radius: var(--radius-md);
  color: var(--action-primary); font-size: 1.25rem; text-decoration: none;
}
.app-offcanvas-gift:hover,
.app-offcanvas-gift:focus-visible { background: var(--surface-muted); color: var(--action-primary-hover); }
.offcanvas .app-club-switch,
.offcanvas .btn { min-height: var(--tap-min, 44px); }
/* Thanh chọn CLB chạm gần hai mép ngăn kéo như các mục menu bên dưới.
   Phần tên nhận khoảng trống còn lại để huy hiệu FREE/PRO luôn nằm cạnh icon đổi CLB. */
#menuOffcanvas .app-club-switch { margin-inline: calc(-1 * var(--space-xs)); }
#menuOffcanvas .app-club-switch .label { flex: 1 1 auto; }
/* `.btn-close` (Bootstrap) 31–32px: giữ kích thước nhìn thấy, mở vùng chạm bằng pseudo như P1. */
.offcanvas .btn-close { position: relative; }
.offcanvas .btn-close::after {
  content: ''; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap-min, 44px); height: var(--tap-min, 44px);
}
/* Nhãn nhóm: 11px → 13px (MB-2), và màu. `--nav-group-label` (#64789B) sinh ra cho sidebar
   navy cố định; trên nền `--surface` SÁNG của offcanvas nó chỉ đạt **4,47:1** (cần 4,5 — MB-7,
   đo 390×844 light). Theme tối đã được task 0.6 sửa bằng luật `[data-bs-theme="dark"]` riêng;
   đây là nốt còn lại của theme sáng, lần này scope theo `.offcanvas` để sidebar desktop —
   nơi #64789B ĐÚNG vì nền navy — không bị chạm. */
.offcanvas .app-sidebar-group-label {
  font-size: var(--fs-caption, .8125rem);
  color: var(--text-muted);
}

/* ============================================================
   Bảng — list-card: bảng >=768px, card xếp chồng <768px.
   Cần <td data-label="..."> trong HTML.
   ============================================================ */
/* Bảng dữ liệu — thoáng, hover nhẹ, bỏ viền dọc, header kiểu caption in hoa. */
.table { color: var(--text); margin: 0; }
.table > thead > tr > th {
  font-size: .7rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-muted); border-bottom: 1px solid var(--border);
  padding: .85rem 1rem; white-space: nowrap;
}
.table > tbody > tr > td { padding: .85rem 1rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
.table > tbody > tr:last-child > td { border-bottom: 0; }
.table.table-hover > tbody > tr:hover > td,
.app-card .table > tbody > tr:hover > td { background: var(--surface-sunken); }
.table > :not(caption) > * > * { box-shadow: none; } /* bỏ đường kẻ mặc định Bootstrap */

/* Bo góc bảng khớp card mà KHÔNG dùng overflow:hidden — nếu bọc overflow:hidden thì dropdown "…"
   trong hàng bị cắt mất. Chỉ bo 4 ô góc để nền hover không tràn ra ngoài góc bo của card. */
.app-card > .list-card > table > thead > tr > th:first-child { border-top-left-radius: var(--radius-card); }
.app-card > .list-card > table > thead > tr > th:last-child  { border-top-right-radius: var(--radius-card); }
.app-card > .list-card > table > tbody > tr:last-child > td:first-child { border-bottom-left-radius: var(--radius-card); }
.app-card > .list-card > table > tbody > tr:last-child > td:last-child  { border-bottom-right-radius: var(--radius-card); }

/* ------------------------------------------------------------
   Ô số tiền CÓ ghi chú lồng bên trong — can-wrap-note (MỌI bề rộng).
   Ô `amount` mang `.money`, và `.money { white-space: nowrap }` là ĐÚNG cho con số
   ("125.000 ₫" không được bẻ đôi). Nhưng vài ô còn cõng thêm ghi chú VĂN BẢN TỰ DO do
   người dùng gõ — "Ghi đè:" (AC-EXP-04, tối đa 255 ký tự), "Thu cứng riêng:" (BR-MEM-16),
   "Hệ số riêng:" (BR-MEM-11) ở expense/fragments.html. Ghi chú là con của ô nên THỪA HƯỞNG
   `nowrap`: dài bao nhiêu cũng nằm đúng MỘT dòng.
   🔴 Đây là lỗi tầng DESKTOP, không phải mobile: `can-wrap-note` trước chỉ được gỡ nowrap
   bên trong tầng mobile ở cuối tệp (breakpoint 767,98px — ~dòng 2841), nên từ 768px trở lên
   luật nowrap vẫn nguyên. Đo ở tab "Chi phí & chốt sổ", 1440x900, lý do 130 ký tự: bảng rộng
   1182px trong card 928px (**tràn 254px**), chữ vượt mép phải card 168px rồi bị
   `overflow:hidden` của `.app-card` CẮT CỤT, cột "Người" bị bóp từ 271px còn 76px.
   Hai khai báo, thiếu một là chưa hết bệnh:
     • `white-space: normal` — cho ghi chú xuống dòng. Một mình nó KHÔNG đủ: lý do là chuỗi
       255 ký tự không có dấu cách (người dùng dán link, gõ liền) vẫn là MỘT "từ" không bẻ
       được ⇒ đo lại vẫn tràn 1282px. Không phải giả định — đã dựng đúng ca đó để đo.
     • `overflow-wrap: anywhere` — cho bẻ giữa từ khi không còn chỗ. Dùng `anywhere` (không
       phải `break-word`) vì chỉ `anywhere` mới hạ luôn kích thước min-content của ô, tức
       thứ mà thuật toán bảng auto-layout dùng để quyết bề rộng cột.
   `max-width` chặn trần để MỘT lý do dài không nuốt hết bề rộng cột "Người" (không cap: ô
   tiền chiếm 581/926px). 18rem giữ ghi chú ~3 dòng ở 1440 và trả cột "Người" về 271px.
   `margin-left: auto` giữ ghi chú căn phải cùng mép với con số (ô là `text-end`).
   Nhắm vào `> div` (ghi chú) chứ KHÔNG vào chính ô: đặt ở ô thì con số cũng mất nowrap và
   bẻ được thành "30.000 / ₫" — đúng thứ `.money` sinh ra để chặn.
   ------------------------------------------------------------ */
.list-card td[data-role="amount"].can-wrap-note > div {
  white-space: normal;
  overflow-wrap: anywhere;
  max-width: 18rem;
  margin-left: auto;
}

/* ============================================================
   Bảng "khóa 1 dòng" — table-nowrap.
   Ngày giờ, số tiền, tên người là dữ liệu atomic: bẻ dòng giữa chúng bắt mắt
   phải ghép lại mới đọc được, và làm chiều cao các dòng so le trông như lỗi.
   Mặc định khóa cả bảng 1 dòng, rồi mở lại đúng cột chữ tự do bằng .col-wrap;
   .col-grow cho cột đó nuốt hết phần rộng dư (chỉ dùng khi có ĐÚNG MỘT cột chữ
   tự do — nhiều cột cùng grow thì trình duyệt lại chia đều, vô nghĩa).
   Đánh dấu bằng class trên <th>/<td> chứ không nth-child: thêm/bớt cột sau này
   không âm thầm dịch quy tắc sang nhầm cột.
   ============================================================ */
@media (min-width: 768px) {
  .table-nowrap > thead > tr > th, .table-nowrap > tbody > tr > td { white-space: nowrap; }
  .table-nowrap .col-wrap { white-space: normal; }
  .table-nowrap .col-grow { width: 100%; }
}

/* ============================================================
   Bảng nhiều cột hẹp — table-tight-x.
   Chỉ bóp đệm NGANG (1rem → .5rem) để các cột atomic (ngày, badge, số tiền)
   sát nhau hơn, nhường phần rộng dôi ra cho cột .col-grow. Đệm DỌC giữ nguyên
   để chiều cao dòng và vùng chạm không đổi (CT-6). Chỉ áp dụng >=768px vì
   <768px bảng đã chuyển sang dạng card, đệm do .list-card quản.
   ============================================================ */
@media (min-width: 768px) {
  .table-tight-x > thead > tr > th, .table-tight-x > tbody > tr > td { padding-left: .5rem; padding-right: .5rem; }
  .table-tight-x > thead > tr > th:first-child, .table-tight-x > tbody > tr > td:first-child { padding-left: 1rem; }
  .table-tight-x > thead > tr > th:last-child, .table-tight-x > tbody > tr > td:last-child { padding-right: 1rem; }
}

/* ============================================================
   Ô ngày giờ 2 dòng — cell-datetime.
   "07/07/2026 14:30" trên một dòng ép cột Ngày rộng bằng cả chuỗi, ăn mất chỗ
   của cột mô tả. Tách giờ xuống dòng dưới (mờ, nhỏ hơn) để cột chỉ còn rộng
   bằng phần ngày. Ngày và giờ vẫn là dữ liệu atomic — mỗi phần tự nó không bẻ
   dòng, chỉ bẻ ĐÚNG chỗ giữa hai phần.
   <768px bảng thành card: giữ cùng một dòng cho gọn.
   ============================================================ */
.cell-datetime { display: inline; }
.cell-datetime__time { color: var(--text-muted); font-size: .8125rem; margin-left: .35rem; }
@media (min-width: 768px) {
  .cell-datetime__time { display: block; margin-left: 0; }
}

/* ============================================================
   Danh sách Khách gộp trong một dòng công nợ (BR-EXP-06)
   Mỗi khách một dòng, tên trái — tiền phải, để mắt dò thẳng xuống cột tiền.
   ============================================================ */
.guest-list { list-style: none; margin: 0; padding: 0; }
/* `flex-wrap` + tên `flex: 1 1 auto` (thay vì để `space-between` tự chia): từ 2026-08-25 dòng khách
   có BA phần — tên · tiền · badge đã trả/chưa trả. Với `space-between` và ba item không co được,
   phần tiền bị đẩy ra GIỮA khoảng trống, trông như một cột thứ ba lơ lửng. Cho tên nuốt phần rộng dư
   thì tiền và badge dính vào nhau ở mép phải như bố cục hai phần cũ; `min-width: 0` để tên dài tự
   wrap thay vì kéo tràn ô (cùng lỗi đã sửa cho tên sân, xem § bảng khóa 1 dòng). Cột này nằm trong
   `.col-wrap` nên khi hẹp thì badge rơi xuống dòng dưới thay vì ép cả bảng rộng ra. */
.guest-list li {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: .25rem .75rem; font-size: .875rem; padding: .1rem 0;
}
.guest-list li + li { border-top: 1px dashed var(--border); padding-top: .25rem; margin-top: .15rem; }
.guest-list__name { color: var(--text-muted); flex: 1 1 auto; min-width: 0; }
.guest-list__amount { white-space: nowrap; }
.guest-list__status { flex: 0 0 auto; }

/* ============================================================
   Vùng chạm điểm danh >=44px trên mobile (OQ-43)
   ============================================================ */
@media (max-width: 991.98px) {
  /* var(--tap-min, 44px): hợp nhất vào token chung (Đợt 3 task 3.3) — --tap-min chỉ tồn tại TRONG
     tầng mobile (<768px, xem ghi chú ở khối định nghĩa token), nên ở dải 768–991,98px token chưa
     định nghĩa và rơi về fallback 44px trong var() — giá trị hiệu dụng KHÔNG đổi ở cả hai dải. */
  .attendance-row .btn { min-height: var(--tap-min, 44px); min-width: 96px; font-size: .95rem; }
  .attendance-row { padding-top: .6rem; padding-bottom: .6rem; }
}

/* ============================================================
   Bảng điểm danh — grid 4 cột: Tên · Giới tính · Trình độ · Trạng thái.
   🔴 Mỗi dòng VẪN là <div> (KHÔNG phải <tr>) để không đụng cơ chế HTMX swap
   từng dòng của nút tick (xem chú thích trong attendance/fragments.html — <tr>
   trong <div> response sẽ bị trình duyệt nuốt mất khi htmx parse). Cột cố định
   px + cột Tên co giãn (1fr) ⇒ các dòng độc lập vẫn thẳng hàng nhau. Bọc trong
   .attendance-scroll (overflow-x) + min-width ⇒ cuộn ngang khi màn hẹp.
   ============================================================ */
.attendance-scroll { overflow-x: auto; }
.attendance-grid {
  display: grid;
  grid-template-columns: minmax(7rem, 1fr) 4.5rem 6.5rem 8.5rem;
  align-items: center;
  gap: .5rem;
  min-width: 28rem;
}
.attendance-grid__name { min-width: 0; }        /* cho phép cột Tên co lại thay vì tràn grid */
/* Tên của dòng KHÁCH là nút mở form sửa (AC-ATT-03.8..10). Không thêm icon vào cột Hành động được: khối
   lưới điện thoại ở cuối file đã đo bản 5 cột 324px (390×844) / 294px (360×800) — vừa đúng hết cuộn
   ngang; một cột nữa 2,25rem + gap đẩy lên ~364/334px là cuộn lại, cắt cụt đúng cột Tên (CT-6).
   Nút mượn diện tích sẵn có của ô Tên ⇒ bảng không rộng thêm pixel nào, và ô Tên cao bằng hàng nên
   vùng chạm đã qua 44px của MB-3. Gạch chân chấm để thấy được là bấm được mà không thành "link xanh"
   giữa một bảng dày chữ. */
.attendance-grid__name .name-edit {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  white-space: inherit;
  text-align: left;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 3px;
}
.attendance-grid__name .name-edit:hover,
.attendance-grid__name .name-edit:focus-visible {
  color: var(--action-primary);
  text-decoration-style: solid;
}
/* Cột thứ 5 "Hành động" (BR-ATT-08 — bỏ người khỏi danh sách). Chỉ bật khi người xem quản lý được
   buổi: Member không có nút nào ở đó, để lưới 5 cột cho họ là mất trắng 2.5rem bề ngang ở đúng chỗ
   chật nhất. Vì thế nó là class RIÊNG chứ không sửa `.attendance-grid` — bản chỉ-đọc giữ nguyên
   từng pixel (MB-8). */
.attendance-grid--actions {
  grid-template-columns: minmax(7rem, 1fr) 4.5rem 6.5rem 8.5rem 2.5rem;
  min-width: 31rem;
}
.attendance-grid__action { justify-self: center; min-width: 0; }
/* Nhãn header hai phiên bản: bản đầy đủ là mặc định ở MỌI bề ngang, bản rút gọn chỉ được bật trong
   khối điện thoại của lưới 5 cột (xem cuối file). Cố ý KHÔNG dùng `d-none`/`d-sm-inline` của
   Bootstrap: hai class đó áp theo breakpoint cho MỌI bảng, sẽ rút nhãn cả ở bản 4 cột chỉ-đọc của
   Member — nơi cột vẫn rộng 72px và không có lý do gì phải cắt chữ (MB-8: không đụng cái đang đúng). */
.attendance-grid__label-short { display: none; }
/* 🔴 Nút trạng thái phải RỘNG BẰNG NHAU ở cả 3 state. Để nút tự co theo chữ thì
   "Chưa điểm danh" (14 ký tự) to gấp ~2,5 lần "Vắng" ⇒ cột Trạng thái nhấp nhô
   theo từng dòng, và mỗi lần tick nút lại nhảy kích thước ngay dưới ngón tay.
   Cho ô chiếm trọn bề rộng cột (8.5rem, đủ chứa nhãn dài nhất) và nút/badge
   width:100% ⇒ mọi state cùng một khung, chỉ đổi màu và chữ. */
.attendance-grid__status { justify-self: stretch; min-width: 0; }
.attendance-grid__status > .btn,
.attendance-grid__status > .badge-soft {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  white-space: nowrap;
}

/* Tablet (576–992px): quy tắc vùng chạm ở trên bơm chữ nút lên .95rem ⇒ "Chưa điểm danh"
   rộng 120.3px, KHÔNG vừa phần lòng 118px của cột 8.5rem (đo được: tràn 2.3px). Nới cột
   riêng cho dải này thay vì nới cho cả desktop (desktop chữ 14px, 8.5rem đã dư 7.2px). */
@media (min-width: 576px) and (max-width: 991.98px) {
  .attendance-grid { grid-template-columns: minmax(7rem, 1fr) 4.5rem 6.5rem 9.25rem; }
  .attendance-grid--actions { grid-template-columns: minmax(7rem, 1fr) 4.5rem 6.5rem 9.25rem 2.5rem; }
}

/* 🔴 `.attendance-row .btn { min-width: 96px }` (khối vùng chạm ở trên, ≤991,98px) phóng CẢ nút xóa
   icon-only lên 96px — cột hành động rộng 2.5rem nên nút tràn ra ngoài, đè lên cột Trạng thái. Nhóm
   `is-icon` cố ý giữ kích thước NHÌN THẤY nhỏ và mở vùng chạm 44px bằng pseudo-element (P1 của
   10_Mobile_Spec § "Vùng chạm"), nên trả sàn bề ngang về 0 ở đúng nhóm đó. */
@media (max-width: 991.98px) {
  .attendance-row .btn.is-icon { min-width: 0; }
}

/* Điện thoại: mọi nút trạng thái rộng bằng nhau ⇒ cột phải ôm được NHÃN DÀI NHẤT. Trên
   mobile nhãn PENDING rút còn "Chưa" (xem attendance/fragments.html), nhãn dài nhất trở
   thành "Có mặt" = 47.2px ⇒ cột 4.5rem (72px) là đủ, thay vì 7.75rem như khi giữ nguyên
   "Chưa điểm danh". Nhờ vậy tổng hàng còn ~416px, vừa màn 430px — trước đó 468px nên bảng
   cuộn ngang và tên người bị cắt cụt. Chiều cao chạm 44px (OQ-43) giữ nguyên.
   🔴 min-width: 0 là BẮT BUỘC: min-width 28rem (448px) ở khối gốc sẽ giữ hàng rộng 448px
   dù các cột đã co lại ⇒ vẫn cuộn ngang, thu cột thành vô nghĩa. */
@media (max-width: 575.98px) {
  /* Cột Trình độ 6.5rem → 5.5rem: đủ để hàng vừa màn 375px (iPhone SE/mini) chứ không chỉ
     390px. Trình độ là free text nên giá trị dài sẽ xuống dòng — chấp nhận cao thêm 1 dòng
     để đổi lấy việc KHÔNG cuộn ngang, vì cuộn ngang cắt cụt cột Tên (lỗi đang gặp). */
  .attendance-grid { grid-template-columns: minmax(6rem, 1fr) 4.5rem 5.5rem 4.5rem; min-width: 0; }
  /* 🔴 Lưới 5 cột ở điện thoại — mọi con số dưới đây ĐO THẬT (Chrome headless, 13px = --fs-caption
     của tầng mobile), không ước lượng:
       nhãn header  "GIỚI TÍNH" 63,1 · "TRÌNH ĐỘ" 63,4 · "GIỚI" 27,7 · "TRÌNH" 39,9
       ô dữ liệu    "Nam" 27,8 · "Nữ" 17,5 · "Trung bình" 62,2 · nút xóa 33,2×44
     Bốn cột cũ (bản chỉ-đọc của Member) VỐN ĐÃ tràn: hàng rộng 372px trong hộp 356px ở 390×844 và
     326px ở 360×800 ⇒ cột Trạng thái bị cắt, phải cuộn ngang. Thêm cột thứ 5 mà giữ nguyên các cột
     kia sẽ đẩy lên 412px — tệ gấp đôi. Nên bản 5 cột đi hướng ngược lại: rút nhãn header hai cột
     giữa ("Giới tính"→"Giới", "Trình độ"→"Trình" — cùng thủ pháp đã dùng cho nút trạng thái và bộ
     đếm ngay trong file này) rồi thu chúng về đúng bề rộng nội dung.
     Kết quả đo lại: 324px ở 390×844 và 294px ở 360×800 ⇒ **KHÔNG còn cuộn ngang** ở cả hai, và cột
     Tên còn 104px (390) / 74px (360) — RỘNG HƠN 96px của bản 4 cột hiện tại ở 390.
     "Trung bình" 62,2px không vừa cột 56px ⇒ xuống 2 dòng: đánh đổi CÓ CHỦ Ý, đúng thứ tự ưu tiên đã
     ghi ở khối 4 cột bên trên (thà cao thêm một dòng còn hơn cuộn ngang, vì cuộn ngang cắt cụt cột
     Tên — thứ người dùng thật sự cần đọc khi đang cầm vợt, CT-6). */
  .attendance-grid--actions {
    grid-template-columns: minmax(4.5rem, 1fr) 2.5rem 3.5rem 4.5rem 2.25rem;
    gap: .25rem;
  }
  .attendance-grid--actions .attendance-grid__label-full { display: none; }
  .attendance-grid--actions .attendance-grid__label-short { display: inline; }
  /* "TRẠNG THÁI" rộng ~73px, nhỉnh hơn cột 72px vài px ⇒ mặc định gãy 2 dòng, đội cao hàng
     tiêu đề từ 33px lên 49px. Bỏ letter-spacing .02em (10 ký tự ⇒ tiết kiệm ~2.4px) là vừa
     đúng 1 dòng mà không phải tràn ra ngoài cột. */
  .attendance-grid--head { letter-spacing: normal; }
  .attendance-grid--head > span { white-space: nowrap; }
  .attendance-row .btn { font-size: .8125rem; min-width: 0; padding-left: .35rem; padding-right: .35rem; }
}

/* Nhãn form — đồng nhất mọi màn */
.form-label { font-weight: 600; font-size: .875rem; color: var(--text); margin-bottom: .35rem; }
.form-text { color: var(--text-muted); }

/* ============================================================
   AUTH & ENTRY — vỏ tối giản căn giữa (chưa đăng nhập, KHÔNG sidebar).
   Dùng cho: đăng nhập/đăng ký/quên-đặt lại MK, nhận lời mời (claim).
   ============================================================ */
.auth-shell {
  min-height: 100vh;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: var(--space-2xl) var(--space-md);
  background: var(--surface-sunken);
}
.auth-topbar { position: fixed; top: var(--space-md); right: var(--space-md); z-index: 10; }
.auth-container { width: 100%; max-width: 400px; }
.auth-brand {
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  font-weight: 800; font-size: 1.25rem; color: var(--text-strong);
  text-decoration: none; margin-bottom: var(--space-lg);
}
.auth-brand .app-brand-mark { font-size: 1.4rem; }
.auth-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--elevation-2);
  padding: var(--space-xl);
}
.auth-card__title { font-size: 1.25rem; margin-bottom: var(--space-2xs); }
.auth-card__subtitle { font-size: .875rem; color: var(--text-muted); margin-bottom: var(--space-lg); }
.auth-links {
  display: flex; justify-content: space-between; gap: var(--space-sm);
  margin-top: var(--space-md); font-size: .875rem; flex-wrap: wrap;
}
.auth-alt { margin-top: var(--space-md); font-size: .875rem; color: var(--text-muted); text-align: center; }
.auth-footer { margin-top: var(--space-lg); font-size: .8125rem; color: var(--text-muted); text-align: center; }
.auth-cta-landing { margin-bottom: var(--space-lg); text-align: center; }
.auth-cta-landing__hint { margin: var(--space-2xs) 0 0; font-size: .8125rem; color: var(--text-muted); }

/* Entry header — trang SAU đăng nhập nhưng CHƯA chọn CLB (danh sách CLB) — không có sidebar. */
.entry-header {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.entry-header .entry-actions { display: flex; align-items: center; gap: var(--space-sm); }
/* Biến thể cho khu quản trị hệ thống: header có THÊM một cụm nav 5 mục ở giữa, nên ba nhóm con
   không đủ chỗ trên một hàng ở màn điện thoại. Không có flex-wrap ⇒ tràn ngang 465px trên viewport
   390px (đo bằng CDP 2026-08-08). Đặt riêng chứ không sửa .entry-header chung, vì các trang entry
   hiện có chỉ có brand + 2 nút và đang vừa khít. */
.entry-header--platform { flex-wrap: wrap; row-gap: var(--space-sm); }
.entry-header--platform > * { min-width: 0; }
.entry-content { max-width: var(--w-wide); margin: 0 auto; padding: var(--space-xl) var(--space-md); }

/* ============================================================
   KHU QUẢN TRỊ HỆ THỐNG (/admin) — vỏ riêng, DESKTOP-FIRST
   ------------------------------------------------------------
   Người vận hành SaaS hầu như chỉ đăng nhập bằng máy tính, nên khu này đảo ngược thứ tự ưu tiên so
   với phần còn lại của app: ≥992px có thanh bên DỌC cố định (dùng lại nguyên `.app-sidebar` của khu
   CLB) và nội dung tràn HẾT bề ngang còn lại; <992px rơi về header ngang + dropdown "Menu" như cũ.

   🔴 KHÔNG chặn max-width ở đây, và đó là chủ đích chứ không phải thiếu sót. Mọi màn trong CLB bị
   ép về 960px (--w-wide) cho đồng nhất, nhưng khu quản trị toàn bảng nhiều cột (Tài khoản, CLB,
   Nhật ký, Đối soát) — đóng khung 960px thì các cột bị nén hoặc phải cuộn ngang trong khi hai bên
   còn cả nghìn pixel trống. `AdminSidebarShellTest` gác cả hai vế (có margin-left · không max-width).

   🔴 Đây là lý do trang quản trị KHÔNG được dùng lại `.entry-content`: luật
   `:root:has(.entry-content) { --sidebar-width: 0px }` (khối quảng cáo dọc, cuối file) sinh ra cho
   các vỏ KHÔNG có thanh bên, nên nó sẽ ép margin-left dưới đây về 0 và nội dung nằm lọt dưới thanh
   bên — không lỗi, không log. Hai class phải tách bạch.
   ============================================================ */
.admin-content { padding: var(--space-lg) var(--space-md); }

@media (min-width: 992px) {
  /* margin-left đọc token chứ không hardcode 248px: `html.sidebar-collapsed` hạ token xuống 76px,
     nên nút thu gọn tự chạy đúng ở khu này mà không cần luật riêng. */
  .admin-content { margin-left: var(--sidebar-width); padding: var(--space-xl); }
}

/* Danh tính người vận hành ở chân thanh bên là khối TĨNH (khu quản trị không có trang hồ sơ để
   dẫn tới), nên phải gỡ hai tín hiệu "bấm được" mà `.app-user` mang sẵn cho khu CLB. */
.app-user--tinh { cursor: default; }
.app-user--tinh:hover { background: transparent; color: var(--nav-text); }

/* ============================================================
   STEP FLOW — trục hành động buổi tập: Điểm danh → Chi phí → Chốt sổ (M-TSK-1).
   Mảnh, không phô: chỉ để dẫn thứ tự, không cạnh tranh với nội dung.
   ============================================================ */
.step-flow { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-bottom: var(--space-lg); }
.step-flow__item {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .35rem .7rem; border-radius: var(--radius-pill);
  font-size: .8125rem; font-weight: 600;
  background: var(--surface-muted); color: var(--text-muted);
  border: 1px solid var(--border);
}
.step-flow__item .num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.2rem; height: 1.2rem; border-radius: 50%;
  background: var(--border-strong); color: var(--surface); font-size: .7rem; font-weight: 700;
}
.step-flow__item.is-done { background: rgba(22,163,74,.12); color: var(--badge-green-fg); border-color: rgba(22,163,74,.24); }
.step-flow__item.is-done .num { background: var(--action-primary-solid, var(--action-primary)); color: #fff; }
.step-flow__item.is-current { background: var(--sidebar-active-bg); color: var(--sidebar-active-text); border-color: var(--action-primary); }
.step-flow__item.is-current .num { background: var(--action-primary-solid, var(--action-primary)); color: #fff; }
.step-flow__sep { color: var(--text-disabled); }

/* Hàng nút hành động theo trạng thái buổi */
.action-bar { display: flex; flex-wrap: wrap; gap: var(--space-xs); align-items: center; margin-bottom: var(--space-lg); }
/* Biến thể "action-bar sống TRONG page-header" (task màn Công nợ): P4 cần một actionBar để gom nút
   phụ vào ⋯ trên mobile, nhưng chuyển thanh nút xuống một hàng riêng dưới header — như
   session/buoi.html — là đổi bố cục desktop, phá MB-8. Đặt nó vào đúng slot `actions` cũ thì phải
   gỡ hai thứ chỉ đúng khi nó đứng riêng: `margin-bottom` (page-header đã có lề dưới của chính nó)
   và gap `--space-xs` (khối cũ dùng utility `gap-2` = .5rem — giữ đúng con số đó để desktop không
   đổi một pixel). */
.page-header__row .action-bar { margin-bottom: 0; gap: .5rem; }

/* P4 — thanh hành động. Desktop: hàng ngang, không có nút ⋯. */
.action-bar__primary, .action-bar__rest { display: contents; }
.action-bar__more { display: none; }

/* ============================================================
   Các trang CÔNG KHAI của một buổi tập — /chia-se/{token} (thẻ "vé mời" + bảng chia tiền)
   VÀ /vote/{token}. Markup chung ở templates/fragments/buoi-hero.html.
   Điểm nhấn DUY NHẤT: khối hero WHEN (ngày + giờ). Còn lại giữ yên.

   🔴 Tiền tố `share-` là DẤU VẾT LỊCH SỬ, không phải ranh giới module: nhóm này ra đời khi chỉ
   trang chia sẻ dùng, nay trang vote dùng chung y hệt. Giữ tên vì đổi là 17 chỗ ở đây cộng 3
   template, sót một chỗ là vỡ layout mà không báo lỗi — xem VT-5 trong
   docs/superpowers/specs/2026-07-28-vote-dung-chung-template-chia-se-design.md.
   ============================================================ */
/* ============================================================
   POPUP CẮT ẢNH (fragments/cat-anh.html + /js/cat-anh.js, 17/09/2026)
   Mở ra khi chọn file ở 4 ô ảnh người. Cropper.js dựng khung thao tác của nó ĐÈ lên thẻ <img>, nên
   phần CSS ở đây chỉ lo ba việc: cho thẻ <img> một kích thước để Cropper bám vào, xếp hàng zoom,
   và bo tròn khung cắt cho ảnh đại diện.
   ============================================================ */
/* 🔴 Khung PHẢI có chiều cao thật trước khi Cropper khởi tạo: Cropper đọc kích thước hiển thị của
   <img> để dựng `.cropper-container`, mà <img> chưa tải xong thì cao 0px ⇒ popup mở ra trống trơn
   và không kéo được gì. Kẹp bằng min() theo vh để trên điện thoại (sheet cao tối đa 85vh) vẫn còn
   chỗ cho tiêu đề, dòng hướng dẫn, hàng zoom và hai nút ở chân — chúng cộng lại ~216px ở 390×844. */
.bmh-cat-anh__khung {
  /* 🔴 Khung ăn theo ĐÚNG tỉ lệ đang cắt (`--bmh-khung` do cat-anh.js đặt trước khi khởi tạo), không
     phải một chiều cao cố định. Đo ở 390×844 với khung cố định 26rem: ảnh bìa 2:1 chỉ cao 114px
     trong một hộp cao 416px ⇒ 300px trống ngay dưới ảnh, đẩy thanh trượt và hai nút xuống khỏi tầm
     mắt mà không thêm chỗ thao tác nào. Mặc định 1 để ảnh đại diện vẫn đúng khi JS chưa kịp chạy.
     🔴 `max-width` do JS đặt theo kích thước ẢNH (rongToiDaCuaKhung) — khung co cho vừa ảnh, chứ
     KHÔNG phóng ảnh cho vừa khung. Xem lý do đầy đủ ở hàm đó: đây là chỗ chữa lỗi "ảnh bị mờ đi".
     margin-inline: auto vì khung nay hẹp hơn thân modal ở ảnh nhỏ — lệch trái trông như lỗi layout. */
  aspect-ratio: var(--bmh-khung, 1);
  max-height: min(58vh, 26rem);
  margin-inline: auto;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}
/* 🔴 `>` chứ KHÔNG phải hậu duệ bất kỳ: sau khi khởi tạo, Cropper dựng cả một cây .cropper-container
   NẰM TRONG khung này, với hai thẻ <img> của riêng nó đã được gán width/height inline. Luật viết
   kiểu hậu duệ sẽ rò `object-fit: contain` vào đúng hai thẻ đó (đo được 17/09/2026) — vô hại ở tỉ lệ
   1:1 nhưng là một quả mìn hẹn giờ mỗi lần Cropper đổi cách dựng. Trước khi khởi tạo thì <img> là
   con TRỰC TIẾP, nên luật vẫn làm đúng việc của nó là cho Cropper một hộp có kích thước để đo. */
.bmh-cat-anh__khung > img { display: block; width: 100%; height: 100%; object-fit: contain; }
/* Khung chỉ hiện khi Cropper đã dựng xong và đã đúng kích thước. Không có bước này thì người dùng
   thấy ảnh xuất hiện ở một cỡ rồi nhảy sang cỡ khác — đúng cái "nháy một phát" đã bị báo. */
.bmh-cat-anh__khung { opacity: 0; transition: opacity .12s ease-out; }
.bmh-cat-anh--san-sang .bmh-cat-anh__khung { opacity: 1; }
.bmh-cat-anh__zoom { display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-sm); }
.bmh-cat-anh__zoom .form-range { flex: 1 1 auto; min-width: 0; }
/* Vùng chạm đủ lớn cho ngón tay ở hai nút −/+ (CT-6). Nút .btn-sm của Bootstrap chỉ 31px. */
.bmh-cat-anh__zoom .btn { flex: none; min-height: var(--tap-min, 44px); min-width: var(--tap-min, 44px); }
/* Khung TRÒN cho ảnh đại diện — thuần thị giác: ảnh xuất ra vẫn vuông, chỗ hiển thị mới là nơi bo
   tròn. Không có nó thì người dùng căn ảnh trong một ô vuông rồi thấy nó hiện ra trong một vòng
   tròn đã ăn mất bốn góc, đúng kiểu "bị cắt bất ngờ" mà popup này sinh ra để dẹp. */
.bmh-cat-anh--tron .cropper-view-box,
.bmh-cat-anh--tron .cropper-face { border-radius: 50%; }
.bmh-cat-anh--tron .cropper-view-box { outline-color: rgba(255, 255, 255, .85); }

/* Ảnh bìa CLB — MỘT ô xem trước duy nhất ở Cài đặt (BR-CLUB-14, người dùng yêu cầu 16/09/2026).
   Trước đây có hai khung cạnh nhau (trang chủ 4:1 · điện thoại 5:2); từ khi trang chủ không còn dải
   ảnh bìa, khung 4:1 không còn chỗ hiển thị thật nào để đối chiếu. Giữ đúng tỉ lệ của `.share-cover`
   — nơi ảnh bìa thật sự còn được vẽ (ba trang chia sẻ công khai).
   🔴 2:1 từ 17/09/2026 (người dùng: dải 5:2 "quá dài nên hiển thị không đẹp"). Con số này sống ở BA
   nơi phải luôn bằng nhau: đây · `.share-cover` · `data-crop-ratio="2"` của ô file ở cai-dat.html
   (khung cắt trong popup). Lệch một chỗ ⇒ người dùng căn ảnh trong một khung rồi thấy nó hiện ra
   trong một khung khác, đúng cái khó chịu popup sinh ra để dẹp. `CaiDatAnhBiaTemplateTest` gác. */
.cover-preview { display: grid; grid-template-columns: minmax(0, 1fr); }
.cover-preview__item { margin: 0; }
.cover-preview__frame {
  aspect-ratio: 2 / 1; overflow: hidden; border-radius: var(--radius-btn);
  border: 1px solid var(--border); background: var(--surface-sunken);
}
.cover-preview__frame img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center; }

/* Avatar căn giữa ngang, 80% nằm trên ảnh bìa; tên có tâm riêng, không tính nút sửa. */
.club-profile { padding-bottom: 4.25rem; margin-bottom: var(--space-sm); }
.club-profile__cover {
  position: relative; aspect-ratio: 2 / 1; overflow: visible;
  border-radius: var(--radius-btn); background: var(--court-600);
}
.club-profile__cover > img { position: relative; z-index: 1; display: block; width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
.club-profile__cover-empty {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  border-radius: inherit; overflow: hidden;
}
.club-profile__cover-empty { color: #fff; font-size: 2rem; }
.club-profile__cover-shade {
  position: absolute; z-index: 2; inset: 0; border-radius: inherit;
  background: linear-gradient(180deg, rgba(6,58,45,.10) 0%, rgba(6,58,45,.62) 100%);
  pointer-events: none;
}
.club-profile__cover-action { position: absolute; z-index: 4; right: .75rem; bottom: .75rem; }
.club-profile__camera,
.club-profile__avatar-camera {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; border: 1px solid var(--border);
  border-radius: 50%; background: var(--surface); color: var(--text-strong);
  box-shadow: 0 1px 5px rgba(0, 0, 0, .2);
}
.club-profile__camera { cursor: pointer; }
.club-profile__identity {
  position: absolute; z-index: 3; top: 100%; left: 0; right: 0;
  display: flex; flex-direction: column; align-items: center;
}
.club-profile__avatar { flex: none; margin-top: -4.4rem; }
.club-profile__avatar-button {
  position: relative; display: block; width: 5.5rem; height: 5.5rem; padding: 0;
  border: 4px solid var(--surface); border-radius: 50%; background: var(--surface-sunken);
  cursor: pointer;
}
.club-profile__avatar-button > img { position: relative; z-index: 1; display: block; width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.club-profile__avatar-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
.club-profile__avatar-empty { background: var(--court-500); color: #fff; font-size: 2rem; }
.club-profile__avatar-camera { position: absolute; z-index: 2; right: -.35rem; bottom: -.15rem; width: 1.7rem; height: 1.7rem; font-size: .75rem; }
.club-profile__name-row {
  position: relative; max-width: calc(100% - 5rem); min-width: 0;
  margin-top: .5rem; text-align: center;
}
.club-profile__name {
  display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 1.375rem; font-weight: 800; color: var(--text-strong);
}
.club-profile__edit-name {
  position: absolute; top: 50%; left: 100%; transform: translateY(-50%);
  margin-left: .4rem; padding: .35rem; border: 0;
  background: transparent; color: var(--text-muted); cursor: pointer;
}
.club-profile__camera:focus-visible,
.club-profile__avatar-button:focus-visible,
.club-profile__edit-name:focus-visible { outline: 3px solid var(--court-500); outline-offset: 2px; }
.club-profile__upload { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.club-profile__viewer .modal-body { display: grid; place-items: center; min-height: 0; padding: 1rem; background: #101820; }
.club-profile__viewer .modal-body img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }
@media (max-width: 991.98px) {
  .club-profile { padding-bottom: 4rem; }
  .club-profile__avatar { margin-top: -3.2rem; }
  .club-profile__avatar-button { width: 4rem; height: 4rem; }
  .club-profile__name { font-size: 1.125rem; }
}

/* Banner ảnh bìa CLB ở đầu ba trang chia sẻ (BR-CLUB-14). Con đầu tiên của .auth-card, lề âm đúng bằng
   padding của thẻ (--space-xl; điện thoại --space-md, xem tầng mobile) để ảnh sát mép — đổi padding thẻ
   thì đổi cả hai chỗ. Bo góc trên trừ 1px viền thẻ.

   🔴 Từ 16/09/2026 banner mang thêm logo + tên CLB đè lên mép dưới ảnh, sao đúng khuôn ảnh vinh danh
   tháng (.honor-anh__banner + .honor-anh__clb, người dùng yêu cầu). Ba hệ quả về CSS:
   (1) KHÔNG còn `overflow: hidden` ở thẻ ngoài — nó cắt cụt đúng cái khối vừa cố ý cho tràn xuống.
       Bo góc chuyển xuống ẢNH và LỚP PHỦ (border-radius: inherit), ảnh vẫn không lòi ra góc vuông.
   (2) Lề dưới phải cộng thêm phần tràn `--cover-de`, nếu không khối CLB đè lên nội dung kế tiếp.
       Hai chỗ đọc chung một biến để không bao giờ lệch nhau.
   (3) Chiều cao khối do `aspect-ratio` quyết dù không còn con nào nằm trong dòng chảy (ảnh và lớp
       phủ đều absolute) — đúng cơ chế ta cần, đừng thay bằng height cứng như bản PNG vinh danh. */
.share-cover {
  /* 🔴 8px chứ KHÔNG 18px (người dùng báo lần thứ hai, 16/09/2026: "tên CLB vẫn bị cắt"). Tên đứng
     cùng hàng với logo và được căn giữa theo logo, nên logo nhô bao nhiêu thì TÊN tụt theo bấy
     nhiêu: ở 18px, baseline của tên rơi cách mép dưới ảnh đúng 3,5px và dấu nặng dưới "ệ" chạm qua
     mép ⇒ viền trắng — thứ duy nhất tách chữ tối khỏi ảnh tối — KHÔNG còn chỗ ở phía dưới, mắt đọc
     cái mép phẳng đó thành "chữ bị xén". Chữ vẫn đủ nét, thứ mất là khoảng thở.
     8px cho lại **8,5px thở** dưới viền (đo trên trang chia sẻ thật, 640px) mà logo vẫn nhô.
     Người dùng chọn phương án này thay vì nâng riêng tên, để tên còn thẳng tâm logo. */
  --cover-de: .5rem;  /* 8px. 🔴 KHÔNG liên thông với `.honor-anh__clb` của ảnh vinh
     danh: hai banner khác tỉ lệ logo/banner nên mỗi bên chốt bằng phép đo riêng. */
  position: relative;
  margin: calc(-1 * var(--space-xl)) calc(-1 * var(--space-xl)) calc(var(--space-md) + var(--cover-de));
  /* 🔴 2:1, KHÔNG 5:2 (người dùng 17/09/2026: "hiện tại đang quá dài nên hiển thị không đẹp"). Đổi
     ở đây thì phải đổi cùng lúc `.cover-preview__frame` và `data-crop-ratio` của ô file — xem khối
     comment ở `.cover-preview__frame`. */
  aspect-ratio: 2 / 1; background: var(--surface-sunken);
  border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
}
.share-cover__anh {
  position: absolute; inset: 0; display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: center; border-radius: inherit;
}
/* Lớp phủ tối để tên CLB (chữ đậm màu, nửa trên đè lên ảnh) đọc được trên MỌI ảnh bìa — cùng lý do
   và cùng gradient với .honor-anh__phu, chỉ nhạt hơn ở mép trên vì ở đây không có chữ nào nằm giữa
   ảnh cần che nền. */
.share-cover__phu {
  position: absolute; inset: 0; border-radius: inherit;
  background: linear-gradient(180deg, rgba(6,58,45,.10) 0%, rgba(6,58,45,.62) 100%);
}
/* Khối logo + tên đè mép dưới ảnh. Quy đổi từ khung 1536px của ảnh vinh danh xuống thẻ 640px:
   viền 8→4px, tên 40→18px; phần tràn giữ đúng tỉ lệ ~1/3 chiều cao logo.
   🔴 Logo 3.5→5.5rem (56→88px) ngày 17/09/2026, HAI nhịp trong cùng ngày: 56→72 rồi 72→88px (người
   dùng: "avatar đi kèm ảnh CLB ở các màn chia sẻ hiển thị hơi nhỏ", rồi "to lên chút nữa, vừa rồi
   tôi thấy vẫn bé"). 56px chỉ chiếm **17,6%** chiều cao banner 319px — quy đổi thẳng từ ảnh vinh
   danh (128/400 = 32% ở thời điểm đó) mà quên rằng banner ở đây thấp hơn, nên cùng phép quy đổi cho
   ra một vòng tròn lọt thỏm. 88px = **27,6%**, đo trên harness ở 1440×900. Đổi cỡ logo KHÔNG
   đụng tới khoảng thở của tên (`BannerTenClbKhoangThoTest`): khối neo `bottom` âm và căn giữa theo
   logo, nên logo to ra thì khối chỉ mọc LÊN — tên rời xa mép dưới (thở 6,5 → **22,5px**) chứ không
   tụt xuống.
   🔴 rem chứ không px: MobileScaleTest cấm bề rộng px cứng trong tầng mobile, và hai tầng viết cùng
   một đơn vị thì đối chiếu được bằng mắt. */
.share-cover__clb {
  position: absolute; left: var(--space-md); bottom: calc(-1 * var(--cover-de)); z-index: 1;
  display: flex; align-items: center; gap: .6rem;
  max-width: calc(100% - var(--space-md) * 2);
}
.share-cover__logo {
  flex: none; width: 5.5rem; height: 5.5rem; border-radius: 50%; object-fit: cover;
  border: 4px solid var(--surface); background: var(--surface);
}
.share-cover__logo--mark {
  display: inline-flex; align-items: center; justify-content: center; background: var(--court-500);
}
/* color: #fff bắt buộc — luật chung .brand-svg tô XANH lên chính SVG, đặt lên nền xanh của tile là
   dấu biến mất (cùng bẫy đã ghi ở .honor-card__mark). */
.share-cover__logo--mark .brand-svg { width: 2.75rem; height: 2.75rem; color: #fff; }
/* 🔴 Chữ TRẮNG cứng, KHÔNG token theme — cùng lý do và cùng số đo với `.honor-anh__ten-clb`
   (17/09/2026). Ô này luôn nằm trên ảnh bìa đã phủ `rgba(6,58,45,…)`, tức nền tối ở MỌI chế độ
   sáng/tối, nên token `--text-strong` là sai chỗ: ở chế độ sáng nó là `--slate-900` và cho tương
   phản **1,24:1** trên ảnh bìa tối. Trắng cho **14,4:1** (ảnh tối) và **4,5:1** (ảnh sáng). */
/* 🔴 Tên KHÔNG còn căn giữa theo logo (17/09/2026, người dùng: *"cho tên CLB dịch xuống dưới đi"*).
   Lý do: khi logo còn 56px thì tâm logo rơi gần mép dưới ảnh nên tên nằm đúng chỗ; logo lên 88px thì
   tâm nó trôi lên 16px và tên đi theo — đó là cái "hơi cao" mà mắt bắt được. Nay tên tự neo theo MÉP
   DƯỚI ảnh bìa, không phụ thuộc cỡ logo nữa.
   🔴 Phép tính của `margin-bottom`: khối CLB có đáy ở `banner + --cover-de`, nên lùi lên đúng
   `--cover-de + 8px` là đáy ô tên nằm **8px TRÊN** mép ảnh — chính khoảng thở mà
   `BannerTenClbKhoangThoTest` sinh ra để bảo vệ (người dùng từng báo "tên CLB bị cắt" hai lần ngày
   16/09/2026; ở 18px tràn nó chỉ còn 1px và viền trắng hết chỗ). Viết bằng `calc()` từ chính
   `--cover-de` để tầng mobile khai lại biến đó là hai chỗ tự khớp nhau.
   🔴 Hệ quả cố ý: đổi cỡ logo từ nay KHÔNG xê dịch tên nữa — quan hệ "logo nhô bao nhiêu thì tên tụt
   bấy nhiêu" trong javadoc cũ của test đã bị cắt đứt ở đây. */
/* 🔴 1.75rem = 28px, không còn 18px (18/09/2026, người dùng: tên CLB đi kèm ảnh bìa "to hơn để cân
   đối với ảnh bìa và avatar"). 18px đứng cạnh logo 88px chỉ bằng ~1/5 chiều cao logo; 28px ≈ 1/3,
   đúng tỉ lệ tên/logo của ảnh vinh danh (60/192). Chữ to ra chỉ mọc LÊN vì tên neo theo mép dưới. */
.share-cover__ten {
  min-width: 0; font-size: 1.75rem; font-weight: 800; color: #fff;
  align-self: flex-end; margin-bottom: calc(var(--cover-de) + .5rem);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;  /* tên CLB tới 255 ký tự */
  text-shadow: 0 1px 6px rgba(3, 30, 23, .6);
}

/* 🔴 Khối buổi tập THU GỌN từ 16/09/2026 (người dùng yêu cầu cùng lúc với banner ảnh bìa): banner
   là điểm nhấn của trang, khối này lùi về vai trò thông tin. Padding lg→md, dòng ngày 1.35rem→17px,
   khoảng cách trong khối hạ một bậc. Đừng trả về cỡ cũ mà không đọc lại lý do. */
.share-hero {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: linear-gradient(135deg, rgba(14,122,95,.12), rgba(22,163,74,.10));
}
.share-hero.is-cancelled { background: var(--surface-muted); }
.share-eyebrow {
  display: flex; align-items: center; gap: .4rem;
  font-weight: 700; color: var(--text-strong);
}
.share-eyebrow .app-brand-mark { font-size: 1.1rem; }
/* Ảnh đại diện CLB thay icon chung khi CLB đã tải logo — cùng dấu chân 1.4rem của .brand-svg icon
   để hàng eyebrow không đổi chiều cao dù hiện nhánh nào. */
.share-eyebrow__logo { width: 1.4rem; height: 1.4rem; border-radius: 50%; object-fit: cover; flex: 0 0 auto; }
/* Ngày + giờ chung một hàng — pill giờ neo bên phải, xuống hàng riêng khi màn quá hẹp cho cả hai. */
.share-hero__when {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; column-gap: .6rem; row-gap: .35rem;
  margin-top: var(--space-xs);
}
/* 🔴 17px, KHÔNG 1.35rem. Con số này vốn là luật RIÊNG của tầng mobile (Đợt 7, 2026-08-21): "Thứ
   Năm"/"Chủ Nhật" + "dd/mm/yyyy" ở 1.35rem rộng hơn bề ngang còn lại của .share-hero ở 390/360px
   (đo: 251,7px cần vs 242/212px có) ⇒ 2/7 thứ bị ngắt dòng, còn ở 17px thì cả 7 thứ đều vừa một
   dòng ở 360px ("Chủ Nhật, 21/08/2026": 205,1px < 212px). Từ 16/09/2026 nó lên luật NỀN, áp cho cả
   máy tính, vì banner ảnh bìa đã nhận vai điểm nhấn — bản mobile trùng lặp đã gỡ. Hệ quả cũ vẫn
   đúng: pill giờ không còn vừa chung hàng, `.share-hero__when` đã flex-wrap nên nó tự xuống dòng. */
.share-hero__date {
  font-size: 1.0625rem; line-height: 1.25; font-weight: 800;
  color: var(--text-strong);
}
.share-hero.is-cancelled .share-hero__date { text-decoration: line-through; color: var(--text-muted); }
.share-time {
  display: inline-flex; align-items: center; gap: .4rem; flex: none;
  padding: .3rem .7rem; border-radius: var(--radius-pill);
  background: var(--surface); border: 1px solid var(--border);
  font-weight: 700; font-variant-numeric: tabular-nums;
}
.share-loc { margin-top: var(--space-sm); color: var(--text); }
.share-loc__label { display: flex; align-items: flex-start; gap: .5rem; }
.share-loc__label > .bi { color: var(--action-primary); margin-top: .15rem; flex: none; }
.share-slots { margin-top: var(--space-sm); }
.share-slots__label { display: flex; align-items: center; gap: .4rem; }
.share-people li { display: flex; align-items: center; gap: .55rem; padding: .35rem 0; }
.share-people li + li { border-top: 1px solid var(--border); }
/* Avatar chữ-cái tái dùng (danh sách thành viên…). 6 tông từ palette, chọn theo hash tên. */
.avatar {
  flex: none; width: 2.5rem; height: 2.5rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: .85rem; line-height: 1; letter-spacing: .01em;
  user-select: none;
}
/* P5: nền dùng var(--badge-*-bg, rgba(...)) — xem lý do ở nơi khai báo biến trong [data-bs-theme="dark"]. */
.avatar--c0 { background: var(--badge-blue-bg, rgba(37,99,235,.14));  color: var(--badge-blue-fg); }
.avatar--c1 { background: var(--badge-green-bg, rgba(22,163,74,.14));  color: var(--badge-green-fg); }
.avatar--c2 { background: var(--badge-amber-bg, rgba(249,115,22,.16)); color: var(--badge-amber-fg); }
.avatar--c3 { background: var(--badge-violet-bg, rgba(124,58,237,.14)); color: var(--violet-500); }
.avatar--c4 { background: var(--badge-red-bg, rgba(220,38,38,.12));  color: var(--badge-red-fg); }
.avatar--c5 { background: var(--badge-slate-bg, rgba(100,116,139,.16)); color: var(--badge-slate-fg); }
[data-bs-theme="dark"] .avatar--c3 { color: var(--violet-300); }
/* Ảnh thật thay chữ cái. Kích thước vòng tròn KHÔNG đổi — mọi chỗ gọi (bảng thành viên 2.5rem,
   khối công nợ công khai 4.5rem) tự co theo vì ảnh lấp đầy 100% ô cha. object-fit: cover để ảnh
   không bị méo khi tỉ lệ khác 1:1; nền xám giữ chỗ trong lúc ảnh đang tải. */
.avatar--img, .share-avatar.has-img { background: var(--badge-slate-bg, rgba(100,116,139,.16)); overflow: hidden; }
.avatar > img, .share-avatar > img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }

/* Khối thông tin thành viên ở đầu trang công nợ công khai (/chia-se/{token} loại DEBT) — avatar to
   hơn bản .avatar mặc định (2.5rem, dùng ở danh sách bảng), phóng qua wrapper riêng thay vì sửa
   fragment gốc để không ảnh hưởng chỗ khác đang dùng cỡ nhỏ (member/thanh-vien.html). */
.debt-member__avatar { display: flex; justify-content: center; margin-bottom: var(--space-sm); }
.debt-member__avatar .avatar { width: 4.5rem; height: 4.5rem; font-size: 1.5rem; }
.debt-member__name { font-size: 1.5rem; font-weight: 800; color: var(--text-strong); margin: 0 0 var(--space-2xs); }
.debt-member__meta { display: flex; flex-wrap: wrap; justify-content: center; gap: .4rem; margin: var(--space-xs) 0 var(--space-sm); }
/* 🔴 Bốn luật dưới đây THAY CHỖ cho utility Bootstrap đã gỡ khỏi template (`text-center` ·
   `mb-2` ở dòng "Công nợ tại <CLB>" · `mb-0` ở dòng email · `w-100 mb-3` ở hai nút hành
   động). Chúng phải in ra ĐÚNG bố cục cũ ở ≥768px — tầng mobile phía dưới mới là chỗ đổi hình.
   Lý do phải chuyển: utility mang `!important`, tầng mobile không đè được. */
.debt-member { text-align: center; }
.debt-member__info { min-width: 0; }
.debt-member .auth-card__subtitle { margin-bottom: var(--space-xs); }
/* 🔴 `overflow-wrap: anywhere` chứ không để mặc định: địa chỉ email là MỘT từ liền, không có chỗ
   nào để trình duyệt ngắt dòng. Ở dạng hàng của điện thoại cột phải chỉ rộng 234px (đo ở 360px),
   nên một email 40 ký tự đẩy scrollWidth vượt viewport — vi phạm MB-1, và đây là kiểu tràn
   `tools/mobile-audit` BẮT ĐƯỢC nhưng chỉ khi gặp đúng người có email dài. Đặt ở luật NỀN (không
   nhốt trong tầng mobile) vì bản xếp dọc trên máy tính cũng tràn được, chỉ là ngưỡng cao hơn. */
.debt-member__email { margin-bottom: 0; overflow-wrap: anywhere; }
.debt-balance { text-align: center; margin: var(--space-md) 0; }
.debt-cta > * { margin-bottom: var(--space-md); }
.debt-cta .btn { width: 100%; }

/* Avatar ở form "Hồ sơ của tôi" (auth/ho-so.html) và ở form Hồ sơ của màn "Sửa thành viên"
   (member/sua.html, BR-MEM-18) — cùng cỡ to đã dùng ở .debt-member__avatar.
   Chữ-cái fallback (.avatar__fallback) chỉ hiện khi #avatarPreview CHƯA có ảnh (còn .d-none) —
   :has() đọc đúng trạng thái DOM hiện tại nên khi file-preview.js gỡ .d-none lúc chọn ảnh mới,
   chữ cái tự ẩn theo mà không cần sửa file-preview.js dùng chung cho cả logo CLB/ảnh QR.
   🔴 Luật neo theo ĐÚNG id #avatarPreview, nên cả hai trang phải giữ nguyên id đó cho thẻ <img>
   xem trước — hai trang không bao giờ cùng tồn tại nên id trùng không xung đột. */
.profile-avatar { width: 4.5rem; height: 4.5rem; font-size: 1.5rem; }
.profile-avatar:has(> #avatarPreview:not(.d-none)) .avatar__fallback { display: none; }

.share-avatar {
  flex: none; width: 2rem; height: 2rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  /* 🔴 MB-2 (task 7.2): .8rem = 12,8px, dưới sàn 13px — không lộ ở 7.1 vì danh sách lúc đó rỗng
     (0 người), phát hiện khi 7.2 dựng dữ liệu thật cho .share-split. .8125rem = 13px khớp đúng sàn
     NHỎ NHẤT TUYỆT ĐỐI đã dùng cho --fs-caption (token đó chỉ tồn tại trong tầng mobile, ở đây cần
     áp mọi viewport nên ghi thẳng giá trị). */
  font-weight: 700; font-size: .8125rem;
  background: rgba(22,163,74,.14); color: var(--badge-green-fg);
}
.share-avatar.is-guest { background: rgba(37,99,235,.14); color: var(--badge-blue-fg); }
.share-empty { padding: .35rem 0; }

/* Icon giới tính cạnh tên trên trang chia sẻ buổi tập công khai (/chia-se/{token} — fragments/buoi-hero
   :: cotNguoi, tham số coGioiTinh). Nam mượn lại --badge-blue-fg (đã có sẵn); Nữ dùng --badge-pink-fg
   (contrast 6,03:1 sáng / 8,07:1 tối — xem khai báo biến). "Chưa rõ" dùng chung icon bi-question-circle
   với bộ đếm ở màn điểm danh nội bộ (attendance/fragments.html :: bo-dem) — cùng một khái niệm thì
   không bịa icon thứ hai. Đây là icon RIÊNG TỪNG NGƯỜI — khác bộ đếm TỔNG số (.badge-soft.is-blue ở
   share/cong-khai.html) vốn giữ TEXT "N nam · N nữ" theo đúng quyết định đã chốt 2026-08-23. */
.gender-icon.is-male    { color: var(--badge-blue-fg); }
.gender-icon.is-female  { color: var(--badge-pink-fg); }
.gender-icon.is-unknown { color: var(--text-muted); }

/* Căn giữa trang chia sẻ buổi tập ở điện thoại (người dùng yêu cầu 2026-08-26): hai nhãn cột
   "Đã đăng ký"/"Khách vãng lai" + khối danh sách avatar/tên — đồng bộ cả khối tổng quan lẫn danh
   sách, không để hai kiểu căn lề khác nhau trên cùng một trang.
   575.98px (không phải 767.98px của khối tầng mobile ở cuối tệp) vì đây đúng breakpoint xếp-chồng
   của .col-sm-6 ở share/cong-khai.html — trên mốc đó hai cột đã nằm cạnh nhau, căn giữa không còn
   hợp lý. Không bị MobileScaleTest chi phối (luật "một khối" chỉ áp cho 767.98px).
   🔴 Hàng bộ đếm ("N nam · N nữ" + "N người tham gia") KHÔNG còn ở đây: từ 07/09/2026 nó có hai
   badge chứ không phải một, nên cần cả khoảng cách giữa chúng lẫn chỗ xuống dòng khi hẹp — việc
   `text-align: center` không làm được. Nó tự căn bằng utility flex ngay trong template
   (`justify-content-center`, KHÔNG kèm mốc breakpoint nào). Và nó căn giữa ở MỌI cỡ màn, khác hẳn
   hai nhãn cột bên dưới: khối tổng quan đứng một mình giữa thẻ thì canh trái ở desktop chỉ làm nó
   dính lệch một bên trong khi phần còn lại của hàng trống trơn. Đừng thêm lại một luật
   `.share-gender-summary` ở đây rồi để hai chỗ cùng canh một khối. */
@media (max-width: 575.98px) {
  .share-roster-col .text-caption.d-flex { justify-content: center; }
  /* 🔴 Canh giữa CẢ KHỐI <ul>, KHÔNG canh `justify-content:center` trên từng `<li>` — bản đầu
     (2026-08-26) làm vậy, đo bằng Chrome headless thấy avatar mỗi dòng rơi vào một vị trí X khác
     nhau (tên dài "Nguyễn Thị Bích Ngọc" kéo avatar dòng đó ra xa hơn dòng "Minh") ⇒ cột avatar
     so le/zigzag, đúng thứ người dùng chê "không đẹp". Cách đúng: từng `<li>` vẫn canh trái VỚI
     NHAU (avatar thẳng cột dọc), chỉ đưa cả khối `<ul>` vào giữa cột bằng width:fit-content +
     margin-inline:auto — khối so le bên PHẢI (độ dài tên khác nhau là bình thường) nhưng cạnh
     TRÁI (avatar) thẳng hàng, và toàn khối nằm giữa cột thay vì dính sát lề trái.
     🔴 Bề rộng CHUNG (max-width theo rem), KHÔNG `width: fit-content` (đổi 13/09/2026, người dùng
     báo): fit-content co mỗi <ul> theo tên dài nhất của RIÊNG cột đó ⇒ trong một cột avatar vẫn
     thẳng, nhưng cột này lệch cột kia — đo ở 390×844: "Đã đăng ký" bắt đầu X=97, "Khách vãng lai"
     X=138; Sân 1/Sân 2 chỉ trùng nhau khi tình cờ có tên dài bằng nhau. Mọi cột cùng một bề rộng thì
     mép trái avatar trùng nhau ở mọi cột, bất kể tên. 18rem (288px) vừa trong thẻ ở cả 360px (lòng
     thẻ ~294px) lẫn 390px. ShareRosterCanhDeuTest gác. */
  .share-roster-col .share-people {
    width: 100%;
    max-width: 18rem;
    margin-inline: auto;
  }
  /* Dòng trống ("Chưa có khách.") căn giữa theo tiêu đề cột: danh sách nay rộng chung 18rem nên chữ
     bám mép trái trông lệch khỏi nhãn căn giữa ngay trên nó. `li` là flex (.share-people li) ⇒ căn
     bằng justify-content, text-align không có tác dụng. */
  .share-roster-col .share-empty { justify-content: center; }
}

/* ---- Lịch đánh trên link chia sẻ (BR-LINEUP-12) -------------------------------------------- */
.share-court-heading { min-height: 2rem; }
.share-court-heading .share-court-schedule-button {
    display: inline-flex; align-items: center; gap: .25rem;
    padding: .15rem .4rem;
    font-size: .75rem; line-height: 1.25;
    white-space: nowrap;
}
.share-court-schedule-button__count {
    display: inline-grid; place-items: center;
    min-width: 1rem; height: 1rem; padding-inline: .2rem;
    border: 1px solid var(--border); border-radius: var(--radius-pill);
    background: var(--surface-muted); color: var(--text-strong);
    font-size: .625rem; font-weight: 700; line-height: 1;
}
.share-schedule-modal .modal-dialog { max-width: var(--w-content); }
.share-schedule-modal .modal-content {
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
}
.share-schedule-modal__header { padding: var(--space-md) var(--space-lg); }
.share-schedule-modal__heading { display: flex; align-items: center; gap: var(--space-sm); min-width: 0; }
.share-schedule-modal__icon {
    display: inline-grid; place-items: center; flex: 0 0 auto;
    width: 2.5rem; height: 2.5rem;
    border-radius: var(--radius-md);
    background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis);
    font-size: 1.125rem;
}
.share-schedule-modal__meta {
    display: flex; align-items: center; flex-wrap: wrap; gap: .25rem var(--space-sm);
    margin-top: .25rem; color: var(--text-muted); font-size: .8125rem;
}
.share-schedule-modal__meta > span { display: inline-flex; align-items: center; gap: .3rem; }
.share-schedule-modal .modal-body { padding: var(--space-md) var(--space-lg) var(--space-lg); background: var(--surface-sunken); }
.share-schedule-modal .share-sets > .text-caption { display: none; }
.share-schedule-modal .share-sets__list { gap: var(--space-xs); }
.share-schedule-modal .share-set { padding: var(--space-sm); }
.share-schedule-empty {
    display: grid; justify-items: center; gap: var(--space-xs);
    padding: var(--space-xl) var(--space-md); text-align: center; color: var(--text-muted);
}
.share-schedule-empty > i { font-size: 2rem; color: var(--action-primary); }
.share-schedule-empty > strong { color: var(--text-strong); }
.share-sets__list { display: grid; gap: .3rem; }
.share-set {
    display: grid; grid-template-columns: 2rem 3rem minmax(0, 1fr); align-items: center; gap: var(--space-xs);
    padding: .3rem .45rem; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface);
    font-size: .8125rem;
}
.share-set__no {
    display: inline-grid; place-items: center;
    width: 2rem; height: 2rem; border-radius: var(--radius-sm);
    background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis); font-weight: 700;
}
/* 🔴 MB-2: sàn 13px = .8125rem (cùng lớp lỗi với .share-avatar phía trên, và cùng nguồn brief với
   .xs-set__type/.xs-set__vs bên khối Xếp sân nội bộ) — trang này đọc gần như toàn bộ trên điện
   thoại, không được thấp hơn --fs-caption. */
.share-set__type { color: var(--text-muted); font-size: .8125rem; }
/* min-width: 0 — ellipsis phải nằm trên chính item chứa chuỗi, không phải trên flex container. */
.share-set__teams {
    min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center; gap: var(--space-xs);
}
.share-set__side { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.share-set__side:first-child { text-align: right; }
.share-set__vs {
    display: inline-grid; place-items: center;
    min-width: 2rem; height: 2rem; padding-inline: .35rem;
    border-radius: var(--radius-pill); background: var(--surface-muted);
    color: var(--text-muted); font-size: .75rem; font-weight: 700; text-transform: uppercase;
}

@media (max-width: 575.98px) {
    .share-schedule-modal .modal-dialog { margin: var(--space-xs); }
    .share-schedule-modal .modal-content { max-height: calc(100dvh - (2 * var(--space-xs))); }
    .share-schedule-modal__header { align-items: flex-start; padding: var(--space-md); }
    .share-schedule-modal__heading { align-items: flex-start; gap: var(--space-xs); }
    .share-schedule-modal__icon { width: 2.25rem; height: 2.25rem; }
    .share-schedule-modal__meta { gap: .15rem var(--space-xs); }
    .share-schedule-modal .modal-body { padding: var(--space-sm); }
    .share-schedule-modal .share-set { padding: var(--space-sm); }
    .share-set { grid-template-columns: 2rem minmax(0, 1fr); }
    .share-set__type { justify-self: end; }
    .share-set__teams {
        grid-column: 1 / -1; grid-template-columns: 1fr; justify-items: center;
        width: 100%; gap: .35rem;
    }
    .share-set__side {
        width: 100%; padding: .55rem var(--space-xs);
        border: 1px solid var(--border); border-radius: var(--radius-sm);
        background: var(--surface-sunken); text-align: center; white-space: normal;
    }
    .share-set__side:first-child { text-align: center; }
    .share-set__vs { width: 2rem; height: 1.5rem; }
}

/* Trang bảng chia tiền công khai — cùng khung "vé mời", nhưng điểm nhấn là TIỀN.
   .share-split dùng lại .share-people (avatar + gạch ngăn) và chỉ thêm cột tiền bên phải. */
.share-split li { align-items: flex-start; padding: .55rem 0; }
.share-split__who { flex: 1 1 auto; min-width: 0; }
.share-split__money { flex: none; text-align: right; }

/* ============================================================
   Sổ giao dịch của trang công nợ công khai (2026-09-07)
   ============================================================
   🔴 Bản đầu dùng flex hai cột như .share-split và trông LỆCH HẲN trên điện thoại — đo ở 390×844:
   cột trái xếp 4 dòng (ngày · giờ · badge loại · ghi chú) trong khi cột phải chỉ có 2 (tiền ·
   badge trạng thái), nên mỗi hàng là một chữ L ngược: badge trạng thái trôi lên cạnh số tiền, badge
   loại nằm trơ một mình bên dưới, và nửa phải của hàng bỏ trống. Ghi chú lại bị nhốt trong cột
   trái nên câu "Trừ từ tiền đóng trước 68.000 ₫ · còn dư 432.000 ₫" xuống dòng ngay sau chữ số,
   tách "₫" khỏi con số của nó. Hàng cao 85–110px ⇒ 10 dòng ăn hơn 1.000px cuộn.

   Grid 2 cột chữa cả ba: hai ô của mỗi hàng ĐỒNG HÀNG (ngày↔tiền, giờ+loại↔trạng thái), còn ghi
   chú chiếm trọn `1 / -1` nên được cả bề ngang thẻ. Không dùng lại .share-split vì flex không có
   khái niệm "hàng" để căn hai cột theo — đó chính là thứ hỏng ở bản đầu. */
.debt-txn {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--space-sm);
  row-gap: .15rem;
  align-items: baseline;
  padding: .6rem 0;
  border-bottom: 1px solid var(--border);
}
.debt-txn:last-child { border-bottom: none; }
.debt-txn__date { grid-column: 1; grid-row: 1; font-weight: 600; }
/* Giờ đứng ngay sau ngày (2026-10-06): hàng 2 nhường trọn cho nhãn loại ↔ số dư, nếu không ở
   390×844 "19:30–21:30" + badge + "Số dư: Nợ 165.000 ₫" không vừa một hàng. */
.debt-txn__time {
  margin-left: .4rem; font-weight: 400; font-size: .8125rem; color: var(--text-muted);
  white-space: nowrap;
}
.debt-txn__amount {
  grid-column: 2; grid-row: 1; text-align: right; font-weight: 700; white-space: nowrap;
}
/* Giờ và loại nằm CÙNG một dòng: ở bản trước loại là badge trên dòng riêng, mà với ~90% hàng nó
   ghi đúng một chữ "Buổi tập" — lặp lại 10 lần theo chiều dọc thành nhiễu, không thành thông tin. */
.debt-txn__meta {
  grid-column: 1; grid-row: 2;
  display: flex; align-items: center; flex-wrap: wrap; gap: .35rem;
  font-size: .8125rem; color: var(--text-muted);
}
/* 2026-10-06 — sổ thành SAO KÊ như màn Chi tiết công nợ nội bộ (BR-DEBT-21): ô bên phải hàng 2 là
   SỐ DƯ sau giao dịch, canh phải ĐÚNG dưới số tiền để đọc dọc như sao kê ngân hàng (thay cho
   badge trạng thái cũ — trạng thái còn lại, nếu có, đã vào hàng nhãn bên trái). Không bao giờ ngắt
   dòng giữa "Dư" với con số. */
.debt-txn__balance {
  grid-column: 2; grid-row: 2; text-align: right; white-space: nowrap; font-size: .875rem;
}
.debt-txn__balance-label { color: var(--text-muted); font-size: .8125rem; }
.debt-txn__note {
  grid-column: 1 / -1;
  margin-top: .15rem;
  font-size: .8125rem; color: var(--text-muted);
  display: flex; align-items: center; flex-wrap: wrap; gap: .35rem;
}
/* 2026-10-07 — tên giao dịch là CHỮ THƯỜNG như sao kê ngân hàng, không badge màu (người dùng:
   "vẫn hơi rối, muốn giống hẳn giao dịch ngân hàng"). Màu chữ chính kể cả khi ô cha là ô meta
   (chữ mờ) trên điện thoại — tên giao dịch là thông tin chính của dòng, chú thích bên dưới mới mờ. */
.debt-ledger__name,
.debt-txn__name { font-weight: 500; color: var(--text); }

/* Khối "Buổi chưa trả" — HOÁ ĐƠN, đứng ngay trên SAO KÊ (.debt-txn).
   🔴 Hai khối này lặp lại nhau ở hai dòng đầu (buổi còn nợ tất nhiên cũng nằm trong lịch sử), và
   đo ở 390×844 thì cả hai lọt cùng một màn hình — trước khi có nền riêng, người xem đọc thành
   "sao danh sách này in hai lần?". Giữ cả hai là có lý do (sao kê bị PHÂN TRANG và LỌC, nên buổi
   cần trả có thể trôi sang trang 2 hoặc bị lọc mất — hoá đơn thì luôn đủ), nên việc phải làm là
   cho chúng KHÁC NHAU về hình, không phải bỏ một cái đi.
   Nền chìm + bo góc là mức phân biệt nhẹ nhất đủ dùng: trang này đã có một con số đỏ to và một nút
   primary ở ngay trên, thêm một khối tô màu nữa là ồn. */
.debt-unpaid {
  background: var(--surface-muted);
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-md);
}
.debt-unpaid .share-people li:last-child { border-bottom: none; }

/* Hàng tiêu đề "Lịch sử giao dịch · N · [nút lọc]". Nút đẩy sát phải và KHÔNG co: ở bản trước cả
   ba nằm trong một .d-flex.flex-wrap nên trên 390px nút rơi xuống một dòng riêng, chiếm trọn 44px
   chiều cao chỉ để hiện một chữ. Nhãn nút rút còn "Tất cả"/"09/2026" (selectedShortLabel) cho vừa
   hàng — tên tháng đầy đủ vẫn nằm trong popup, nơi có chỗ. */
.debt-txn-head {
  display: flex; align-items: center; gap: .5rem;
  margin-bottom: var(--space-xs);
}
/* Nút đẩy sát mép PHẢI (người dùng chốt 2026-09-07 vòng 3).
   🔴 Một mình `margin-left: auto` thì nút chui xuống dưới nút đổi sáng/tối: `.auth-topbar` là
   `position: fixed; top:16px; right:16px; z-index:10` nên nó phủ VĨNH VIỄN góc trên-phải viewport
   — đo ở 360×800, nút lọc kết thúc ở x=312 còn nút kia bắt đầu ở x=292, chồng 20px đúng lúc hàng
   tiêu đề cuộn lên đầu màn hình, và cú chạm rơi vào nút đổi giao diện. Vòng trước tôi né bằng cách
   bỏ `margin-left:auto`; lần này chữa ĐÚNG CHỖ ĐAU: xem luật `.auth-shell:has(.auth-card--list)`
   ngay dưới. */
.debt-txn-head__filter { flex: none; margin-left: auto; }

/* 🔴 Nút đổi sáng/tối THÔI cố định trên trang có danh sách dài. `position: fixed` là đúng cho các
   trang auth (một form ngắn, không cuộn — nút ở đâu cũng thấy), nhưng trang công nợ công khai cuộn
   hàng nghìn pixel, và một hộp 37×37 ghim ở góc trên-phải là một VÙNG CHẾT phủ lên mọi thứ trôi qua
   đó: số tiền của từng dòng, badge trạng thái, và nút lọc. Chuyển sang `absolute` (mốc là
   `.auth-shell`) thì nút nằm ở đầu trang và cuộn đi cùng nội dung — hết chồng lấn, đổi lại người
   xem phải cuộn lên đầu mới đổi được giao diện, đánh đổi đáng.
   `:has()` khoanh đúng những trang có `.auth-card--list`; đăng nhập/đăng ký giữ nguyên hành vi cũ
   (codebase đã dùng `:has` sẵn — xem `.page-header:has(...)` và `.action-bar`). */
.auth-shell { position: relative; }
.auth-shell:has(.auth-card--list) .auth-topbar { position: absolute; }

/* 🔴 Ba trang chia sẻ công khai KHÔNG căn giữa theo chiều dọc nữa (người dùng báo 2026-09-07:
   "danh sách công nợ ít dữ liệu sao nội dung lại bị đẩy xuống"). Nguyên nhân ở `.auth-shell` gốc:
   `min-height: 100vh` + `justify-content: center` là khuôn của MỘT FORM ĐĂNG NHẬP — nội dung ngắn
   hơn một màn hình thì phần dư bị chia ĐỀU cho trên và dưới, nên trang càng ít dữ liệu càng tụt
   xuống giữa màn hình; nhiều dữ liệu (cao hơn 100vh) lại dính sát đỉnh, đó là lý do lỗi chỉ lộ ra
   ở CLB ít giao dịch. Ba trang này là NỘI DUNG ĐỌC TỪ TRÊN XUỐNG ⇒ luôn neo đỉnh.
   Khoanh bằng `.auth-shell--public` chứ không phải `:has(.auth-card--list)` (chỉ trang công nợ
   có), vì trang buổi tập và bảng chia tiền dính y hệt khi buổi ít người — người dùng chốt áp cho
   cả ba. Đăng nhập/đăng ký/quên mật khẩu, bốn trang lỗi và member/nhan-loi-moi không mang marker
   này nên giữ nguyên căn giữa.
   Luật ở NGOÀI mọi @media: khoảng hở trên máy tính còn to hơn điện thoại (48px padding + nửa phần
   dư), đây không phải lỗi riêng của mobile. */
.auth-shell--public { justify-content: flex-start; }

/* Mục "Tất cả các tháng" trong popup lọc: vùng chạm cao hơn 44px (CT-6), và CẢ HÀNG bấm được chứ
   không riêng cái nút tròn bé xíu của radio. */
.thang-option { display: block; }
.thang-option .form-check-input { margin-top: .55rem; }
.thang-option .form-check-label {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  min-height: var(--tap-min); padding: .45rem 0; cursor: pointer;
}
.thang-option .form-check-label .badge-soft { margin-left: auto; }

/* ============================================================
   Lịch chọn tháng trong popup lọc (2026-09-07)
   ============================================================ */
.thang-nam { border: none; padding: 0; margin: 0 0 var(--space-md); min-width: 0; }
.thang-nam__legend {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  float: none;                       /* <legend> mặc định float trong một số UA reset */
  margin-bottom: var(--space-xs); padding: 0;
  font-size: .8125rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-muted);
}
.thang-nam__legend .badge-soft { margin-left: auto; text-transform: none; letter-spacing: 0; }

/* 3 cột: ở 279px nội dung (360px thiết bị) mỗi ô còn ~76px — vừa đủ cho "Tháng 12" ở .8125rem mà
   không phải viết tắt. 4 cột sẽ bóp còn ~55px và ép viết tắt kiểu "T12", khó đọc lướt hơn hẳn. */
.thang-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; }
.thang-o { position: relative; min-width: 0; }
/* Radio ẩn khỏi mắt nhưng KHÔNG khỏi bàn phím/trình đọc màn hình: vẫn nằm trong luồng tiêu điểm,
   nên Tab vẫn tới được và :focus-visible bên dưới vẽ vòng sáng lên đúng ô. */
.thang-o__input {
  position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0;
  top: 0; left: 0; cursor: pointer;
}
.thang-o__label {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .1rem;
  min-height: var(--tap-min); padding: .3rem .2rem;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface); cursor: pointer;
  font-size: .8125rem; line-height: 1.15; text-align: center;
}
.thang-o__so { font-size: .75rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.thang-o__input:checked + .thang-o__label {
  background: var(--action-primary-solid, var(--action-primary));
  border-color: var(--action-primary-solid, var(--action-primary));
  color: #fff; font-weight: 700;
}
.thang-o__input:checked + .thang-o__label .thang-o__so { color: rgba(255, 255, 255, .85); }
.thang-o__input:focus-visible + .thang-o__label { outline: 2px solid var(--action-primary); outline-offset: 2px; }
/* Tháng không có giao dịch: VẪN chiếm ô (giữ 12 ô đúng vị trí) nhưng nhạt và không bấm được. */
.thang-o.is-empty .thang-o__label {
  background: var(--surface-muted); color: var(--text-muted);
  border-style: dashed; cursor: default; opacity: .65;
}

/* Một dòng "nhãn + giá trị + nút chép" trong popup Mã thanh toán.
   .copy-field__value dùng break-all: số tài khoản/tên chủ TK dài không được đẩy nút chép ra khỏi popup. */
.copy-field {
  display: flex; align-items: center; gap: var(--space-sm);
  padding: var(--space-sm) 0;
}
.copy-field + .copy-field { border-top: 1px solid var(--border); }
.copy-field__text { flex: 1 1 auto; min-width: 0; }
/* 🔴 MB-2 (task 7.2): .8rem = 12,8px, dưới sàn 13px — cùng lớp lỗi với .share-avatar phía trên,
   phát hiện khi mở popup "Mã thanh toán" thật lần đầu (dùng chung ở đây, debt/chi-tiet.html,
   share/cong-khai-cong-no.html). */
.copy-field__label { font-size: .8125rem; color: var(--text-muted); }
/* white-space: normal — gỡ nowrap mà .money áp lên số tài khoản: ở đây break-all mới
   là hành vi đúng, nowrap sẽ đẩy nút chép văng khỏi popup. */
.copy-field__value { font-weight: 700; color: var(--text-strong); word-break: break-all; white-space: normal; }
.copy-btn { flex: none; white-space: nowrap; }
.copy-btn__text { margin-left: .3rem; }

.qr-box {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--surface-muted);
}
/* Nền trắng cố định: QR ngân hàng là ảnh đen-trên-trắng, để nó trong suốt ở giao diện tối thì
   các ô đen chìm vào nền và app ngân hàng không quét được. */
.qr-box__img {
  display: block; margin: 0 auto var(--space-sm);
  max-width: 260px; width: 100%; height: auto;
  background: #fff; padding: var(--space-xs); border-radius: var(--radius-sm);
}

/* ============================================================
   Hàng đối chiếu trong modal "Đăng ký hộ" (BR-VOTE-07)
   ============================================================ */
/* 🔴 Ô chọn thành viên và hai nút icon đứng SÁT NHAU trên một dòng nhưng KHÔNG tự cao bằng nhau —
   đo bằng Chrome/Playwright ở 1440×900 và 390×844 (07/09/2026): `.form-select-sm` cho `.ts-control`
   34px, còn `.btn-sm` ra 31px ở desktop và bị `min-height: var(--tap-min)` của tầng mobile kéo
   thẳng lên 44px. Mép trên/dưới lệch 1,5px ở desktop và 5px ở điện thoại — đủ nhìn thấy vì ba thứ
   cùng bo góc và cách nhau đúng một `gap: .5rem`, người dùng báo ngay khi mở modal.

   Ghim MỘT chiều cao cho cả ba thay vì chỉnh từng cái: đổi cỡ sau này chỉ sửa một con số, và không
   thể xảy ra ca "sửa nút quên sửa ô chọn". Dùng `min-height` chứ không `height` để nội dung dài
   (tên thành viên xuống dòng) vẫn nới được thay vì bị cắt.

   Ở điện thoại lấy đúng `--tap-min`: nút icon vốn đã 44px ở đó nhờ luật chung, nên nâng ô chọn lên
   cho bằng vừa hết lệch vừa cho `MB-3` một vùng chạm rộng hơn — màn này dùng khi đứng ở sân. */
.doi-chieu-chon { --doi-chieu-cao: 34px; }
.doi-chieu-chon .ts-control,
.doi-chieu-chon .btn { min-height: var(--doi-chieu-cao); }
/* `min-height` một mình chỉ KÉO DÀI hộp — cùng lớp lỗi (và cùng cách chữa) với `.btn { display:
   inline-flex; align-items: center }` ở tầng mobile: có `min-height` thì phải có thứ phân đôi phần
   dư, nếu không nó dồn hết về một phía.
   🔴 `align-items: center` một mình KHÔNG đủ ở đây: thư viện có `.form-select-sm.has-items
   .ts-control { padding-bottom: 0 }` — luật đó sinh ra để bù lề dưới của các chip ở chế độ NHIỀU
   lựa chọn, nhưng nó áp cả cho ô MỘT lựa chọn, để lại padding trên 10px và dưới 0 ⇒ tâm vùng nội
   dung tụt 5px so với tâm ô (đo ở 390×844). Trả hai phía về 0 rồi để `min-height` quyết chiều cao.
   🔴 Selector phải nêu ĐỦ `.ts-wrapper.form-select`: luật padding đang giữ con số 10px là
   `.ts-wrapper.form-select .ts-control` ở khối Tom Select phía trên (độ đặc hiệu 0,3,0), mà
   `.doi-chieu-chon .ts-control` chỉ 0,2,0 ⇒ thua, và thua IM LẶNG — CSS vẫn nạp, quy tắc vẫn nằm
   trong stylesheet, chỉ là không thắng. Đã mất một vòng đo mới nhận ra. */
.doi-chieu-chon .ts-wrapper.form-select .ts-control {
  align-items: center;
  padding-top: 0;
  padding-bottom: 0;
}
/* Giá trị của `--doi-chieu-cao` ở điện thoại nằm trong TẦNG MOBILE cuối tệp, KHÔNG mở một khối
   media mobile riêng ở đây — `MobileScaleTest.khongCoMediaMobileMoCoi` cấm khối mồ côi để luật
   mobile chỉ có một chỗ để đọc, và nó quét cả chữ trong chú thích chứ không riêng luật thật. */

/* ============================================================
   Thanh phân trang dùng chung (fragments/ui :: pagination)
   ============================================================ */
/* margin-bottom: thanh phân trang là phần tử CUỐI của trang (nằm sau .card) — không có nó thì dòng
   "Hiển thị 1–10 trong tổng số N" dính sát mép dưới viewport, khó đọc. Trên <992px khoảng này cộng
   thêm vào padding-bottom chừa chỗ bottom-nav của .page-content, vẫn cách đều. */
.pagination-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-md);
  justify-content: space-between; margin-top: var(--space-md); margin-bottom: var(--space-lg);
}
.pagination-bar__info { color: var(--text-muted); font-size: .875rem; }
.pagination-bar__pages { display: flex; align-items: center; gap: .25rem; list-style: none; margin: 0; padding: 0; }
.pagination-bar__btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.25rem; height: 2.25rem; padding: 0 .5rem;
  border: 1px solid var(--border); border-radius: var(--radius-btn);
  background: var(--surface); color: var(--text); text-decoration: none;
  font-size: .875rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.pagination-bar__btn:hover { border-color: var(--border-strong); color: var(--text); }
.pagination-bar__btn.is-active {
  background: var(--action-primary-solid, var(--action-primary));
  border-color: var(--action-primary-solid, var(--action-primary));
  color: #fff;
}
.pagination-bar__btn.is-disabled { opacity: .4; pointer-events: none; }
.pagination-bar__ellipsis { padding: 0 .35rem; color: var(--text-muted); }
.pagination-bar__size .btn-ghost { border: 1px solid var(--border); }
@media (max-width: 575.98px) {
  .pagination-bar { justify-content: center; }
  .pagination-bar__info { width: 100%; text-align: center; order: 3; }
}

/* Tôn trọng prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* Nút quay lại (.page-header__back) chỉ có ý nghĩa khi breadcrumb bị ẩn ở mobile (P3 bên dưới) — ẩn
   lại ở desktop/tablet để không trùng breadcrumb đã hiện. 🔴 Đặt TRƯỚC khối đánh dấu tầng mobile dù
   thuộc cùng primitive P3: đây là luật cho viewport >=768px (desktop), không phải luật mobile —
   MobileScaleTest quét từ mốc đó đến hết tệp và không cho phép luật desktop nằm sau mốc. */
@media (min-width: 768px) {
  .page-header__back { display: none; }
}

/* Cột "Không tham gia" ở màn Quản lý vote (vote/quan-ly.html) — viền DỌC chỉ có nghĩa khi 2 cột
   đứng cạnh nhau (≥768px, `row-cols-md-2`); dưới mốc đó chúng xếp chồng dọc (task 7.7, xem chú
   thích ở template) nên đổi sang viền NGANG — luật đó nằm trong tầng mobile bên dưới. */
@media (min-width: 768px) {
  .vote-result-col { border-left: 1px solid var(--border); }
}

/* ------------------------------------------------------------
   Nút nổi Góp ý / Báo lỗi (fragments/gop-y.html)
   ------------------------------------------------------------
   z-index 1035: TRÊN .app-bottom-nav (1030) để không bị thanh điều hướng che, nhưng DƯỚI backdrop
   của modal (1050) để khi modal mở thì nút mờ đi cùng phần còn lại của trang.

   🔴 Đặt TRƯỚC mốc tầng mobile: MobileScaleTest quét từ mốc đó đến hết tệp và cấm bề rộng px cứng
   (48px của vùng chạm, 1px của honeypot). Đây là luật nền cho MỌI viewport, không phải luật mobile.

   🔴 Dưới 992px .app-bottom-nav (d-lg-none) chiếm đáy màn hình. Để FAB ở `bottom: var(--space-md)`
   là NẰM ĐÈ lên thanh đó, che mất một ô điều hướng. Nâng lên bằng :has() để chỉ trang THẬT SỰ có
   bottom-nav mới bị đẩy (/clb, /toi… không có).
   FAB dùng chiều cao nav theo trạng thái qua --fab-nav-height, còn .page-content luôn chừa chiều cao
   đầy đủ để nội dung không nhảy khi cuộn. Bản đầu tin vào
   `min-height: 56px` đọc được ở .app-bottom-nav-item và FAB đè lên nav đúng 1px ở 390×844 (đo bằng
   tools/mobile-audit). Từ 2026-08-28 token --bottom-nav-height MANG ĐÚNG chiều cao đo được (69px,
   xem :root) nên --space-md ở đây là khoảng thở thật, không còn kiêm việc bù cho token thiếu.
   ------------------------------------------------------------ */
.bmh-fab {
  --fab-nav-height: var(--bottom-nav-height);
  position: fixed;
  right: var(--space-md);
  bottom: var(--space-md);
  z-index: 1035;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--fab-height);     /* vùng chạm đủ lớn cho ngón tay (CT-6/MB-3) */
  min-width: var(--fab-height);
  padding: 0 var(--space-md);
  border: none;
  border-radius: 999px;
  background: var(--action-primary);
  color: #fff;
  box-shadow: 0 6px 20px rgba(15, 23, 42, .25);
  cursor: pointer;
  transition: bottom var(--motion-base) var(--ease-out),
              transform var(--motion-instant) ease-out;
}

body:has(.app-bottom-nav.is-compact) .bmh-fab {
  --fab-nav-height: var(--bottom-nav-compact-height);
}

@media (prefers-reduced-motion: reduce) {
  .bmh-fab { transition: none; }
}

.bmh-fab:hover,
.bmh-fab:focus-visible { filter: brightness(1.08); }

.bmh-fab i { font-size: 1.15rem; }

/* Breakpoint 991,98px KHÔNG thuộc tầng mobile (tầng đó sở hữu 767,98px) — bottom-nav mang
   d-lg-none nên sống tới 991,98px, phải né đúng phạm vi đó. */
@media (max-width: 991.98px) {
  body:has(.app-bottom-nav) .bmh-fab {
    bottom: calc(var(--fab-nav-height) + env(safe-area-inset-bottom, 0px) + var(--space-md));
  }
  /* 🔴 FAB che dòng CUỐI của trang (người dùng báo 11/09/2026: nút Góp ý đè nút xóa của thành viên
     cuối cùng ở tab Điểm danh). `.page-content` chỉ chừa bottom-nav + --space-md, mà FAB lại nổi
     đúng từ mốc đó lên thêm 48px ⇒ cuộn hết cỡ thì 48px cuối của nội dung vẫn nằm dưới FAB — và
     mép PHẢI là chỗ các nút hành động của hàng (xóa, ⋯) đứng. Chừa thêm đúng một FAB + một khoảng
     thở để dòng cuối cuộn lên được HẲN phía trên nút. :has(.bmh-fab) để trang không có FAB (tắt
     góp ý, quản trị hệ thống) không bị thừa dải trắng. */
  body:has(.app-bottom-nav):has(.bmh-fab) .page-content {
    padding-bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px)
                         + var(--space-md) + var(--fab-height) + var(--space-md));
  }
}

/* Honeypot chống bot (form Góp ý): đẩy hẳn ra ngoài khung nhìn. KHÔNG dùng display:none — một số
   bot bỏ qua trường display:none nhưng vẫn điền trường "nhìn thấy được" theo DOM. */
.bmh-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ------------------------------------------------------------
   Preview ảnh đính kèm (form Góp ý) — lưới thumbnail dựng bằng JS trong
   fragments/layout.html, xem #gopY-images-preview.

   Lưới TỰ CO theo bề rộng modal (auto-fill + minmax) chứ không cố định số cột: modal góp ý rộng
   ~500px trên desktop và ~358px ở 390×844, cùng một luật cho ra 5 cột / 3 cột mà không cần
   breakpoint riêng ở tầng mobile.

   `gap` phải ≥ .5rem: vùng chạm của nút X được nới ra ngoài viền ảnh 8px (xem ::after bên dưới),
   gap hẹp hơn thì hai vùng chạm cạnh nhau chồng lên nhau và bấm xóa dễ trúng nhầm ảnh bên cạnh.
   ------------------------------------------------------------ */
.bmh-anh-preview {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: var(--space-xs);
  margin-top: var(--space-xs);
}

.bmh-anh-preview:empty { display: none; }   /* không chừa khoảng trống khi chưa chọn ảnh nào */

.bmh-anh-thumb {
  position: relative;
  aspect-ratio: 1;
}

.bmh-anh-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;                        /* ảnh chụp màn hình rất dài — cắt còn hơn bóp méo */
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-muted);         /* nền lúc ảnh chưa decode xong */
  display: block;
}

/* Nút X: vòng tròn NHÌN THẤY 28px cho gọn trên thumbnail ~84px, nhưng vùng CHẠM nới lên 44×44 bằng
   ::after (28 + 8×2) để qua MB-3. Nền tối đặc + chữ trắng thay vì token theme: nút nằm ĐÈ LÊN ẢNH
   của người dùng, ảnh nào cũng có thể sáng hoặc tối nên không có nền theme nào bảo đảm được tương
   phản — chỉ lớp phủ riêng của nút mới bảo đảm. Vì vậy khối này KHÔNG có bản đối ứng theme tối. */
.bmh-anh-thumb__xoa {
  position: absolute;
  top: var(--space-2xs);
  right: var(--space-2xs);
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: rgba(15, 23, 42, .78);
  color: #fff;
  cursor: pointer;
  line-height: 1;
}

.bmh-anh-thumb__xoa i { font-size: 16px; }  /* MB-4: cỡ icon chỉ thuộc {16, 20, 24} */

.bmh-anh-thumb__xoa::after {
  content: '';
  position: absolute;
  inset: -8px;
}

.bmh-anh-thumb__xoa:hover,
.bmh-anh-thumb__xoa:focus-visible { background: var(--status-danger); }

/* Thanh tìm kiếm + lọc của màn Thành viên (member/thanh-vien.html).
   🔴 Không dùng utility `align-items-center` như ban đầu: `.form-control` của app có đệm dọc 10px
   còn `.btn` chỉ 6px, nên ô tìm kiếm cao 46px trong khi nút "Lọc" đứng cạnh chỉ 38,6px — đo được ở
   1440px, và lệch cả trục dọc (y 234,8 vs 238,5). Trong `.input-group` nút kính lúp không dính lỗi
   này vì input-group vốn `align-items: stretch`; nút "Lọc" nằm NGOÀI nhóm đó nên không được kéo
   theo. `stretch` ở đây kéo mọi nút bằng đúng chiều cao ô tìm kiếm, không cần đoán con số nào.
   Người dùng báo 2026-08-22.

   `inline-flex` + `align-items: center` là BẮT BUỘC đi kèm: kéo cao một hộp `inline-block` (mặc
   định của `.btn`) chỉ nối dài hộp xuống dưới, chữ vẫn nằm ở dòng đầu và nút trông lệch lên — cùng
   cái bẫy đã ghi ở P1/task 5.11b cho tầng mobile. `gap` cũng bắt buộc: chuyển sang flex thì khoảng
   trắng giữa <i> và nhãn nằm trong anonymous flex item và bị nuốt, icon sẽ dính sát chữ. */
.filter-bar { align-items: stretch; }

.filter-bar > .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
}

/* ==========================================================================
   Cài đặt riêng khi chia tiền (member/sua.html) — chuyển pane bằng CSS THUẦN
   ==========================================================================
   🔴 Không dùng JS gắn/gỡ class: màn Sửa thành viên tự render lại chính nó (POST → 302 → htmx swap
   #page-content), nên class do JS gắn bị attribute settling của htmx đè — bẫy đã lộ thật ở
   `club/cau-hinh-chia-tien.html` (xem templates/CLAUDE.md). Radio ẩn + `:checked ~` không phụ thuộc
   class nên miễn nhiễm, và trạng thái ban đầu vẫn do server quyết qua `th:checked`.

   Radio ẩn bằng `position:absolute; opacity:0` chứ KHÔNG `display:none`: `display:none` làm input
   rời khỏi cây tiêu điểm ⇒ bàn phím Tab không tới được, người dùng chỉ dùng bàn phím mất hẳn đường
   đổi pane. */
.rieng-switch__radio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* Hai nút bằng nhau, xuống dòng gọn ở 390px (CT-6): grid 2 cột thay vì btn-group cố định. */
.rieng-switch__tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xs, .5rem);
}

/* Chiều cao vùng chạm giữ nguyên 44px của `.btn` (MB-3) — không hạ. */
.rieng-switch__tab {
  margin: 0;
  cursor: pointer;
}

.rieng-switch__pane { display: none; }

.rieng-switch__radio#cheDoHeSo:checked ~ .rieng-switch__pane--heso,
.rieng-switch__radio#cheDoThuCung:checked ~ .rieng-switch__pane--thucung {
  display: block;
}

/* Nút đang chọn tô như `.btn-primary` — dùng token của app, không hardcode mã màu. */
.rieng-switch__radio#cheDoHeSo:checked ~ .rieng-switch__tabs .rieng-switch__tab--heso,
.rieng-switch__radio#cheDoThuCung:checked ~ .rieng-switch__tabs .rieng-switch__tab--thucung {
  background-color: var(--action-primary);
  border-color: var(--action-primary);
  color: #FFFFFF;
}

/* Bàn phím: radio vô hình nên vòng tiêu điểm phải vẽ lên NHÃN tương ứng, nếu không người dùng Tab
   tới mà không thấy mình đang ở đâu. */
.rieng-switch__radio#cheDoHeSo:focus-visible ~ .rieng-switch__tabs .rieng-switch__tab--heso,
.rieng-switch__radio#cheDoThuCung:focus-visible ~ .rieng-switch__tabs .rieng-switch__tab--thucung {
  box-shadow: 0 0 0 .25rem var(--focus-ring);
}

/* 🔴 Cụm quảng cáo dán đáy + hai cột dọc PHẢI nằm trước mốc tầng mobile bên dưới:
   MobileScaleTest coi mọi thứ sau mốc đó là luật mobile và cấm bề rộng px cứng ở đó — mà ngưỡng
   1480px/1600px của cột dọc lại chính là bề rộng px. Đặt đúng chỗ này thì cả hai luật cùng đúng:
   ngưỡng là luật DESKTOP, còn phần mobile chỉ đổi ba token trong tầng mobile. */

/* ------------------------------------------------------------
   Thanh quảng cáo dán đáy (fragments/ads :: oQuangCaoDinh) — CÓ ở cả ba nhóm thiết bị, chỉ khác
   khổ: 728×90 trên máy tính/máy tính bảng, 320×50 trên điện thoại (token đổi ở tầng mobile).

   🔴 Các lớp cố định ở đáy màn hình, từ dưới lên — thứ tự này là hợp đồng:
      .app-bottom-nav (z 1030, chỉ <992px) → .bmh-ad-sticky (z 1031) → .bmh-fab (z 1035).
   Quảng cáo nằm TRÊN thanh điều hướng và DƯỚI nút Góp ý, nên không vùng chạm nào của app bị khối
   quảng cáo che — bấm nhầm quảng cáo khi định bấm "Buổi tập" vừa làm người dùng bực, vừa là lượt
   bấm Google tính là không hợp lệ và trừ lại.

   🔴 height là CON SỐ CHỐT (--sticky-ad-band): hai chỗ khác cộng đúng token này để né. Một thanh
   cao hơn nó khai báo sẽ che dòng cuối của MỌI trang mà không có gì báo lỗi.
   ------------------------------------------------------------ */
.bmh-ad-sticky {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1031;
  /* 🔴 min-height, KHÔNG phải height. AdSense chọn khổ trong số các khổ NGANG khớp bề rộng đã xin —
     ở 320px nó có hai lựa chọn hợp lệ: 320×50 và 320×100, và nó trả khổ nào là quyền của nó
     (2026-08-28 người dùng nhận 320×100). Với `height` cứng + overflow:hidden thì quảng cáo 100px bị
     cắt còn 50px — vừa là hiển thị không hợp lệ theo AdSense, vừa khiến `--sticky-ad-band` (thứ mà
     .page-content và nút Góp ý cộng vào để né) NÓI DỐI về chiều cao thật của thanh, nên dòng cuối
     trang lại chui xuống dưới quảng cáo đúng như trước. Thanh cao theo quảng cáo THẬT, còn con số
     để né thì script trong fragments/ads.html đo lại và ghi đè --sticky-ad-band. Token ở :root chỉ
     còn là giá trị KHỞI ĐẦU, dùng cho lần vẽ đầu trước khi quảng cáo về. */
  min-height: var(--sticky-ad-band-init);
  padding: var(--space-2xs) var(--space-2xs) 0;
  overflow: hidden;
  background: var(--surface);
  border-top: 1px solid var(--border);
  text-align: center;
  /* AdSense đặt `display: inline-block` inline lên thẻ <ins>, nên ô quảng cáo nằm trong một dòng
     văn bản và ăn thêm phần chân chữ của line-height — đo được 5,5px thừa ở đáy thanh, tức 5,5px
     màn hình điện thoại mất mà không hiện gì. Nhãn "Quảng cáo" là thẻ block có line-height riêng
     nên không bị ảnh hưởng. */
  line-height: 0;
}
/* Nhãn "Quảng cáo": AdSense không cho quảng cáo trông giống nội dung của sản phẩm, và ở đây điều
   đó còn quan trọng hơn vì thanh dán ngay cạnh thanh điều hướng thật.
   Cỡ chữ đúng bậc nhỏ nhất của thang (13px, MB-1), không được nhỏ hơn. */
.bmh-ad-sticky__label {
  font-size: var(--fs-caption, .8125rem);
  line-height: 1;
  margin-bottom: var(--space-2xs);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: .04em;
}
.bmh-ad-sticky__slot {
  display: block;
  width: 100%;
  max-width: var(--sticky-ad-max);
  margin: 0 auto;
  /* min-height = chỗ giữ trước khi quảng cáo về. Khi về, AdSense ghi `height` INLINE lên chính thẻ
     này (inline luôn thắng luật class) nên ô tự cao đúng bằng khổ nhận được. */
  min-height: var(--sticky-ad-height);
  /* 🔴 overflow:hidden ở ĐÂY, không chỉ ở .bmh-ad-sticky. Lý do: AdSense dựng iframe con và tự ghi
     bề rộng cho nó bằng style INLINE — số đó do máy phục vụ quảng cáo quyết, không phải ta. Nếu
     iframe rộng hơn khổ đã kẹp thì nó tràn ra khỏi <ins> và trông như một khối rộng bằng cả màn
     hình (người dùng báo 2026-08-28: khối 390px trên máy 390×844, trong khi thanh điều hướng ngay
     dưới chỉ 320px). .bmh-ad-sticky cắt theo CHIỀU CAO nhưng nó rộng suốt màn hình nên không cắt
     được chiều ngang — chỗ duy nhất biết khổ đúng là thẻ này. */
  overflow: hidden;
}
/* Chốt chặn cuối theo CHIỀU NGANG: dù AdSense trả khổ nào, iframe cũng không được rộng quá khung đã
   kẹp. Cần !important vì bên kia là style inline do script của Google ghi thẳng lên phần tử.
   🔴 KHÔNG kẹp chiều CAO ở đây. Bản đầu có `max-height: var(--sticky-ad-height)` và nó cắt đôi
   quảng cáo 320×100 hợp lệ — thà thanh cao thêm 50px còn hơn hiển thị một quảng cáo bị cắt.
   Dùng selector con-cháu (không phải `>`): Google có lúc lồng iframe trong một <div> trung gian. */
.bmh-ad-sticky__slot iframe {
  max-width: 100% !important;
}

/* 🔴 Dòng cuối của mọi trang phải cuộn lên khỏi thanh quảng cáo — người dùng chốt 2026-08-28:
   "kéo xuống phải xem được nội dung". Không có luật này thì nút hành động cuối cùng (Chốt sổ,
   Lưu…) nằm dưới quảng cáo: vừa mất giây của M-TSK-1, vừa đúng loại "quảng cáo che nội dung" mà
   AdSense cấm. :has() để chỉ chừa chỗ khi thanh THẬT SỰ được render — CLB gói PRO hay chưa cấu
   hình mã đơn vị thì trang không được thừa ra một dải trắng vô cớ.

   🔴 Khoảng thở ở đây là --space-lg (24px), KHÔNG phải --space-md (16px) như các lớp chỉ né
   bottom-nav. Lý do là khác biệt về BẢN CHẤT chứ không phải khẩu vị: bottom-nav là một phần của
   app, dòng cuối trang dừng sát nó vẫn đọc được và người dùng biết dưới đó không còn gì. Quảng cáo
   là nội dung LẠ, nền khác, thường có viền và ảnh — dòng cuối dừng cách nó 16px trông như đang bị
   nó cắn vào, và người dùng báo đúng cảm giác đó 2026-08-28: "không kéo thêm lên trên được qua
   quảng cáo". Đo ở 390×844 khi đã cuộn hết: trước 16,5px, sau 24px, và tổng đường cuộn dài thêm
   17px (9 từ token nav được sửa cho đúng + 8 từ khoảng thở này). Free phải xem quảng cáo, nhưng
   không được vì thế mà mất nội dung. */
body:has(.bmh-ad-sticky:not([data-bmh-ad-trong])) .page-content {
  padding-bottom: calc(var(--sticky-ad-band) + var(--space-lg));
}
body:has(.bmh-ad-sticky:not([data-bmh-ad-trong])) .bmh-fab {
  bottom: calc(var(--sticky-ad-band) + var(--space-lg));
}

/* 🔴 Từ 992px thanh điều hướng là SIDEBAR DỌC chiếm suốt chiều cao mép trái, và nó ở z-index 1030
   — thấp hơn thanh quảng cáo (1031). Để `left: 0` thì thanh quảng cáo cắt ngang đáy sidebar, che
   mất mục cuối của menu (người dùng báo 2026-08-28). Bắt đầu từ đúng mép phải sidebar thay vì đè
   lên nó: dùng --sidebar-width nên tự khớp cả khi người dùng thu gọn sidebar (html.sidebar-collapsed
   hạ token xuống 76px). Hệ quả tốt kèm theo: banner căn giữa theo VÙNG NỘI DUNG chứ không theo cả
   màn hình, nên nó thẳng hàng với nội dung phía trên. */
@media (min-width: 992px) {
  .bmh-ad-sticky { left: var(--sidebar-width); }
}

/* ------------------------------------------------------------
   Hai cột quảng cáo dọc (fragments/ads :: oQuangCaoDoc) — máy tính màn rộng.

   🔴 TẮT mặc định và chỉ bật khi ĐO ĐƯỢC là có chỗ. Sidebar 248 + nội dung 960 (--w-wide) là cố
   định, nên bề rộng còn lại mỗi bên = (100vw − 248 − 960) / 2. Cột chỉ được bật khi khoảng đó chứa
   nổi cả quảng cáo lẫn khoảng thở:
     • 120×600 cần (100vw − 1208)/2 ≥ 136  ⇒  100vw ≥ 1480
     • 160×600 cần (100vw − 1208)/2 ≥ 176  ⇒  100vw ≥ 1560, lấy 1600 cho chắc (thanh cuộn ~15px)
   Người dùng chốt 2026-08-28: KHÔNG thu hẹp nội dung để nhét quảng cáo — laptop 1440 và máy tính
   bảng vì thế không có cột dọc, và đó là lựa chọn có ý thức chứ không phải thiếu sót.

   🔴 Cột được căn giữa khoảng trống đó, không dán sát mép: người dùng yêu cầu nó nằm GIỮA thanh
   điều hướng và cạnh nội dung.
   ------------------------------------------------------------ */
.bmh-ad-side { display: none; }

/* 🔴 TRANG CÔNG KHAI KHÔNG CÓ SIDEBAR, nên --sidebar-width ở đó phải là 0.

   Cả ba luật vị trí quảng cáo phía trên (.bmh-ad-sticky lẫn hai cột dọc) trừ đi --sidebar-width vì
   chúng sinh ra cho vỏ đã đăng nhập. Khu nội dung công khai (/cam-nang, /huong-dan) dùng vỏ
   fragments/public.html — chỉ có .entry-header nằm trên cùng, không có thanh dọc bên trái — nhưng
   token vẫn giữ nguyên 248px, nên toàn bộ phép căn giữa lệch đi đúng 248px về bên phải.

   Đo thật 2026-09-04 trên /cam-nang ở 1480×900 TRƯỚC khi có luật này (dựng hai cột giả rồi đo
   getBoundingClientRect):
     • cột trái  252,25 → 372,25 · nội dung bắt đầu ở 372,5  ⇒  khe hở 0,25px, dán chặt vào chữ
     • cột phải  1340,75 → 1460,75 trên khung nhìn 1465      ⇒  cách nội dung 248px, sát mép phải
   Chưa đè lên chữ nên không có test nào bắt được, nhưng bố cục lệch hẳn và cột trái chạm nội dung —
   đúng thứ Task 6 Step 11 của kế hoạch nội dung để lại phải kiểm bằng mắt.

   Đặt token về 0 thì hai công thức cũ tự đúng, không phải viết luật vị trí thứ hai cho trang công
   khai. .entry-content là dấu hiệu tin được: không template nào vừa có nó vừa có .app-sidebar —
   sidebar chỉ sống trong fragments/nav :: nav, còn mọi trang .entry-content đều dùng entryHeader
   hoặc header của vỏ công khai. */
/* 🔴 Từ 2026-09-05 vỏ công khai (fragments/public.html) không còn dùng .entry-content — nó đã
   chuyển hẳn sang hệ .lp- dùng chung với trang `/`. Giữ cả hai dấu hiệu: .entry-content vẫn là vỏ
   của các trang ĐÃ đăng nhập nhưng chưa chọn CLB (danh sách CLB, hồ sơ, lỗi), còn .lp-body là vỏ
   công khai. Bỏ vế thứ hai ⇒ hai cột quảng cáo dọc ở /cam-nang lệch lại đúng 248px như trước. */
:root:has(.entry-content),
:root:has(.lp-body) { --sidebar-width: 0px; }

/* 🔴 Vỏ công khai KHÔNG có .app-bottom-nav (thanh điều hướng đáy chỉ tồn tại sau khi đăng nhập),
   nên ở ≤991px thanh quảng cáo dán đáy đang treo lơ lửng cách mép dưới đúng 69px = chiều cao của
   một thanh không tồn tại. Đặt token về 0 cho vỏ này thì công thức `bottom: calc(...)` ở dưới tự
   đúng, không phải viết luật vị trí thứ hai. Custom property kế thừa nên khai ở <body> là đủ.

   Cùng lý do: .lp-foot phải tự chừa chỗ cho thanh quảng cáo, và nút "lên đầu trang" phải nhấc lên
   trên nó — hai việc mà .page-content và .bmh-fab đã làm cho khu đã đăng nhập, còn vỏ công khai
   thì không có hai lớp đó. */
body.lp-body { --bottom-nav-height: 0px; }
body.lp-body:has(.bmh-ad-sticky:not([data-bmh-ad-trong])) .lp-foot {
  padding-bottom: calc(var(--sticky-ad-band) + var(--space-lg));
}
body.lp-body:has(.bmh-ad-sticky:not([data-bmh-ad-trong])) .lp-totop {
  bottom: calc(var(--sticky-ad-band) + var(--space-md));
}

@media (min-width: 1480px) and (min-height: 760px) {
  .bmh-ad-side {
    display: block;
    position: fixed;
    z-index: 1029;
    width: var(--side-ad-width);
    /* Căn giữa theo chiều dọc phần màn hình CÒN LẠI sau khi trừ thanh dán đáy — căn giữa cả 100vh
       thì ở màn cao 800px chân quảng cáo dọc chui xuống dưới thanh đáy. */
    top: calc((100vh - var(--sticky-ad-band) - var(--side-ad-height)) / 2);
  }
  .bmh-ad-side--trai {
    left: calc(var(--sidebar-width)
               + ((100vw - var(--sidebar-width) - var(--w-wide)) / 2 - var(--side-ad-width)) / 2);
  }
  .bmh-ad-side--phai {
    right: calc(((100vw - var(--sidebar-width) - var(--w-wide)) / 2 - var(--side-ad-width)) / 2);
  }
  .bmh-ad-side__label {
    font-size: var(--fs-caption, .8125rem);
    line-height: 1;
    margin-bottom: var(--space-2xs);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: .04em;
  }
  .bmh-ad-side__slot {
    display: block;
    width: var(--side-ad-width);
    height: var(--side-ad-height);
  }
}

/* Từ 1600px khoảng trống đủ cho khổ rộng hơn — 160×600 có nhiều quảng cáo khớp khổ hơn 120×600.
   Chỉ đổi token, không lặp lại luật vị trí. */
@media (min-width: 1600px) and (min-height: 760px) {
  :root { --side-ad-width: 160px; }
}

/* 🔴 Dưới 992px còn có .app-bottom-nav chiếm đáy màn hình, nên thanh quảng cáo phải leo lên TRÊN
   nó, còn khoảng chừa của nội dung và nút Góp ý phải cộng CẢ HAI lớp. Khối này đặt SAU các luật
   không-media ở trên là có chủ đích: chúng cùng độ đặc hiệu (body:has(...) .x), nên chỗ đứng sau
   mới là chỗ thắng — đảo thứ tự sẽ làm tablet mất khoảng chừa của thanh điều hướng, một lỗi chỉ lộ
   ở đúng dải 768–991px mà không viewport nào khác thấy. */
@media (max-width: 991.98px) {
  .bmh-ad-sticky {
    bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px));
  }
  body:has(.bmh-ad-sticky:not([data-bmh-ad-trong])) .page-content {
    padding-bottom: calc(var(--bottom-nav-height) + var(--sticky-ad-band)
                         + env(safe-area-inset-bottom, 0px) + var(--space-lg));
  }
  body:has(.bmh-ad-sticky:not([data-bmh-ad-trong])) .bmh-fab {
    bottom: calc(var(--fab-nav-height) + var(--sticky-ad-band)
                 + env(safe-area-inset-bottom, 0px) + var(--space-lg));
  }
}

/* ── Thông báo in-app (ADR-22) ────────────────────────────────────────────────
   Chuông ở topbar điện thoại + mục sidebar máy tính, panel trong modal, và màn
   hình đầy đủ /clb/{id}/thong-bao đều dùng chung khối này.

   🔴 Vùng chạm 44×44 là HỢP ĐỒNG của .bmh-chuong, không phải hệ quả tình cờ của
   một chuỗi utility — bài học .app-offcanvas-link: tổ hợp `d-flex align-items-
   center gap-2 py-2` cho đúng 40px, thiếu 4px so với MB-3, và thiếu y hệt nhau ở
   cả 390/360/768px. */
.bmh-chuong {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font-size: 1.15rem;
}

.bmh-chuong__so {
  position: absolute;
  top: 4px;
  right: 4px;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--bs-danger);
  color: #fff;
  font-size: .68rem;
  line-height: 18px;
  font-weight: 600;
  text-align: center;
}

/* Trong sidebar huy hiệu nằm cuối dòng chứ không phải góc icon. */
.app-sidebar-nav .bmh-chuong__so {
  position: static;
  margin-left: auto;
}

/* Sidebar thu gọn: nhãn bị ẩn nên huy hiệu quay về dạng chấm góc, không đẩy icon lệch. */
html.sidebar-collapsed .app-sidebar-nav .bmh-chuong__so {
  position: absolute;
  top: 6px;
  right: 6px;
  margin-left: 0;
}

/* Vạch trạng thái nằm ở `border-left` của <li> chứ không phải `box-shadow: inset`: nút bên trong
   trải hết chiều rộng và có nền riêng lúc hover, nên shadow inset sẽ bị chính nó che mất. Dòng đã
   đọc giữ vạch TRONG SUỐT cùng bề rộng để hai trạng thái không lệch nhau 3px. */
.bmh-tb-item {
  border-bottom: 1px solid var(--bs-border-color);
  border-left: 3px solid transparent;
}
.bmh-tb-item:last-child { border-bottom: 0; }
/* ĐÃ ĐỌC vs CHƯA ĐỌC — hai trạng thái phải nhìn một cái là biết khác nhau (`TB-4`).

   🔴 Ba dấu hiệu chồng lên nhau, KHÔNG chỉ màu: (1) nền nhấn, (2) VẠCH dọc bên trái, (3) tiêu đề
   đậm hơn và ăn màu chữ mạnh. Chỉ dùng màu là bỏ rơi người mù màu và mọi người khác khi cầm điện
   thoại ngoài nắng ở sân (cùng lý lẽ với `T-5` của phần tiền).

   Bản đầu (08/09/2026) chỉ có `background: var(--bs-tertiary-bg)` — trong `.modal-body` của chuông
   nó gần như trùng nền, nên hai trạng thái trông y hệt nhau (người dùng báo 09/09/2026). */
.bmh-tb-item--chua-doc {
  background: var(--tb-unread-bg);
  border-left-color: var(--tb-unread-bar);
}
.bmh-tb-item--chua-doc .bmh-tb-title {
  font-weight: 700;
  color: var(--text-strong);
}

/* Dòng ĐÃ ĐỌC lùi hẳn về sau: chữ nhạt một bậc và icon giảm độ đậm. Không có bước này thì "chưa
   đọc" chỉ nổi lên nhờ nền, và khi hộp thư toàn dòng chưa đọc thì lại chẳng còn gì để so sánh. */
.bmh-tb-item:not(.bmh-tb-item--chua-doc) .bmh-tb-title { color: var(--text-muted); }
.bmh-tb-item:not(.bmh-tb-item--chua-doc) .bmh-tb-ic { opacity: .65; }

/* Dòng thông báo là <button type="submit"> chứ không phải <a>: bấm vào phải ĐÁNH DẤU ĐÃ ĐỌC rồi
   mới đi tiếp (AC-NOTI-02.4), tức một POST. Reset lại vẻ ngoài của nút cho giống hệt một dòng
   danh sách — width 100% để cả hàng là vùng chạm (MB-3), text-align/left để chữ không bị canh
   giữa như nút mặc định. */
.bmh-tb-link {
  display: flex;
  gap: .75rem;
  align-items: flex-start;
  width: 100%;
  min-height: 44px;
  padding: .7rem .25rem;
  border: 0;
  background: none;
  text-align: left;
  color: inherit;
  text-decoration: none;
}
/* 🔴 Hover phải RẼ NHÁNH theo trạng thái. Một luật `.bmh-tb-link:hover` duy nhất sẽ tô đè nền
   nhấn của dòng chưa đọc — trỏ chuột vào là dòng đó trông như đã đọc, đúng lúc người dùng đang
   nhắm bắn nó. */
.bmh-tb-item:not(.bmh-tb-item--chua-doc) .bmh-tb-link:hover { background: var(--bs-secondary-bg); }
.bmh-tb-item--chua-doc .bmh-tb-link:hover { background: var(--tb-unread-bg-hover); }

.bmh-tb-ic {
  flex: 0 0 34px;
  height: 34px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.bmh-tb-ic--warning { background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis); }
.bmh-tb-ic--success { background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis); }
.bmh-tb-ic--secondary { background: var(--bs-secondary-bg); color: var(--bs-secondary-color); }

/* min-width:0 để tên dài cắt được bằng ellipsis thay vì đẩy vỡ hàng ở 360px. */
.bmh-tb-body { display: flex; flex-direction: column; min-width: 0; }
.bmh-tb-title { font-weight: 600; }
.bmh-tb-desc,
.bmh-tb-time { font-size: .85rem; color: var(--bs-secondary-color); }

/* Hai nút chân modal chuông ("Đánh dấu tất cả đã đọc" / "Xem tất cả") — người dùng báo 09/09/2026:
   ở 390×844 hai nhãn dài chiếm gần trọn bề ngang footer nên chúng nặng mắt hơn cả danh sách mà
   chúng phục vụ.

   🔴 Chỉ hạ CỠ CHỮ + padding ngang, KHÔNG đụng chiều cao: tầng mobile áp
   `.btn { min-height: var(--tap-min) }` và `.btn:not(.is-icon) { min-width: var(--tap-min) }` cho
   mọi nút, và đó là `MB-3` — thu nhỏ vùng chạm để "cho gọn" là hạ cấp một tiêu chí đã chốt, không
   phải quyết định thẩm mỹ. Độ đặc hiệu 0,0,1,1 nên luật này thắng `.btn { font-size }` của tầng
   mobile (0,0,1,0) mà không cần !important. */
.bmh-tb-actions .btn {
  font-size: .8125rem;
  padding-inline: .5rem;
}

/* ============================================================
   Tab "Xếp sân" (BR-LINEUP-01..06) — lineup/fragments.html + /js/xep-san.js.
   Spec docs/superpowers/specs/2026-09-13-xep-san-buoi-tap-design.md § 5.

   🔴 Luồng chạm-chọn-chạm-sân KHÔNG cần JS và dựa hẳn vào CSS ở đây:
     • thẻ đang chọn  = .xs-chip:has(.xs-check:checked)
     • khay "sẵn sàng" = .xs-form:has(.xs-check:checked) .xs-target
     • "Đã chọn N"     = CSS counter xs-chon (reset ở .xs-form, tăng ở mỗi ô đã tick)
   Đổi tên một lớp ở đây mà quên template là khay im lặng không đổi màu và bộ đếm mất số.

   Cỡ chữ/vùng chạm dùng token của tầng mobile kèm giá trị dự phòng — hai token đó chỉ được khai
   trong khối @media của tầng mobile, ra ngoài khối là rỗng.
   ============================================================ */
.xs-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-xs); margin-bottom: var(--space-xs);
}
.xs-toolbar__actions { display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.xs-toolbar__actions form { margin: 0; }
.xs-tally { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.xs-tally strong { color: var(--text-strong); }
.xs-hint { font-size: var(--fs-caption, .8125rem); color: var(--text-muted); margin-bottom: var(--space-sm); }

.xs-form { counter-reset: xs-chon; }
.xs-board { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-sm); }

.xs-group {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-sm);
  scroll-margin-top: calc(var(--topbar-height) + var(--space-md));
}
.xs-group--pool { background: transparent; border-style: dashed; border-color: var(--border-strong); }
.xs-group__head {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: var(--space-2xs) var(--space-xs); margin-bottom: var(--space-xs);
}
.xs-group__title { font-size: 1rem; font-weight: 700; margin: 0; color: var(--text-strong); }
.xs-time {
  font-size: var(--fs-caption, .8125rem); font-weight: 400; color: var(--text-muted);
  margin-left: var(--space-2xs); font-variant-numeric: tabular-nums;
}
.xs-meta {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  font-size: var(--fs-caption, .8125rem); color: var(--text-muted); white-space: nowrap;
}

.xs-list {
  list-style: none; margin: 0; padding: 0; min-height: 48px;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2xs);
}
@media (min-width: 992px) { .xs-list { grid-template-columns: minmax(0, 1fr); } }
.xs-roster { min-width: 0; display: grid; align-content: start; gap: var(--space-2xs); }
.xs-empty { font-size: var(--fs-caption, .8125rem); color: var(--text-muted); margin: var(--space-2xs) 0 0; }

.xs-chip {
  position: relative; display: flex; align-items: center; gap: var(--space-xs);
  min-height: var(--tap-min, 44px); margin: 0; padding: var(--space-2xs) var(--space-xs) var(--space-2xs) var(--space-2xs);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface-sunken); color: var(--text-strong); cursor: pointer;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; touch-action: manipulation;
}
.xs-group--pool .xs-chip { background: var(--surface); }
.xs-chip.is-readonly { cursor: default; }
/* Tay nắm kéo thả: phủ mép trái thẻ, đứng NGOÀI <label> (chạm vào không tick chọn). touch-action: none
   là thứ làm kéo thả bắt đầu được NGAY trên màn cảm ứng — thiếu nó trình duyệt giành cú vuốt để cuộn.
   Có tay nắm thì bỏ avatar chữ: ở 390px mỗi thẻ chỉ rộng ~168px, giữ cả hai là tên còn chưa tới 70px;
   giới tính vẫn đọc được qua icon ở cuối thẻ. */
.xs-item { position: relative; }
.xs-grip {
  position: absolute; top: 0; bottom: 0; left: 0; z-index: 1; width: 32px;
  display: grid; place-items: center; color: var(--text-muted); cursor: grab;
  touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.xs-grip:active { cursor: grabbing; }
.xs-grip + .xs-chip { padding-left: 32px; }
.xs-grip + .xs-chip .xs-avatar { display: none; }
.xs-check { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.xs-check:checked { counter-increment: xs-chon; }
.xs-chip:has(.xs-check:checked) {
  border-color: var(--action-primary); background: var(--bs-primary-bg-subtle);
  box-shadow: inset 0 0 0 1px var(--action-primary);
}
.xs-chip:has(.xs-check:focus-visible) { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
.xs-avatar {
  flex: none; width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center;
  font-size: var(--fs-caption, .8125rem); font-weight: 700;
  background: var(--badge-blue-bg, rgba(37, 99, 235, .14)); color: var(--badge-blue-fg);
}
.xs-avatar.is-female { background: rgba(236, 72, 153, .14); color: var(--badge-pink-fg); }
.xs-avatar.is-guest { background: var(--badge-slate-bg, rgba(100, 116, 139, .16)); color: var(--badge-slate-fg); }
.xs-chip:has(.xs-check:checked) .xs-avatar { background: var(--action-primary); color: #fff; }
.xs-name { flex: 1; min-width: 0; display: grid; line-height: 1.2; }
.xs-name > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xs-name small { font-size: var(--fs-caption, .8125rem); color: var(--text-muted); }
/* Trình độ: mã ngắn ở dòng phụ dưới tên + trung bình ở đầu sân. 🔴 Không đặt cạnh tên — đo ở 390px
   tên còn 33–55px (XepSanTrinhDoRenderTest). */
.xs-sub { display: flex; align-items: center; gap: var(--space-2xs); min-width: 0; white-space: nowrap; }
.xs-skill {
  flex: none; display: inline-flex; align-items: center; gap: 3px;
  padding: 0 6px; border-radius: 999px; line-height: 1.6;
  font-size: var(--fs-caption, .8125rem); font-weight: 600; font-variant-numeric: tabular-nums;
  background: var(--badge-slate-bg, rgba(100, 116, 139, .16)); color: var(--badge-slate-fg);
}
/* Ở dòng phụ: thấp hơn một chút để thẻ hai dòng vẫn vừa vùng chạm 44px. */
.xs-sub .xs-skill { line-height: 1.35; padding: 0 5px; font-size: .75rem; }

/* SortableJS: chỗ trống để lại + bóng thẻ đang kéo (fallbackOnBody ⇒ bóng nằm ngoài .xs-board). */
.xs-list .sortable-ghost .xs-chip { opacity: .4; border-style: dashed; }
.xs-drag-ghost { list-style: none; }
.xs-drag-ghost .xs-chip { box-shadow: 0 12px 28px -8px rgba(15, 23, 42, .45); }

/* Khay: dính đáy. Nằm trong .xs-form nên chỉ dính khi tab đang hiện trên màn hình. */
.xs-tray {
  position: sticky; bottom: 0; z-index: 5; display: none;
  margin-top: var(--space-sm); padding: var(--space-xs) var(--space-sm) var(--space-sm);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  box-shadow: 0 -10px 24px -18px rgba(15, 23, 42, .4);
}
.xs-tray__head {
  display: flex; align-items: center; gap: var(--space-xs); min-height: 32px;
  font-size: var(--fs-caption, .8125rem); color: var(--text-muted);
}
.xs-tray__count { display: none; color: var(--text-strong); }
.xs-tray__count::after { content: counter(xs-chon); font-weight: 700; }
/* 🔴 Hai lớp (.xs-tray .xs-tray__reset), KHÔNG một lớp: nút này mang .btn, và tầng mobile đứng SAU
   khối này có `.btn { display: inline-flex }` cùng độ đặc hiệu ⇒ luật một lớp thua theo thứ tự và "Bỏ
   chọn" hiện ra khi chưa chọn ai (lộ thật 13/09/2026). XepSanKhayCssTest gác. */
.xs-tray .xs-tray__reset { display: none; margin-left: auto; min-height: 32px; }
.xs-form:has(.xs-check:checked) .xs-tray__hint { display: none; }
.xs-form:has(.xs-check:checked) .xs-tray__count { display: inline; }
.xs-form:has(.xs-check:checked) .xs-tray .xs-tray__reset { display: inline-flex; align-items: center; }
.xs-tray__targets { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: var(--space-xs); }
.xs-target-wrap { position: relative; }
.xs-target {
  width: 100%; min-height: 56px; display: grid; align-content: center; justify-items: center; gap: 2px;
  border: 1.5px dashed var(--border-strong); border-radius: var(--radius-md);
  background: var(--surface-sunken); color: var(--text-strong); font-weight: 700; line-height: 1.2;
}
.xs-target__count { font-size: var(--fs-caption, .8125rem); font-weight: 400; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.xs-form:has(.xs-check:checked) .xs-target {
  border-style: solid; border-color: var(--action-primary);
  background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis);
}
.xs-target-wrap.is-over .xs-target { background: var(--action-primary); border-color: var(--action-primary); color: #fff; }
.xs-target-wrap.is-over .xs-target__count { color: #fff; }
/* Chỗ THẢ phủ lên nút — chỉ nhận con trỏ khi đang kéo, để cú chạm thường vẫn tới nút submit. */
.xs-drop { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; pointer-events: none; }
.xs-dang-keo .xs-drop { pointer-events: auto; }
.xs-drop > li { position: absolute; inset: 0; opacity: 0; pointer-events: none; }

/* Desktop: mỗi sân là một workspace ngang, thay vì ép "Chưa xếp + 3 sân" thành bốn cột hẹp.
   Danh sách người nằm ở rail trái; lịch set dùng phần rộng bên phải. Cấu trúc này giữ cả ba sân cùng
   hiện diện để kéo/thả, nhưng tên đội và nút thao tác không còn bị bóp vỡ. */
@media (min-width: 992px) {
  .xs-board { gap: var(--space-md); }
  .xs-group--pool { padding: var(--space-md); }
  .xs-group--pool .xs-list { grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); }
  .xs-group--court {
    display: grid;
    grid-template-columns: minmax(13.5rem, .72fr) minmax(0, 2fr);
    grid-template-rows: auto auto;
    column-gap: var(--space-lg);
    padding: var(--space-md);
  }
  .xs-group--court > .xs-group__head { grid-column: 1 / -1; }
  .xs-group--court > .xs-roster { grid-column: 1; grid-row: 2; align-self: start; }
  .xs-group--court > .xs-schedule {
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    min-width: 0;
    padding-left: var(--space-lg);
    border-left: 1px solid var(--border);
  }
  .xs-group--court > .xs-schedule > .xs-sets {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }
}
.xs-form:has(.xs-check:checked) .xs-tray,
body.xs-dang-keo .xs-tray { display: block; }

/* Có thanh quảng cáo dán đáy (máy tính): khay đứng trên nó. */
body:has(.bmh-ad-sticky:not([data-bmh-ad-trong])) .xs-tray { bottom: var(--sticky-ad-band); }
/* < 992px có bottom-nav, và quảng cáo (nếu có) ngồi ngay trên nav — khay đứng trên cả hai.
   Khi khay hiện, ẩn nút Góp ý để các nút chọn sân không bị che.

   🔴 `fixed`, KHÔNG `sticky`, và đây là lỗi đã ĐO ĐƯỢC (20/09/2026) chứ không phải khẩu vị. Hộp
   sticky bị KẸP trong containing block của nó (.xs-form): trình duyệt không được phép nâng nó lên
   cao hơn mép trên khối cha. Ở đầu trang (scrollY = 0) mép trên .xs-form nằm ở 632px, trong khi chỗ
   khay cần đứng để né đủ nav + quảng cáo là 584px (390×844, thanh quảng cáo 74px) ⇒ khay bị kẹp lại
   ở 644..761 và thanh quảng cáo `fixed` (z 1031) VẼ ĐÈ lên nó. Đo bằng
   `tools/mobile-audit/probe-khay-xep-san.js`, ảnh chụp cho thấy cả ba nút sân biến mất:

     390×844 + quảng cáo 74px  → khay chồng quảng cáo 60px  · cả 3 nút KHÔNG chạm tới được
     360×800 + quảng cáo 74px  → khay chồng quảng cáo 104px · cả 3 nút KHÔNG chạm tới được (tới y=50)
     360×800 không quảng cáo   → khay chồng bottom-nav 30px · nút còn chạm được ở tâm
     390×844 không quảng cáo   → vừa khít, không chồng (may, không phải do luật nào bảo đảm)

   Cuộn xuống ~50-200px thì khối cha trôi lên, kẹp được gỡ và khay về đúng chỗ — tức bug chỉ sống ở
   đúng khoảnh khắc người dùng mở tab. `fixed` đứng ngoài dòng chảy nên không còn khối cha nào kẹp
   được; bù lại nó không chiếm chỗ nữa, nên #tab-xep-san phải tự chừa đúng chiều cao khay (bên dưới).
   Lề 12px khớp cả ba bề rộng đã đo (390/360/768 đều cho khay 12px..W-12px). */
@media (max-width: 991.98px) {
  body:has(.app-bottom-nav) .xs-tray {
    bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px));
  }
  body:has(#tab-xep-san.active) .xs-tray {
    position: fixed;
    left: var(--space-sm);
    right: var(--space-sm);
    /* Trên quảng cáo (1031) và bottom-nav (1030): khay là CÔNG CỤ đang thao tác,
       không được để một khối quảng cáo vẽ đè lên nút người dùng đang nhắm tay vào. */
    z-index: 1032;
  }
  body:has(.app-bottom-nav):has(.bmh-ad-sticky:not([data-bmh-ad-trong])) .xs-tray {
    bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px) + var(--sticky-ad-band));
  }
  /* Khay `fixed` rời khỏi dòng chảy ⇒ tab tự chừa lại đúng chiều cao nó (xep-san.js đo vào
     --xs-khay-cao, cùng khuôn --sticky-ad-band của thanh quảng cáo: chiều cao THẬT phải đo, không
     được khai). Số dự phòng 132px = khay 3 nút một hàng đo ở 390×844 (117px) + một khoảng thở. */
  body:has(#tab-xep-san.active .xs-check:checked) #tab-xep-san {
    padding-bottom: calc(var(--xs-khay-cao, 132px) + var(--space-md));
  }
  /* Khay hiện khi chọn người hoặc đang kéo thả; ẩn FAB trong cả hai trạng thái.
     Ngoài tab Xếp sân, FAB vẫn đi theo menu mobile như bình thường. */
  body:has(#tab-xep-san.active .xs-check:checked) .bmh-fab,
  body.xs-dang-keo:has(#tab-xep-san.active) .bmh-fab {
    display: none;
  }
}

/* ---- Lịch đánh trong sân (BR-LINEUP-07..11) ------------------------------------------------ */
.xs-schedule { min-width: 0; }
.xs-schedule__trigger { display: none; }
.xs-sets {
  container: xs-schedule / inline-size;
  margin-top: var(--space-md);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border);
}
.xs-sets__scroll { min-width: 0; }
.xs-sets__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-sm); }
.xs-sets__heading { min-width: 0; flex: 1 1 10rem; display: grid; gap: .125rem; }
.xs-sets__title { color: var(--text-strong); font-size: 1rem; font-weight: 700; letter-spacing: -.01em; }
.xs-sets__hint { color: var(--text-muted); font-size: var(--fs-caption, .8125rem); }
.xs-sets__count {
  flex: none;
  padding: .25rem .5rem;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--text-muted);
  font-size: var(--fs-caption, .8125rem);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.xs-sets__create-actions { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-left: auto; }
.xs-sets__add { flex: none; min-height: var(--tap-min, 44px); }
.xs-sets__close { display: none; }
.xs-sets__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-xs); }
.xs-sets__empty {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-md);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-md);
  color: var(--text-muted);
}
.xs-sets__empty-icon {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-muted);
  color: var(--action-primary);
  font-size: 1.125rem;
}
.xs-sets__empty strong { display: block; color: var(--text-strong); font-size: .875rem; }
.xs-sets__empty small { display: block; margin-top: .125rem; font-size: var(--fs-caption, .8125rem); }

.xs-set {
  display: grid;
  grid-template-columns: 4rem minmax(0, 1fr) auto;
  align-items: stretch;
  gap: var(--space-sm);
  padding: var(--space-sm);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  transition: border-color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast);
}
/* Hover chỉ dành cho thiết bị CÓ con trỏ. Trên cảm ứng, trình duyệt giữ :hover ở phần tử vừa chạm
   cho tới khi chạm chỗ khác ⇒ thẻ/nút "dính" ở trạng thái nhấc lên sau khi đã buông tay. */
@media (hover: hover) {
  .xs-set:hover { border-color: var(--border-strong); box-shadow: 0 .35rem 1rem rgba(15, 23, 42, .07); }
  [data-bs-theme="dark"] .xs-set:hover { box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2); }
}
.xs-set.is-canh-bao { border-color: var(--status-warning); background: var(--alert-warning-bg, rgba(249,115,22,.08)); }
.xs-set__meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  padding-right: var(--space-sm);
  border-right: 1px solid var(--border);
}
.xs-set__index { display: inline-flex; align-items: baseline; gap: .2rem; color: var(--text-strong); }
.xs-set__index small { color: var(--text-muted); font-size: var(--fs-caption, .8125rem); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
.xs-set__index strong { font-size: 1.375rem; line-height: 1; font-variant-numeric: tabular-nums; }
.xs-set__type {
  padding: .15rem .4rem;
  border-radius: .375rem;
  background: var(--surface-muted);
  color: var(--badge-slate-fg);
  font-size: var(--fs-caption, .8125rem);
  font-weight: 600;
}
.xs-set__teams { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--space-sm); }
.xs-set__side {
  min-width: 0;
  display: grid;
  gap: .125rem;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}
.xs-set__side-label { color: var(--text-muted); font-size: var(--fs-caption, .8125rem); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
.xs-set__side-name { min-width: 0; overflow: hidden; color: var(--text-strong); font-size: .875rem; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.xs-set__plus { color: var(--text-muted); font-weight: 500; }
/* Nhãn sân gốc của người MƯỢN từ sân khác (BR-LINEUP-08) — nhỏ và mờ: nó chú thích cho cái tên,
   không tranh chỗ với tên trong một dòng vốn đã chật ở 390px. */
.xs-set__home {
  margin-left: .25rem;
  padding: 0 .3rem;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--text-muted);
  font-size: .6875rem;
  font-weight: 600;
}
.xs-set__vs {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-muted);
  color: var(--text-muted);
  font-size: var(--fs-caption, .8125rem);
  font-weight: 700;
  text-transform: uppercase;
}
.xs-set__player.is-missing { text-decoration: line-through; color: var(--text-muted); }
.xs-set__warn {
  grid-column: 2 / -1;
  margin: 0;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--alert-warning-bg, rgba(249,115,22,.08));
  color: var(--status-warning-text, var(--badge-amber-fg));
  font-size: var(--fs-caption, .8125rem);
}
.xs-set__actions { display: flex; align-items: center; gap: var(--space-xs); padding-left: var(--space-2xs); }
.xs-set__action-group { display: inline-flex; gap: var(--space-2xs); }
.xs-set__actions .btn {
  min-width: var(--tap-min, 44px);
  min-height: var(--tap-min, 44px);
  border-radius: var(--radius-sm);
  transition: transform var(--motion-fast), background-color var(--motion-fast), border-color var(--motion-fast);
}
.xs-set__actions .btn:active { transform: scale(.96); }
.xs-soset { font-size: var(--fs-caption, .8125rem); color: var(--text-muted); }

/* Modal soạn set: diễn tả đúng cấu trúc hai bên sân thay vì bốn dropdown rời rạc. */
#modalSetDau:has([data-set-dau-form]) .modal-dialog { max-width: 42rem; }
.set-editor__header { align-items: flex-start; padding: var(--space-lg); border-bottom: 1px solid var(--border); }
.set-editor__heading { min-width: 0; display: flex; align-items: center; gap: var(--space-sm); }
.set-editor__icon {
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--court-100);
  color: var(--court-700);
  font-size: 1.25rem;
}
[data-bs-theme="dark"] .set-editor__icon { background: rgba(62, 196, 158, .14); color: var(--court-300); }
.set-editor__eyebrow { display: block; margin-bottom: .125rem; color: var(--action-primary); font-size: var(--fs-caption, .8125rem); font-weight: 700; letter-spacing: .07em; text-transform: uppercase; }
.set-editor__header .btn-close { flex: none; min-width: var(--tap-min, 44px); min-height: var(--tap-min, 44px); margin-top: -.25rem; margin-right: -.5rem; }
.set-editor__heading .modal-title { margin: 0; font-size: 1.25rem; line-height: 1.3; letter-spacing: -.02em; }
.set-editor__subtitle { display: block; margin-top: .125rem; color: var(--text-muted); font-size: var(--fs-caption, .8125rem); }
.set-editor__body { display: grid; gap: var(--space-md); padding: var(--space-lg); background: var(--surface-sunken); }
.set-editor__format { min-width: 0; margin: 0; padding: 0; border: 0; }
.set-editor__format legend { margin-bottom: var(--space-xs); color: var(--text-strong); font-size: var(--fs-caption, .8125rem); font-weight: 700; }
.set-editor__format-options { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-xs); }
.set-editor__format-option {
  min-height: 3.5rem;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
  transition: border-color var(--motion-fast), background-color var(--motion-fast), color var(--motion-fast), transform var(--motion-fast);
}
.set-editor__format-option:hover { border-color: var(--action-primary); color: var(--text-strong); }
.set-editor__format-option:active { transform: scale(.985); }
.set-editor__format-option > .bi:first-child { flex: none; font-size: 1.25rem; }
.set-editor__format-option > span { display: grid; gap: .0625rem; }
.set-editor__format-option strong { color: var(--text-strong); font-size: .875rem; }
.set-editor__format-option small { font-size: var(--fs-caption, .8125rem); }
.set-editor__format-check { margin-left: auto; color: var(--action-primary); opacity: 0; transform: scale(.7); transition: opacity var(--motion-fast), transform var(--motion-fast); }
.set-editor__format .btn-check:checked + .set-editor__format-option { border-color: var(--action-primary); background: var(--court-50); color: var(--action-primary); box-shadow: inset 0 0 0 1px var(--action-primary); }
[data-bs-theme="dark"] .set-editor__format .btn-check:checked + .set-editor__format-option { background: rgba(62, 196, 158, .1); }
.set-editor__format .btn-check:checked + .set-editor__format-option .set-editor__format-check { opacity: 1; transform: scale(1); }
.set-editor__format .btn-check:focus-visible + .set-editor__format-option { outline: 3px solid color-mix(in srgb, var(--action-primary) 28%, transparent); outline-offset: 2px; }
.set-editor__tip { display: flex; align-items: flex-start; gap: var(--space-xs); color: var(--text-muted); font-size: var(--fs-caption, .8125rem); }
.set-editor__tip .bi { color: var(--status-warning-text); }
.set-editor__court { display: grid; grid-template-columns: minmax(0, 1fr) 2.5rem minmax(0, 1fr); align-items: center; }
.set-editor__team {
  min-width: 0;
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-md);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}
.set-editor__team-head { display: flex; align-items: center; gap: var(--space-xs); padding-bottom: var(--space-xs); border-bottom: 1px solid var(--border); }
.set-editor__team-head > span:last-child { display: grid; }
.set-editor__team-head strong { color: var(--text-strong); font-size: .875rem; }
.set-editor__team-head small { color: var(--text-muted); font-size: var(--fs-caption, .8125rem); }
.set-editor__team-mark { width: 1.75rem; height: 1.75rem; border-radius: .45rem; display: inline-flex; align-items: center; justify-content: center; background: var(--court-100); color: var(--court-700); font-size: var(--fs-caption, .8125rem); font-weight: 800; }
[data-bs-theme="dark"] .set-editor__team-mark { background: rgba(62, 196, 158, .14); color: var(--court-300); }
.set-editor__field { display: grid; gap: var(--space-2xs); }
.set-editor__field[hidden] { display: none; }
.set-editor__field > label { color: var(--text-muted); font-size: var(--fs-caption, .8125rem); font-weight: 600; }
.set-editor__versus {
  z-index: 1;
  width: 2.5rem;
  height: 2.5rem;
  border: .25rem solid var(--surface-sunken);
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--text-strong);
  color: var(--surface);
  font-size: var(--fs-caption, .8125rem);
  font-weight: 800;
  letter-spacing: .04em;
}
.set-editor__status {
  min-height: 2.5rem;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--text-muted);
  font-size: var(--fs-caption, .8125rem);
}
.set-editor__status.is-ready { background: var(--court-50); color: var(--court-700); }
[data-bs-theme="dark"] .set-editor__status.is-ready { background: rgba(62, 196, 158, .1); color: var(--court-300); }
.set-editor__status.is-error { background: var(--red-50); color: var(--red-700); }
[data-bs-theme="dark"] .set-editor__status.is-error { background: rgba(248, 113, 113, .1); color: var(--red-400); }
.set-editor__footer { gap: var(--space-xs); padding: var(--space-md) var(--space-lg); border-top: 1px solid var(--border); }
.set-editor__footer .btn { min-width: 7.5rem; min-height: var(--tap-min, 44px); }

/* Một sân chỉ chiếm nửa bảng trên desktop. Query theo CHÍNH cột sân thay vì viewport: ở màn hình
   rộng nhưng có 2 cột, bề ngang hữu dụng vẫn hẹp và bốn nút không được phép đâm ra ngoài thẻ. */
@container xs-schedule (max-width: 42rem) {
  .xs-set { grid-template-columns: 3.75rem minmax(0, 1fr); gap: var(--space-xs); }
  .xs-set__teams { gap: var(--space-xs); }
  .xs-set__side { padding: var(--space-xs); }
  .xs-set__side-name { overflow: visible; font-size: var(--fs-caption, .8125rem); text-overflow: clip; white-space: normal; }
  .xs-set__actions {
    grid-column: 1 / -1;
    justify-content: flex-end;
    padding: var(--space-xs) 0 0;
    border-top: 1px solid var(--border);
  }
  .xs-set__warn { grid-column: 1 / -1; }
}

/* Desktop là màn hình thao tác nhanh: mỗi set là một hàng dữ liệu gọn, vẫn giữ đủ tên và nút. */
@media (min-width: 992px) {
  .xs-sets__head { margin-bottom: .375rem; }
  .xs-sets__list { gap: .25rem; }
  .xs-set {
    grid-template-columns: 3.25rem minmax(0, 1fr) auto;
    gap: .375rem;
    padding: .35rem .5rem;
    border-radius: var(--radius-md);
  }
  .xs-set__meta { gap: .125rem; padding-right: .375rem; }
  .xs-set__index small,
  .xs-set__type,
  .xs-set__side-label { font-size: .75rem; }
  .xs-set__index strong { font-size: 1rem; }
  .xs-set__type { padding: .1rem .3rem; }
  .xs-set__teams { gap: .375rem; }
  .xs-set__side { gap: 0; padding: .25rem .5rem; }
  .xs-set__side-name { font-size: .8125rem; }
  .xs-set__vs { width: 1.625rem; height: 1.625rem; font-size: .7rem; }
  .xs-set__actions { gap: .25rem; padding-left: 0; }
  .xs-set__actions { grid-column: auto; justify-content: flex-start; padding-top: 0; border-top: 0; }
  .xs-set__action-group { gap: .2rem; }
  .xs-set__actions .btn { min-width: 2.125rem; min-height: 2.125rem; padding: .25rem; }
}

/* Điện thoại: mỗi set thành một thẻ đọc từ trên xuống; modal dùng gần trọn chiều ngang và hai đội
   xếp dọc để tên/dropdown luôn có đủ chỗ. Breakpoint riêng vì cấu trúc này đổi ở điện thoại hẹp. */
@media (max-width: 575.98px) {
  .xs-schedule__trigger {
    width: 100%;
    min-height: var(--tap-min, 44px);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
    padding: .625rem .75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
    color: var(--text-strong);
    font-family: inherit;
    font-size: .875rem;
    font-weight: 700;
    text-align: left;
  }
  .xs-schedule__trigger > span { display: inline-flex; align-items: center; gap: .45rem; }
  .xs-schedule__trigger-meta { color: var(--text-muted); font-size: var(--fs-caption, .8125rem); font-weight: 500; }
  .xs-schedule__trigger-meta strong { color: var(--action-primary); font-weight: 700; }
  .xs-schedule__trigger:active { transform: scale(.99); }
  .xs-sets {
    position: fixed;
    inset: auto var(--space-sm) var(--space-sm);
    z-index: 1051;
    display: none;
    flex-direction: column;
    max-height: calc(100dvh - 5rem);
    margin: 0;
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: 0 1.25rem 3rem rgba(15, 23, 42, .28);
  }
  .xs-sets.is-open { display: flex; animation: xs-sheet-in var(--motion-fast) ease-out; }
  .xs-sets__scroll {
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--space-sm);
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  .xs-schedule-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1050;
    border: 0;
    background: rgba(15, 23, 42, .54);
    backdrop-filter: blur(2px);
  }
  body.xs-schedule-open { overflow: hidden; }
  @keyframes xs-sheet-in {
    from { opacity: 0; transform: translateY(1rem); }
    to { opacity: 1; transform: translateY(0); }
  }
  .xs-sets__head {
    flex: none;
    flex-wrap: wrap;
    margin: 0;
    padding: var(--space-sm);
    border-bottom: 1px solid var(--border);
  }
  .xs-sets__heading { flex: 1 1 10rem; }
  .xs-sets__count { order: 2; }
  .xs-sets__create-actions { order: 4; width: 100%; margin-left: 0; }
  .xs-sets__add { width: 100%; margin-left: 0; }
  .xs-sets__close {
    order: 3;
    width: var(--tap-min, 44px);
    height: var(--tap-min, 44px);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: auto;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: var(--surface-muted);
    color: var(--text-strong);
  }
  .xs-set { grid-template-columns: 1fr auto; gap: .375rem; padding: .625rem; border-radius: var(--radius-md); }
  .xs-set__meta { flex-direction: row; justify-content: flex-start; padding: 0 0 var(--space-xs); border-right: 0; border-bottom: 1px solid var(--border); }
  .xs-set__index strong { font-size: 1rem; }
  .xs-set__teams { grid-column: 1 / -1; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: stretch; gap: var(--space-2xs); }
  .xs-set__side { height: 100%; align-content: start; padding: .375rem; }
  .xs-set__side-name { overflow: visible; font-size: var(--fs-caption, .8125rem); font-weight: 500; line-height: 1.35; text-overflow: clip; white-space: normal; }
  .xs-set__vs { align-self: center; width: 1.75rem; height: 1.75rem; }
  .xs-set__actions { grid-column: 1 / -1; justify-content: space-between; padding: .375rem 0 0; border-top: 1px solid var(--border); }
  .xs-set__actions .btn {
    position: relative;
    width: var(--tap-min, 44px);
    min-width: var(--tap-min, 44px);
    height: var(--tap-min, 44px);
    min-height: var(--tap-min, 44px);
    padding: 0;
    font-size: .8125rem;
  }
  .xs-set__warn { grid-column: 1 / -1; }

  #modalSetDau:has([data-set-dau-form]) .modal-dialog {
    width: calc(100% - 2rem);
    height: min(40rem, calc(100dvh - 2rem));
    max-width: none;
    margin: 1rem auto;
  }
  #modalSetDau:has([data-set-dau-form]) .modal-content { width: 100%; height: 100%; max-height: 100%; }
  .set-editor__header { padding: .75rem; }
  .set-editor__icon { width: 2.25rem; height: 2.25rem; font-size: 1rem; }
  .set-editor__heading .modal-title { font-size: 1.0625rem; }
  .set-editor__eyebrow { font-size: .75rem; }
  .set-editor__subtitle { display: none; }
  .set-editor__body { gap: .625rem; padding: .75rem; touch-action: pan-y; }
  .set-editor__format-option { min-height: var(--tap-min, 44px); padding: var(--space-xs); }
  .set-editor__format-option > .bi:first-child { display: none; }
  .set-editor__tip { display: none; }
  .set-editor__court { grid-template-columns: 1fr; gap: var(--space-xs); }
  .set-editor__versus { width: 2rem; height: 2rem; justify-self: center; margin: -.75rem 0; }
  .set-editor__team { gap: .375rem; padding: .625rem; }
  .set-editor__team-head { padding-bottom: var(--space-2xs); }
  .set-editor__team-head small { display: none; }
  .set-editor__status { min-height: 2rem; padding: .375rem .625rem; }
  .set-editor__footer {
    flex-wrap: nowrap;
    padding: .625rem .75rem calc(.625rem + env(safe-area-inset-bottom, 0px));
    background: var(--surface);
  }
  .set-editor__footer .btn { flex: 1 1 0; min-width: 0; margin: 0; }
}

/* Báo cáo tài chính: tháng luôn ở đúng tâm, nút chuyển tháng ôm hai bên. */
.report-month-nav { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--space-sm); }
.report-month-nav > :first-child { justify-self: end; }
.report-month-nav > :last-child { justify-self: start; }
.report-month-nav .btn { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: .25rem; white-space: nowrap; }
.report-month-nav h2 { white-space: nowrap; }

/* =====================================================================================
   Vinh danh (spec 2026-09-14, BR-HON-05/13)
   Viền chỉ tồn tại trong tháng HIỆN TẠI; chip là danh hiệu nổi bật. Màu viền cố định cho cả hai
   theme — vòng hở 2px dùng --surface nên tách được khỏi nền ở cả sáng lẫn tối.
   🔴 Đặt TRƯỚC khối tầng mobile (MobileScaleTest quét từ mốc của khối đó tới hết file) và không chữ
   nào dưới --fs-caption (13px) — kể cả nhãn "Hạng 1" và chip emoji.
   ===================================================================================== */
.honor-avatar { position: relative; display: inline-flex; flex: none; border-radius: 50%; }
.honor-avatar--gold .avatar      { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px #E0A100; }
.honor-avatar--silver .avatar    { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px #94A3B8; }
.honor-avatar--bronze .avatar    { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px #B8733B; }
.honor-avatar--spotlight .avatar { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--action-primary); }
/* Huy chương danh hiệu (achievement/icons.html) — thay emoji từ 14/09/2026: người dùng thấy emoji
   "hoạt hình", mỗi hệ điều hành vẽ một kiểu, nhiều màu. Hai kiểu: `net` (nền nhạt, viền mảnh — lưới
   danh hiệu) và `dac` (nền đặc, biểu tượng trắng — chấm trên avatar, dòng nhỏ, ảnh card: ở cỡ 24px kiểu
   nét quá mờ, đo ở bản xem trước). Hai bậc: `moc` xanh thảm (cột mốc) · `vang` vàng cổ (danh hiệu tháng).
   Kích thước mặc định 2.5rem; từng ngữ cảnh tự đặt width/height, SVG luôn chiếm 50%. */
.honor-medal {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 2.5rem; height: 2.5rem; border-radius: 50%; line-height: 1;
}
.honor-medal svg { display: block; width: 50%; height: 50%; }
.honor-medal--net.honor-medal--moc  { background: #EAF4F0; color: #0B6650; box-shadow: inset 0 0 0 1px #BFDDD2; }
.honor-medal--net.honor-medal--vang { background: #FBF5E6; color: #8A6414; box-shadow: inset 0 0 0 1px #E2C98B; }
.honor-medal--dac.honor-medal--moc  { background: #0E7A5F; color: #fff; }
.honor-medal--dac.honor-medal--vang { background: linear-gradient(145deg, #C9A24A, #94701E); color: #fff; }
/* Bạc/đồng: chỉ dùng cho huy chương MỨC trên ảnh card tháng (mức 2, 3). */
.honor-medal--dac.honor-medal--bac  { background: linear-gradient(145deg, #C3CAD3, #7F8A98); color: #fff; }
.honor-medal--dac.honor-medal--dong { background: linear-gradient(145deg, #C98E5E, #8A5530); color: #fff; }
[data-bs-theme="dark"] .honor-medal--net.honor-medal--moc  { background: rgba(14, 122, 95, .22); color: #7FD9BC; box-shadow: inset 0 0 0 1px rgba(127, 217, 188, .35); }
[data-bs-theme="dark"] .honor-medal--net.honor-medal--vang { background: rgba(201, 162, 74, .16); color: #E2C98B; box-shadow: inset 0 0 0 1px rgba(226, 201, 139, .38); }
/* Dòng "huy chương + chữ" (danh hiệu nổi bật, nhãn Gương mặt, lịch sử). */
.honor-label { display: flex; align-items: center; gap: .4rem; min-width: 0; }
.honor-label .honor-medal { width: 1.25rem; height: 1.25rem; }
.honor-avatar__chip {
  position: absolute; right: -5px; bottom: -5px;
  width: 1.4rem; height: 1.4rem; border-radius: 50%;
  display: inline-flex; box-shadow: 0 0 0 2px var(--surface);
}
.honor-avatar__chip .honor-medal { width: 100%; height: 100%; }

.honor-month-nav { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.honor-month-nav .btn { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }

.honor-spotlight { display: flex; align-items: center; gap: var(--space-md); }
.honor-spotlight .avatar { width: 3.5rem; height: 3.5rem; font-size: 1.1rem; }
/* Hàng nút Đổi / Bỏ chọn / Chọn Gương mặt (BR-HON-10). Màn hẹp: wrap xuống dòng dưới, dạt phải;
   vùng chạm 44px (MB-3) đặt ở đây vì .btn mặc định thấp hơn trên desktop. */
.honor-spotlight { flex-wrap: wrap; }
.honor-spotlight > .min-w-0 { flex: 1 1 12rem; }
.honor-spotlight__actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-left: auto; }
.honor-spotlight__actions .btn { min-height: 44px; display: inline-flex; align-items: center; gap: .25rem; }
.honor-spotlight--empty { border-style: dashed; }

/* Hai thẻ vinh danh ở trang chủ (US-HON-04). Nút cuối thẻ là vùng chạm 44px (MB-3) — link chữ trần
   thì không đủ trên điện thoại. */
.honor-home__head { display: flex; align-items: center; gap: var(--space-md); }
.honor-home__stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-sm); margin-top: var(--space-md); }
.honor-home__cta { min-height: 44px; display: flex; align-items: center; justify-content: center; gap: .25rem; }

/* Ảnh card vinh danh (BR-HON-12, spec § 9).
   🔴 .honor-card là KHUNG XUẤT ẢNH 1080×1350 px CỐ ĐỊNH — ngoại lệ có chủ đích với luật "không hardcode
   px": đó là kích thước file PNG, không phải bố cục trang. Trên màn hình chỉ .honor-export__scale co nó
   bằng transform (JS tính --s); node xuất ảnh không mang transform nên PNG luôn đúng kích thước.
   🔴 Chỉ dùng bảng --court-* + trắng (không token bề mặt đổi theo giao diện tối): ảnh phải giống nhau dù
   người tải đang ở chế độ nào. */
.honor-export__actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-bottom: var(--space-sm); }
/* Khung xem trước ăn theo kích thước PNG do fragment bangXuatAnh đặt (--anh-rong/--anh-cao): ảnh
   tháng 1536×1136, ảnh cá nhân 1080×1350 tới Đợt D. max-width kẹp hai đầu — theo chiều cao để ảnh
   dọc không cao quá 675px trên máy tính, theo 720px để ảnh ngang không tràn .content-wide (960px). */
.honor-export { --anh-rong: 1080; --anh-cao: 1350; }
.honor-export__stage {
  position: relative; width: 100%;
  max-width: min(720px, calc(675px * var(--anh-rong) / var(--anh-cao)));
  aspect-ratio: var(--anh-rong) / var(--anh-cao); overflow: hidden;
  border-radius: var(--radius-lg); box-shadow: 0 8px 24px rgba(6, 40, 31, .12);
}
.honor-export__scale {
  width: calc(var(--anh-rong) * 1px); height: calc(var(--anh-cao) * 1px);
  transform: scale(var(--s, .3)); transform-origin: top left;
}
/* Modal "Lưu ảnh vào máy" trên màn cảm ứng (người dùng 15/09/2026): PNG hiện ra để NHẤN GIỮ lưu vào Ảnh.
   🔴 touch-callout/user-select ghi TƯỜNG MINH: -webkit-touch-callout kế thừa, một tổ tiên đặt none (như
   .xs-chip ở màn xếp sân) là iPhone mất luôn menu "Lưu vào Ảnh". max-height theo vh để cả ảnh + câu
   hướng dẫn + nút Đóng nằm gọn trong một màn 390×844, không phải cuộn. */
.honor-luu-anh .modal-body { text-align: center; }
/* Máy tính bảng cảm ứng (768px) đứng NGOÀI tầng mobile nên nút Đóng rơi về 38px của Bootstrap — đo
   15/09/2026. Modal này chỉ mở trên màn cảm ứng, nên sàn 44px đặt ở luật gốc cho mọi bề ngang. */
.honor-luu-anh .modal-footer > .btn { min-height: var(--tap-min, 44px); }
.honor-luu-anh__anh {
  display: block; margin-inline: auto; max-width: 100%; max-height: 60vh; height: auto;
  border-radius: var(--radius-md); box-shadow: 0 4px 16px rgba(6, 40, 31, .16);
  -webkit-touch-callout: default; -webkit-user-select: auto; user-select: auto;
}
/* 🔴 Ngân sách chiều cao: khung 1350px, overflow hidden ⇒ thứ gì tràn là BỊ CẮT NGAY TRONG PNG, không
   báo lỗi. Ca xấu nhất card tháng = Gương mặt + lời khen 2 dòng + 7 dòng + "+N người nữa" (8 dòng trước khi
   thêm câu phụ + tiêu đề danh sách; bản 7 dòng + avatar dòng 56px dư 46px, đo 14/09/2026); card cá nhân =
   6 danh hiệu + tên dài. Bộ cỡ trước (padding 80/64, gap 36, avatar dòng 88px) tràn 80px CHỈ với 6 dòng
   — chân card bị cắt nửa — và 371px ở ca xấu nhất (đo bằng Playwright ở 7.1, 14/09/2026; 6.5 chỉ thử
   tháng trống). Bộ hiện tại vừa khít cả hai ca xấu nhất. Tăng một cỡ chữ/khoảng cách nào ở đây ⇒ dựng
   lại ca xấu nhất trong trình duyệt và đo `scrollHeight` của #the-vinh-danh ≤ 1350 (mẹo: tạm bỏ margin-top:auto
   của .honor-card__chan để thấy chỗ dư thật — sau khi thay emoji bằng huy chương, dòng nhãn và "+N" siết
   line-height 1.2 cho ca xấu nhất còn dư 19px thay vì 1px, đo 14/09/2026) — test Java không có
   layout engine nên KHÔNG bắt được lỗi này. */
.honor-card {
  box-sizing: border-box; width: 1080px; height: 1350px; padding: 64px 72px 56px;
  display: flex; flex-direction: column; gap: 20px; overflow: hidden;
  background: linear-gradient(165deg, var(--court-50) 0%, #fff 55%); color: var(--court-950);
  font-family: 'Be Vietnam Pro', system-ui, sans-serif;
  /* 🔴 PNG phải giống nhau ở cả hai giao diện: avatar chữ cái (.avatar--cN) và chấm danh hiệu lấy
     màu từ token ĐỔI THEO THEME ⇒ ở dark mode avatar thành tròn navy chữ sáng ngay trong ảnh tải về
     (đo ở 7.1, 14/09/2026). Ghim về giá trị theme sáng: nền = giá trị dự phòng của .avatar--cN,
     chữ = :root sáng. TheVinhDanhScriptTest.khungCardGhimMauAvatarSang gác. */
  --surface: #fff; --border: var(--slate-200);
  --badge-blue-bg: rgba(37,99,235,.14);   --badge-blue-fg: #1E40AF;
  --badge-green-bg: rgba(22,163,74,.14);  --badge-green-fg: #0F6B30;
  --badge-amber-bg: rgba(249,115,22,.16); --badge-amber-fg: #7C380B;
  --badge-violet-bg: rgba(124,58,237,.14);
  --badge-red-bg: rgba(220,38,38,.12);    --badge-red-fg: #8A1919;
  --badge-slate-bg: rgba(100,116,139,.16); --badge-slate-fg: #475569;
}
.honor-card .avatar--c3 { color: var(--violet-500); }
.honor-card__clb { display: flex; align-items: center; gap: 20px; min-width: 0; font-size: 36px; font-weight: 700; color: var(--court-700); }
.honor-card__ten-clb, .honor-card__ten, .honor-card__gm-ten, .honor-card__nguoi-ten { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.honor-card__logo { flex: none; width: 96px; height: 96px; border-radius: 24px; object-fit: cover; }
.honor-card__mark { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 96px; height: 96px; border-radius: 24px; background: var(--court-500); color: #fff; }
/* color: #fff bắt buộc: luật chung .brand-svg tô xanh lên chính SVG ⇒ xanh trên nền xanh, dấu biến mất
   cả trên màn hình lẫn trong PNG (lộ ở 6.5, 14/09/2026 — TheVinhDanhScriptTest gác). */
.honor-card__mark .brand-svg { width: 60px; height: 60px; color: #fff; }
.honor-card__mark--nho { width: 48px; height: 48px; border-radius: 12px; }
.honor-card__mark--nho .brand-svg { width: 30px; height: 30px; }
.honor-card__tieu-de { margin: 0; font-size: 68px; line-height: 1.1; font-weight: 800; color: var(--court-900); }
/* Câu chữ tự giải thích cho người xem nhóm Zalo chưa từng mở app (người dùng 14/09/2026). Câu phụ ghép
   với tiêu đề trong .honor-card__dau (gap 8px, không phải gap 20px của khung) để giữ ngân sách chiều cao. */
.honor-card__dau { display: flex; flex-direction: column; gap: 8px; }
.honor-card__phu { margin: 0; font-size: 30px; line-height: 1.25; color: var(--court-700); }
.honor-card__gm-nguon { color: var(--court-600); white-space: nowrap; }
.honor-card__bang-dau {
  display: flex; justify-content: space-between; gap: 16px; padding-bottom: 10px; border-bottom: 3px solid var(--court-100);
  font-size: 24px; line-height: 1.2; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--court-700);
}
.honor-card__guong-mat { display: flex; align-items: center; gap: 28px; padding: 24px 28px; border-radius: 28px; background: #fff; border: 4px solid var(--court-200); }
.honor-card__gm-chu { min-width: 0; }
.honor-card__nhan { display: flex; align-items: center; gap: 10px; font-size: 28px; line-height: 1.2; color: var(--court-700); }
.honor-card__nhan .honor-medal { width: 34px; height: 34px; }
.honor-card__gm-ten { font-size: 46px; font-weight: 800; }
.honor-card__loi-khen {
  margin: 4px 0 0; font-size: 30px; line-height: 1.35; color: var(--court-800);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.honor-card__bang { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.honor-card__dong { display: flex; align-items: center; gap: 22px; font-size: 36px; }
/* Huy chương MỨC thay cột số hạng (người dùng 14/09/2026: "1, 1, 5" vô nghĩa với người xem ảnh). Ô rộng
   cố định để tên thẳng cột kể cả dòng không có huy chương (mức 4 trở đi). */
.honor-card__muc { flex: none; width: 48px; display: flex; align-items: center; justify-content: center; }
.honor-card__muc .honor-medal { width: 44px; height: 44px; }
.honor-card__ten { flex: 1 1 auto; font-weight: 600; }
.honor-card__so { flex: none; font-weight: 600; color: var(--court-700); }
.honor-card__them, .honor-card__trong { margin: 0; font-size: 32px; line-height: 1.2; color: var(--court-700); }
.honor-card__chan { margin-top: auto; display: flex; align-items: center; gap: 16px; font-size: 28px; color: var(--court-700); }
/* Cùng hàng chân ⇒ không tốn thêm chiều cao (ngân sách 1350px ở trên). */
.honor-card__ten-mien { margin-left: auto; font-weight: 700; color: var(--court-800); white-space: nowrap; }
.honor-card .avatar { width: 64px; height: 64px; font-size: 26px; }
/* Dòng bảng ảnh tháng 56px (không 64): nhường chỗ cho câu phụ + tiêu đề danh sách (14/09/2026). */
.honor-card__dong .avatar { width: 56px; height: 56px; font-size: 22px; }
.honor-card .honor-avatar__chip { width: 32px; height: 32px; box-shadow: 0 0 0 3px #fff; }
.honor-card .honor-avatar--gold .avatar      { box-shadow: 0 0 0 4px #fff, 0 0 0 8px #E0A100; }
.honor-card .honor-avatar--silver .avatar    { box-shadow: 0 0 0 4px #fff, 0 0 0 8px #94A3B8; }
.honor-card .honor-avatar--bronze .avatar    { box-shadow: 0 0 0 4px #fff, 0 0 0 8px #B8733B; }
.honor-card .honor-avatar--spotlight .avatar { box-shadow: 0 0 0 4px #fff, 0 0 0 8px var(--court-500); }
.honor-card__guong-mat .avatar { width: 128px; height: 128px; font-size: 46px; }
.honor-card__nguoi { display: flex; flex-direction: column; align-items: center; gap: 20px; min-width: 0; text-align: center; }
.honor-card__nguoi .avatar { width: 280px; height: 280px; font-size: 96px; }
.honor-card__nguoi .honor-avatar__chip { width: 72px; height: 72px; right: 4px; bottom: 4px; box-shadow: 0 0 0 5px #fff; }
.honor-card__nguoi .honor-avatar--gold .avatar      { box-shadow: 0 0 0 5px #fff, 0 0 0 10px #E0A100; }
.honor-card__nguoi .honor-avatar--silver .avatar    { box-shadow: 0 0 0 5px #fff, 0 0 0 10px #94A3B8; }
.honor-card__nguoi .honor-avatar--bronze .avatar    { box-shadow: 0 0 0 5px #fff, 0 0 0 10px #B8733B; }
.honor-card__nguoi .honor-avatar--spotlight .avatar { box-shadow: 0 0 0 5px #fff, 0 0 0 10px var(--court-500); }
.honor-card__nguoi-ten { max-width: 100%; font-size: 68px; font-weight: 800; }
.honor-card__noi-bat { display: flex; align-items: center; justify-content: center; gap: 14px; font-size: 40px; color: var(--court-700); }
.honor-card__noi-bat .honor-medal { width: 48px; height: 48px; }
.honor-card__thong-ke { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
.honor-card__o { padding: 24px; border-radius: 28px; background: #fff; border: 4px solid var(--court-100); text-align: center; }
.honor-card__o-so { font-size: 52px; font-weight: 800; color: var(--court-800); }
.honor-card__o-nhan { font-size: 28px; color: var(--court-700); }
.honor-card__danh-hieu { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; font-size: 30px; font-weight: 600; }
.honor-card__danh-hieu li { display: flex; align-items: center; gap: 14px; min-width: 0; padding: 18px 20px; border-radius: 24px; background: var(--court-50); }
.honor-card__danh-hieu .honor-medal { width: 40px; height: 40px; }
.honor-card__dh-ten { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.honor-podium { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-sm); }
@media (min-width: 768px) {
  .honor-podium { grid-template-columns: repeat(auto-fit, minmax(9rem, 11rem)); }
}
.honor-podium__card {
  display: flex; flex-direction: column; align-items: center; gap: .375rem; min-width: 0;
  padding: var(--space-md) var(--space-sm); text-align: center;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  color: inherit; text-decoration: none;
  transition: background-color var(--motion-fast) var(--ease-standard);
}
.honor-podium__card:hover { background: var(--surface-sunken); color: inherit; }
.honor-podium__card .avatar { width: 3.5rem; height: 3.5rem; font-size: 1.1rem; }
.honor-podium__rank { font-size: var(--fs-caption, .8125rem); font-weight: 700; color: var(--text-muted); }
.honor-podium__name { max-width: 100%; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.honor-podium__value { font-size: .85rem; color: var(--text-muted); }

.honor-list { list-style: none; margin: 0; padding: 0; }
.honor-row {
  display: flex; align-items: center; gap: .75rem; min-height: 3.25rem; padding: .5rem .25rem;
  border-bottom: 1px solid var(--border); color: inherit; text-decoration: none;
}
.honor-list > li:last-child .honor-row { border-bottom: 0; }
.honor-row:hover { background: var(--surface-sunken); color: inherit; }
.honor-row__rank { flex: none; width: 1.75rem; text-align: center; font-weight: 700; color: var(--text-muted); }
/* Tên xuống tối đa 2 dòng rồi mới "…": Gắn bó lâu nhất hiện đủ ngày (14/09/2026) làm cột giá trị rộng
   thêm ~25px ⇒ bản một dòng cắt 5/10 tên ở 768px, 8/10 ở 360px. Ở 1440px tên vẫn vừa một dòng. */
.honor-row__name {
  flex: 1 1 auto; min-width: 0; font-weight: 500; line-height: 1.3; overflow: hidden;
  white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}
.honor-row__value { flex: none; font-size: .9rem; color: var(--text-muted); }
.honor-row.is-me, .honor-podium__card.is-me { background: var(--tb-unread-bg); }
.honor-more { padding: .5rem .25rem; font-size: .9rem; color: var(--text-muted); }
.honor-me { padding: .75rem 1rem; border-radius: var(--radius-lg); background: var(--tb-unread-bg); font-weight: 600; }
.honor-empty__icon { font-size: 2rem; line-height: 1; }

.honor-hero { display: flex; align-items: center; gap: var(--space-md); }
.honor-hero .avatar { width: 4.5rem; height: 4.5rem; font-size: 1.5rem; }
.honor-hero .honor-avatar__chip { width: 1.75rem; height: 1.75rem; }
.honor-hero__name { font-size: 1.25rem; font-weight: 700; }
.honor-hero__rank, .honor-title__dat { color: var(--money-positive); }
.honor-stat__value { font-size: 1.25rem; font-weight: 700; }
.honor-title.is-locked .honor-medal { filter: grayscale(1); opacity: .55; }
.honor-progress { height: 6px; }

/* Ảnh vinh danh tháng khổ NGANG (spec 2026-09-15 § 4, BR-HON-12).
   🔴 Cùng ngoại lệ có chủ đích với .honor-card: 1536×1136 px là kích thước FILE PNG, không phải bố
   cục trang — .honor-export__scale co nó trên màn hình bằng transform.
   🔴 Ngân sách chiều cao: overflow hidden ⇒ thứ gì tràn là BỊ CẮT NGAY TRONG PNG, không báo lỗi.
   Ca xấu nhất (spec § 4.4): 5 dòng tên dài + "+N người nữa" · đủ 5 ô · lời khen 140 ký tự · 4 avatar
   tên dài · tên CLB 255 ký tự. Test Java KHÔNG bắt được — đổi cỡ chữ/khoảng cách nào ở đây thì phải
   dựng lại ca xấu nhất trong trình duyệt và đo scrollHeight của #the-vinh-danh ≤ 1136 và scrollWidth
   ≤ 1536 (Task 7 của plan Đợt C).
   🔴 Cao 1136 chứ không 1024 nữa (17/09/2026, người dùng: ảnh bìa "ở ảnh vinh danh cũng hiển thị độ
   rộng nhiều hơn"). Banner dày thêm đúng 112px (400→512, và 260→372 ở card cá nhân), và 112px đó
   lấy từ chiều cao TỔNG chứ không lấy của ba cột nội dung — ngân sách 624px của chúng đã từng tràn
   một lần (16/09) và ở card cá nhân chỉ còn dư 43px. Cắt vào đó để đổi lấy một dải ảnh dày hơn là
   đánh đổi sai: ảnh đẹp hơn một chút, đổi lấy nguy cơ mất chữ trong PNG mà không ai được báo.
   🔴 Màu ghim cố định, không token đổi theo giao diện tối: PNG phải giống nhau dù người tải đang ở
   chế độ nào (bài học 7.1, 14/09/2026).
   🔴 Một dấu ĐÓNG chú thích lạc vào giữa khối này (thêm nhầm 17/09/2026 cùng lúc với đoạn "Cao 1136")
   đã GIẾT nguyên luật `.honor-anh` suốt một ngày: phần chữ còn lại bị trình duyệt đọc thành bộ chọn,
   nuốt luôn cặp ngoặc của luật ⇒ mất `width/height/overflow/display:flex/background/font-family` và
   **cả 10 biến `--badge-*` + `--surface`/`--border` khai tại đây** — đúng những token mà dòng ngay
   trên nói là phải ghim cứng để PNG không đổi theo giao diện tối. Ảnh vẫn ra hình vì
   `.honor-export__scale` áp đúng 1536×1136 cho thẻ bọc. Sửa lại 17/09/2026 (phát hiện khi đo logo
   trên harness: khung rộng 1585px thay vì 1536px). */
.honor-anh {
  box-sizing: border-box; width: 1536px; height: 1136px; overflow: hidden;
  display: flex; flex-direction: column; background: #F3F7F5; color: var(--court-950);
  font-family: 'Be Vietnam Pro', system-ui, sans-serif;
  --surface: #fff; --border: var(--slate-200);
  --badge-blue-bg: rgba(37,99,235,.14);   --badge-blue-fg: #1E40AF;
  --badge-green-bg: rgba(22,163,74,.14);  --badge-green-fg: #0F6B30;
  --badge-amber-bg: rgba(249,115,22,.16); --badge-amber-fg: #7C380B;
  --badge-violet-bg: rgba(124,58,237,.14);
  --badge-red-bg: rgba(220,38,38,.12);    --badge-red-fg: #8A1919;
  --badge-slate-bg: rgba(100,116,139,.16); --badge-slate-fg: #475569;
}
.honor-anh .avatar--c3 { color: var(--violet-500); }

/* --- banner --- */
.honor-anh__banner {
  position: relative; flex: none; height: 512px;
  background: linear-gradient(135deg, #0B5E4A 0%, #0E7A5F 100%);
}
.honor-anh__banner-anh { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Lớp phủ là gradient THƯỜNG — không backdrop-filter/blur (WebKit vẽ sai trong foreignObject). */
.honor-anh__phu {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(6,58,45,.55) 0%, rgba(6,58,45,.85) 100%);
}
.honor-anh__banner-chu {
  position: relative; height: 100%; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px; padding: 32px 96px 128px;
  /* 🔴 Đệm dưới 88 → 128px (17/09/2026, cùng lượt "avatar to lên chút nữa"): khối chữ được căn
     GIỮA phần còn lại sau padding, nên đệm dưới chính là cần gạt kéo nó lên. Trước khi nới, ảnh
     tháng hở 116,4px ở đỉnh nhưng chỉ 44,4px trên khối logo+tên — lệch gần 3 lần, và chính 44px
     đó là thứ chặn không cho logo to thêm. Nới 40px chia lại cho cân: hai khoảng hở nay xấp xỉ
     nhau ở ảnh tháng (ca xấu nhất: có đủ câu phụ). */
  text-align: center; color: #fff;
}
.honor-anh__vong { display: flex; align-items: center; gap: 24px; }
.honor-la { display: inline-flex; width: 56px; height: 112px; color: #F2C14E; opacity: .9; }
.honor-la svg { display: block; width: 100%; height: 100%; }
.honor-la--phai { transform: scaleX(-1); }
/* 🔴 Màu ĐẶC + đổ bóng, KHÔNG background-clip: text — WebKit vẽ mất chữ trong html-to-image. */
.honor-anh__tieu-de {
  margin: 0; font-size: 76px; line-height: 1.05; font-weight: 800; letter-spacing: .06em;
  text-transform: uppercase; color: #F2C14E; text-shadow: 0 4px 14px rgba(3, 30, 23, .5);
}
.honor-anh__phu-de { margin: 0; font-size: 40px; font-weight: 700; letter-spacing: .02em; }
.honor-anh__cau-phu { margin: 0; font-size: 24px; line-height: 1.3; color: rgba(255,255,255,.92); }
/* 🔴 Phần tràn -32px. Hai lần chỉnh trong ngày 16-17/09/2026, chốt bằng MẮT người dùng trên bản
   dựng 1:1 bốn mức (-44 / -32 / -26 / -18) — đừng suy lại con số này bằng tỉ lệ.
   Cơ chế: tên căn giữa theo logo nên logo nhô bao nhiêu thì tên tụt bấy nhiêu, mà mép dưới banner
   (y=400) là một đường ngang CỨNG cắt qua đúng vùng đó.
     -44px (bản gốc): baseline y=394,5 ⇒ mực dấu nặng dưới "ệ" (thòng 10px) + viền trắng (2px) rơi
       tới y=406,5, tức ĐÁY CHỮ NẰM NGOÀI ảnh ⇒ mất nền tương phản, trông như bị xén.
     -18px (lần vá đầu): baseline y=368,5, cách mép 31,5px ⇒ chữ thoát mép nhưng trôi lên cao và
       mép banner đọc thành một ĐƯỜNG KẺ dưới chân chữ (người dùng: "để cao quá").
     -32px (chốt): baseline y=382,5, cách mép 17,5px, viền trắng kết ở y=394,5 ⇒ **5,5px thở**.
       Thấp nhất trong bốn mức mà chân chữ vẫn thoát hẳn khỏi mép.
   🔴 KHÔNG suy con số này từ `--cover-de` của `.share-cover` hay ngược lại: hai banner khác tỉ lệ
   logo/banner (nay 160/512 và 144/372, so với 72/319 của trang chia sẻ) nên cùng một tỉ lệ cho ra
   hai cảm giác khác nhau. */
.honor-anh__clb {
  position: absolute; left: 64px; bottom: -32px; z-index: 1;
  display: flex; align-items: center; gap: 24px; max-width: calc(100% - 128px);
}
/* 🔴 192px chứ không 128px (17/09/2026, HAI nhịp: 128→160 rồi 160→192px — người dùng *"avatar CLB
   ở các ảnh vinh danh cũng cho to lên nhé"*, rồi *"to lên chút nữa, vừa rồi tôi thấy vẫn bé"*).
   Con số cũ chốt hồi banner còn cao 400px (32%); khi ảnh đổi sang khổ ngang, banner lên 512px mà
   logo đứng yên ⇒ tụt còn **25%**. 192/512 = **37,5%**.
   🔴 Logo to ra KHÔNG lấn xuống nội dung: khối `.honor-anh__clb` neo `bottom: -32px` và căn giữa
   theo logo nên nó chỉ mọc LÊN — mép dưới logo vẫn ở +32px (chỗ mà `padding-top` của cột nội dung
   card cá nhân tính theo), phần tràn `-32px` mà `BannerTenClbKhoangThoTest` gác không đổi, và
   khoảng thở dưới tên CLB còn TĂNG (2 → 34px, đo trên harness 1:1).
   Thứ bị ăn là khoảng hở PHÍA TRÊN, giữa khối chữ giữa banner và khối CLB. Ở 192px nó chỉ còn dương
   được nhờ đệm dưới của `.honor-anh__banner-chu` nới 88→128px cùng lúc (xem chú thích ở đó): ca xấu
   nhất (ảnh tháng đủ `cauPhu` 3 dòng) hở **32,4px**, đỉnh banner hở 96,4px — hai đầu nay xấp xỉ
   nhau. 🔴 Trần đã đo: 208px làm hở trên ÂM, chữ đè lên khối CLB. */
.honor-anh__logo {
  flex: none; width: 192px; height: 192px; border-radius: 50%; object-fit: cover;
  border: 8px solid #F3F7F5; background: #fff;
}
.honor-anh__logo--mark {
  display: inline-flex; align-items: center; justify-content: center; background: var(--court-500);
}
/* 102px = 68 × (192/128): icon con cầu giữ đúng tỉ lệ cũ trong vòng tròn, không tự nhiên bé đi. */
.honor-anh__logo--mark .brand-svg { width: 102px; height: 102px; color: #fff; }
/* 🔴 Chữ TRẮNG, không phải `--court-900` + viền trắng (17/09/2026, người dùng gửi ảnh: tên CLB
   trông rỗng và nhoè). Đo màu mới thấy gốc: nền ở đáy banner là lớp phủ `rgba(6,58,45,.85)` ≈
   `rgb(6,58,45)`, còn `--court-900` là `#093A2F` = `rgb(9,58,47)` — **gần như CÙNG MỘT MÀU**, tương
   phản **1,0:1**. Nét chữ chìm hẳn, chỉ còn cái viền vẽ ra hình chữ.
   🔴 Cái viền trắng 8 hướng (thêm 16/09) chính là miếng vá cho đúng triệu chứng này, và nó che lỗi
   suốt một ngày. Lỗi chỉ lộ hẳn khi tên được kéo VÀO TRONG banner tối (`bottom: -32px`): trước đó
   nửa dưới chữ còn nằm trên nền sáng `#F3F7F5` nên vẫn đọc được một nửa.
   Trắng cho **12,7:1** trên ảnh bìa tối và **8,2:1** trên ảnh bìa sáng (lớp phủ .85 kéo mọi ảnh về
   nền tối), nên bỏ được viền cứng; bóng mềm chỉ để tách chữ khỏi chi tiết rối của ảnh.
   `TheVinhDanhScriptTest.tenClbDoiLapVoiLopPhu` gác. */
/* 🔴 Cùng thay đổi với `.share-cover__ten` (17/09/2026): tên neo theo MÉP DƯỚI banner thay vì căn
   giữa theo logo. Khối CLB có đáy ở `banner + 32px`, nên `margin-bottom: 40px` đặt đáy ô tên **8px
   TRÊN** mép ảnh. Đừng hạ xuống dưới 40px: 32px là tên chạm đúng mép (bài học -44px, đáy chữ nằm
   ngoài ảnh ⇒ mất nền tương phản và trông như bị xén). */
/* 🔴 60px, không còn 40px (18/09/2026, người dùng: "tên CLB to hơn để cân đối với ảnh bìa và
   avatar" — 40px cạnh logo 192px trên banner 512px trông lọt thỏm). Chữ to ra chỉ mọc LÊN vì tên neo
   theo mép dưới (`margin-bottom: 40px` giữ nguyên ⇒ khoảng thở 8px không đổi). */
.honor-anh__ten-clb {
  min-width: 0; font-size: 60px; font-weight: 800; color: #fff;
  align-self: flex-end; margin-bottom: 40px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;  /* E4: tên CLB 255 ký tự */
  text-shadow: 0 2px 10px rgba(3, 30, 23, .6);
}

/* --- nội dung: bảng trái + lưới ô --- */
/* 🔴 5fr/8fr (18/09/2026) thay ba cột 3fr/2fr/2fr: ô không còn xếp theo cột mà vào MỘT lưới bên
   phải (.honor-anh__o-luoi), nên vùng phải cần đủ rộng cho 3 ô ~272px ở ca đông nhất. Bảng trái
   còn ≈531px — đủ cho tên 28px + dòng phụ "Chuỗi 12 buổi liên tiếp" nhờ nhịp 14px ở dưới. */
.honor-anh__noi-dung {
  flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: 5fr 8fr;
  gap: 28px; padding: 40px 64px 0;
  /* 🔴 minmax(0, 1fr), KHÔNG để hàng auto: hàng auto cao theo nội dung nên hai cột tràn xuống ĐÈ
     chân ảnh mà khung chỉ báo thừa 3px (đo 16/09/2026: cột cần 551px trong 452px). Ghim hàng về
     đúng chỗ còn lại ⇒ sai sót tương lai bị cắt gọn TRONG khối, không bò ra chân ảnh. */
  grid-template-rows: minmax(0, 1fr);
}
/* overflow: hidden là lưới an toàn — ô không co được (flex-basis auto) nên nếu tổng vượt cột thì
   phải bị cắt TRONG cột, không được bò xuống đè chân ảnh như lỗi 16/09/2026. */
.honor-anh__cot { display: flex; flex-direction: column; gap: 16px; min-height: 0; overflow: hidden; }
/* 🔴 Ô cao theo NỘI DUNG, không giãn chia đều chiều cao cột (người dùng 16/09/2026 — sửa lại spec
   § 4.2). Giãn chỉ đẹp khi cột có 2-3 ô; mà CLB mới lập thì không ai đạt ngưỡng nào (Cán mốc cần 10
   buổi, Bền bỉ cần chuỗi 4 tuần…) nên cột thường chỉ có MỘT ô, và nó phình ra thành mảng màu 520px
   chứa ~140px chữ. Phần dư để lộ nền #F3F7F5. */
.honor-anh__cot > * { flex: 0 0 auto; min-height: 0; display: flex; }
.honor-anh__cot > * > .honor-anh__o { flex: 1 1 auto; }

/* --- cột trái: Chăm đi tập nhất --- */
.honor-anh__bang {
  display: flex; flex-direction: column; gap: 12px; min-width: 0; min-height: 0;
  padding: 20px 28px; border-radius: 28px; background: #fff; border: 4px solid var(--court-100);
}
.honor-anh__bang-dau {
  display: flex; justify-content: space-between; gap: 16px; padding: 8px 16px;
  border-radius: 16px; background: var(--court-500); color: #fff;
  font-size: 22px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}
.honor-anh__ds {
  list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px;
  min-height: 0;
}
.honor-anh__dong { display: flex; align-items: center; gap: 14px; min-width: 0; }
.honor-anh__muc { flex: none; width: 44px; display: flex; align-items: center; justify-content: center; }
.honor-anh__muc .honor-medal { width: 42px; height: 42px; }
.honor-anh .honor-anh__dong .avatar { width: 56px; height: 56px; font-size: 24px; }
.honor-anh__dong-chu { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.honor-anh__ten {
  font-size: 28px; line-height: 1.2; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.honor-anh__dong-phu {
  font-size: 20px; line-height: 1.2; color: var(--court-700);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.honor-anh__so { flex: none; font-size: 30px; font-weight: 800; color: var(--court-700); }
.honor-anh__them { margin: 0; font-size: 22px; line-height: 1.2; color: var(--court-700); }
.honor-anh__trong {
  flex: 1 1 auto; margin: 0; display: flex; align-items: center; justify-content: center;
  padding: 0 96px; font-size: 32px; text-align: center; color: var(--court-800);
}

/* --- lưới ô màu --- */
/* 🔴 MỘT lưới ô đều nhau thay hai cột ô cố định (18/09/2026, người dùng gửi ảnh): bố cục cũ dồn ô
   theo cột nên cột giữa hở nửa dưới trong khi cột phải tràn — ô Người kết nối bị cắt chân, ô Bền bỉ
   ép ba avatar và tên vào 386px nên tên thành "ÂN THÁO…". Nay số cột/hàng do VinhDanhTrang.boCucO
   chọn theo số ô, ô "Tháng M của CLB" lấp khi số ô lẻ, nên lưới KHÔNG BAO GIỜ có khe:
     mot: 1 ô (chỉ ô tổng kết) · hai: 1 cột × 2 hàng · bon: 2 × 2 · sau: 3 × 2.
   Hàng ghim minmax(0, 1fr) — mọi ô cùng cao, và nếu nội dung lỡ vượt thì bị cắt TRONG ô chứ không
   bò xuống chân ảnh (bài học 16/09/2026). Ô hẹp nhất (lưới sau) ≈ 272×250px: mọi khối bên trong
   được đo cho kích thước này. */
.honor-anh__o-luoi {
  display: grid; gap: 16px; min-width: 0; min-height: 0;
  grid-template-columns: minmax(0, 1fr); grid-template-rows: repeat(2, minmax(0, 1fr));
}
.honor-anh__o-luoi--mot { grid-template-rows: minmax(0, 1fr); align-content: start; }
.honor-anh__o-luoi--bon { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.honor-anh__o-luoi--sau { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.honor-anh__o-khe { display: flex; min-width: 0; min-height: 0; }
.honor-anh__o-khe > .honor-anh__o { flex: 1 1 auto; }
/* Ô tổng kết một mình không giãn cao hết vùng — một mảng màu 500px chứa hai dòng chữ là thứ người
   dùng đã chê ngày 16/09/2026. */
.honor-anh__o-luoi--mot .honor-anh__o-khe { align-self: start; }

.honor-anh__o {
  display: flex; flex-direction: column; justify-content: center; gap: 8px;
  min-width: 0; min-height: 0; overflow: hidden;
  padding: 16px 20px; border-radius: 28px; color: #fff; background: var(--court-700);
}
/* 🔴 Mỗi ô một tông (người dùng 16/09/2026 — cùng lần đổi màu lưới danh hiệu ở ảnh cá nhân): trước
   đó 5/6 ô cùng họ xanh teal (#0E7A5F · #14866B · #0B4F3F · #1E6B5A) nên ảnh tháng nhìn đơn sắc.
   Cùng HỌ màu với .honor-anh__luoi--* của ảnh cá nhân để hai ảnh là một hệ, nhưng ô ở đây là nền
   ĐẬM chữ trắng (ảnh cá nhân là nền nhạt chữ đậm) nên phải lấy nấc đậm hơn — kiểm bằng WCAG với
   #fff và với .honor-anh__o-mo-ta (trắng .88), ngưỡng 4.5 cho cả hai:
     guong-mat #9A6B12 (4.68 / 4.02 — giữ nguyên: mọi nấc hổ phách tươi hơn đều rớt dưới 4.5)
     ben-bi    #B34A38 (5.31 / 4.48)   tan-binh #1E6FA8 (5.39 / 4.58)
     can-moc   #0E7A5F (5.29 / 4.48 — xanh thảm thương hiệu, giữ) tong-ket #6B4E9E (6.54 / 5.51)
     ket-noi   #B34459 (5.40 / 4.57) — KHÔNG dùng #C25A6E như ảnh cá nhân: trên nền đậm nó chỉ được
               4.22, chữ trắng bắt đầu nhoè. Lệch một nấc là cố ý, đừng "sửa cho khớp".
     lao-lang  #4F6B2A (≈6.1 / ≈5.1) — ô mới 18/09/2026. KHÔNG lấy tím #6B4E9E của Lão làng ở ảnh cá
               nhân: tím đã là ô tổng kết, hai ô tím cạnh nhau đọc thành một. Xanh rêu giữ được
               nghĩa "lâu năm" mà không trùng tông ô nào. */
.honor-anh__o--guong-mat { background: #9A6B12; }
.honor-anh__o--ben-bi    { background: #B34A38; }
.honor-anh__o--tan-binh  { background: #1E6FA8; }
.honor-anh__o--can-moc   { background: #0E7A5F; }
.honor-anh__o--lao-lang  { background: #4F6B2A; }
.honor-anh__o--ket-noi   { background: #B34459; }
.honor-anh__o--tong-ket  { background: #6B4E9E; }
/* 🔴 Huy chương phải đổi theo ô, y như ở .honor-anh__luoi (ảnh cá nhân) nhưng ĐẢO CHIỀU: ô nền đậm
   ⇒ huy chương nền trắng đặc, icon lấy đúng hex của ô. Để nguyên luật dùng chung thì mọi ô mang
   huy chương nền xanh #0E7A5F (--dac.--moc) hoặc gradient vàng (--dac.--vang) — xanh nằm trên ô
   đỏ/tím, vàng chìm hẳn vào ô hổ phách. Nền trắng đặc chứ không trắng mờ: mờ .22 chỉ cho icon
   tương phản ~3.1 với nền huy chương, trắng đặc đưa nó lên đúng tỉ số của ô (4.68–6.54). */
.honor-anh__o .honor-medal { background: #fff; }
.honor-anh__o--guong-mat .honor-medal { color: #9A6B12; }
.honor-anh__o--ben-bi    .honor-medal { color: #B34A38; }
.honor-anh__o--tan-binh  .honor-medal { color: #1E6FA8; }
.honor-anh__o--can-moc   .honor-medal { color: #0E7A5F; }
.honor-anh__o--lao-lang  .honor-medal { color: #4F6B2A; }
.honor-anh__o--ket-noi   .honor-medal { color: #B34459; }
/* tong-ket KHÔNG có luật icon vì nó không có huy chương: VinhDanhTrang.oTongKet truyền
   huyChuong = null (ô là hai con số của cả CLB, không gắn với ai) và fragment anhO bọc huy chương
   trong th:if. Thêm luật ở đây là CSS chết. */
.honor-anh__o-dau {
  flex: none; display: flex; align-items: center; gap: 10px; min-width: 0;
  font-size: 17px; line-height: 1.2; font-weight: 700; text-transform: uppercase; letter-spacing: .01em;
}
.honor-anh__o-dau .honor-medal { flex: none; width: 28px; height: 28px; }
/* Tiêu đề là flex ITEM mang ellipsis (bẫy ellipsis-trên-flex ở templates/CLAUDE.md). */
/* Tiêu đề XUỐNG DÒNG (tối đa 2) thay vì cắt "…": đo 18/09/2026 ở ô 272px, "GƯƠNG MẶT THÁNG" và
   "THÁNG 9/2026 CỦA CLB" đều bị cắt còn "GƯƠNG MẶT THÁ…" — tên danh hiệu cụt là mất nghĩa. */
/* 🔴 `padding-block`/`margin-block` KHÔNG phải để lấy khoảng cách — chúng nới đúng chỗ CẮT của
   `overflow: hidden` (overflow xén ở mép padding box), rồi margin âm trả lại nguyên vị trí cũ nên
   bố cục không đổi một px nào. Thiếu chúng: chữ hoa tiếng Việt có MŨ + DẤU (Ố Ế Ề Ấ) cao hơn hộp
   dòng `line-height: 1.2` của .honor-anh__o-dau đúng 3px ở cỡ 17px, và 3px đó nằm ngay trên đỉnh
   dấu ⇒ "CÁN MỐC MỚI" in ra thành "CÁN MÔC MƠI", "NGƯỜI KẾT NỐI" thành "NGƯỜI KÊT NÔI" — người
   dùng báo 20/09/2026, đo lại bằng ink box của canvas. Chữ thường không dính (dấu nằm thấp hơn),
   nên lỗi chỉ hiện ở HÀNG TIÊU ĐỀ và chỉ với vài danh hiệu — trông như lỗi dữ liệu chứ không như
   lỗi CSS. Nới `line-height` cũng chữa được nhưng ăn vào ngân sách 1136px của khung ảnh. */
.honor-anh__o-tieu-de {
  flex: 1 1 auto; min-width: 0; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  padding-block: 4px; margin-block: -4px;
}
.honor-anh__o-them {
  flex: none; padding: 2px 12px; border-radius: 999px; background: rgba(255, 255, 255, .22);
  font-size: 18px; font-weight: 800; letter-spacing: 0;
}
/* Ô nhóm: avatar xếp CHỒNG (−18px) thay vì dàn hàng — ba avatar 56px chỉ tốn 132px thay vì 192px,
   chừa trọn bề ngang cho tên ở dòng dưới. Viền trắng tách từng người trên nền màu. */
.honor-anh__o-chong { flex: none; display: flex; align-items: center; }
.honor-anh__o-ai { display: flex; }
.honor-anh__o-ai + .honor-anh__o-ai { margin-left: -18px; }
.honor-anh .honor-anh__o-chong .avatar {
  width: 56px; height: 56px; font-size: 24px; box-shadow: 0 0 0 3px #fff;
}
.honor-anh__o-ten {
  flex: none; min-width: 0; font-size: 24px; line-height: 1.2; font-weight: 800;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.honor-anh__o-pill {
  flex: none; align-self: flex-start; padding: 3px 14px; border-radius: 999px;
  background: rgba(255, 255, 255, .18); font-size: 20px; font-weight: 800;
}
.honor-anh__o-mo-ta { flex: none; font-size: 18px; line-height: 1.25; color: rgba(255, 255, 255, .88); }
.honor-anh__o-trich {
  flex: none; margin: 0; font-size: 18px; line-height: 1.3; font-style: italic;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Ô danh sách (Cán mốc mới, Lão làng): mỗi người MỘT dòng, nhãn mốc nằm dưới tên chứ không bên phải
   — ở ô 272px, avatar + tên + nhãn trên một hàng chỉ để lại ~90px cho tên. */
.honor-anh__o-ds {
  flex: none; list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px; min-width: 0;
}
.honor-anh__o-ds-dong { display: flex; align-items: center; gap: 10px; min-width: 0; }
.honor-anh .honor-anh__o-ds .avatar { width: 40px; height: 40px; font-size: 16px; box-shadow: 0 0 0 2px #fff; }
.honor-anh__o-ds-chu { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.honor-anh__o-ds-ten {
  min-width: 0; font-size: 20px; line-height: 1.15; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.honor-anh__o-ds-nhan { font-size: 17px; line-height: 1.15; font-weight: 600; color: rgba(255, 255, 255, .88); }
.honor-anh__o--ds { justify-content: flex-start; }
/* Avatar chữ cái có nền tint TRONG SUỐT — đặt lên ô màu đậm thì chữ cái chìm vào nền ô (đo
   18/09/2026: "TH" nâu trên nâu ở ô Gương mặt). Ghim nền sáng đặc, giữ màu chữ theo từng người. */
.honor-anh .honor-anh__o .avatar { background-color: #F3F7F5; }
/* Ảnh tháng bỏ chấm danh hiệu, GIỮ viền (người dùng 14/09/2026). Viền ghim màu, không token. */
.honor-anh .honor-avatar--gold .avatar      { box-shadow: 0 0 0 4px #fff, 0 0 0 8px #E0A100; }
.honor-anh .honor-avatar--silver .avatar    { box-shadow: 0 0 0 4px #fff, 0 0 0 8px #94A3B8; }
.honor-anh .honor-avatar--bronze .avatar    { box-shadow: 0 0 0 4px #fff, 0 0 0 8px #B8733B; }
.honor-anh .honor-avatar--spotlight .avatar { box-shadow: 0 0 0 4px #fff, 0 0 0 8px var(--court-500); }

/* --- ảnh THÀNH TÍCH CÁ NHÂN (Đợt D, spec § 5) --- */
/* 🔴 Ngân sách chiều cao — ĐO THẬT trên app 16/09/2026 (app-7b8edf7f…css), không phải ước tính.
   Khả dụng 1136 − 372 banner − 64 chân − 56 padding = 644px (17/09/2026: tổng +112 và banner +112
   nên con số này KHÔNG đổi — đó chính là lý do chọn cách cao thêm file thay vì bóp cột). Ca xấu nhất dựng ngay trên DOM thật
   bằng cách bơm đủ 7 danh hiệu với tên dài nhất hệ thống sinh được (Gương mặt tháng 12 lần · Chăm
   nhất tháng 12 lần · Chăm chỉ 500 buổi · Bền bỉ 52 tuần · Lão làng 5 năm · Tân binh sáng giá ·
   Người kết nối 30 lượt khách — 4/7 ô xuống 2 dòng, lưới 221px) cộng 3 mốc: **cần 601px ⇒ dư 43px**.
   Trước ba nấc dưới, đúng ca đó cần 658px ⇒ ÂM, và vì .honor-anh__cot có overflow: hidden nên nó bị
   CẮT CÂM ngay trong PNG chứ không vỡ layout. Bẫy đã làm lệch phép tính lần đầu: .honor-anh__khoi-dau
   không khai line-height nên kế thừa 1.5 của Bootstrap, không phải 1.2. Ba nấc mua lại 57px:
   padding .honor-anh__khoi 20/28/24 → 16/24/18 (−20) · line-height khối-dau (−13) · padding ô lưới
   14 → 10 (−24). 🔴 Đệm 43px là MỎNG — thêm bất cứ gì vào cột phải phải đo lại ca 7 danh hiệu, đừng
   suy ra từ ca thường (người có 3 danh hiệu chỉ cần 450px, dư 194px, nhìn rất an toàn mà không phải).
   Hai nấc dự phòng chưa dùng tới: gap .honor-anh__khoi 14→10 (−16px), .honor-anh__thanh 16→12 +
   row-gap 8→6 (−18px). KHÔNG hạ font-size của .honor-anh__moc-ten: hàng đó bị sàn bởi huy chương
   32px và .honor-anh__moc-so (22px × 1.5) nên mua được 0px. */
/* 🔴 Banner card cá nhân BẰNG ảnh tháng từ 17/09/2026 (người dùng: *"ảnh bìa của ảnh vinh danh cá
   nhân cho rộng như ảnh vinh danh tháng đi"*) — modifier hạ banner xuống 372px đã bỏ, cả hai ảnh
   nay cùng dùng 512px của luật chung, nên cỡ chữ + logo banner cũng không còn phải thu riêng.
   🔴 ĐỔI KÈM, không tách rời: khung PNG của card cá nhân cao **1276px** thay vì 1136px. 140px banner
   dày thêm PHẢI lấy từ chiều cao TỔNG, không được lấy của ba cột nội dung — ngân sách của chúng là
   644/687px (đo ca xấu nhất: 7 danh hiệu), dư đúng 43px, nên cắt 140px vào đó là mất chữ CÂM trong
   PNG. Con số 1276 sống ở BA nơi phải luôn bằng nhau: luật dưới đây · `data-cao` của
   `the-ca-nhan.html` · đối số `cao` truyền cho `bangXuatAnh`. `AnhVinhDanhRenderTest` +
   `TheVinhDanhScriptTest.bannerCaNhanBangAnhThang` gác. */
.honor-anh--ca-nhan { height: 1276px; }
/* 🔴 Ba luật thu cỡ chữ banner (tiêu đề 57px · phụ đề 30px · vòng nguyệt quế 84px — hệ số 0,75) đã
   BỎ ngày 17/09/2026. Chúng sinh ra chỉ vì banner card cá nhân thấp hơn ảnh tháng: khối chữ dùng
   nguyên cỡ của ảnh tháng thì chạy xuống sát ô tên CLB, chừa đúng 5px trong khi ảnh tháng chừa
   54,4px. Nay banner hai bên BẰNG NHAU nên tiền đề đó không còn — hở trên đo lại được 44,4px, đúng
   bằng ảnh tháng. 🔴 Hạ banner card cá nhân xuống lần nữa mà quên trả ba luật này về là lỗi cũ quay
   lại y nguyên; `BannerTenClbKhoangThoTest.bannerCaNhanDungChungNhipVoiAnhThang` gác cặp điều kiện
   đó (banner bằng nhau ⇔ không thu cỡ chữ). */
/* 🔴 Modifier logo riêng (144px) cũng đã bỏ cùng lý do: banner bằng nhau ⇒ hai ảnh dùng chung một
   cỡ logo của luật nền. */
/* padding-top 56px chứ không 40px như ảnh tháng: khối .honor-anh__clb (logo 144px từ 17/09/2026) neo dưới mép
   banner nên nó phủ xuống vùng nội dung — cột trái phải bắt đầu DƯỚI mép logo. Từ 16/09/2026 phần
   tràn còn 32px (mép logo ở +32 thay vì +44) nên 56px dư ra 12px; giữ nguyên vì đó là khoảng thở
   của bố cục dọc đã đo, không phải hằng số suy ra từ độ nhô. */
.honor-anh__noi-dung--ca-nhan { grid-template-columns: 1fr 2fr; padding: 56px 64px 0; }

/* 🔴 overflow: hidden (bổ sung của controller, không có trong brief gốc): khối này là con của lưới
   .honor-anh__noi-dung vốn ghim grid-template-rows: minmax(0, 1fr), nhưng khác .honor-anh__cot ở
   cột phải, nó KHÔNG có lưới an toàn riêng. Ca xấu nhất (tên 2 dòng + 3 ô số) tràn ra sẽ đè lên chân
   ảnh mà khung chỉ báo thừa vài px — đúng lớp lỗi đã lộ thật 16/09/2026 ở ảnh tháng. */
.honor-anh__ho-so {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  min-width: 0; min-height: 0; text-align: center; overflow: hidden;
  padding: 24px 24px 28px; border-radius: 28px; background: #fff; border: 4px solid var(--court-100);
}
.honor-anh .honor-anh__ho-so .avatar { width: 220px; height: 220px; font-size: 84px; }
/* Viền avatar dày hơn dòng danh sách vì avatar to gấp 4 (cùng luật với .honor-card__nguoi cũ). */
.honor-anh__ho-so .honor-avatar--gold .avatar      { box-shadow: 0 0 0 5px #fff, 0 0 0 10px #E0A100; }
.honor-anh__ho-so .honor-avatar--silver .avatar    { box-shadow: 0 0 0 5px #fff, 0 0 0 10px #94A3B8; }
.honor-anh__ho-so .honor-avatar--bronze .avatar    { box-shadow: 0 0 0 5px #fff, 0 0 0 10px #B8733B; }
.honor-anh__ho-so .honor-avatar--spotlight .avatar { box-shadow: 0 0 0 5px #fff, 0 0 0 10px var(--court-500); }
/* Tên 2 dòng rồi "…": ca xấu nhất là họ tên đầy đủ có đệm ở cột hẹp nhất của ảnh. */
.honor-anh__ho-so-ten {
  max-width: 100%; font-size: 44px; line-height: 1.15; font-weight: 800;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.honor-anh__ho-so-tu { font-size: 22px; line-height: 1.2; color: var(--court-700); }
.honor-anh__so-lieu {
  width: 100%; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;
}
.honor-anh__o-so {
  padding: 12px 8px; border-radius: 20px; background: var(--court-50); min-width: 0;
}
.honor-anh__o-so-gt { font-size: 32px; font-weight: 800; color: var(--court-800); }
.honor-anh__o-so-nhan { font-size: 18px; line-height: 1.2; color: var(--court-700); }

/* 🔴 Khối cao theo NỘI DUNG (thừa kế .honor-anh__cot > * { flex: 0 0 auto }) — không giãn chia đều
   chiều cao cột. Cùng bài học với ô 520px rỗng của ảnh tháng (người dùng 16/09/2026). */
.honor-anh__khoi {
  display: flex; flex-direction: column; gap: 14px; min-width: 0;
  padding: 16px 24px 18px; border-radius: 28px; background: #fff; border: 4px solid var(--court-100);
}
.honor-anh__khoi-dau {
  font-size: 22px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  line-height: 1.2; color: var(--court-700);
}
/* 3 cột chứ không 2: 7 danh hiệu (trần tự nhiên) vừa đúng 3 hàng, tiết kiệm ~150px chiều cao. */
.honor-anh__luoi {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;
}
.honor-anh__luoi li {
  display: flex; align-items: center; gap: 12px; min-width: 0;
  padding: 10px 16px; border-radius: 20px; background: var(--court-50);
}
.honor-anh__luoi .honor-medal { flex: none; width: 36px; height: 36px; }
.honor-anh__luoi-ten {
  min-width: 0; font-size: 22px; line-height: 1.15; font-weight: 600;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* 🔴 Mỗi danh hiệu một tông màu (người dùng 16/09/2026): trước đó cả 7 ô cùng nền var(--court-50)
   nên ảnh gần như đơn sắc, lại trùng họ màu với viền avatar Gương mặt tháng (var(--court-500)).
   Màu ghim bằng hex, KHÔNG token đổi theo giao diện tối — PNG phải giống nhau ở cả hai giao diện.
   Huy chương trong ô phải đè nền theo tông của ô: luật dùng chung .honor-medal--dac.honor-medal--moc
   cho ra nền xanh #0E7A5F, để nguyên thì huy chương xanh nằm trên ô hồng/tím. Đè ở đây, trong phạm vi
   .honor-anh__luoi, để không ảnh hưởng trang Thành tích / chip avatar / ảnh tháng. */
.honor-anh__luoi--guong-mat-thang { background: #FBF0DA; color: #7A4F06; }
.honor-anh__luoi--cham-nhat-thang { background: #FCEFE4; color: #7A3C12; }
.honor-anh__luoi--cham-chi        { background: #E4F4EE; color: #0A5342; }
.honor-anh__luoi--ben-bi          { background: #FBEAE6; color: #7E3126; }
.honor-anh__luoi--lao-lang        { background: #F0EBFA; color: #4A3570; }
.honor-anh__luoi--tan-binh        { background: #E6F1F9; color: #134A72; }
.honor-anh__luoi--ket-noi         { background: #FCEBEF; color: #8A3547; }
.honor-anh__luoi--guong-mat-thang .honor-medal { background: #B4790F; color: #fff; }
.honor-anh__luoi--cham-nhat-thang .honor-medal { background: #A8551C; color: #fff; }
.honor-anh__luoi--cham-chi        .honor-medal { background: #0E7A5F; color: #fff; }
.honor-anh__luoi--ben-bi          .honor-medal { background: #B34A38; color: #fff; }
.honor-anh__luoi--lao-lang        .honor-medal { background: #6B4E9E; color: #fff; }
.honor-anh__luoi--tan-binh        .honor-medal { background: #1E6FA8; color: #fff; }
.honor-anh__luoi--ket-noi         .honor-medal { background: #C25A6E; color: #fff; }
.honor-anh__chua-co { margin: 0; font-size: 24px; line-height: 1.3; color: var(--court-800); }

/* Hàng mốc: huy chương | tên | tỉ lệ, thanh trải hết bề ngang ở hàng dưới. */
.honor-anh__moc {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; column-gap: 12px; row-gap: 8px; min-width: 0;
}
.honor-anh__moc .honor-medal { width: 32px; height: 32px; }
.honor-anh__moc-ten {
  font-size: 24px; line-height: 1.2; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.honor-anh__moc-so { font-size: 22px; font-weight: 700; color: var(--court-700); white-space: nowrap; }
.honor-anh__thanh {
  grid-column: 1 / -1;
  display: block; height: 16px; border-radius: 999px; background: var(--court-100); overflow: hidden;
}
/* 🔴 Màu ghim hex, KHÔNG var(--action-primary): token đó đổi theo giao diện tối ⇒ hai người tải
   cùng một ảnh ra hai màu (cùng lớp lỗi avatar đổi màu trong PNG, 14/09/2026). */
.honor-anh__thanh-day { display: block; height: 100%; border-radius: 999px; background: #0E7A5F; }

/* --- chân ảnh --- */
.honor-anh__chan {
  flex: none; display: flex; align-items: center; gap: 16px; padding: 12px 64px 16px;
  font-size: 24px; color: var(--court-700);
}
.honor-anh__chan .honor-anh__mark { width: 36px; height: 36px; }
.honor-anh__mark {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; border-radius: 12px; background: var(--court-500);
}
.honor-anh__mark .brand-svg { width: 30px; height: 30px; color: #fff; }
.honor-anh__ten-mien {
  margin-left: auto; font-weight: 700; color: var(--court-800); white-space: nowrap;
}

/* ============================================================
   === TẦNG MOBILE ===
   🔴 MỌI luật riêng cho điện thoại sống TRONG khối này và chỉ trong khối này.
   Lý do: desktop được bảo vệ VỀ MẶT CẤU TRÚC, không nhờ người sửa cẩn thận.
   Đổi token ở :root để "sửa cho nhanh" là vi phạm MB-8.
   Spec: docs/design/10_Mobile_Spec.md § 3. Test gác: MobileScaleTest.
   ============================================================ */
@media (max-width: 767.98px) {
  :root {
    /* P1 — thang chữ. Không bậc nào dưới 13px: 11px đo được ở nhãn nhóm menu là
       không đọc nổi trên màn 390px khi đang cầm vợt (CT-6). */
    --fs-caption: .8125rem;   /* 13 — nhãn phụ, chip meta. NHỎ NHẤT TUYỆT ĐỐI */
    --fs-body:    .9375rem;   /* 15 — chữ thường, nhãn form, chữ trong nút */
    --fs-strong:  1.0625rem;  /* 17 — danh tính một dòng dữ liệu */
    --fs-title:   1.25rem;    /* 20 — tiêu đề card, H2 */
    --fs-page:    1.375rem;   /* 22 — H1 của trang */
    --fs-money:   1.75rem;    /* 28 — số tiền hero, ĐÚNG MỘT LẦN mỗi màn */

    /* P1 — thang icon. Ba bậc. Đo được 7 cỡ khác nhau trên một trang trước khi sửa. */
    --icon-sm: 1rem;      /* 16 — icon nằm trong dòng chữ */
    --icon-md: 1.25rem;   /* 20 — icon trong nút, bottom-nav */
    --icon-lg: 1.5rem;    /* 24 — icon tile stat-card, icon trạng thái */

    /* P1 — vùng chạm ngón tay (Apple HIG). Đo được 31/34/36/38px trước khi sửa. */
    --tap-min: 44px;
  }

  body { font-size: var(--fs-body); }

  /* Icon: gán theo NGỮ CẢNH, không theo từng chỗ dùng — nếu không lại sinh ra 7 cỡ. */
  .bi { font-size: var(--icon-sm); vertical-align: -.125em; }
  .btn > .bi, .app-bottom-nav-item > .bi, .app-topbar-mobile .bi { font-size: var(--icon-md); }
  .stat-card__icon > .bi, .app-alert__body > .bi { font-size: var(--icon-lg); }

  /* Chữ nhỏ nhất của khung.
     🔴 Task 1.1 đã DỜI hai luật khỏi đây: nhãn bottom-nav (`.app-bottom-nav-item`) và nhãn nhóm
     menu (`.app-sidebar-group-label`, chỉ hiện trong offcanvas ở mobile) — cả hai thuộc vỏ
     `d-lg-none` sống tới 991,98px, nên bậc chữ của chúng nằm ngay tại component (xem
     `.app-bottom-nav-item` và `.offcanvas .app-sidebar-group-label` ở khối "Bottom-nav"/"Offcanvas"
     phía trên tệp). Giữ bản sao ở đây nữa chỉ tạo hai chỗ để quên đồng bộ. */
  .stat-card__label { font-size: var(--fs-caption); }
  .badge-soft, .form-text, .cell-datetime__time { font-size: var(--fs-caption); }
  /* Bộ đếm tab Điểm danh: ba chỉ số PHẢI nằm trên MỘT dòng ở điện thoại (người dùng chốt
     2026-08-23). Đo bằng tools/mobile-audit ở 360×800 (hộp chứa 336px), cùng bộ markup rút gọn
     `d-sm-none` trong attendance/fragments.html: padding .7rem ⇒ cần 347px ⇒ vỡ 2 dòng; .5rem cùng
     `gap-1` ⇒ 281px (309px khi có thêm cụm "chưa rõ") ⇒ một dòng, còn dư ~27px. Chỉ bóp padding,
     KHÔNG hạ cỡ chữ: 15,2px là mức đọc được từ xa khi đứng ở sân (CT-6). */
  .attendance-counter .badge-soft { padding: .35rem .5rem; }

  /* Hai hàng đầu tab Điểm danh — ba badge đếm và cặp nút "Thêm người khác"/"Thêm khách" — TRẢI HẾT
     bề ngang màn ở điện thoại (người dùng chốt 2026-08-24): phần dư dồn vào KHOẢNG CÁCH giữa các
     mục chứ không để trống một mảng bên phải. Đo ở 390×844 (hộp 366px) trước khi sửa: hàng badge
     kết thúc ở 291,3px ⇒ **thừa 74,7px** bên phải; hàng nút kết thúc ở 325,3px ⇒ thừa 40,7px. Cùng
     markup ở 360×800 (hộp 336px) thừa 44,7 và 10,7px.
     `flex-wrap: wrap` giữ nguyên: nếu về sau nội dung dài ra và phải xuống dòng, dòng cuối
     dồn về trái như thường — vẫn đọc được, không vỡ.
     🔴 Chỉ trong tầng ≤767,98px. Từ 768px trở lên hai hàng giữ nguyên bố cục dồn-trái: ở đó nhãn nút
     quay lại dạng đầy đủ ("Thêm khách vãng lai") và badge có lại phần chữ, hộp chứa cũng rộng gấp
     nhiều lần — trải đều ra sẽ đẩy hai nút ra hai đầu một dòng 1000px+, trông như hai nút không liên
     quan đến nhau. Đó cũng là điều kiện `MB-8` (desktop không đổi một pixel).
     KHÔNG dùng `flex: 1` cho các nút để chúng tự giãn: cặp nút này rộng không bằng nhau (176,4 và
     140,9px) nên kéo bằng nhau sẽ đổi cả tương quan thị giác giữa hai hành động, trong khi thứ người
     dùng yêu cầu là giãn KHOẢNG CÁCH. */
  /* 🔴 `space-evenly` chứ KHÔNG phải `space-between` (người dùng chốt lại cùng ngày): `space-between`
     ghim mục đầu vào sát mép trái và mục cuối vào sát mép phải — người dùng báo "sát 2 bên quá".
     `space-evenly` chia phần dư thành các khoảng BẰNG NHAU ở cả hai lề lẫn giữa các mục.
     `column-gap: 0` là BẮT BUỘC đi kèm, nếu không hai loại khoảng cách KHÔNG bằng nhau: `gap-1`/`gap-2`
     bị trừ ra TRƯỚC, rồi `space-evenly` mới chia phần còn lại ⇒ khoảng giữa hai mục = phần chia + gap,
     còn khoảng ở lề chỉ = phần chia (đo ở 390×844: 22,7 so với 18,7px — lệch đúng bằng 4px gap).
     `row-gap` giữ lại: nó chỉ có tác dụng khi hàng phải wrap, và `space-evenly` không lo trục dọc. */
  .attendance-counter, .attendance-quick-add {
    justify-content: space-evenly;
    column-gap: 0;
    row-gap: .5rem;
  }
  /* .text-caption dùng chung toàn app ở 12px (desktop) — đo được ở H2 "Buổi tới" trên dashboard
     mobile (390px). Nâng lên --fs-caption riêng cho tầng mobile, giữ nguyên desktop. */
  .text-caption { font-size: var(--fs-caption); }
  /* Dòng trạng thái của ô tìm nhanh trong tab Điểm danh: nút "Bỏ lọc" là một `.btn`, nên luật chung
     `.btn { font-size: var(--fs-body) }` ngay bên dưới ĐÈ `.text-caption` (cùng độ đặc hiệu 0,1,0 và
     khai sau) ⇒ đo ở 390×844: nhãn "Đang hiện 3/28" 13px đứng cạnh nút 15px, trông như hai bậc chữ
     trong cùng một dòng phụ. Chỉ có ở tầng mobile — desktop không có luật `.btn { font-size }` nào
     nên `.text-caption` của app.css (khai sau Bootstrap) đã thắng sẵn, cả hai cùng 12px.
     Chỉ đổi CỠ CHỮ: vùng chạm 44×44 vẫn do `.btn`/`.btn:not(.is-icon)` lo (đo được 63,8×44). */
  #ket-qua-loc-diem-danh .btn { font-size: var(--fs-caption); }

  /* Vùng chạm của link tên trong banner Gương mặt tháng (MB-3): đo 214×23 ở 360×800 (14/09/2026).
     `line-height` chứ không `display: flex`: link mang `text-truncate` (một dòng, "…"), mà flex
     container không cắt chữ của anonymous flex item. Một dòng nên line-height 44px = hộp 44px. */
  .honor-spotlight__ten { line-height: var(--tap-min); }

  /* Vùng chạm: nút thường phóng thẳng lên 44px. */
  .btn { min-height: var(--tap-min); font-size: var(--fs-body); }
  /* 🔴 …nhưng `min-height` trên một hộp `inline-block` (mặc định của `.btn`) chỉ KÉO DÀI hộp xuống
     dưới: chữ vẫn nằm ở dòng đầu, phần dư 12–13px dồn hết xuống đáy ⇒ nút trông "béo" và chữ lệch
     lên trên. Chỉ lộ ở mobile vì chỉ mobile mới có `min-height` (desktop nút cao đúng bằng nội dung,
     không có phần dư nào để lệch). `inline-flex` + `align-items: center` phân đôi phần dư về hai
     phía — cùng cách đã dùng cho `.nav-tabs .nav-link`, `.dropdown-item`, `.list-card a` ở dưới.
     `gap` là BẮT BUỘC đi kèm: khoảng trắng giữa `<i class="bi">` và nhãn nằm trong một anonymous
     flex item nên bị flex layout NUỐT, không có `gap` thì icon dính sát chữ ở mọi nút của app. */
  .btn { display: inline-flex; align-items: center; justify-content: center; gap: .35rem; }
  /* 🔴 Ngoại lệ DUY NHẤT của luật trên: hai nút chọn kênh trong modal Góp ý mang thêm một dòng phụ
     `<span class="d-block">` bên dưới nhãn. Biến nút thành flex container (hàng ngang) sẽ kéo dòng
     phụ đó lên NẰM CÙNG HÀNG với nhãn — vỡ hẳn bố cục hai dòng của chúng. Chúng cũng không cần luật
     trên: `btn-lg` hai dòng nên đã cao hơn 44px sẵn, `min-height` không hề kéo dài chúng ⇒ không có
     phần dư nào để chữ lệch lên. Đã soát toàn bộ `templates/`: đây là hai `.btn` duy nhất của app có
     phần tử block bên trong. Thêm nút kiểu đó ⇒ nhớ lại chỗ này. */
  #bmh-gop-y-chon > .btn { display: block; }
  /* 🔴 …và phóng cả theo CHIỀU NGANG (task 5.6). `min-height` một mình chỉ đóng được một nửa `MB-3`:
     nhãn tiếng Việt có thể rất ngắn, đo được nút "Tắt" ở màn Lịch tập chỉ **43,6px rộng** dù đã cao đủ
     44 — cùng lớp lỗi "hụt vùng chạm theo chiều ngang" mà 5.4 gặp ở link chữ (và đã sửa ở pseudo).
     Nút nào đang hẹp hơn 44px thì tự nó đã vi phạm `MB-3`, nên nới ra là SỬA ở mọi màn chứ không phải
     hồi quy. Trừ đúng nhóm icon-only: `is-icon` cố ý giữ kích thước NHÌN THẤY nhỏ và mở vùng chạm bằng
     pseudo ngay dưới đây — ép `min-width` cho chúng là phá đúng mật độ mà luật kia đang giữ.
     (`.app-theme-toggle`/`.pagination-bar__btn` không mang class `.btn` nên không dính luật này.) */
  .btn:not(.is-icon) { min-width: var(--tap-min); }

  /* 🔴 Nút icon-only GIỮ NGUYÊN kích thước nhìn thấy — phóng to chúng làm vỡ mật độ.
     Mở rộng vùng chạm bằng pseudo-element trong suốt phủ lên trên.
     .app-brand (logo topbar, đo được 182×35) cũng vào nhóm này: .app-topbar-mobile chỉ có
     min-height 56px (không phải chiều cao cố định), phóng .app-brand thẳng lên 44px (44+padding
     16px = 60px) sẽ đội topbar cao thêm — phải giữ nguyên kích thước nhìn thấy như nút icon-only. */
  .btn-sm.is-icon, .app-theme-toggle, .pagination-bar__btn, .app-brand { position: relative; }
  .btn-sm.is-icon::after, .app-theme-toggle::after, .pagination-bar__btn::after, .app-brand::after {
    content: ''; position: absolute;
    top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: var(--tap-min); height: var(--tap-min);
  }

  /* Task 1.8 — trang /clb (danh-sach) tự dựng .entry-header, KHÔNG dùng .app-topbar-mobile, nên tên
     sản phẩm "CLB Cầu Lông" ở đây thiếu luật chống-wrap của .app-topbar-mobile__club (task 0.4).
     Cạnh 3 nút hành động icon-only (Hồ sơ/Đăng xuất ẩn nhãn ở mobile), ở 360px chuỗi này wrap 2 dòng,
     đội header từ 69px lên 77,8px. nowrap giữ 1 dòng (đo: header 360 → 69px, không tràn). Đây là
     chuỗi CỐ ĐỊNH (không phải tên CLB biến thiên) nên nowrap an toàn, không cần thêm ellipsis. */
  .entry-header .app-brand { white-space: nowrap; }

  /* Cụm hành động cấp tài khoản (/clb và /toi): ba icon cùng hộp nhìn thấy 36px,
     vùng chạm 44px vẫn do pseudo-element ở trên mở rộng. Gap 8px giữ hàng vừa 360px. */
  .entry-header:not(.entry-header--platform) .entry-actions { gap: var(--space-xs); }
  .entry-header .entry-action {
    width: 2.25rem; height: 2.25rem; min-height: 2.25rem;
    padding: 0; flex: 0 0 auto; line-height: 1;
    border: 1px solid var(--border); border-radius: var(--radius-md);
    background: var(--surface); color: var(--text-muted);
  }
  .entry-header .entry-action .bi { font-size: var(--icon-md); }
  .entry-header .entry-action:hover { border-color: var(--border-strong); color: var(--text); }
  .entry-header .entry-action:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

  /* `.entry-actions` của khu quản trị (task 8.7 — nút Menu dropdown) bọc xuống dòng RIÊNG dưới
     `.app-brand` ở điện thoại, và người dùng muốn 2 mục bên trong nó tách về HAI ĐẦU dòng đó
     (theme-toggle sát trái, nút Menu sát phải) — không phải cả cụm dồn về một phía như vòng sửa 1
     (`margin-left: auto`, đã bỏ). `.entry-actions` mặc định `flex: 0 1 auto` (co theo nội dung) nên
     `justify-content: space-between` của chính nó không có khoảng trống nào để giãn; phải cho nó
     `flex: 1 1 auto` để CHIẾM HẾT bề ngang dòng nó đang đứng một mình, rồi `justify-content:
     space-between` mới có tác dụng đẩy 2 con ra 2 mép. Chỉ đặt trong TẦNG MOBILE: ở ≥768px
     `.entry-actions` còn chung dòng với `.app-brand`/`<nav>` (không wrap riêng), cho nó `flex-grow`
     ở đó sẽ nuốt hết khoảng trống `justify-content: space-between` của `.entry-header` đang lo, đẩy
     lệch cả `<nav>` — vi phạm `MB-8`. */
  .entry-header--platform .entry-actions { flex: 1 1 auto; justify-content: space-between; }

  /* Link chữ đứng riêng trong một dòng dữ liệu/breadcrumb cũng phải đủ 44px.
     .data-card là tên khái niệm của primitive P2 trong spec — class thật trong code là
     .list-card (bảng-thành-thẻ), nên chỉ giữ .list-card ở đây.
     .page-header__crumb (breadcrumb "Trang chủ …") đứng riêng một hàng phía trên tiêu đề, không kề
     sát nút nào khác — phóng thẳng lên 44px không đội layout xung quanh. */
  .list-card a:not(.btn), .page-header__crumb a {
    display: inline-flex; align-items: center; min-height: var(--tap-min);
  }
  /* 🔴 …và `min-width` đi CÙNG CẶP với `min-height` (P1, cùng lập luận đã dùng cho `.btn` ở task 5.6
     và cho pseudo của link chữ ở 5.4): `min-height` một mình chỉ đóng một nửa `MB-3`. Tên người
     Việt có thể rất ngắn — đo được ở màn Công nợ (390×844 và 360×800) link tên "Hung" chỉ **43×44**,
     thiếu đúng 1px bề ngang, và `audit.js` đếm nó là `nút<44`. Link nào đang hẹp hơn 44px thì tự nó
     đã vi phạm, nên nới ra là SỬA ở mọi màn chứ không phải hồi quy (đo lại Thành viên · Sổ quỹ ·
     Sân sau khi thêm: không màn nào đổi số). Ngoại lệ đã có sẵn luật riêng bên dưới không bị đụng:
     link trong `list-card__title-block` và trong ô `meta` đã tự trả `min-height: 0` + mở vùng chạm
     bằng pseudo, và pseudo của chúng vốn đã khai đủ cả hai chiều. */
  .list-card a:not(.btn) { min-width: var(--tap-min); }

  /* Tab dạng segmented (.nav-pills, vd "Danh sách"/"Yêu cầu tham gia") là link có chữ, không phải
     icon-only — áp dụng cùng kiểu phóng thẳng như .btn ở trên, không dùng pseudo-element. */
  .nav-pills .nav-link { display: inline-flex; align-items: center; min-height: var(--tap-min); }

  /* ============================================================
     Task 1.4 — thanh tab CUỘN NGANG có chủ ý, KHÔNG wrap/cắt, KHÔNG thu nhỏ chữ.
     Đo được: nav-tabs buổi tập (Điểm danh · Chi phí & chốt sổ · Thanh toán, tổng ~450px) và nav-pills
     Cài đặt (Chung · Cách chia tiền · Sân · Loại cầu) đều WRAP xuống 2 hàng trên 390/360px — thanh tab
     gãy làm đôi trông như lỗi. Cho cuộn ngang + scroll-snap để mọi tab nằm trên MỘT hàng, vuốt ngang
     khi cầm vợt (CT-6). MB-1 tự bỏ qua phần tràn nằm trong vùng overflow-x:auto có chủ ý. */
  .nav-pills, .nav-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
  }
  /* nav-pills là inline-flex (track ôm sát nội dung): kẹp max-width 100% để bộ tab HẸP vẫn ôm gọn,
     chỉ bộ rộng hơn màn mới bị giới hạn rồi cuộn — không kéo giãn track đầy ngang một cách vô cớ. */
  .nav-pills { max-width: 100%; }
  .nav-pills .nav-item, .nav-tabs .nav-item { flex: 0 0 auto; }
  .nav-pills .nav-link, .nav-tabs .nav-link { white-space: nowrap; scroll-snap-align: start; }
  /* nav-tabs buổi tập đo được 42px cao (thiếu 2px so 44) — phóng lên đủ vùng chạm ngón tay (MB-3). */
  .nav-tabs .nav-link { display: inline-flex; align-items: center; min-height: var(--tap-min); }

  /* Thanh tab màn chi tiết buổi tập (`.buoi-tabs`, session/buoi.html) — NGOẠI LỆ của luật cuộn
     ngang ngay trên (người dùng báo 11/09/2026: "độ rộng không đủ, xuất hiện thanh cuộn"). Đo ở
     390×844: 3 tab rộng 434–435px trong khung 366px, tab thứ ba bị cắt còn "Chi phí & c", và vạch
     xám dưới thanh tab chính là thanh cuộn. Ép cả hàng ngang lại cho vừa là bất khả: riêng chữ
     "Chi phí & chốt sổ" ở 15px đã ~125px, 4 tab (Đăng ký hộ · Điểm danh · Chi phí & chốt sổ ·
     Thanh toán) cộng chữ không thôi đã ~350px. Nên đổi hình: tab CHIA ĐỀU bề ngang, icon trên chữ
     dưới — đúng khuôn .app-bottom-nav người dùng đã quen ngay đáy màn hình — chữ về --fs-caption
     (bậc nhỏ nhất thang chữ) và được xuống 2 dòng khi 4 tab cùng có mặt. Không còn gì để cuộn.
     Scope theo class riêng: nav-tabs/nav-pills ở màn khác (Cài đặt…) vẫn cuộn ngang như cũ. */
  .buoi-tabs { overflow-x: visible; scroll-snap-type: none; }
  .buoi-tabs .nav-item { flex: 1 1 0; min-width: 0; }
  .buoi-tabs .nav-link {
    width: 100%; height: 100%;
    flex-direction: column; justify-content: center; gap: var(--space-2xs);
    padding: var(--space-xs) var(--space-2xs);
    white-space: normal; text-align: center; line-height: 1.2;
    font-size: var(--fs-caption);
  }
  .buoi-tabs .nav-link .bi { font-size: var(--icon-md); line-height: 1; }

  /* Khối "Thông tin buổi tập" (session/buoi.html) — nén trên điện thoại (người dùng 11/09/2026),
     giữ nguyên lưới 2×2 + đủ 4 nhãn. Phần khoảng cách tiêu đề/nhãn/hàng do utility *-md-* trong
     template lo; ở đây chỉ hạ đệm thẻ 24 → 16px. */
  .session-info { padding: var(--space-md); }

  /* Thanh tab sát thanh hành động ngay trên (người dùng yêu cầu 06/10/2026): khoảng 24px mặc định
     của .action-bar tách hai khối như hai phần không liên quan, trong khi màn này đã chật dọc. */
  .buoi-chi-tiet .action-bar { margin-bottom: var(--space-xs); }

  /* Khung "Đã thu x / y ₫" đầu tab Thanh toán (settlement/fragments.html) — thu nhỏ để chừa chỗ cho
     bảng thành viên ngay dưới (người dùng 06/10/2026). Số tiền buổi là số PHỤ ở màn này (không phải
     số hero) nên hạ money-lg 20px → --fs-body; đệm thẻ 24 → 8/12px. Nhãn "Đã thu" và số chung MỘT
     dòng (nhãn trái, số phải — khuôn .quy-gon), nút đủ chữ "Chia sẻ bảng thanh toán" một dòng dưới.
     🔴 KHÔNG cố nhồi nút lên cùng hàng với số: đo ở 390×844, số buổi cỡ triệu ("1.404.000 /
     1.404.000 ₫") ~170px + nút đủ chữ ~215px > 340px chỗ trống ⇒ flex-wrap đẩy nút xuống dòng riêng
     và dồn số sang phải lởm chởm. Hai hàng có chủ đích gọn hơn một hàng vỡ. */
  .app-card.tt-tong { padding: var(--space-xs) var(--space-sm); }
  .tt-tong > .d-flex {
    width: 100%; margin-left: 0 !important;
    flex-direction: column; align-items: stretch !important; gap: var(--space-xs) !important;
  }
  .tt-tong .text-end {
    display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-xs);
    text-align: start !important;
  }
  .tt-tong .small { font-size: var(--fs-caption); }
  .tt-tong .money-lg { font-size: var(--fs-body); line-height: 1.4; text-align: end; }
  .tt-tong .btn { width: 100%; justify-content: center; }

  /* ============================================================
     Task 1.5 — mục dropdown (component dùng chung). Bootstrap để .dropdown-item cao ~32px, dưới ngưỡng
     ngón tay. Vùng mù: dropdown ĐÓNG mặc định nên audit tĩnh không đếm — chỉ lộ khi đo bằng BMH_OPEN
     (mở dropdown chọn "size / trang" của pagination ra: nút<44 6→8, hai mục "10/25/50 / trang" đều
     158×32). Là component dùng chung (dropdown size phân trang · menu avatar topbar Hồ sơ/Đăng xuất ·
     club switcher…) ⇒ phóng đủ 44px một chỗ, mọi dropdown hưởng. Menu ⋯ của actionBar KHÔNG dùng
     .dropdown-item (dùng .action-bar__menu .btn, đã 44px từ P4) nên không đụng. */
  .dropdown-item { display: flex; align-items: center; min-height: var(--tap-min); }

  /* Nút đóng toast (task 1.2). Đo được **18,4×22,8px** ở 390px và **17,6×22,8px** ở 360px — nhỏ hơn
     mọi nút icon-only khác trong app, mà toast lại là thứ hay bị đóng bằng ngón tay nhất khi nó che
     mất chỗ đang cần xem (nó ghim ở góc trên-phải, đè lên topbar — xem ghi chú ở khối
     #bmh-toast-container). `audit.js` không đếm được nó vì toast chỉ tồn tại sau khi JS gọi show();
     số trên đo bằng probe riêng của task 1.2.
     Giữ kích thước nhìn thấy + mở vùng chạm bằng pseudo, đúng luật P1 cho nút icon-only. Đặt TRONG
     tầng mobile (khác `.offcanvas .btn-close` của task 1.1 — offcanvas mang `d-lg-none` nên chỉ tồn
     tại ở mobile, còn toast sống ở MỌI bề rộng): 44px là ngưỡng ngón tay, không phải ngưỡng chuột. */
  #bmh-toast-container .btn-close { position: relative; }
  #bmh-toast-container .btn-close::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: var(--tap-min); height: var(--tap-min);
  }

  /* Nút X của MỌI modal (task 5.2). Bootstrap để `.btn-close` **31×31** — đo được ở modal "Cho rời
     CLB" (`member/sua.html`), nhưng đây là component DÙNG CHUNG: app có ~20 modal, tất cả cùng một
     nút này. Vùng mù giống hệt offcanvas ở task 1.1: modal đóng thì `audit.js` bỏ qua vì
     `visibility: hidden`, phải đo bằng `BMH_OPEN` mới thấy. Cùng cách xử lý đã dùng cho offcanvas
     (1.1) và toast (1.2): giữ kích thước NHÌN THẤY, mở vùng CHẠM bằng pseudo-element. */
  .modal .btn-close { position: relative; }
  .modal .btn-close::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: var(--tap-min); height: var(--tap-min);
  }

  /* Checkbox/radio: hộp 15×15 của Bootstrap quá nhỏ cho ngón tay, nhưng phóng to chính ô vuông sẽ phá
     mật độ form (và trông như lỗi). Vùng chạm THẬT là cả nhãn — bấm `<label for>` toggle đúng ô đó,
     hành vi HTML gốc chứ không phải mẹo CSS; `audit.js` từ task 5.2 đã tính nhãn vào vùng chạm
     (`via: "label"`). Đo ở modal "Cho rời CLB": nhãn 340×**39** ⇒ chỉ thiếu 5px. Nâng nhãn cho đủ
     ngưỡng ngón tay. 🔴 Dùng `display: block` chứ KHÔNG `flex`: nhãn ở đây có `<strong>` lồng giữa câu
     ("… — **giữ nguyên** khoản nợ …"), biến nhãn thành flex container sẽ tách câu thành nhiều flex
     item không wrap được, chữ tràn ngang. */
  .form-check-label { display: block; min-height: var(--tap-min); }
  /* 🔴 …và cũng phải đủ 44px theo CHIỀU NGANG (task 5.6, cùng lớp lỗi với `.btn` ở trên và với link
     chữ ở 5.4). Nhãn tiếng Việt có thể cực ngắn: đo ở modal "Tạo Lịch tập", nhóm 7 checkbox thứ trong
     tuần nằm trong `d-flex flex-wrap` nên mỗi `.form-check` co về bề rộng nội dung ⇒ nhãn "Tư" **41px**
     và "Ba" **42px**, hụt vùng chạm dù đã cao đủ 44. Nhãn dài (câu trong modal "Cho rời CLB" ở 5.2)
     không bị chạm vì `min-width` chỉ nâng SÀN. */
  .form-check-label { min-width: var(--tap-min); }

  /* Hàng hành động nguy hiểm ở cuối màn sửa (task 5.5 — `court/sua.html`; task 5.9 gắn thêm cho
     `shuttlecock/sua.html`, cùng lỗi y hệt, không phải đổi một luật nào ở đây). Hàng này có HAI hình
     thái tùy trạng thái: sân đang dùng ⇒
     `[Ẩn sân] [Xóa sân]`, sân đã ẩn ⇒ câu trạng thái `Sân này đã bị ẩn.` + `[Xóa sân]`. Hình thái thứ
     hai mới là vấn đề: `<p>` là một flex item bình thường nên nó đứng SÁT nút, và ở 360px hai thứ ăn
     trọn bề ngang thẻ với đúng 8px `gap` ⇒ đọc thành một cụm "nhãn + nút" (như thể nút là hành động
     CỦA câu đó), thay vì "một câu trạng thái" và "một hành động" tách bạch. Không tiêu chí nào bắt
     được — `MB-1..MB-8` đều sạch ở cả 4 tổ hợp; chỉ thấy bằng ảnh (lần thứ năm của lớp lỗi này).
     `flex-basis: 100%` đẩy câu xuống hẳn dòng riêng, nút giữ nguyên bề rộng tự nhiên và vẫn căn giữa
     ở dòng dưới — cố ý KHÔNG cho nút full-width: `P4` § 3 nói hành động phá hủy phải KÉM nổi bật, kéo
     nó ra trọn bề ngang là đi ngược lại. Desktop không đọc class này nên `MB-8` không đụng. */
  .danger-actions > p { flex-basis: 100%; text-align: center; }

  /* Ảnh XEM TRƯỚC trong modal (task 6.1 — modal "Thông tin chuyển khoản" ở `club/cai-dat.html`).
     🔴 Lỗi chỉ thấy bằng ẢNH, mọi tiêu chí số đều sạch: `MB-1` không đo được vì modal không tràn
     NGANG, `MB-5` đo `scrollHeight` của TRANG chứ không của lớp phủ. Đo tay ở 390×844:
     `#qrPreview` cao **379px** (ảnh QR người dùng tải lên là ẢNH CHỤP MÀN HÌNH điện thoại — dọc,
     tỉ lệ ~1:2; template chỉ kẹp cạnh NGANG bằng style inline nên chiều cao thả nổi) ⇒
     `.modal-dialog` cao **970px** trên viewport **844px**, và `.modal-footer` bắt đầu ở **y=900** —
     tức hai nút Hủy/Lưu nằm HOÀN TOÀN dưới nếp gấp: mở modal ra không thấy nút lưu ở đâu.
     Kẹp chiều cao ảnh xem trước; cạnh ngang do style inline giữ nguyên nên trình duyệt co theo cạnh
     chặt hơn và tự giữ tỉ lệ (đo sau: 88×180, dialog 772px, nút Lưu ở y=719). Đây là ảnh XEM TRƯỚC (để biết mình vừa chọn đúng tệp), không phải chỗ để quét mã
     — bản đầy đủ vẫn hiển thị nguyên cỡ cho thành viên ở màn công nợ, nên không vi phạm § 7 "không
     ẩn bớt thông tin". Desktop không đọc luật này (nằm trong tầng mobile) ⇒ `MB-8` an toàn. */
  .modal-body .img-thumbnail { max-height: 180px; }

  /* ------------------------------------------------------------
     Lưới lịch tháng (task 5.11 — `session/lich.html`). Đây là màn DUY NHẤT của app có một bảng thật
     KHÔNG chuyển sang thẻ ở mobile: 7 cột của một tuần là ngữ nghĩa của chính màn hình, xếp chồng là
     mất luôn thứ mà lịch tồn tại để làm. Đã đo trước khi quyết (plan gốc dặn đúng chỗ này): ép lưới
     vừa 360px cho ô rộng trong chỉ **44px**, trong khi "17:30–19:30" cần **78,1px** và ngay cả nửa
     đầu "17:30–" đã là **43px** ⇒ mọi buổi đều bị cắt chữ. Nên giữ bề rộng tối thiểu 640px (khai ở
     style inline của chính `<table>`, NGOÀI tầng này) + cuộn ngang có chủ đích — đúng ngoại lệ mà
     `MB-1` chừa sẵn — và sửa cho việc cuộn đó tử tế.
     🔴 Đừng viết lại con số đó thành một khai báo `min-width` ở đây: `MobileScaleTest` quét NGUYÊN
     VĂN tầng này (kể cả trong comment) và cấm mọi bề rộng px cứng khác 44px — đúng luật, vì bề rộng
     px cứng trong tầng mobile là cách nhanh nhất làm vỡ màn 360px.
     ------------------------------------------------------------ */
  /* (1) `MB-2`: `.table > thead > tr > th` toàn cục là `.7rem` = **11,2px**. Mọi màn khác thoát vì
     `.list-card table thead { display: none }` giấu hẳn hàng tiêu đề ở mobile — lịch thì không phải
     `.list-card` nên hàng `T2…CN` hiện ra và là chữ dưới sàn 13px DUY NHẤT còn lại của Đợt 5. Nâng
     đúng bảng này ở đúng tầng mobile; `letter-spacing` giảm theo để 7 nhãn không phình ngang. */
  .lich-grid > thead > tr > th { font-size: var(--fs-caption); letter-spacing: .02em; }
  /* (2) Chữ giờ bị CẮT ở 7/20 buổi — lỗi đang chạy thật, không phải hệ quả của task này. Ô rộng
     trong 90px, chuỗi giờ 78,1px: thừa chỗ. Nhưng ngày có từ 3 buổi thì hộp `.lich-day` bật thanh
     cuộn dọc, nuốt ~15px ⇒ còn 75px < 78,1px, và `.money { white-space: nowrap }` (đúng cho SỐ TIỀN,
     sai cho khoảng giờ) không cho xuống dòng nên chữ bị xén. Cho phép bẻ dòng: dấu `–` (en dash) là
     điểm ngắt hợp lệ nên chuỗi tự thành "17:30–" / "19:30", không mất chữ nào. */
  .lich-grid .money { white-space: normal; }
  /* (3) `MB-3`: nút buổi tập cao **20px**. Đây là vùng chạm THẬT trên màn hình dùng tại sân, không
     phải nút phụ. Không dùng mẹo pseudo của `P1` ở đây — các nút xếp CHỒNG cách nhau 20px nên vùng
     chạm ảo 44px sẽ đè lên nhau và bấm trúng nhầm buổi, tệ hơn cả nút nhỏ. Nới chiều cao thật, và nới
     luôn ô chứa để hai buổi cùng ngày vẫn nằm gọn (18px số ngày + 2×48 = 114). Ngày từ 3 buổi trở lên
     vẫn cuộn trong ô như thiết kế cũ. */
  .lich-grid tbody button { min-height: var(--tap-min); display: flex; align-items: center; }
  /* (3a) Chiều cao ô: `130px` của lần trước là chiều cao CỐ ĐỊNH tính cho "hai buổi cùng một ngày",
     nên ngày TRỐNG — đại đa số ô của một tháng — cũng cao 130px và cả lịch thành một cột ô rỗng dài
     ngoằng. Từ khi bỏ cuộn lồng ở (3b), `height` chỉ còn là chiều cao TỐI THIỂU: ô nhiều buổi tự nở
     đúng phần nó cần. Nên hạ sàn xuống vừa MỘT buổi — 19,5px số ngày + 4px `mt-1` + 44px vùng chạm +
     8px `p-1` của `<td>` = 75,5px — và để những ngày đông buổi tự lo phần còn lại. */
  .lich-grid > tbody > tr > td { height: 5rem; }
  /* (3b) Bỏ hẳn `max-height` của hộp ngày ở mobile — tức bỏ luôn việc CUỘN LỒNG bên trong ô. Nút cao
     44px làm ngày 3 buổi cần 164px, quá hộp 114px ⇒ ô đó sinh một vùng cuộn dọc rộng ~90px nằm trong
     một trang cũng đang cuộn dọc: trên màn cảm ứng gần như không điều khiển được, và buổi thứ ba bị
     giấu sau một thao tác mà người dùng không có cách nào biết là có. Cho hàng tuần đó tự cao lên
     thay vì cuộn. Đo được: **3 ô cuộn lồng → 0**, đổi lại `cuộn` +0,10 màn (2 hàng nở 130 → 173px).
     Đánh đổi có chủ ý, cùng loại với 4.3/5.7. Desktop giữ nguyên `max-height: 94px`: ở đó ô rộng
     90px, nút vẫn 20px, ba buổi vừa đúng hộp — không có gì phải sửa. */
  .lich-grid .lich-day { max-height: none; overflow-y: visible; }
  /* (4) Hàng nút ở page-header. Lần trước cho nút chính chiếm trọn một dòng và đẩy ba nút điều hướng
     xuống dòng dưới: đúng ở 360px, nhưng ở 430px thì thừa chỗ mà vẫn ăn hai dòng, và một nút primary
     kéo hết bề ngang trông nặng hơn hẳn thứ nó làm. Nay ép CẢ BỐN nút về một dòng ở mọi bề rộng
     điện thoại: hạ cỡ chữ của riêng hàng này xuống `--fs-caption` (13px — đúng bậc thấp nhất của
     thang, không phá `MB-2`) để ngân sách bề ngang vừa cả 360px. Ba nút điều hướng gom trong
     `__nav` và dính vào mép phải; nút chính neo mép trái. `flex-wrap: nowrap` là phần cưỡng chế:
     không có nó, chỉ cần một nhãn dài hơn dự tính là hàng lại vỡ đôi như cũ.
     Ba nút điều hướng KHÔNG được co (`flex-shrink: 0`) — chúng đã ở đúng sàn vùng chạm 44px; chỗ
     thiếu (nếu có) phải lấy từ nút chính, nơi còn chữ để cắt bớt.
     Ngân sách đo ở 360px (màn hẹp nhất, `container` = 336px): nút chính 154 + `‹` 44 + `Hôm nay` 70,3
     + `›` 44 + 3 khoảng cách 6px = **330,3px**. Dư 5,7px — chật, nên mỗi con số dưới đây đều là một
     phần của ngân sách đó, đừng nới lại lẻ một cái. Cỡ chữ 15 → 13px trả về ~24px, `padding` ngang
     .5 → .4rem trả thêm ~6px, `gap` 8 → 6px trả 6px. */
  .lich-header-actions { flex-wrap: nowrap; width: 100%; }
  .lich-header-actions, .lich-header-actions__nav { gap: .375rem; }
  .lich-header-actions .btn { font-size: var(--fs-caption); padding-left: .4rem; padding-right: .4rem; }
  .lich-header-actions__nav { margin-left: auto; flex: 0 0 auto; }
  .lich-header-actions > .btn-primary { min-width: 0; }

  /* Hàng nút cuối trang — cùng bài toán ngân sách bề ngang với hàng header ở trên, nên cùng cách
     chữa: hạ cỡ chữ của riêng hàng này xuống `--fs-caption` (13px, bậc thấp nhất của thang ⇒ không
     phá `MB-2`) và bóp `padding` ngang. ĐO THẬT (Chrome, 360px — màn hẹp nhất, `container` = 336px):
     "Tạo buổi ngày 25/08" 177px + "Xem danh sách →" 125,6px + khoảng cách 6px = **308,6px**, dư
     27,4px. Riêng chữ "dạng" bị ẩn ở tầng này (`d-none d-md-inline` trong template) — không có nó thì
     nhãn đầy đủ cần 344px > 336px, cả hai nút bị ép co và chữ bẻ xuống hai dòng (nút cao 49px).
     Vùng chạm không đổi: `.btn { min-height: var(--tap-min) }` của tầng này vẫn giữ 44px. */
  .lich-footer-actions { gap: .375rem; }
  .lich-footer-actions .btn { font-size: var(--fs-caption); padding-left: .4rem; padding-right: .4rem; }

  /* (5) Bộ chọn chế độ xem (Ngày · Tuần · Tháng — 2026-08-30). Ba nút là bộ lọc CHÍNH của màn, dùng
     tại sân bằng ngón cái ⇒ mỗi nút phải đạt vùng chạm `--tap-min` và chia đều bề ngang màn hình.
     Cả cụm chiếm trọn một dòng, chú thích màu status tự rơi xuống dòng dưới — đây là lý do
     `.lich-toolbar` bỏ căn hai đầu ở tầng này: căn hai đầu trên MỘT dòng chỉ có một cụm sẽ đẩy chú
     thích dính mép phải, đọc như hai khối rời nhau. */
  .lich-toolbar { justify-content: flex-start; }
  .lich-mode { width: 100%; }
  .lich-mode .btn { flex: 1 1 0; min-height: var(--tap-min); }

  /* (6) Lưới TUẦN về MỘT cột: 7 ngày xếp chồng thành lịch trình đọc dọc. Đây chính là thứ lưới THÁNG
     không làm được (bảng 7 cột là ngữ nghĩa của nó, xem (1) ở trên) — và cũng là lý do chế độ Tuần
     đáng có riêng: ở điện thoại nó cho xem đủ một tuần mà KHÔNG phải cuộn ngang.
     Bỏ sàn chiều cao của thân cột: khi xếp chồng, một ngày trống chỉ cần cao vừa dòng chữ "Không có
     buổi" thay vì giữ nguyên khối cao bằng cột desktop. */
  .lich-tuan { grid-template-columns: 1fr; }
  .lich-tuan__than { min-height: 0; }
  /* Xếp chồng ⇒ cột trải hết bề ngang, thừa chỗ cho thứ và ngày nằm cùng MỘT hàng (desktop phải
     xếp dọc vì cột chỉ ~93px, xem ghi chú ở component). */
  .lich-tuan__dau { flex-direction: row; align-items: baseline; gap: .35rem; }
  /* Chip buổi tập nằm ngang (giờ · sân · trạng thái trên một dòng, tự xuống dòng khi tên sân dài):
     xếp chồng dọc như desktop sẽ cho mỗi buổi một khối cao ~66px trong khi cả bề ngang màn hình
     đang bỏ trống. Chiều cao tối thiểu giữ đúng vùng chạm `--tap-min`. */
  .lich-tuan .lich-buoi--day-du {
    flex-direction: row; flex-wrap: wrap; align-items: center;
    gap: .35rem; min-height: var(--tap-min);
  }

  /* ------------------------------------------------------------
     Bảng "Đã đăng ký" (task 5.12 — `session/dang-ky.html`). Màn thứ HAI giữ nguyên dạng bảng ở
     mobile, nhưng vì lý do khác hẳn màn Lịch: bảng này chỉ có 2 cột (tên+dòng phụ · nút Hủy) nên nó
     VỐN ĐÃ là một thẻ 2 dòng — chuyển sang `.list-card` chỉ làm nó to ra. Đo thật ở 390×844 với 11
     người (bật `data-role` + `list-card` bằng `BMH_EVAL` ngay trên trang đang chạy): mỗi hàng
     **72,2 → 96,3px**, cả trang **1573 → 1882px**, `cuộn` **1,86 → 2,23** màn. Và `.list-card table
     thead { display: none }` sẽ nuốt luôn hàng `Đã đăng ký (11)` — hàng đó KHÔNG phải nhãn cột mà là
     tiêu đề khối mang SỐ ĐẾM, ẩn đi là mất thông tin thật (§ 7 của spec cấm), còn đưa nó ra ngoài
     bảng thì đổi markup desktop ⇒ phá `MB-8`. Nên giữ bảng, và chỉ vá đúng chỗ hàng tiêu đề đó lộ ra
     luật toàn cục `.table > thead > tr > th` (cỡ chữ `.7rem` = **11,2px**) — cùng một lỗ hổng `MB-2`
     mà màn Lịch đã gặp ở (1) phía trên, vì cùng một lý do "không phải `.list-card`".
     🔴 Bài học 5.11 lặp lại NGUYÊN XI ở chính đoạn này: câu trên thoạt đầu viết con số dưới dạng
     `thuộc-tính: giá-trị` và `MobileScaleTest` bắt ngay — nó quét NGUYÊN VĂN cả comment. Muốn nhắc
     một khai báo trong tầng này thì tách tên thuộc tính ra khỏi giá trị, như câu hiện tại.
     ------------------------------------------------------------ */
  .dang-ky-list > thead > tr > th { font-size: var(--fs-caption); }

  /* ============================================================
     P2 — .data-card: bảng thành THẺ CÓ TIÊU ĐỀ, không phải danh sách nhãn/giá trị.
     Cơ chế cũ (td[data-label]::before) nở mỗi ô thành một dòng "Nhãn ··· Giá trị":
     hàng 7 cột ⇒ thẻ 7 dòng, tên người bị đẩy sang phải sau nhãn, ô rỗng vẫn ăn
     một dòng. Đo được: 10 giao dịch quỹ = 4,19 màn; 4 thành viên = 2,11 màn.

     Hợp đồng với template: đánh dấu vai trò từng ô bằng data-role trên <td>.
     Bảng desktop KHÔNG đổi gì — data-role không có tác dụng ở >=768px.
     ============================================================ */
  .list-card table thead { display: none; }
  /* 🔴 Bug phát hiện sau task 3.4 (báo lại 2026-08-18): thiếu `tfoot` trong danh sách reset display
     này. `<tfoot>` không bị chạm nên vẫn giữ `display: table-footer-group` mặc định trong khi `table`
     cha đã bị ép `display: block` — trình duyệt coi `tfoot` mồ côi như một "bảng" con riêng, tính bề
     rộng kiểu shrink-to-fit thay vì kế thừa 100% khối cha, khiến dòng "Tổng chi phí"/"Chênh lệch làm
     tròn" co hẹp lại và chữ vỡ dòng giữa từ. Thêm `tfoot` vào cùng danh sách để nó cũng thành khối
     block 100% như `table`/`tbody`/`tr`/`td`. */
  .list-card table, .list-card tbody, .list-card tfoot, .list-card tr, .list-card td { display: block; width: 100%; }
  /* 🔴 Bug phát hiện sau task 3.4 (báo lại 2026-08-18): Bootstrap `.table` khai `--bs-table-bg:
     var(--bs-body-bg)`, mà app này override `--bs-body-bg` toàn cục thành `--surface-sunken` (xám
     nhạt, dùng cho NỀN TRANG — xem comment ở khối Tom Select ~dòng 513). Selector gốc của Bootstrap
     `.table > :not(caption) > * > *` tự sơn nền đó lên TỪNG `<td>` — vô hình trên desktop vì các ô kề
     sát nhau tạo thành một mảng liền, nhưng ở mobile mỗi `<td>` giờ là một "dòng" riêng trong thẻ card
     TRẮNG (`.list-card tbody tr { background: var(--surface) }`), nên mảng xám của riêng nó lộ thành
     một khối xám xấu tách rời — rõ nhất ở ô `meta` nhiều chữ (chiếm cả bề ngang, nhiều dòng). Trả nền
     `<td>` về trong suốt để chỉ còn đúng nền trắng của `tr` cha. */
  .list-card td { background-color: transparent; }

  /* 🔴 C1/C2 (review cuối đợt 0, phát hiện bằng render Chrome headless thật — không ai render P2
     lần nào suốt 9 review từng-task trước đó): dòng ngay trên ép `width: 100%` cho MỌI td, nhưng
     các luật data-role bên dưới không khai lại `width` ⇒ mỗi ô vẫn rộng bằng cả hàng (flex-basis kế
     thừa 100%), xếp DỌC từng ô một dòng thay vì cùng dòng theo `order`; ô amount/status/actions còn
     `flex: 0 0 auto` (flex-shrink: 0) nên tràn khỏi thẻ thay vì co lại. Tương tự, `.table > tbody >
     tr > td` (dòng ~957) đặt `padding: .85rem 1rem` + `border-bottom` cho MỌI td — nhánh fallback
     (~ cuối khối này) đã tự reset hai thứ đó, nhánh data-role thì chưa, khiến thẻ cao dư và mỗi chip
     meta có một gạch chân lạ. Đo được trước khi sửa (hàng giao dịch quỹ 7 ô): mọi ô rộng 417–419px,
     thẻ cao 175px, `+108.000 ₫` bị cắt ở mép phải; sau khi thêm `width: auto` + reset padding/border:
     thẻ còn ~68px, đúng 2 dòng, không tràn. */
  .list-card td[data-role] { width: auto; padding: 0; border: 0; }

  /* 🔴 flex-wrap + order, KHÔNG dùng grid-template-areas. Với grid, hai <td> cùng
     data-role="meta" rơi vào cùng một ô và CHỒNG LÊN NHAU — grid area không cho
     nhiều phần tử chảy nối tiếp. Flex + order thì số ô meta bao nhiêu cũng được.
     Ép xuống dòng giữa hàng 1 và hàng 2 bằng ::after (pseudo-element của flex
     container LÀ một flex item, nên nhận được order và flex-basis). */
  .list-card tbody tr {
    display: flex; flex-wrap: wrap; align-items: center;
    column-gap: .5rem; row-gap: .15rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    margin-bottom: .5rem;
    padding: .625rem .75rem;
    background: var(--surface);
  }
  .list-card tbody tr::after {
    content: ''; order: 4; flex-basis: 100%; height: 0;   /* dấu ngắt dòng */
  }

  /* `.list-card__ca-the` (opt-in, gắn lên link CHÍNH của hàng) — chạm đâu trên thẻ cũng mở hàng đó.
     Trước đó chỉ nút "Xem" ở góc bấm được, trong khi cả thẻ trông như một khối bấm được và nằm
     trọn trong vùng ngón cái (12/10/2026). Link tự phủ ::after lên cả hàng; vẫn là MỘT thẻ <a> thật
     nên bàn phím, trình đọc màn hình và preload="mousedown" không đổi gì.
     🔴 KHÔNG dùng `tr::after` cho lớp phủ: nó đã là dấu ngắt dòng ngay trên.
     🔴 `transform: none` lúc :active là BẮT BUỘC, không phải gỡ hiệu ứng cho gọn: phần tử có
     transform trở thành containing block của con absolute, nên cú lún chung của `.btn:active` sẽ
     kéo lớp phủ co về đúng bằng cái nút NGAY LÚC ngón tay chạm xuống — ngón tay nhấc lên ở ngoài
     link, trình duyệt không bắn click, và thẻ "bấm không ăn". Phản hồi nhấn chuyển lên cả hàng.
     Nút khác trong hàng (vd "Xóa" của buổi đã hủy) nổi lên trên lớp phủ để vẫn bấm được riêng. */
  .list-card tbody tr:has(.list-card__ca-the) { position: relative; }
  .list-card .list-card__ca-the { position: static; }
  .list-card .list-card__ca-the::after { content: ''; position: absolute; inset: 0; }
  .list-card .list-card__ca-the:active { transform: none; }
  .list-card tbody tr:has(.list-card__ca-the:active) { background: var(--surface-sunken); }
  .list-card tbody tr:has(.list-card__ca-the) .btn:not(.list-card__ca-the) { position: relative; z-index: 1; }

  /* Hàng 1 — order 1..3 */
  .list-card td[data-role="title"] {
    order: 1; flex: 1 1 auto; min-width: 0;
    display: flex; align-items: center; gap: .4rem;
    font-size: var(--fs-strong); font-weight: 600; color: var(--text-strong);
  }
  /* ------------------------------------------------------------
     `list-card__title-block` (task 5.1) — cho ô title mang cả một KHỐI DANH TÍNH nhiều dòng
     (avatar + tên + badge "Chưa kích hoạt" + SĐT trong CÙNG một <td>, vì tách ra ô khác là đổi cột
     desktop ⇒ phá MB-8). Bốn luật dưới đây đều đến từ số đo, không phải thẩm mỹ; scope bằng class vì
     4 template khác cũng dùng `title` (debt/cong-no · expense/fragments · expense/bang-chia-tien ·
     settlement/fragments) mà chưa được đo lại — ứng viên nâng lên primitive khi tới lượt chúng.
     ------------------------------------------------------------ */
  /* (1) Ô title "nặng" chiếm trọn hàng 1 ⇒ `status` (vai trò) bị đẩy xuống một hàng RIÊNG, lơ lửng
     giữa thẻ và tốn ~25px mỗi dòng. Cho status nhập luôn vào hàng meta: "Thành viên · Nữ · Đang sinh
     hoạt" đọc liền mạch và thẻ về đúng 2 khối như hình mẫu spec § 3.2. Đã thử 4 biến thể khác (ép
     `flex-basis: 0` cho title, kéo actions lên hàng 1…) và đo cả 4: đây là biến thể duy nhất vừa
     không có lỗi thị giác vừa đạt `MB-5` ở 360px — xem bảng biến thể ở docs/design/11_Mobile_Progress.md
     § "Chi tiết 5.1". */
  .list-card td.list-card__title-block ~ td[data-role="status"] { order: 5; margin-left: 0; }
  /* (2) `.list-card a:not(.btn) { min-height: var(--tap-min) }` (task 0.2) phóng tên người lên 44px
     CAO THẬT. Ở bản fallback cũ mỗi ô là một dòng riêng nên 44px đó không đội gì — đúng như 0.2 ghi.
     Trong khối danh tính nhiều dòng thì khác hẳn: 44px của dòng tên CỘNG THÊM vào dòng badge/SĐT bên
     dưới (ô title cao 70px thay vì ~52px, 10 dòng dôi ~188px = 0,23 màn ở 360px). Trả chiều cao NHÌN
     THẤY về tự nhiên và mở vùng CHẠM bằng pseudo-element phủ lên — đúng nguyên tắc P1 vẫn dùng cho nút
     icon-only; `MB-3` vẫn đạt (đo lại: smallTargets = 0). */
  .list-card td.list-card__title-block a:not(.btn) { min-height: 0; position: relative; }
  /* 🔴 `min-width` KHÔNG phải đề phòng — đo được ở task 5.4 (màn Sân): tên dữ liệu có thể NGẮN. Ba
     link "Sân 3"/"Sân 4"/"Sân 7" rộng 41–43px và link phụ "Xem bản đồ" rộng 40px ⇒ `width: 100%`
     của pseudo cũng chỉ ra 41–43px, vùng chạm vẫn dưới 44 theo CHIỀU NGANG (MB-3 tính cả hai
     chiều). Màn Thành viên (5.1) không lộ vì tên người luôn dài hơn 44px. Vùng chạm dôi ra chỉ nằm
     trong ô title vốn còn chỗ trống bên phải nên không đè lên phần tử bấm được nào. */
  .list-card td.list-card__title-block a:not(.btn)::after {
    content: ''; position: absolute;
    top: 50%; left: 0; transform: translateY(-50%);
    width: 100%; min-width: var(--tap-min); height: var(--tap-min);
  }
  /* (2b) Task 5.4 — ô title mang TÊN + một link phụ đứng ngay sau nó trong CÙNG <td> ("Xem bản đồ"
     ở màn Sân; trên desktop link này nằm dưới tên vì nó được bọc trong một <div> block). Không đổi
     `.list-card__title-block` trần vì màn Thành viên (5.1) đã đo xong với bố cục hiện tại — scope
     bằng class riêng, đúng cách đã dùng cho `list-card__action-inline` / `list-card__meta-fill`.
     Hai luật đến từ ảnh chụp 360px, không phải thẩm mỹ: thiếu `flex-wrap` thì <div> chứa link là
     một flex item bị CO lại tới min-content ⇒ "Xem bản đồ" vỡ thành 2–3 dòng dựng đứng cạnh tên;
     `nowrap` + `flex: 0 0 auto` giữ nó nguyên cụm rồi cho cả cụm rơi xuống dòng dưới khi tên dài. */
  /* (2d) Ô title là một CHUỖI CHỮ thuần, không phải khối danh tính nhiều phần tử (task 5.6 — cột
     "Các thứ" ở màn Lịch tập: `Hai, Tư, Sáu`, mỗi thứ là một `<span>` riêng do vòng lặp Thymeleaf).
     `display: flex` của luật gốc biến MỖI span thành flex item, và flexbox **bỏ hẳn** khoảng trắng
     giữa các item rồi thay bằng `gap: .4rem` ⇒ dấu phẩy trôi ra giữa khoảng trống, đọc thành
     "Hai ,  Tư ,  Sáu". Trả về `block` thì chuỗi chảy như chữ bình thường, khoảng trắng trong nguồn
     giữ nguyên và kết quả khớp đúng bản desktop. Cùng lớp lập luận với `meta` ở task 4.4 (flex sai
     khi nội dung là CÂU/chuỗi chữ, không phải các chip rời). */
  .list-card td[data-role="title"].list-card__title-plain { display: block; }
  .list-card td.list-card__title-linked { flex-wrap: wrap; }
  .list-card td.list-card__title-linked > div { flex: 0 0 auto; white-space: nowrap; }
  /* (2c) 🔴 Người dùng báo sau 5.4 (2026-08-19): ở 390px, số tiền của sân TÊN DÀI rơi xuống một dòng
     RIÊNG, lơ lửng giữa thẻ (`margin-left: auto` đẩy nó khỏi mép trái, nút `⋯` chiếm mép phải) — đo
     được 3/10 thẻ và mỗi thẻ đó cao thêm 27,9px. Nguyên nhân KHÔNG phải thiếu chỗ mà là cách flexbox
     ngắt dòng: nó xếp item vào dòng theo kích thước GIẢ ĐỊNH, tức `flex-basis` — `title` thừa kế
     `flex: 1 1 auto` nên basis = max-content = BỀ RỘNG TÊN + "Xem bản đồ" TRÊN CÙNG MỘT DÒNG (338px
     cho "Sân 1 — Nhà thi đấu Cầu Giấy"), vượt chỗ trống ⇒ amount bị đẩy sang dòng sau, rồi title mới
     co lại và tự wrap. `min-width: 0` ở luật gốc không cứu được: nó chỉ hạ SÀN co, không đổi kích
     thước giả định dùng để ngắt dòng. Basis 0 thì tổng giả định của dòng 1 luôn vừa, title nở ra
     chiếm phần còn lại sau amount + `⋯` và chữ tự wrap BÊN TRONG nó — cùng một lập luận (và cùng một
     cách sửa) đã dùng cho `list-card__meta-fill` (4.4) và `list-card__meta-grow` (5.4).
     Đặc hiệu (0,3,1) để thắng CHẮC luật gốc `td[data-role="title"]` (0,2,1) chứ không dựa vào thứ tự
     nguồn — bài học C3 của đợt 0. Scope bằng class: `list-card__title-linked` chỉ dùng ở màn Sân và
     màn Loại cầu (task 5.8 — cùng khuôn bảng, ô title là một LINK đứng cùng hàng với amount) nên 4
     màn khác đang dùng `data-role="title"` không phải đo lại. */
  .list-card td[data-role="title"].list-card__title-linked { flex-basis: 0; }
  /* (2e) `list-card__title-fill` — CÙNG một lỗi và cùng một cách sửa như (2c), nhưng cho ô title là
     một CHUỖI CHỮ THUẦN (tên người ở bảng "CLB đang nợ tiền ứng" — fund/quy.html) chứ không phải một
     link như `title-linked`. Tách class riêng vì `title-linked` còn kéo theo `flex-wrap` + `> div
     nowrap` dành cho link phụ đứng cạnh tên, ở đây không có gì để wrap. Đo ở 360×800, hàng "Nguyễn
     Thị Thu Phương" + "1.250.000 ₫": thiếu luật này thì basis = max-content = bề rộng TÊN trên MỘT
     dòng (190px) + amount 120px = 310px > 300px khả dụng ⇒ flexbox ngắt dòng, số tiền rơi xuống một
     dòng RIÊNG lơ lửng giữa thẻ và thẻ cao 140,4px; có luật này thì tên tự wrap BÊN TRONG ô và số
     tiền ở lại hàng 1, thẻ còn 121,8px. */
  /* 2026-08-24 — ô này không còn là chuỗi chữ THUẦN: bảng tiền ứng gom theo người nên tên có thể
     kèm badge "n khoản". `display: flex` của luật gốc biến tên và badge thành HAI flex item, và
     flex-shrink bóp cái tên cho badge vừa chỗ thay vì đẩy badge xuống dòng — đo ở 390×844 với
     "Nguyễn Thị Thu Phương": tên vỡ 3 dòng, badge lơ lửng giữa khối chữ, thẻ cao 147px.
     `flex-wrap: wrap` KHÔNG cứu được (đã thử, vẫn 147px): item co được thì flexbox co chứ không
     ngắt dòng. Trả ô về `block` để tên + badge chảy như CHỮ bình thường và tự wrap — cùng cách sửa
     với `list-card__title-plain` ở (2d), thẻ còn 122px. `flex-basis: 0` vẫn giữ vì đó là thuộc
     tính của ô với tư cách ITEM của hàng, không liên quan tới display của chính nó. */
  .list-card td[data-role="title"].list-card__title-fill {
    flex-basis: 0; display: block;
  }
  /* (2f) `list-card__title-debtor` — LẦN THỨ BA của cùng một lỗi flex-basis (sau 2c màn Sân và 2e
     bảng tiền ứng), lần này ở màn Công nợ. Khác hai lần trước ở chỗ ô title mang MỘT CỤM nhiều
     mảnh: link tên + badge "Khách"/"Chưa kích hoạt"/"Chờ xác nhận" + dòng "↳ dẫn bởi …" của khách
     vãng lai, tất cả trong CÙNG một `<td>` (tách ra ô khác là đổi cột desktop ⇒ phá `MB-8`).
     Đo trước khi sửa ở 390×844 (5 người, có tên dài): ô title chiếm trọn 338px bề ngang thẻ ⇒
     `amount` bị đẩy xuống một dòng RIÊNG lơ lửng giữa thẻ, mỗi thẻ **114,8px** (thẻ tên dài 121,8).
     Ba luật, không phải một:
       · `flex-basis: 0` — đúng cách sửa của 2c/2e: tổng kích thước giả định của dòng 1 luôn vừa nên
         `amount` ở lại hàng 1, cụm danh tính tự wrap BÊN TRONG ô.
       · `flex-wrap: wrap` — bắt buộc đi kèm ở đây (2e không cần vì ô đó là chuỗi chữ thuần): basis 0
         mà không cho wrap thì badge và dòng "dẫn bởi" bị NÉN theo chiều ngang, đúng cảnh chữ vỡ dọc
         từng ký tự đã thấy ở ảnh audit bản cũ.
       · `> div { flex: 1 0 100% }` — dòng "dẫn bởi …" là một `<div>` (khối), phải chiếm trọn dòng
         riêng thay vì chen vào hàng badge. Cố ý KHÔNG dùng `white-space: nowrap` như `title-linked`:
         tên người dẫn dài thì câu này phải wrap được, không thì tràn ngang. */
  .list-card td[data-role="title"].list-card__title-debtor {
    flex-basis: 0; flex-wrap: wrap; row-gap: .2rem;
  }
  .list-card td.list-card__title-debtor > div { flex: 1 0 100%; }
  .list-card td.list-card__title-debtor .small { font-size: var(--fs-caption); }
  /* (3) Khối danh tính vốn là 2 tầng cứng (tên ở trên, badge+SĐT ở dưới). Cho nó chảy thành MỘT hàng
     tự wrap: màn rộng thì "Tên · Chưa kích hoạt · 0966442200" nằm gọn một dòng, màn hẹp mới xuống
     dòng — thay vì luôn tốn 2 dòng. `flex-wrap` ở hàng con là bắt buộc chứ không phải đề phòng: thiếu
     nó, SĐT tràn khỏi hộp title và ĐÈ LÊN badge vai trò bên phải (thấy bằng ảnh 360×800, `audit.js`
     không báo vì phần tràn vẫn nằm trong màn hình — `MB-1` chỉ đo mép phải viewport). */
  .list-card td.list-card__title-block .min-w-0 {
    display: flex; flex-wrap: wrap; align-items: center; column-gap: .4rem;
  }
  .list-card td.list-card__title-block .min-w-0 .d-flex { flex-wrap: wrap; margin-top: 0 !important; }
  /* (4) `.small` (Bootstrap, .875em) trong ô title tính trên NỀN 17px của title ⇒ ~14,9px: to hơn cả
     chữ `meta` 13px ngay bên dưới, dù đây là thông tin phụ ("(bạn)", SĐT). Ghim về đúng bậc caption
     của thang P1 — cùng lớp sửa với `meta .small` ở task 3.4, chỉ khác nền tính. */
  .list-card td.list-card__title-block .small { font-size: var(--fs-caption); }
  .list-card td[data-role="amount"] {
    order: 2; flex: 0 0 auto; margin-left: auto;
    font-size: var(--fs-strong); font-weight: 700;
    font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right;
  }
  /* Một số amount mang theo ghi chú phụ lồng bên trong (lý do ghi đè AC-EXP-04, "Thu cố định"
     BR-EXP-20 — expense/fragments.html) thay vì là amount thuần một dòng. `flex: 0 0 auto` +
     `white-space: nowrap` ở trên đúng cho số tiền ngắn, nhưng ép ghi chú dài thành MỘT dòng không
     xuống được ⇒ tràn ngang thật đo được (override reason ~90 ký tự tràn quá mép phải ~140px ở
     390px, xem docs/design/11_Mobile_Progress.md § Chi tiết 3.4). Scope bằng class riêng — không
     đổi hành vi `amount` một dòng mặc định ở nơi khác chưa có ghi chú lồng bên trong.
     🔴 Khối này CHỈ lo phần flex của thẻ mobile. Việc gỡ `nowrap` cho chính ghi chú nằm ở
     luật NỀN `.can-wrap-note > div` (mục "Bảng — list-card", ~dòng 1626) và áp cho mọi bề
     rộng — vì bệnh tràn ngang có ở CẢ desktop, không riêng mobile (2026-08-25). Đừng gom
     ngược luật nền vào đây. */
  .list-card td[data-role="amount"].can-wrap-note {
    flex-shrink: 1; min-width: 0; white-space: normal;
  }
  /* Nhãn cho ô `amount` — cùng lý do đã ghi cho `meta.is-labeled` ở dưới (spec § 3.2: trường MƠ HỒ
     vẫn phải giữ nhãn trên mobile), chỉ khác chỗ đặt: `thead` bị giấu ở mobile nên số tiền ở hàng 1
     mất hết ngữ cảnh cột. Lộ ra ở bảng "CLB đang nợ tiền ứng" (fund/quy.html): thẻ đọc thành "Chị
     Hằng 47.000 ₫ / Đã ứng 270.000 ₫" — SỐ ÍT quan trọng hơn thì có nhãn, số phải HÀNH ĐỘNG thì
     không, dễ hiểu ngược hẳn hai con số.
     🔴 `display: block` chứ KHÔNG inline (đo, không phải thẩm mỹ): nhãn inline nối vào số làm cụm
     amount rộng 205px ở 360×800, ăn hết chỗ của tên ⇒ tên dài vỡ 3 dòng, thẻ 147,3px (so với 124,2px
     khi chưa có nhãn). Nhãn xuống dòng riêng thì ô rộng đúng `max(nhãn, số)` ≈ 120px, thẻ 112,5px cho
     hàng thường và 121,8px cho hàng tên dài — đều đặn, không có hàng nào bị đội đột biến. Giá phải
     trả: +16px mỗi thẻ; đổi lại không còn con số nào không rõ nghĩa. Nhãn đọc từ `data-label` nên
     template chỉ thêm class, desktop không đổi một pixel (MB-8). */
  .list-card td[data-role="amount"].is-labeled::before {
    content: attr(data-label); display: block; line-height: 1.25;
    font-size: var(--fs-caption); font-weight: 400; color: var(--text-muted);
  }
  .list-card td[data-role="status"] { order: 3; flex: 0 0 auto; margin-left: auto; }
  /* Có cả amount lẫn status ⇒ status đã hết chỗ ở hàng 1, để nó theo amount sát phải.
     🔴 C3b (review cuối đợt 0): `~` so khớp theo THỨ TỰ TRONG DOM, không theo `order` hiển thị —
     bảng sổ quỹ map cột thật theo thứ tự "Loại chi"(status) ĐỨNG TRƯỚC "Số tiền"(amount), nên chỉ
     viết một chiều `amount ~ status` là chưa đủ: khi status đứng trước amount trong markup, cả hai
     ô cùng còn `margin-left: auto` ⇒ mỗi ô tự đẩy sát phải, tràn ngang ~24px. Viết cả hai chiều. */
  .list-card td[data-role="amount"] ~ td[data-role="status"] { margin-left: 0; }
  .list-card td[data-role="status"] ~ td[data-role="amount"] { margin-left: 0; }

  /* Hàng 2 — order 5..6 */
  /* 🔴 `display: block`, KHÔNG phải `flex` (đổi ở task 4.4 — đo được, không phải thẩm mỹ): một ô meta
     chứa CÂU (mô tả giao dịch quỹ, ghi chú xóa nợ…) chứ không chỉ chip ngắn. Trong flex container,
     đoạn chữ trần là MỘT anonymous flex item và flexbox ngắt dòng theo kích thước max-content của cả
     item ⇒ câu dài luôn bị đẩy nguyên khối xuống dòng mới, bỏ lại `::before` (dấu `·` / nhãn) đứng
     TRƠ MỘT MÌNH ở cuối dòng trên — vừa xấu vừa tốn nguyên một dòng ~19,5px. Với `block`, `::before`
     là inline nằm ngay đầu câu và câu tự wrap giữa chừng như chữ bình thường. Đo ở sổ quỹ (10 dòng,
     mô tả dài do hệ thống tự sinh): `cuộn` 3,09 → 2,49 ở 390px. Con của meta vốn đều inline
     (span/a/badge) nên vẫn nằm ngang; `gap` mất đi được thay bằng khoảng trắng HTML sẵn có. */
  .list-card td[data-role="meta"] {
    order: 5; flex: 0 1 auto; min-width: 0;
    display: block;
    font-size: var(--fs-caption); color: var(--text-muted);
  }
  /* Link nằm TRONG ô meta (vd link buổi tập ở cột "Liên quan" của sổ quỹ): luật 0.2 phóng mọi
     `.list-card a` lên 44px CAO THẬT — đúng khi link là cả một dòng dữ liệu, nhưng ở đây nó là chú
     thích 13px nằm giữa câu, 44px đó cộng thẳng vào chiều cao thẻ (đo: 6/10 dòng có link ⇒ ~120px).
     Cùng cách xử đã dùng cho tên người ở `list-card__title-block` (5.1) và nút icon-only (P1): trả
     chiều cao NHÌN THẤY về tự nhiên, mở vùng CHẠM bằng pseudo-element phủ 44px. `MB-3` vẫn đạt
     (`smallTargets = 0` sau khi đo lại). */
  .list-card td[data-role="meta"] a:not(.btn) {
    display: inline; min-height: 0; position: relative;
  }
  .list-card td[data-role="meta"] a:not(.btn)::after {
    content: ''; position: absolute;
    top: 50%; left: 0; transform: translateY(-50%);
    width: 100%; height: var(--tap-min);
  }
  /* Nhiều ô meta ⇒ ngăn cách bằng dấu chấm giữa thay vì xuống dòng.
     🔴 `:not(.is-blank)` KHÔNG phải trang trí (thêm ở task 5.1): tổ hợp anh-em `~` so khớp theo DOM,
     không theo những gì đang hiển thị — ô `is-blank` tuy `display: none` vẫn là anh-em đứng trước, nên
     bản cũ gắn dấu `·` MỞ ĐẦU cho ô meta hiện đầu tiên mỗi khi mọi ô meta trước nó đều rỗng (màn Thành
     viên có tới 4 ô meta, 2 trong đó thường rỗng ⇒ lộ ngay). Yêu cầu ô đứng trước phải là meta CÓ nội
     dung thì dấu chấm mới đúng nghĩa "ngăn cách hai giá trị". */
  .list-card td[data-role="meta"]:not(.is-blank) ~ td[data-role="meta"]::before {
    content: '·'; margin-right: .35rem; opacity: .5;
  }
  /* Trường MƠ HỒ vẫn phải giữ nhãn trên mobile (spec § 3.2 liệt kê: Hệ số riêng · Loại chi · Liên
     quan) — "0.50" trần giữa "Nam · Đang sinh hoạt" không tự giải thích được. Nhãn đọc từ chính
     `data-label` mà template đã có sẵn cho nhánh fallback, nên template chỉ cần thêm class: không
     thêm markup ⇒ desktop không đổi một pixel (MB-8).
     🔴 Hai luật vì `::before` là DUY NHẤT một cái cho mỗi phần tử — ô vừa cần nhãn vừa cần dấu ngăn
     cách phải viết gộp cả hai vào một `content`. Đặc hiệu tự xếp đúng thứ tự: luật dưới (0,5,3) thắng
     luật ngăn cách (0,4,3), luật ngăn cách lại thắng luật nhãn trần (0,3,2) — nên ô is-labeled đứng
     đầu lấy nhãn trần, đứng sau một meta có nội dung thì lấy "· Nhãn". */
  .list-card td[data-role="meta"].is-labeled::before {
    content: attr(data-label) ' '; opacity: .8;
  }
  .list-card td[data-role="meta"]:not(.is-blank) ~ td[data-role="meta"].is-labeled::before {
    content: '· ' attr(data-label) ' '; opacity: .8;   /* .8 chứ không .5 của dấu ngăn cách: đây là CHỮ phải đọc được */
  }
  /* ------------------------------------------------------------
     Hàng meta 2 DÒNG (task 4.4, người dùng chốt bố cục) — dùng ở sổ quỹ: dòng trên "Danh mục · Mô
     tả", dòng dưới "Liên quan" bên trái và "Người ghi" sát phải. Ba luật đi cùng nhau, thiếu một cái
     là hỏng:
     (1) Ngắt dòng thứ hai. `tr::after` (order 4) đã là dấu ngắt giữa hàng 1 và hàng meta; `::before`
         của chính `tr` còn trống nên dùng làm dấu ngắt thứ hai ở order 6. 🔴 PHẢI scope bằng class
         trên `<tr>`: `actions` mặc định cũng là order 6, gắn dấu ngắt này cho MỌI hàng sẽ đẩy nút
         thao tác của các màn khác xuống một dòng riêng.
     (2) Ô "Liên quan" `flex: 1 1 0` — flex-basis 0 chứ không phải `auto`. Flexbox quyết định ngắt
         dòng theo kích thước GIẢ ĐỊNH (max-content khi basis là auto): để auto thì "Liên quan" +
         "Người ghi" cộng lại thường vượt bề ngang thẻ ⇒ mỗi ô lại rơi một dòng, đúng thứ ta muốn
         tránh. Basis 0 thì tổng giả định luôn vừa, ô này nở ra chiếm phần còn lại và chữ tự wrap
         BÊN TRONG nó.
     (3) Ô "Người ghi" giữ bề rộng tự nhiên (`flex: 0 0 auto`) + `margin-left: auto` + canh phải.
     Đo được: mỗi hàng bớt một dòng ~19,5px. ------------------------------------------------------ */
  .list-card tbody tr.list-card__meta-2line::before {
    content: ''; order: 6; flex-basis: 100%; height: 0;
  }
  .list-card td[data-role="meta"].list-card__meta-fill { order: 7; flex: 1 1 0; }
  .list-card td[data-role="meta"].list-card__meta-end {
    order: 7; flex: 0 0 auto; margin-left: auto; text-align: right;
  }
  /* Hai ô này KHÔNG lấy dấu `·` mở đầu nữa: dấu đó có nghĩa "ngăn cách hai giá trị cùng dòng", mà ô
     fill luôn MỞ ĐẦU một dòng còn ô end đứng tách hẳn sang mép phải — dấu chấm ở đó chỉ tốn chỗ và
     đọc như dấu đầu dòng. Đặc hiệu (0,5,3) để thắng chắc luật "· + nhãn" (0,5,2) chứ không dựa vào
     thứ tự nguồn — đúng bài học C3 của đợt 0. */
  .list-card tbody tr.list-card__meta-2line td[data-role="meta"].list-card__meta-fill.is-labeled::before,
  .list-card tbody tr.list-card__meta-2line td[data-role="meta"].list-card__meta-end.is-labeled::before {
    content: attr(data-label) ' '; opacity: .8;
  }
  /* Link buổi tập nằm trong ô fill vốn hẹp: để nó tự wrap thì "12/08/2026 19:00–21:00" gãy làm đôi
     giữa giờ, tốn thêm một dòng ở đa số hàng. Nó là một mốc thời gian — đọc theo cụm hoặc không đọc
     được, nên cấm xuống dòng. */
  .list-card td[data-role="meta"].list-card__meta-fill a:not(.btn) { white-space: nowrap; }
  /* ------------------------------------------------------------
     `list-card__meta-grow` (task 5.4) — ô meta chứa một CÂU DÀI (địa chỉ sân) đứng cùng dòng với
     badge trạng thái. Mặc định `meta` là `flex: 0 1 auto`, tức flex-basis = max-content: một địa chỉ
     40–70 ký tự luôn "xin" trọn bề ngang thẻ ⇒ badge (và nút ⋯ ở màn khác) bị đẩy xuống một dòng
     RIÊNG. Basis 0 thì ô này nở ra chiếm phần còn lại sau badge và câu tự wrap BÊN TRONG nó — cùng
     lập luận đã dùng cho `list-card__meta-fill` ở 4.4, chỉ khác là giữ nguyên order 5 (màn này không
     có hàng meta thứ hai). Đo ở màn Sân (10 sân, 360×800): `cuộn` 2,12 → 2,06 và 3/10 thẻ bớt đúng
     một dòng.
     Luật thứ hai trả lại `margin-left: auto` mà (1) của `list-card__title-block` đã tắt: ở màn Thành
     viên status nối ĐUÔI câu meta nên phải sát trái, còn ở đây thẻ có một "rãnh phải" thẳng hàng
     (số tiền · nút ⋯) nên badge phải rơi vào đúng rãnh đó. Không có luật này, sân KHÔNG khai địa chỉ
     (ô meta `is-blank`) sẽ hiện badge lạc sang mép trái. Khi có địa chỉ thì ô meta đã nuốt hết chỗ
     trống nên `auto` không đổi gì. ------------------------------------------------------------ */
  .list-card td[data-role="meta"].list-card__meta-grow { flex: 1 1 0; }
  .list-card td.list-card__meta-grow ~ td[data-role="status"] { margin-left: auto; }
  /* `.small` (Bootstrap, .875em) lồng trong một `meta` td (vd chi tiết cầu ở expense/fragments.html)
     trước đây tính trên nền 15px (fallback `--fs-body`) ⇒ ~13,1px, đạt `MB-2`. Nay nền đã là 13px
     (`--fs-caption` của chính `meta`) ⇒ .875 × 13 ≈ 11,4px, DƯỚI ngưỡng. `meta` đã tự thay việc của
     `.small` (nhỏ hơn + màu muted) nên bên trong nó `.small` không cần thu thêm — ép về đúng 13px
     bằng `inherit`. Desktop không đổi: luật này chỉ sống trong TẦNG MOBILE, `.small` gốc (87,5% của
     ngữ cảnh cha ở bảng thật) không bị chạm. */
  .list-card td[data-role="meta"] .small { font-size: inherit; }
  /* Cùng họ bug trên nhưng KHÔNG cần `.list-card`/`meta` để lộ ra (task 8.3): `<code>` (Bootstrap,
     .875em) lồng trong `.small` (13,125px = 87,5% của --fs-body 15px, đã tự đạt MB-2) tự thu thêm
     87,5% nữa ⇒ 11,48px — dưới ngưỡng, dù bản thân `.small` không sao. Đo ở mã CLB
     (`platform/clb.html`, thẻ mobile `.stat-card` liệt kê "Mã · Chủ CLB · …"). Ép `inherit` như cách
     đã dùng cho `.small` lồng trong `meta` — cùng lý do: phần tử con không cần thu thêm khi ngữ cảnh
     cha đã đủ nhỏ. Không giới hạn `.list-card` vì đây là `<code>` bất kỳ nằm trong `.small` bất kỳ. */
  .small code { font-size: inherit; }
  /* `platform/clb.html` (task 8.3), hàng 1 của thẻ mobile: tên CLB + cụm [badge, nút "⋯"] trong một
     `justify-content-between flex-wrap`. Tên CLB không khai `flex`, mặc định `flex: 0 1 auto` ⇒
     flex-basis = 'auto' = BỀ RỘNG NỘI DUNG (vd 125px), TÍNH TRƯỚC khi co — bộ đóng dòng của flexbox
     quyết định ngắt dòng dựa trên tổng flex-basis (không phải kích thước sau co), nên dù `shrink:1`
     lẽ ra đủ chỗ sau khi co, 125+8(gap)+168(cụm) = 301px vẫn vượt 278px khả dụng ⇒ cụm bị đẩy XUỐNG
     DÒNG DƯỚI dù thừa chỗ (đo: 360×800, tên dài 15 ký tự "CLB Test Nologo", row1 75px thay vì ~30px).
     Cùng lỗi/cùng cách sửa đã dùng cho `list-card__title-linked` (5.4b) và `list-card__meta-grow`
     (5.4): ép flex-basis 0 để bộ đóng gói dòng dùng đúng kích thước co được. Card này không dùng
     `.list-card` (bảng riêng `d-lg-none` tự dựng) nên đặt tên class riêng. */
  .stat-card__title-grow { flex: 1 1 0; min-width: 0; }
  .list-card td[data-role="actions"] {
    order: 6; flex: 0 0 auto; margin-left: auto;
    display: flex; align-items: center; gap: .25rem;
  }
  /* `.btn { min-height: var(--tap-min) }` (P1) phóng cả nút "⋯" icon-only lên 44px CAO THẬT, mà nó lại
     là phần tử cao nhất của hàng 2 ⇒ mỗi thẻ cao thêm ~13px chỉ vì một nút 16px icon (đo ở task 5.1:
     10 dòng = 2,63 màn ở 360px, trượt MB-5). Với nút đã mang `is-icon`, vùng chạm 44×44 do
     pseudo-element lo — đúng nguyên tắc P1 "giữ nguyên kích thước NHÌN THẤY, mở rộng vùng CHẠM" — nên
     ở đây trả chiều cao nhìn thấy về tự nhiên. Scope trong ô `actions` của thẻ dữ liệu: không đụng nút
     icon-only ở topbar/entry-header (task 1.8 đã đo với chiều cao hiện tại). */
  .list-card td[data-role="actions"] .btn-sm.is-icon { min-height: 0; }
  /* 🔴 Bug phát hiện sau task 3.4 (báo lại 2026-08-18): bảng "Xem trước bảng chia tiền"
     (expense/fragments.html) không có ô `meta` — nút "Ghi đè" (`actions`, order 6) mất bạn đồng hành
     nên rơi một mình xuống hàng 2, canh phải giữa khoảng trắng trống trơn (hàng 1 chỉ có
     title+amount, hàng 3-4 là Hệ số/Tiền thô dạng fallback riêng dòng) — trông lạc lõng và bị phóng
     đại kích thước so với xung quanh. Gắn thêm class này CHỈ ở bảng đó để kéo nút lên cùng hàng 1,
     ngay sau amount (order bằng amount ⇒ giữ đúng thứ tự DOM amount-rồi-đến-actions). KHÔNG đổi
     `order` mặc định (6) — bảng "Khoản chi" vẫn dùng dropdown "⋯" cạnh `meta` ở hàng 2 như cũ, đã ổn
     định, không phải sửa. */
  .list-card td[data-role="actions"].list-card__action-inline { order: 2; }
  /* `list-card__action-row1-end` (task 5.10 — màn Danh sách buổi tập). Cũng kéo `actions` lên hàng 1
     như `action-inline`, nhưng đặt nó SAU `status` thay vì trước. Vì sao cần biến thể thứ hai thay vì
     dùng lại `action-inline`: ở màn này ô `actions` là một nút CHỮ ("Xem"), và `order: 2` nhét nó vào
     GIỮA ngày và badge trạng thái ⇒ cắt ngang mạch đọc "ngày → trạng thái" và lệch hẳn thứ tự cột của
     bảng desktop (Trạng thái đứng trước cột nút). `order: 3` cho nó BẰNG `status`, và flexbox xếp các
     item cùng `order` theo THỨ TỰ DOM — mà trong markup `<td>` Trạng thái đứng trước `<td>` nút — nên
     badge tự ra trước, nút theo sau, không cần đụng tới markup desktop. `margin-left: 0` là bắt buộc:
     luật gốc cho `actions` là `margin-left: auto`, để nguyên thì CẢ HAI ô cùng tự đẩy sát phải và tách
     rời nhau ra hai đầu (cùng cái bẫy hai chiều `amount`/`status` đã ghi ở C3b) — nay chỉ `status` giữ
     `auto`, kéo nguyên cụm "badge + nút" về sát phải như một khối.
     Chọn bằng số, không phải thẩm mỹ: 4 biến thể đo ở 10 buổi tập thật, xem
     docs/design/11_Mobile_Progress.md § "Chi tiết 5.10". */
  .list-card td[data-role="actions"].list-card__action-row1-end { order: 3; margin-left: 0; }

  /* Ô rỗng biến mất hoàn toàn. CSS không so khớp được nội dung chữ, nên template PHẢI tự đánh dấu:
     th:classappend="${x == null} ? 'is-blank'".
     🔴 C3 (review cuối đợt 0): luật này TỪNG đứng ngay sau `.list-card tbody tr::after` — TRƯỚC cụm
     `td[data-role="title"|"meta"|"actions"] { display: flex }` ở trên. Cả hai phía cùng độ đặc hiệu
     (0,2,1) ⇒ luật đứng SAU (cụm data-role) thắng theo thứ tự nguồn, `display: none` bị đè lại thành
     `display: flex`, ô rỗng vẫn hiện (đã đo thấy `data-role="meta" class="is-blank"` vẫn in `—`). Di
     chuyển luật xuống ĐÂY (sau toàn bộ cụm data-role, kể cả actions) để phần cascade quyết định theo
     thứ tự nguồn, không cần tăng đặc hiệu hay `!important`. */
  .list-card td.is-blank { display: none; }
  /* Sao kê công nợ cá nhân (debt/chi-tiet.html, 2026-10-02). Thẻ 2 hàng: ngày ↔ số tiền ở hàng 1,
     "Giao dịch" ↔ "Số dư" ở hàng 2 — số dư canh phải ĐÚNG dưới số tiền, đọc dọc như sao kê ngân
     hàng. Mô tả `flex: 1 1 0` (basis 0, cùng lý do `list-card__meta-fill`) để nó nhường chỗ và tự
     wrap bên trong thay vì đẩy số dư xuống một dòng riêng; số dư bỏ dấu `·` ngăn cách (hai ô đã ở
     hai mép, dấu chấm lơ lửng giữa khoảng trắng) và không bao giờ ngắt dòng giữa "Dư" với con số. */
  .list-card .debt-ledger td[data-role="meta"].debt-ledger__desc { flex: 1 1 0; }
  .list-card .debt-ledger td[data-role="meta"].debt-ledger__balance {
    flex: 0 0 auto; margin-left: auto; text-align: right; white-space: nowrap;
  }
  .list-card .debt-ledger td[data-role="meta"]:not(.is-blank) ~ td[data-role="meta"].debt-ledger__balance::before {
    content: attr(data-label) ' ';
  }

  /* Fallback: ô CHƯA đánh data-role giữ mẫu nhãn/giá trị cũ, xếp dưới cùng.
     Cho phép chuyển từng màn một thay vì phải sửa hết 50 template trong một commit.
     🔴 Bug phát hiện sau task 3.4 (báo lại 2026-08-18): scope PHẢI là `tbody`, không phải mọi `td`
     trong `.list-card`. `<tfoot>` cũng chứa `<td>` không mang `data-role` (nhãn "Tổng chi phí"/"Chênh
     lệch làm tròn"), nên khớp CẢ selector này — và vì `:not([data-role])` tính đặc hiệu như một class,
     cụm này (0,2,1) đặc hiệu HƠN `.list-card tfoot td` (0,1,2) nên LUÔN thắng bất kể thứ tự nguồn,
     đè `flex-basis: 100%` (dành cho ô xếp riêng một dòng trong tbody) lên đúng ô label của tfoot ⇒ ô
     đó bị ép co hẹp lại (chia đôi 100%+100% trong một `tr` không có `flex-wrap`), chữ vỡ dòng giữa từ
     ("Chênh lệch làm" / "tròn2.000 ₫" dính liền không có khoảng trắng). Thêm `tbody` vào selector để
     loại trừ tfoot — đúng phạm vi ban đầu của luật (dữ liệu tbody chưa migrate), không đổi hành vi tbody. */
  .list-card tbody td:not([data-role]) {
    order: 7; flex-basis: 100%;
    display: flex; justify-content: space-between; align-items: center;
    gap: .5rem; border: none !important; padding: .3rem 0 !important;
    text-align: right !important; font-size: var(--fs-body);
  }
  .list-card tbody td:not([data-role])[data-label]::before {
    content: attr(data-label);
    font-weight: 600; color: var(--text-muted); text-align: left;
  }
  /* 🔴 `text-align: left` đi CÙNG CẶP với `justify-content` ở đây, không phải trang trí (2026-08-22):
     luật cha đặt `text-align: right !important` cho cặp "nhãn — giá trị", nhưng ô KHÔNG có
     `data-label` chỉ chứa một câu trần (dòng rỗng "Chưa có … nào."). `justify-content: flex-start`
     kéo được cả cụm chữ về trái, còn `text-align` vẫn là right ⇒ câu dài WRAP thì dòng thứ hai bị
     đẩy sang mép phải, đọc thành hai khối lệch nhau. Chỉ lộ khi câu đủ dài để xuống dòng, nên
     "Chưa có giao dịch nào." (sổ cái) không bao giờ thấy còn "Chưa có khoản ứng nào CLB còn nợ."
     (tab Tiền ứng, 360×800) thì thấy ngay. Sửa ở primitive vì 5 màn khác cũng có dòng rỗng dài y hệt
     (Sân · Lịch tập · Buổi tập · Công nợ · Loại cầu) và đang cùng lỗi này. */
  .list-card tbody td:not([data-role]):not([data-label]) {
    justify-content: flex-start; text-align: left !important;
  }

  .list-card tfoot tr {
    display: flex; justify-content: space-between; align-items: center;
    background: var(--sidebar-active-bg);
    border-radius: var(--radius-md); padding: .5rem .75rem; font-weight: 700;
  }
  .list-card tfoot td { display: block; width: auto; border: none !important; padding: 0 !important; }
  .list-card tfoot td:empty { display: none; }
  .list-card tfoot td.money { margin-left: auto; }

  /* Trong chế độ thẻ, danh sách khách gộp phải co lại thành cột phải. */
  .list-card .guest-list { min-width: 60%; }

  /* ============================================================
     P3 — header trang nén. Đo được: trên 844px, nội dung thật của màn buổi tập
     bắt đầu ở ~420px vì topbar + breadcrumb + H1 40px + thanh nút 3 hàng.
     ============================================================ */
  .page-content { padding-top: var(--space-sm); }
  .page-header { margin-bottom: var(--space-md); }
  .page-header__crumb { display: none; }           /* breadcrumb thay bằng nút "‹" (nay ở cùng dòng tiêu đề) */
  /* Hàng trên header (breadcrumb + club-pill) rỗng hoàn toàn trên mobile sau khi nút "‹" chuyển
     xuống page-header__row (task 1.3) — ẩn cả hàng để bỏ luôn 4px margin rỗng đẩy nội dung xuống. */
  .page-header__top { display: none; }
  .page-header__subtitle { display: none; }        /* nội dung phụ, không đáng một dòng trên 390px */
  /* Ngoại lệ có chủ đích: subtitle mang CON SỐ CHÍNH của trang, không phải câu mô tả. Màn Chi tiết
     công nợ là ca đầu tiên — "đang nợ X ₫" / "CLB nợ bạn X ₫" chỉ được in ở đây, bảng bên dưới
     liệt kê từng giao dịch chứ không cộng tổng. Ẩn nó trên điện thoại là giấu mất câu trả lời duy
     nhất mà người mở trang đang đi tìm (người dùng báo 2026-09-07). Chỉ gắn `--keep` khi subtitle
     KHÔNG lặp lại thông tin đã có chỗ khác trên trang. */
  .page-header__subtitle--keep { display: block; }


  /* 🔴 Nút phân trang NHÌN THẤY vẫn 36px — đó là quyết định đã ghi ở khối "nút icon-only" phía
     trên, và vùng chạm 44px đã được mở bằng pseudo-element ở đó. Nhưng khoảng cách giữa hai nút
     chỉ 4px ⇒ tâm cách nhau 40px trong khi mỗi vùng chạm rộng 44px: hai vùng CHỒNG NHAU 4px, và
     cú chạm sát mép rơi sang trang bên cạnh. Nới khoảng cách lên 8px cho tâm cách nhau đúng 44px —
     vừa khít, không còn chồng. Đo ở 360×800 với sổ 10 trang: thanh phân trang rộng 254px trong
     279px có được, vẫn không tràn. */
  .pagination-bar__pages { gap: .5rem; }
  .page-header__top .club-pill { display: none; }  /* tên CLB đã lên topbar */
  .page-header__title { font-size: var(--fs-page); line-height: 1.75rem; }

  /* Nút "‹" nằm CÙNG DÒNG tiêu đề (task 1.3): row xếp từ trái, actions (con duy nhất không phải
     title/back) đẩy về phải bằng margin-left:auto — giữ đúng hành vi cũ (vừa thì nằm phải, dài thì
     rớt xuống dòng). align-items:center để chevron thẳng hàng dọc với dòng tiêu đề (trường hợp phổ
     biến: tiêu đề 1 dòng). */
  .page-header__row { justify-content: flex-start; align-items: center; column-gap: .5rem; }
  .page-header__row > :not(.page-header__title):not(.page-header__back) { margin-left: auto; }

  /* Danh sách buổi tập: Tạo buổi tập đột xuất sát trái, Lọc sát phải.
     Cho phép nhãn xuống dòng khi huy hiệu "Đang lọc" chiếm thêm chỗ trên máy hẹp. */
  .session-list-toolbar { gap: .5rem; flex-wrap: nowrap; }
  .session-list-toolbar > .btn { min-width: 0; white-space: normal; }

  /* Nhóm 4 tab Cài đặt (Chung / Cách chia tiền / Sân / Loại cầu): trên mobile subtitle bị ẩn nên
     chiều cao header do HÀNG TIÊU ĐỀ quyết định. Tab có nút hành động (Thêm sân / Thêm loại cầu /
     Mã CLB) cao 44px vì `.btn` bị phóng lên var(--tap-min); tab "Cách chia tiền" KHÔNG có nút nên
     hàng chỉ cao ~1 dòng tiêu đề (~28px) ⇒ header thấp hơn ~16px ⇒ thanh tab con bên dưới lệch vị
     trí khi chuyển tab. Ghim min-height hàng tiêu đề = tap-min để cả 4 tab cao bằng nhau (tab thiếu
     nút chỉ dư một khoảng trắng nhỏ). Scope qua marker `--reserve` (chỉ có ở 4 trang này) bằng
     :has — codebase đã dùng :has (xem .action-bar phía dưới), không phải cú pháp mới. */
  .page-header:has(.page-header__subtitle--reserve) .page-header__row { min-height: var(--tap-min); }

  .page-header__back {
    display: inline-flex; align-items: center; justify-content: center;
    flex: 0 0 auto;                                 /* trong row: không co, luôn giữ 44px */
    width: var(--tap-min); height: var(--tap-min);
    margin-left: -.5rem;                            /* chevron hơi lấn trái, căn quang học với lề nội dung */
    color: var(--text-muted); text-decoration: none;
  }
  .page-header__back .bi { font-size: var(--icon-md); }
  /* 🔴 Luật "‹ cùng dòng tiêu đề" ở trên chỉ đúng khi tiêu đề NGẮN. Hàng mang `flex-wrap: wrap`, nên
     khi bề rộng tự nhiên của h1 (chữ + huy hiệu trạng thái) lớn hơn chỗ trống cạnh nút thì CẢ h1 rớt
     xuống dòng dưới, bỏ "‹" đứng trơ một hàng riêng — đúng thứ task 1.3 định bỏ. Người dùng báo
     11/09/2026 ở màn buổi tập, 390×844: "Buổi tập 11/09/2026" + "Đang điểm danh" rộng ~382px, chỗ
     cạnh nút chỉ 322px. Kẹp max-width của h1 bằng đúng phần còn lại (nút 44px lấn trái .5rem + gap
     .5rem ⇒ 100% − 44px): flexbox tính chỗ xuống dòng theo kích thước ĐÃ KẸP nên h1 luôn ở cạnh "‹",
     chữ tự xuống dòng BÊN TRONG h1 (huy hiệu sang dòng hai). Nút hành động (nếu có) giữ nguyên hành
     vi cũ: vừa thì nằm phải, không vừa thì rớt xuống dòng. overflow-wrap cho tiêu đề động (tên người)
     không có dấu cách. */
  .page-header__back + .page-header__title {
    max-width: calc(100% - var(--tap-min));
    overflow-wrap: anywhere;
  }
  /* Màn chi tiết buổi tập: tiêu đề "Buổi tập dd/MM/yyyy" + huy hiệu trạng thái LUÔN chung một dòng
     (người dùng chốt 11/09/2026 — luật trên thì cho huy hiệu xuống dòng hai). Ngân sách đo thật ở
     360px, chỗ cạnh nút "‹" = 292px, nhãn dài nhất "Đang nhập chi phí":
       tiêu đề 17px (--fs-strong) ≈ 175 + khoảng trắng ≈ 4 + huy hiệu 11px/đệm 5px = 108,4 ⇒ ≈ 288px.
     Cặp 18px + 11px vẫn dư ~6px (298) nên tiêu đề phải xuống đúng bậc --fs-strong.
     🔴 Huy hiệu 11px là NGOẠI LỆ DUY NHẤT của sàn 13px (MB-2) trong tầng mobile, do người dùng chọn
     khi được đưa ba phương án (bỏ chữ "Buổi tập" / rút gọn nhãn / chữ nhỏ hơn). MobileScaleTest cho
     phép đúng một selector này và khoá cứng con số — đừng mượn nó cho chỗ khác. */
  .buoi-chi-tiet .page-header__back + .page-header__title {
    font-size: var(--fs-strong);
    white-space: nowrap;
  }
  .buoi-chi-tiet .page-header__title .badge-soft {
    font-size: .6875rem;
    padding-left: .3125rem; padding-right: .3125rem;
  }

  /* 🔴 Luật chống wrap tên CLB ở topbar (P3) ĐÃ DỜI ra khối "Topbar tối giản (<992px)" ở phía trên
     tệp — task 1.1. Lý do: topbar mang `d-lg-none` nên sống tới 991,98px, còn khối này chỉ với tới
     767,98px ⇒ dải 768–991px không có bảo vệ (đo được: tên 95 ký tự đội topbar 56px → 103,4px ở
     768px và 991px). Không để lại bản sao ở đây. */

  /* ============================================================
     P4 — thanh hành động phân cấp. Đo được ở màn buổi tập: 5 nút cùng kiểu
     outline, cùng trọng lượng, wrap 3 hàng lởm chởm trên 390px.
     ============================================================ */
  .action-bar { display: flex; gap: var(--space-xs); align-items: stretch; }
  .action-bar__primary { display: block; flex: 1 1 auto; min-width: 0; }
  /* 🔴 Bộ luật dưới đây dùng DESCENDANT (` `), KHÔNG dùng con trực tiếp (`>`) — đổi 07/09/2026.
     Từ khi nút "Đăng ký đi tập"/"Hủy đăng ký của tôi" thành form POST (BR-SESS-13), slot primary có
     thể là `<form><button class="btn">` chứ không còn luôn là một `.btn` đứng trần. Với `>` thì cả
     hai luật bên dưới trượt hết: nút không còn full-width, và không được "thăng cấp" thành nền đặc
     ⇒ ở điện thoại nó tụt về nút outline hẹp giữa hàng, khác hẳn mọi màn khác.
     🔴 `display: contents` trên <form> KHÔNG cứu được: nó đổi cây HỘP, còn `>` khớp trên cây DOM.
     Nới thành descendant an toàn vì slot primary theo thiết kế chỉ chứa ĐÚNG MỘT hành động (xem
     khuôn gọi actionBar ở fragments/ui.html) — không có nút thứ hai nào ở đó để vạ lây. */
  .action-bar__primary .btn { width: 100%; }
  /* "Thăng cấp" nút primary thành nút NỀN ĐẶC chỉ ở mobile — markup giữ NGUYÊN class gốc trước
     task 0.5 (`.btn-outline-info`, xem session/buoi.html) để desktop không đổi một pixel nào
     (MB-8, fix round 1 2026-08-15). 🔴 KHÔNG đổi sang `.btn-outline-primary` dù nghe "đúng ngữ
     nghĩa" hơn — đo bằng CDP mới phát hiện `--bs-info`/`--bs-primary` mà app override ở :root
     KHÔNG áp dụng vào `.btn-outline-*` (Bootstrap dùng giá trị hex gốc `#0dcaf0`/`#0d6efd`, hai màu
     THẬT SỰ khác nhau ở app này — ratio tương phản đo được 1.87 vs 4.3), nên đổi class sẽ đổi màu
     desktop thật, không phải chỉ lý thuyết suông.
     🔴 Vì vậy nút "thăng cấp" phải ép màu trực tiếp qua CSS (không nhờ vào class variant nào), và
     phải ép ĐỦ CẢ BA trạng thái (thường/hover/focus/active) bằng chính selector này (2 class +
     pseudo ⇒ độ đặc hiệu cao hơn `.btn-outline-*:hover` gốc của Bootstrap ở mọi thứ tự nạp CSS) —
     nếu chỉ đè `background-color` ở trạng thái thường, kiểu viền/nền trong suốt gốc của Bootstrap
     sẽ lộ lại khi chạm/focus.
     🔴 C4 (review cuối đợt 0): `--action-primary` ở dark = `--court-400` (#3EC49E, trước đợt đổi
     màu là `--blue-400` #60A5FA) — chọn để làm CHỮ/ICON nổi trên nền tối, KHÔNG phải để làm nền đặc
     cho chữ trắng (trắng/court-400 = 2,19:1 — dưới ngưỡng AA 4,5:1, y như blue-400 (2,54:1) trước đây). Đổi
     sang `--action-primary-solid` (dark = `--court-600`, 7,11:1) — token đã có sẵn, dựng đúng cho
     vai trò "nền đặc + chữ trắng" (xem khai
     báo ở trên, dòng ~272), trước đó bị cố ý bỏ qua vì "ngoài phạm vi task 0.6". LIGHT không định
     nghĩa `--action-primary-solid` ⇒ fallback về `--action-primary`/`--action-primary-hover` y hệt
     trước ⇒ `MB-8` (1440px) không đổi. */
  .action-bar__primary .btn {
    background-color: var(--action-primary-solid, var(--action-primary));
    border-color: var(--action-primary-solid, var(--action-primary));
    color: #fff;
  }
  .action-bar__primary .btn:hover,
  .action-bar__primary .btn:focus,
  .action-bar__primary .btn:focus-visible,
  .action-bar__primary .btn:active {
    background-color: var(--action-primary-solid, var(--action-primary-hover));
    border-color: var(--action-primary-solid, var(--action-primary-hover));
    color: #fff;
  }
  .action-bar__rest { display: none; }          /* bản hàng ngang — chỉ desktop */
  .action-bar__more { display: block; flex: 0 0 auto; }
  /* Nút trong dropdown trông như mục menu, không như nút. */
  .action-bar__menu .btn {
    display: flex; align-items: center; gap: .5rem;
    width: 100%; min-height: var(--tap-min);
    border: 0; border-radius: 0; background: none;
    justify-content: flex-start; text-align: left;
    padding: 0 1rem; color: var(--text);
  }
  .action-bar__menu .btn-outline-danger, .action-bar__menu .btn-danger { color: var(--status-danger); }
  /* Đường kẻ trước nhóm "danger" chỉ có nghĩa khi nhóm đó có nút. Slot danger vắng (màn Thành viên
     truyền ~{}; màn buổi tập khi buổi không hủy được) thì <hr> thành phần tử CUỐI của menu — một
     vạch lơ lửng dưới mục chót. :last-child xét phần tử, không xét khoảng trắng, nên khớp đúng ca đó. */
  .action-bar__menu > .dropdown-divider:last-child { display: none; }
  /* Không có primary ⇒ nút ⋯ tự chiếm trọn bề ngang thay vì đứng lẻ bên phải.
     🔴 KHÔNG dùng :empty — slot vắng (~{}) để lại các node khoảng trắng bên trong
     <div>, nên :empty không bao giờ khớp. :has() kiểm "có phần tử con nào không"
     và đúng trong cả hai trường hợp. */
  .action-bar:not(:has(.action-bar__primary > *)) .action-bar__primary { display: none; }
  .action-bar:not(:has(.action-bar__primary > *)) .action-bar__more { flex: 1 1 auto; }
  .action-bar:not(:has(.action-bar__primary > *)) .action-bar__more > .btn { width: 100%; }

  /* ============================================================
     Đợt 2 — vỏ auth (dang-nhap/dang-ky/quen-mat-khau/da-dat-lai/dat-lai-that-bai/doi-mk-bat-buoc +
     member/nhan-loi-moi). Cả 7 màn dùng chung .auth-shell/.auth-container/.auth-card ⇒ sửa ở ĐÂY ăn
     cả cụm. Số đo tĩnh (Chrome headless + bootstrap.min.css thật) ghi kèm từng luật.
     ============================================================ */
  /* Ô nhập cao ≥ 48px cho dễ gõ một tay trên điện thoại (việc riêng Đợt 2). Đo trước: 46px. */
  .auth-card .form-control { min-height: 48px; }
  /* Nút chính auth khớp chiều cao ô nhập (đo trước: 44px) — CTA/submit đầu trang cần bấm chắc tay. */
  .auth-card .btn, .auth-cta-landing .btn { min-height: 48px; }
  /* Link chữ đứng riêng trong vỏ auth (Tạo tài khoản · Quên mật khẩu? · ← Quay lại · footer) phải đủ
     44px vùng chạm. Đo trước: 17–21px cao. */
  .auth-links a, .auth-alt a, .auth-footer a {
    display: inline-flex; align-items: center; min-height: var(--tap-min);
  }
  /* Logo thương hiệu đầu vỏ auth mang class .auth-brand (KHÔNG phải .app-brand) nên luật pseudo 44px
     của P1 không chạm tới. Đo trước: cao 33,6px. Căn giữa nội dung trong 44px, giữ kích thước nhìn thấy. */
  .auth-brand { min-height: var(--tap-min); }

  /* ============================================================
     BA TRANG CHIA SẺ CÔNG KHAI (`.auth-shell--public`) — nén phần đầu trang.
     /chia-se/{token} có ba loại link: buổi tập · bảng chia tiền · công nợ.
     Người dùng báo 2026-09-07 ở trang công nợ ("thông tin thành viên hơi to, khoảng cách với
     CLB Cầu Lông hơi cao, các button cũng to") rồi yêu cầu áp cùng phong cách cho cả ba.

     Gốc của vấn đề dùng CHUNG cho cả ba: vỏ `.auth-shell`/`.auth-card` dựng cho một FORM ĐĂNG
     NHẬP căn giữa màn hình — 48px hở trên, 32px padding thẻ, nút sàn 48px đều là khoảng thở đúng
     cho ba ô nhập. Ba trang này ngược lại là NỘI DUNG DÀI mà người ta mở ra để ĐỌC, nên cùng
     khoảng thở đó biến thành chỗ trống đẩy dữ liệu xuống dưới nếp gấp.

     🔴 Khoanh bằng `.auth-shell--public` — marker CHỈ có ở ba trang share/cong-khai*.html.
     Đăng nhập/đăng ký/quên mật khẩu, bốn trang lỗi và member/nhan-loi-moi (những màn còn lại dùng
     chung vỏ auth) KHÔNG đổi một pixel nào. Gắn marker vào màn mới ⇒ đọc lại khối này trước.
     ============================================================ */

  /* (1) −44px: khoảng hở trên đầu — chính là "khoảng cách với CLB Cầu Lông" người dùng nêu.
     Nút đổi sáng/tối vẫn ở đúng chỗ: ở trang công nợ nó position:absolute (luật
     .auth-shell:has(.auth-card--list) .auth-topbar phía trên), hai trang kia vẫn fixed — cả hai
     kiểu đều nằm NGOÀI luồng nên không ăn theo padding của vỏ. */
  .auth-shell--public { padding-top: var(--space-md); padding-bottom: var(--space-lg); }
  .auth-shell--public .auth-brand { margin-bottom: var(--space-sm); }

  /* (2) −32px: padding thẻ 32→16 ở CẢ BỐN phía.
     🔴 Luật này THAY THẾ bản chỉ-ngang `.auth-card--list { padding-left/right }` trước đây (đã
     gỡ). Hai bản sinh ra từ hai lần đo khác nhau — bản ngang vì TRÀN NGANG ở 360px (64px padding
     ăn 1/5 bề ngang, chỉ còn 247px nội dung ⇒ tiêu đề "LỊCH SỬ GIAO DỊCH" gãy hai dòng và 3/8
     dòng giao dịch bị đẩy xuống hàng thứ ba; sau khi hạ: 279px, tiêu đề một dòng, 0/10 dòng gãy),
     bản dọc vì CHIỀU CAO — nhưng kết luận trùng nhau và cả ba trang đều cần, nên một luật dễ đọc
     hơn hai. */
  .auth-shell--public .auth-card { padding: var(--space-md); }

  /* (3) Nút: sàn 48px của `.auth-card .btn` là cỡ dành cho nút SUBMIT của form đăng nhập. Trong
     ba trang này nó phóng cả nút PHỤ: "Báo đã chuyển" của TỪNG buổi chưa trả (btn-sm) lên 48px
     làm mỗi dòng cao 119px, nút lọc tháng, nút "Thanh toán"… Trả về đúng sàn chung `--tap-min`
     (44px) — vẫn qua P1/CT-6, bấm bằng ngón cái vẫn chắc tay.
     🔴 Ba class nên độ đặc hiệu (0,3,0) THẮNG `.auth-card .btn` (0,2,0) không phụ thuộc thứ tự
     trong file — khác bản trước dùng `.auth-card--list .btn` (0,2,0), hoà độ đặc hiệu và chỉ
     thắng nhờ đứng sau. Đừng hạ lại về hai class. */
  .auth-shell--public .auth-card .btn { min-height: var(--tap-min); }

  /* (4) Khối sân (fragments/buoi-hero :: sanBox) — dùng chung ở trang buổi tập và bảng chia tiền,
     và là thứ chiếm gần trọn màn hình đầu tiên của hai trang đó. Cùng lý do như (2): 24px padding
     là khoảng thở của một thẻ giới thiệu, thừa với một khối 5 dòng dữ liệu đặc. Luật nền đã hạ
     lg→md từ 16/09/2026, ở đây hạ tiếp một bậc nữa (người dùng yêu cầu thu gọn khối này). */
  .auth-shell--public .share-hero { padding: var(--space-sm); }

  /* Khớp `.auth-shell--public .auth-card { padding: var(--space-md) }` ở trên; phần tràn của khối
     logo + tên CLB vẫn cộng vào lề dưới, nên chỉ khai lại `--cover-de` là cả hai chỗ đọc nó tự
     theo — đừng viết số tràn thành hằng trong `margin` ở đây.
     Logo: tầng nền 5.5rem (88px) → 4rem (64px) ở đây. 🔴 64px còn to hơn cỡ tầng nền CŨ (56px),
     thứ từng bị hạ xuống 3rem vì "56px chiếm gần 1/6 bề ngang thẻ và đẩy tên CLB vào sát mép phải"
     — lần đó phép đo thiếu: bề ngang KHÔNG phải chỗ đau, mỗi 8px logo chỉ lấy đi 8px của ô tên (đo
     ở 390×844: banner rộng 341px, ô tên 230,7px, không ellipsis) và tên dài vẫn ellipsis như luật
     nền. Chỗ đau thật là chiều cao: banner mobile chỉ cao 170,5px nên 48px đọc ra còn nhỏ hơn cả
     bản desktop.
     64px = **37,5%** chiều cao banner, đúng bằng tỉ lệ 192/512 của ảnh vinh danh; ở mốc hẹp nhất
     360×800 là 41,2% mà khối vẫn nằm trong banner (dư 12px) và không cuộn ngang (17/09/2026).
     🔴 Phần tràn giữ ĐÚNG .5rem như tầng nền (trước đây 1rem): với logo mobile 54px, tràn 8px cho
     tên **8,6px thở** dưới viền — cùng lý do đã ghi ở `.share-cover`, đừng nới lại. Logo to lên
     64px (2026-09-17) chỉ NỚI THÊM khoảng thở đó (khối mọc lên, tên đi theo), nên phép đo cũ vẫn là
     cận dưới an toàn — nhưng phần tràn thì đừng đụng. */
  .auth-shell--public .share-cover {
    --cover-de: .5rem;
    margin: calc(-1 * var(--space-md)) calc(-1 * var(--space-md)) calc(var(--space-sm) + var(--cover-de));
  }
  .auth-shell--public .share-cover__clb { left: var(--space-sm); max-width: calc(100% - var(--space-sm) * 2); }
  .auth-shell--public .share-cover__logo { width: 4rem; height: 4rem; border-width: 4px; }
  .auth-shell--public .share-cover__logo--mark .brand-svg { width: 2rem; height: 2rem; }
  /* 1.375rem = 22px (18/09/2026, người dùng: tên CLB cạnh ảnh bìa + logo "to hơn để cân đối"; trước đó
     1rem nằm lọt thỏm cạnh logo 64px). Tên CLB dài vẫn cắt bằng ellipsis như luật nền. */
  .auth-shell--public .share-cover__ten { font-size: 1.375rem; }

  /* ---- Từ đây là luật RIÊNG trang công nợ (.debt-*) ---- */

  /* (5) −117px: khối thành viên đổi từ "hồ sơ xếp dọc căn giữa" (avatar 72px ở giữa, 4 dòng chữ
     xếp chồng — 211px) sang MỘT HÀNG avatar-trái/thông-tin-phải (94px). Cùng chừng ấy thông tin,
     không bỏ chữ nào: tên · "Công nợ tại <CLB>" · badge giới tính/trình độ · email.
     Avatar 4.5rem→3rem: ở dạng hàng nó là dấu nhận diện đi kèm cái tên, không còn là ảnh chân
     dung chiếm trọn bề ngang. 3rem = 48px, vẫn trên sàn --tap-min dù nó không phải vùng chạm. */
  .debt-member { display: flex; align-items: center; gap: var(--space-sm); text-align: left; }
  .debt-member__avatar { flex: none; margin-bottom: 0; }
  .debt-member__avatar .avatar { width: 3rem; height: 3rem; font-size: 1.0625rem; }
  .debt-member__info { flex: 1 1 auto; }
  .debt-member__name { font-size: 1.125rem; }
  .debt-member .auth-card__subtitle { margin-bottom: 0; }
  /* Badge giới tính/trình độ và email dồn CHUNG một dòng (−26px nữa): trong 264px của cột phải,
     badge chiếm ~60px còn email ~150px — để riêng hai hàng là bỏ trống quá nửa mỗi hàng. Cả hai
     đổi sang inline chứ không ghép bằng flex-wrap: người không khai email (hoặc không khai giới
     tính) thì hàng tự co lại, không để lại ô rỗng; và email dài vẫn tự xuống dòng, không tràn. */
  .debt-member__meta { display: inline-flex; vertical-align: middle; margin: var(--space-2xs) .35rem 0 0; }
  .debt-member__email { display: inline; }

  /* (6) −16px: số dư giữ nguyên cỡ chữ (fs-4) — nó LÀ câu trả lời của trang, nén nó là nén nhầm
     chỗ — chỉ thu khoảng cách trên/dưới 16→8. */
  .debt-balance { margin: var(--space-xs) 0; }

  /* (7) Hai nút hành động chính của trang công nợ: khoảng cách giữa/dưới 16→8. */
  .debt-cta > * { margin-bottom: var(--space-xs); }

  /* Nút nổi Góp ý: nhãn "Góp ý" chỉ hiện từ 768px (d-md-inline), nên dưới mốc đó bỏ padding ngang
     để nút tròn đều. Bề rộng/chiều cao vẫn do min-width/min-height ở luật nền lo — không khai lại
     px ở đây (MobileScaleTest cấm bề rộng px cứng trong tầng mobile). */
  .bmh-fab { padding: 0; }

  /* Thanh quảng cáo dán đáy ở điện thoại: cùng MỘT thanh với tablet/desktop, chỉ hạ khổ. Đổi ba
     token là đủ vì mọi luật vị trí (bottom, z-index, khoảng chừa của .page-content và nút Góp ý)
     đều viết theo token — thêm một luật riêng ở đây là mở đường cho hai bản lệch nhau.
     320×50 là khổ banner ngang NHỎ NHẤT của AdSense trên điện thoại: 4 + 13 nhãn + 4 + 50 + 1 = 72,
     làm tròn lên 74. Đây là số cho lần vẽ ĐẦU thôi — AdSense hoàn toàn có thể trả 320×100 (đã gặp
     2026-08-28), lúc đó thanh tự cao 122px và script đo lại rồi ghi vào --sticky-ad-band.
     🔴 --sticky-ad-max KHÔNG phải `none`: để banner giãn trọn bề rộng thì AdSense vẫn trả đúng khổ
     320px rồi bỏ trống phần thừa, nên khối quảng cáo trông rộng hơn hẳn hàng mục của thanh điều
     hướng ngay dưới nó — người dùng báo 2026-08-28. Kẹp đúng 320px và căn giữa (text-align của
     .bmh-ad-sticky) thì hai dải xếp chồng thẳng hàng. Vẫn vừa ở màn hẹp nhất 360px (320 < 352). */
  :root {
    --sticky-ad-height: 50px;
    --sticky-ad-band-init: 74px;
    --sticky-ad-max: 320px;
  }
  /* Banner chiếm trọn bề rộng ở đây nên nhãn căn trái mới thẳng mép quảng cáo; trên máy tính banner
     hẹp hơn thanh và căn giữa, nhãn cũng căn giữa theo (luật nền). */
  .bmh-ad-sticky__label { text-align: left; }


  /* ============================================================
     Đợt 7 — hero buổi tập của trang công khai (share/cong-khai.html · share/cong-khai-bang-chia-
     tien.html · vote/cong-khai.html), người dùng báo lỗi 2026-08-21.
     ============================================================ */
  /* 🔴 Luật `.share-hero__date` từng nằm ở đây (Đợt 7, 2026-08-21) đã LÊN LUẬT NỀN ngày 16/09/2026
     — desktop nay dùng chung 17px, nên bản mobile chỉ còn là dòng trùng lặp. Phép đo gốc ("Chủ
     Nhật, 21/08/2026" ở 360px) được chuyển nguyên văn lên chỗ khai luật nền, đừng khai lại ở đây. */

  /* Cột "Không tham gia" ở màn Quản lý vote (vote/quan-ly.html, task 7.7) — 2 cột `row-cols-2` xếp
     CẠNH NHAU ở MỌI viewport khiến tên dài wrap 2 dòng sát nút "Xóa" trong ~158px/cột ở 390px; đổi
     sang `row-cols-1 row-cols-md-2` (xếp chồng dưới 768px) nên viền dọc `.border-start` cũ không
     còn hợp lý — thay bằng viền NGANG + khoảng cách phía trên khi đứng riêng một hàng. Bản đối ứng
     viền dọc cho ≥768px nằm ở khối desktop phía trên (trước mốc tầng mobile). */
  .vote-result-col { border-top: 1px solid var(--border); padding-top: var(--space-sm); margin-top: var(--space-sm); }

  /* Nút "Xem bản đồ" (fragments/buoi-hero :: sanBox, khối .share-loc) sống lồng trong .auth-card
     (vỏ dùng chung của 3 trang công khai — auth-shell không có bản riêng cho "trang công khai không
     phải form đăng nhập") nên vô tình ăn `.auth-card .btn { min-height: 48px }` — cỡ dành cho nút
     SUBMIT CHÍNH của form đăng nhập/đăng ký, quá to so với một hành động PHỤ nằm trong khối hero dày
     đặc (badge/chip xung quanh chỉ 13-16px). Người dùng báo 2026-08-21. Trả về đúng sàn --tap-min
     chung (44px) — vẫn đủ P1, không còn thừa 4px "nặng tay" so với phần còn lại của hero. */
  .share-hero .btn { min-height: var(--tap-min); }

  /* ============================================================
     Đợt 8 — `.modal-sheet`: modal dạng BOTTOM SHEET trên điện thoại.
     Dùng đầu tiên ở modal "Lọc thành viên" (member/thanh-vien.html), 2026-08-22.
     ============================================================ */
  /* 🔴 Đây là lời đáp cho cái bẫy đã ghi ở task 6.1 phía trên: hộp thoại căn giữa cao hơn viewport
     thì `.modal-footer` rơi HOÀN TOÀN dưới nếp gấp — mở modal ra không thấy nút lưu ở đâu, và
     KHÔNG tiêu chí số nào bắt được (MB-1 không đo được vì modal không tràn ngang). Sheet đóng hẳn
     cả lớp lỗi đó: chiều cao bị kẹp ở 85vh, phần vượt đi vào vùng cuộn của `.modal-body`, còn
     `.modal-footer` là anh em ruột nằm ngoài vùng cuộn nên luôn thấy được.
     Lợi ích thứ hai, đo bằng ngón tay chứ không bằng pixel: nút hành động tụt xuống sát đáy màn —
     vùng ngón cái với tới dễ nhất khi cầm một tay (CT-6). */
  .modal-sheet .modal-dialog {
    position: fixed;
    inset: auto 0 0 0;
    margin: 0;
    max-width: none;
  }

  /* Bo góc CHỈ ở trên: cạnh dưới dính mép màn hình, bo nốt sẽ hở hai khe tam giác ở đáy. */
  /* `dvh` sau `vh`: trên Safari/Chrome điện thoại `vh` tính theo viewport KHI THANH ĐỊA CHỈ ĐÃ THU,
     nên lúc thanh còn hiện, 85vh cao hơn 85% phần đang thấy và đầu sheet chui lên sát mép trên.
     Dòng `vh` ở lại làm dự phòng cho trình duyệt chưa hiểu `dvh`. */
  .modal-sheet .modal-content {
    max-height: 85vh;
    max-height: 85dvh;
    border-radius: var(--radius-card) var(--radius-card) 0 0;
  }

  /* Vùng cuộn DUY NHẤT của sheet. Không có dòng này thì `.modal-content` tự cắt cụt nội dung
     (overflow mặc định) và các nhóm lọc cuối biến mất không dấu vết. */
  .modal-sheet .modal-body { overflow-y: auto; }

  /* Tay nắm — làm bằng pseudo-element, KHÔNG thêm thẻ vào từng modal dùng class này. Nó là tín hiệu
     "khối này trượt lên từ đáy", thứ người dùng điện thoại đọc được ngay mà không cần chữ. Kích
     thước bằng rem: tầng mobile cấm hardcode bề rộng px (MobileScaleTest).
     Từ 12/10/2026 nó không còn thuần trang trí: cả `.modal-header` là vùng NẮM để kéo sheet xuống
     đóng (static/js/sheet-keo.js). `touch-action: none` giao trọn cử chỉ ở vùng này cho script —
     thiếu nó, trình duyệt giành lấy cú kéo để cuộn/kéo-làm-mới trang và bắn `pointercancel` sau
     vài px. Chỉ ở đầu sheet: `.modal-body` vẫn cuộn bình thường. */
  .modal-sheet .modal-header {
    position: relative;
    padding-top: var(--space-lg);
  }
  /* Bộ chọn hẹp hơn luật trên một nấc, khớp đúng bộ chọn trong sheet-keo.js: chỉ sheet mang class
     ở chính `.modal` mới kéo được. */
  .modal.modal-sheet .modal-header { touch-action: none; }

  .modal-sheet .modal-header::before {
    content: '';
    position: absolute;
    top: .375rem;
    left: 50%;
    transform: translateX(-50%);
    width: 2.25rem;
    height: .25rem;
    border-radius: var(--radius-pill);
    background: var(--border-strong);
  }

  /* Trượt lên TỪ ĐÁY thay vì rơi từ trên xuống (Bootstrap mặc định translate(0, -50px)) — hướng
     chuyển động phải khớp với chỗ khối này thật sự nằm, nếu không mắt sẽ đọc sai nó là hộp thoại. */
  .modal-sheet.fade .modal-dialog { transform: translateY(100%); }
  .modal-sheet.show .modal-dialog { transform: none; }

  /* Chip lọc (btn-check) trong sheet — thu theo CHIỀU NGANG, người dùng báo pill quá to (2026-08-22).
     🔴 Chỉ hạ cỡ chữ và đệm ngang; chiều cao vẫn là 44px của `.btn { min-height: var(--tap-min) }`
     phía trên. Hạ chiều cao mới là thứ phá MB-3 — đúng loại lỗi mà đợt 0-7 vừa đi dẹp (31/34/36/38px
     về 44px), nên không đánh đổi. Hẹp lại vẫn giải quyết được vấn đề thật: nhóm "Trạng thái" 5 chip
     rơi từ 3 hàng xuống 2, mỗi nhóm bớt một hàng ⇒ sheet thấp hẳn.
     `--fs-caption` (13px, sàn thấp nhất của thang P1) thay vì `--fs-body`: đây là CHIP lọc đọc lướt
     một cụm từ, không phải nút hành động — cùng vai trò với chip meta mà token này vốn dành cho. */
  .filter-pills .btn {
    font-size: var(--fs-caption);
    padding-left: .5rem;
    padding-right: .5rem;
  }

  /* ============================================================
     `.bmh-confirm` — hộp thoại XÁC NHẬN dùng chung (fragments/confirm.html) trên điện thoại.
     ============================================================ */
  /* 🔴 Người dùng báo 07/09/2026: bản đầu dùng `.modal-sheet` (dính cả hai mép trái/phải và mép
     đáy) nên hộp thoại "tràn viền" — nó đọc như một TRANG MỚI vừa mở ra, chứ không như một câu hỏi
     chen ngang. Sheet đúng cho bảng LỌC (nội dung dài, người dùng ở lại lâu, đó là một tác vụ);
     sai cho một câu hỏi hai lựa chọn. Nay là hộp thoại nổi, chiếm 90% bề ngang — phần nền còn nhìn
     thấy hai bên chính là thứ nói "trang của bạn vẫn ở đó, đây chỉ là một câu hỏi".
     % chứ không px: tầng mobile cấm hardcode bề rộng (MobileScaleTest). */
  .bmh-confirm .modal-dialog {
    width: 90%;
    max-width: 90%;
    margin-left: auto;
    margin-right: auto;
  }

  /* 🔴 xac-nhan-xoa-set (lineup/fragments.html, BR-LINEUP-10) render vào #modalSetDau-body — TỨC
     LÀ BÊN TRONG .modal-content/.modal-dialog của vỏ dùng CHUNG với modal soạn set (bốn ô chọn
     người, Task 7). `.bmh-confirm` ở đó nằm trên <div> bọc form, là CON CHÁU của .modal-content chứ
     không phải tổ tiên — ngược chiều với fragments/confirm.html (nơi lớp này nằm trên chính .modal)
     — nên luật ".bmh-confirm .modal-dialog" phía trên KHÔNG chạm tới được (selector đòi modal-dialog
     là con cháu của .bmh-confirm). Không gắn .bmh-confirm lên #modalSetDau: vỏ đó dùng chung với
     modal soạn set, làm vậy sẽ co luôn dialog bốn-ô-chọn-người xuống 90% — một đổi giao diện không
     ai duyệt cho màn hình khác việc. :has() (đã dùng sẵn nhiều nơi trong file này) để chỉ co hẹp
     dialog LÚC ĐANG hiển thị đúng nội dung xác nhận xóa, không đụng modal soạn set.
     🔴 SỬA 22/09/2026: câu "shell đã mang sẵn modal-dialog-scrollable nên tự cuộn được" từng đứng
     ở đây là SAI — xem luật `#modalSetDau .modal-content > form` ở cuối file (ngoài mọi @media, vì
     lỗi nó chữa không phụ thuộc bề rộng màn). */
  #modalSetDau:has(.bmh-confirm) .modal-dialog {
    width: 90%;
    max-width: 90%;
    margin-left: auto;
    margin-right: auto;
  }

  /* Giữ lại lợi ích DUY NHẤT đáng giữ của sheet mà không cần dính mép: thân cuộn được, còn
     `.modal-footer` là anh em ruột NGOÀI vùng cuộn nên nút xác nhận luôn thấy được — câu giải
     thích dài mấy cũng không đẩy được nút xuống dưới nếp gấp (bẫy đã ghi ở task 6.1). */
  .bmh-confirm .modal-content { max-height: 85vh; }
  .bmh-confirm .modal-body { overflow-y: auto; }

  /* Lề trong rộng hơn mặc định Bootstrap: người dùng yêu cầu chữ đừng dính viền. Giữ CĂN TRÁI —
     câu giải thích ở đây dài 2-4 dòng, căn giữa làm mỗi dòng bắt đầu một chỗ khác nhau, mắt phải
     đi tìm đầu dòng. */
  .bmh-confirm .modal-header,
  .bmh-confirm .modal-body,
  .bmh-confirm .modal-footer {
    padding-left: var(--space-lg);
    padding-right: var(--space-lg);
  }

  /* Nút TO và RÕ: chia đôi hàng, mỗi nút một nửa — thay vì hai nút nhỏ dồn về góc phải, nơi ngón
     cái phải với xa nhất. Chiều cao đã có sàn 44px từ `.btn { min-height: var(--tap-min) }` của
     chính tầng này (`CT-6`). `margin: 0` để gỡ `.modal-footer > * { margin: .25rem }` của
     Bootstrap, nếu không `gap` cộng thêm vào margin cũ thành khe đôi. */
  .bmh-confirm .modal-footer {
    flex-wrap: nowrap;
    gap: var(--space-sm);
  }

  .bmh-confirm .modal-footer > .btn {
    flex: 1 1 0;
    margin: 0;
    font-weight: 600;
  }

  /* Modal "Lưu ảnh vào máy" (achievement/fragments :: bangXuatAnh): cùng dáng hộp thoại nổi 90% như
     `.bmh-confirm` — phần nền còn thấy hai bên — và nút Đóng trải hết hàng cho ngón cái. */
  .honor-luu-anh .modal-dialog {
    width: 90%;
    max-width: 90%;
    margin-left: auto;
    margin-right: auto;
  }

  .honor-luu-anh .modal-footer > .btn {
    flex: 1 1 0;
    margin: 0;
  }

  /* Hàng đối chiếu trong modal "Đăng ký hộ" (BR-VOTE-07): nâng chiều cao chung của ô chọn + hai nút
     icon lên đúng `--tap-min`. Hai nút vốn đã 44px ở tầng này nhờ `.btn { min-height }` phía trên,
     nên nâng ô chọn cho bằng vừa hết lệch mép (người dùng báo 07/09/2026) vừa cho `MB-3` một vùng
     chạm rộng hơn — màn này dùng khi đứng ở sân, một tay cầm vợt.
     Định nghĩa gốc của biến + lý do đầy đủ ở khối "Hàng đối chiếu…" giữa tệp. */
  .doi-chieu-chon { --doi-chieu-cao: var(--tap-min, 44px); }

  /* ============================================================
     TRANG CHỦ CLB — nén khối ở điện thoại (21/09/2026, người dùng yêu cầu)
     Chủ thể: dashboard/tong-quan.html (Owner/Admin) và dashboard/cua-toi.html (Member).
     Mục tiêu đo được: ở 390×844, SỐ TIỀN CHÍNH (quỹ CLB / "Bạn đang nợ") nằm trên nếp gấp
     ngay cả khi có cảnh báo. Trước khi sửa, ba thẻ full-width xếp dọc + khối cảnh báo đẩy nó
     xuống quá 640px.
     🔴 Cả khối nằm trong tầng ≤767,98px ⇒ desktop không đổi một pixel (MB-8).
     ============================================================ */

  /* ① HAI Ô NỬA BỀ NGANG (công nợ · chi phí tháng). `--fs-money` 28px là bậc dành cho ĐÚNG MỘT
     số mỗi màn; ở đây số chính là quỹ CLB, nên hai ô còn lại rơi về `--fs-strong` 17px. Thẻ
     cũng bớt padding và bỏ tile icon 24px — trong ô rộng ~175px, tile chiếm chỗ của chính con
     số nó đang gán nhãn. */
  .stat-card--gon { padding: var(--space-md); }
  .stat-card--gon .stat-card__head { gap: .4rem; margin-bottom: var(--space-xs); }
  .stat-card--gon .stat-card__icon {
    width: 1.5rem; height: 1.5rem; border-radius: 7px;
  }
  .stat-card--gon .stat-card__icon > .bi { font-size: var(--icon-sm); }
  .page-content .stat-card--gon .money-hero {
    font-size: var(--fs-body); line-height: 1.4; letter-spacing: -.01em;
  }
  .stat-card--gon .stat-card__meta { font-size: var(--fs-caption); }
  /* 🔴 THỦ PHẠM của "số tiền đâm ra khỏi khối" (người dùng báo 21/09/2026) — KHÔNG phải con số
     chính mà là badge "CLB nợ lại N người: … ₫" (AC-RPT-02.3) nằm trong thẻ Công nợ. `.badge-soft`
     mang `white-space: nowrap` (đúng cho một badge trạng thái ngắn), nhưng badge NÀY chứa cả một
     câu lẫn một số tiền: đo ở 390×844 nó rộng **219px** trong ô **168px** ⇒ tràn **85px** ra ngoài
     thẻ. Không làm trang cuộn ngang nên không có dấu hiệu nào khác ngoài mắt thường.
     Cho nó xuống dòng trong ô hẹp; `.money` bên trong (nếu có) vẫn giữ nowrap riêng của nó nên
     con số không bao giờ bị bẻ làm đôi. */
  .stat-card--gon .badge-soft {
    display: inline-block; white-space: normal; max-width: 100%;
    text-align: left; line-height: 1.35;
  }
  /* Số hero của trang chủ: 28px chỉ vừa tới 11 ký tự trong thẻ 351px — quỹ 9 chữ số ("−123.450.000 ₫")
     là tràn. Hạ một bậc xuống 24px: vẫn là số to nhất màn (gấp rưỡi hai ô bên dưới), mà dư chỗ.
     Chỉ trong hai màn trang chủ, không đụng `--fs-money` của các màn khác.
     🔴 `:not(.stat-card--gon)` là BẮT BUỘC: không có nó, luật này (đặc hiệu 0,2,1) đè luật của hai
     ô nhỏ ngay phía trên (0,2,0) và cả ba số lại về cùng một cỡ — đúng thứ vừa sửa. */
  .page-content .stat-card:not(.stat-card--gon) .money-hero {
    font-size: 1.5rem; line-height: 2rem;
  }
  /* Giá trị rất dài thu theo bề ngang trên điện thoại; flex sẽ xuống hàng trước khi đè nhãn. */
  .page-content .stat-card.fund-balance .money-hero {
    font-size: clamp(var(--fs-strong), 5vw, var(--fs-title));
  }
  /* Đuôi "· xem ai nợ →" bỏ đi: cả THẺ đã là link (PP-3), mũi tên chỉ lặp lại điều đó bằng
     hai dòng chữ nữa. Số người/số buổi thì giữ — đó là dữ liệu, không phải chỉ dẫn. */
  .stat-card--gon .stat-card__meta-them { display: none; }

  /* ② DÒNG "N việc cần xem" thay cho khối cảnh báo phụ. Là một thẻ <a> mang .app-alert nên
     phải tự bỏ gạch chân và giữ màu chữ của alert. */
  .app-alert--link {
    text-decoration: none;
    color: inherit;
    background: var(--surface-sunken, rgba(148,163,184,.12));
    border-color: var(--border);
    padding: var(--space-sm) var(--space-md);
    font-size: var(--fs-body);
    flex-wrap: nowrap;
    min-height: var(--tap-min);
  }
  .app-alert--link .app-alert__body { min-width: 0; }
  .app-alert--link .arrow { color: var(--action-primary); flex: none; }

  /* ③ THẺ "Bạn đang nợ" (màn Member): hàng Tháng này / Tổng đã đóng là dòng phụ, không phải
     hai cột số ngang hàng với số hero — hạ về --fs-caption và thu hẹp đường kẻ. */
  .the-toi hr { margin-top: var(--space-sm) !important; margin-bottom: var(--space-sm) !important; }
  .the-toi .row { font-size: var(--fs-caption); }

  /* ④ THẺ "Quỹ CLB" của màn Member: số thứ HAI trên màn, nên không giữ bậc money-lg. */
  /* Nhãn và số nằm CÙNG MỘT HÀNG (nhãn trái, số phải) thay vì hai dòng chồng nhau: đây là số
     phụ của màn — số chính là "Bạn đang nợ" ngay trên — nên nó chỉ cần một dòng để liếc qua. */
  .quy-gon {
    padding: var(--space-sm) var(--space-md);
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm);
  }
  .quy-gon .stat-card__label { margin-bottom: 0; }
  .quy-gon .money-lg { font-size: var(--fs-strong); }
  /* Thẻ "Bạn đang nợ" (màn Member) cũng dùng `.money-hero`, nên nó đã theo luật 24px ở trên. */

  /* ⑤ "Buổi tới": ba dòng ngày·giờ·sân không cần padding của một danh sách thao tác — chúng
     chỉ để đọc, mỗi dòng dẫn sang màn buổi tập. Vùng chạm của link vẫn do `.btn`/link-min lo. */
  .buoi-toi { padding: var(--space-md); }
  .buoi-toi > h2 { margin-bottom: var(--space-sm) !important; }
  .buoi-toi .border-bottom:last-child { border-bottom: 0 !important; }
  /* 🔴 MB-3 — vi phạm CÓ SẴN, đo được ở cả bản trước khi nén: link ngày·giờ·sân chỉ cao **23px**
     (247×23 ở 360×800), tức chưa tới nửa vùng chạm tối thiểu, trên đúng màn hay được mở khi đang
     đứng ở sân (CT-6). Nâng bằng `line-height` chứ không `min-height`: link là một dòng chữ, và
     `min-height` trên hộp inline chỉ kéo dài hộp xuống dưới, chữ vẫn nằm ở dòng đầu.
     `padding` của hàng bị triệt tiêu cùng lúc, nếu không mỗi dòng phình lên 60px: vùng chạm 44px
     ĐÃ bao trọn khoảng thở của dòng. Nút "Tôi sẽ đi" (`.btn`) đã 44px sẵn nên đứng ngoài luật.
     🔴 `min-height` + flex chứ KHÔNG `line-height: 44px`: ở màn Member, nút "Tôi sẽ đi" đứng cạnh
     nên chuỗi ngày·giờ·sân xuống 2 dòng — `line-height` nhân đôi theo số dòng, đo được hàng cao
     88px với hai dòng chữ trôi xa nhau. `min-height` chỉ nâng khi CẦN nâng. */
  .buoi-toi .py-2 { padding-top: .25rem !important; padding-bottom: .25rem !important; }
  .buoi-toi a[href]:not(.btn) {
    display: flex; align-items: center; min-height: var(--tap-min);
  }
  /* Hồ sơ CLB: điện thoại dùng avatar lớn hơn bậc 991,98px (dời từ khối @media mồ côi cạnh
     .club-profile — MobileScaleTest). */
  .club-profile__avatar { margin-top: -4rem; }
  .club-profile__avatar-button { width: 5rem; height: 5rem; }
}


/* ============================================================
   GÓI CLB (FREE / PRO) — ô quảng cáo, banner mời nâng cấp, huy hiệu PRO
   BR-TIER-01..05. Xem fragments/ads.html và fragments/nang-cap.html.
   ============================================================ */

/* 🔴 Ô quảng cáo không có gì để hiện thì BIẾN MẤT HẲN, không để lại khung rỗng. Đặt bởi script
   trong fragments/ads.html khi AdSense trả unfilled, khi trình chặn quảng cáo nuốt script, hoặc
   khi chạy local. Đo 2026-08-28 ở 390×844: khung rỗng ăn 391px tính từ nút hành động cuối cùng —
   46% màn hình không có lấy một pixel nội dung. display:none kéo theo cả nhãn "Quảng cáo" lẫn mọi
   khoảng chừa, nên trang co lại đúng như chưa từng có ô nào (đo lại sau khi thu: còn 76px, vừa
   bằng chỗ của thanh điều hướng).
   🔴 Trạng thái này KHÁC hẳn "ẩn quảng cáo đã tải" mà BR-TIER-05 cấm: ở đây không có quảng cáo nào
   được tải, không ai bị theo dõi, và Google chính là bên khuyến nghị thu ô unfilled. */
.bmh-ad-sticky[data-bmh-ad-trong],
.bmh-ad-side[data-bmh-ad-trong] {
  display: none !important;
}


/* Huy hiệu PRO cạnh tên CLB. Dùng chính màu thương hiệu (--court-500) chứ không phải vàng/cam:
   PRO ở đây không phải huy chương, nó là trạng thái của CLB. */
.bmh-pro-badge {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  padding: .1rem .4rem;
  border-radius: var(--radius-sm);
  background: var(--sidebar-active-bg);
  color: var(--sidebar-active-text);
  font-size: var(--fs-caption, .8125rem);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: .02em;
}

/* Huy hiệu FREE — cặp song sinh của .bmh-pro-badge, CÙNG hình dạng và chỉ khác màu (BR-TIER-12).
   Cùng hình dạng là có chủ đích: hai huy hiệu đứng cạnh nhau ở danh sách CLB, khác cỡ hay khác bo
   góc thì trông như hai loại thông tin khác nhau chứ không phải hai giá trị của cùng một thứ.
   Màu muted vì FREE là mặc định, không phải thành tựu — nó chỉ trả lời "gói nào", không quảng cáo.
   🔴 KHÔNG gộp thành .bmh-tier-badge--pro/--free: .bmh-pro-badge đang được 3 template dùng, đổi
   tên chỉ để cho đối xứng là sửa những chỗ không liên quan tới việc đang làm. */
.bmh-free-badge {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  padding: .1rem .4rem;
  border-radius: var(--radius-sm);
  background: var(--badge-slate-bg, rgba(100, 116, 139, .16));
  color: var(--badge-slate-fg);
  font-size: var(--fs-caption, .8125rem);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: .02em;
}

/* Banner mời nâng cấp ở trang chủ CLB. Là nội dung PERSISTENT (không tự ẩn) nên đúng chỗ dùng
   .app-alert theo luật toast của templates/CLAUDE.md — nhưng vẽ riêng vì nó có nút hành động và
   phải xuống hàng gọn ở 390px. */
.bmh-upsell {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--action-primary);
  border-radius: var(--radius-card);
  padding: var(--space-md);
  margin-bottom: var(--space-md);
}
.bmh-upsell__icon {
  flex: 0 0 auto;
  font-size: 1.5rem;
  color: var(--action-primary);
  line-height: 1;
}
.bmh-upsell__body { flex: 1 1 16rem; min-width: 0; }
.bmh-upsell__title { font-weight: 600; color: var(--text-strong); margin-bottom: .15rem; }
.bmh-upsell__desc  { font-size: var(--fs-caption, .8125rem); color: var(--text-muted); margin: 0; }
/* Ở 390px nút phải chiếm trọn hàng của nó: chen cạnh đoạn mô tả thì cả hai cùng vỡ. */
@media (max-width: 575.98px) {
  .bmh-upsell__action, .bmh-upsell__action .btn { width: 100%; }
}

/* Thẻ FREE / PRO ở màn Nâng cấp. PRO chứa lựa chọn thời hạn và một giá cập nhật theo lựa chọn. */
.bmh-plan {
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-lg);
}
.bmh-plan--pro { border-color: var(--action-primary); box-shadow: var(--elevation-1); }
.bmh-plan__name { font-size: var(--fs-caption, .8125rem); text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); }
.bmh-plan__price { font-size: 1.5rem; font-weight: 700; color: var(--text-strong); }
.bmh-plan__list { list-style: none; padding: 0; margin: var(--space-md) 0 0; }
.bmh-plan__list li { display: flex; gap: .5rem; padding: .35rem 0; }
.bmh-plan__list .bi { flex: 0 0 auto; margin-top: .15rem; }
.bmh-plan__yes { color: var(--money-positive); }
.bmh-plan__no  { color: var(--text-disabled); }

/* Trang thanh toán gói PRO (BR-PAY-13).
   📱 Cạnh QR đặt bằng `rem` chứ KHÔNG px cứng (MobileScaleTest gác): 15rem ≈ 240px ở cỡ chữ gốc,
   nhưng co theo khi người dùng phóng chữ hệ thống, và `width: 100%` giữ nó không tràn ở 360px.
   Nền trắng + padding là yêu cầu của chính mã QR — máy quét đọc không ra nếu nền tối hoặc mã dính
   sát mép. */
.bmh-qr {
  width: 100%;
  max-width: 15rem;
  height: auto;
  border-radius: var(--radius-md, 8px);
  background: #fff;
  padding: .5rem;
}
/* Vùng chạm ngón tay (CT-6, --tap-min): btn-sm của Bootstrap chỉ ~31px — quá nhỏ để bấm bằng ngón
   tay khi đang đứng ở sân. 44px là ngoại lệ px duy nhất MobileScaleTest cho phép. */
.bmh-copy { min-width: 44px; min-height: 44px; }

/* Upgrade and checkout reuse the club's court-green semantic palette in both themes. */
.bmh-upgrade, .bmh-checkout {
  --upgrade-surface: color-mix(in srgb, var(--surface) 88%, var(--action-primary) 12%);
  --upgrade-highlight: color-mix(in srgb, var(--surface) 94%, var(--action-primary) 6%);
  --upgrade-icon-surface: var(--bs-primary-bg-subtle);
  --upgrade-border: var(--bs-primary-border-subtle);
  --upgrade-ink: var(--action-primary-solid, var(--action-primary));
  --upgrade-accent: var(--action-primary);
  --upgrade-icon-ink: var(--action-primary);
  --upgrade-on-ink: #fff;
  --upgrade-cancel-ink: var(--badge-red-fg);
}
.bmh-upgrade, .bmh-checkout { max-width: 68rem; }
.bmh-upgrade .page-header { margin-bottom: .75rem; }
.bmh-upgrade__intro { text-align: center; padding: 1rem 1rem 1.25rem; }
.bmh-upgrade__eyebrow, .bmh-plan__eyebrow, .bmh-checkout__eyebrow {
  display: inline-flex; align-items: center; gap: .5rem; color: var(--text-muted);
  font-size: .8125rem; font-weight: 700; letter-spacing: .1em;
}
.bmh-upgrade__intro h2 { color: var(--text-strong); font-size: clamp(1.75rem, 2.5vw, 2.35rem); line-height: 1.2; letter-spacing: -.04em; font-weight: 650; margin: .4rem 0 .35rem; }
.bmh-upgrade__intro p { color: var(--text-muted); margin: 0; }
.bmh-upgrade__plans { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; align-items: stretch; }
.bmh-upgrade .bmh-plan { min-height: 31rem; padding: clamp(1.4rem, 3vw, 2.25rem); border-radius: 1.2rem; }
.bmh-upgrade .bmh-plan--pro { background: var(--upgrade-surface); border-color: var(--upgrade-border); box-shadow: var(--elevation-2); }
.bmh-upgrade .bmh-plan__eyebrow .bi { font-size: 1rem; color: var(--upgrade-accent); }
.bmh-upgrade .bmh-plan__name { color: var(--text-strong); font-size: 2rem; line-height: 1.1; letter-spacing: -.04em; font-weight: 700; text-transform: none; margin: 1.2rem 0 .3rem; }
.bmh-upgrade .bmh-plan__description { color: var(--text-muted); margin: 0 0 1.5rem; min-height: 1.5rem; }
.bmh-upgrade .bmh-plan__price { font-size: 2.25rem; font-weight: 650; letter-spacing: -.045em; line-height: 1.2; }
.bmh-upgrade .bmh-plan__price small { font-size: .88rem; font-weight: 400; letter-spacing: 0; color: var(--text-muted); }
.bmh-plan__rule { height: 1px; background: var(--border); margin: 1.5rem 0 1.2rem; }
.bmh-plan__includes { font-size: .82rem; font-weight: 700; color: var(--text-strong); margin: 0; }
.bmh-upgrade .bmh-plan__list { margin: .55rem 0 0; }
.bmh-upgrade .bmh-plan__list li { padding: .42rem 0; line-height: 1.4; }
.bmh-upgrade .bmh-plan__yes { color: var(--upgrade-accent); }
.bmh-plan__footer { margin-top: auto; padding-top: 1.5rem; }
.bmh-plan__current { display: flex; align-items: center; justify-content: center; min-height: 3rem; border: 1px solid var(--border); border-radius: .7rem; color: var(--text-muted); font-weight: 600; gap: .5rem; }
.bmh-plan__periods { border: 0; padding: 0; margin: 0 0 1.3rem; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .5rem; }
.bmh-plan__periods legend { float: none; grid-column: 1 / -1; width: 100%; font-size: .82rem; font-weight: 700; margin-bottom: .6rem; }
.bmh-plan__period { position: relative; cursor: pointer; min-width: 0; }
.bmh-plan__period input { position: absolute; opacity: 0; inset: 0; width: 100%; height: 100%; cursor: pointer; }
.bmh-plan__period span { display: flex; align-items: center; justify-content: center; min-height: 3rem; border: 1px solid var(--upgrade-border); border-radius: .7rem; background: var(--surface); font-weight: 600; transition: border-color .15s, background .15s; }
.bmh-plan__period:hover span { border-color: var(--upgrade-accent); }
.bmh-plan__period input:checked + span { color: var(--upgrade-on-ink); border-color: var(--upgrade-ink); background: var(--upgrade-ink); }
.bmh-plan__period input:focus-visible + span { outline: 3px solid var(--action-primary); outline-offset: 3px; }
.bmh-plan__monthly { color: var(--text-muted); font-size: .85rem; margin: .35rem 0 0; }
.bmh-upgrade .bmh-plan form, .bmh-plan__offline { display: flex; flex: 1; flex-direction: column; }
.bmh-plan__cta { width: 100%; min-height: 3.1rem; display: flex; align-items: center; justify-content: center; gap: .75rem; }
.bmh-upgrade__footnote { display: flex; justify-content: center; gap: .5rem; color: var(--text-muted); font-size: .85rem; text-align: center; margin: 1.7rem 0 2rem; }
.bmh-checkout__layout { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(16rem, .85fr); gap: 1.25rem; margin-top: 1.25rem; align-items: start; }
.bmh-checkout__main, .bmh-checkout__summary { background: var(--surface); border: 1px solid var(--border); border-radius: 1.1rem; padding: clamp(1.2rem, 3vw, 2rem); }
.bmh-checkout__heading { display: flex; gap: 1rem; align-items: flex-start; }
.bmh-checkout__step { display: grid; place-items: center; flex: 0 0 2.2rem; height: 2.2rem; border-radius: 50%; background: var(--upgrade-ink); color: var(--upgrade-on-ink); font-size: .8125rem; font-weight: 700; }
.bmh-checkout__heading h2 { color: var(--text-strong); font-size: 1.35rem; font-weight: 700; margin: .1rem 0 .3rem; }
.bmh-checkout__heading p { color: var(--text-muted); font-size: .9rem; margin: 0; }
.bmh-checkout__qr-wrap { text-align: center; margin: 1.4rem auto .9rem; }
.bmh-checkout__qr-wrap .bmh-qr { border: 1px solid var(--border); box-shadow: 0 4px 22px rgba(0,0,0,.05); }
.bmh-checkout__qr-wrap p { margin: .6rem 0 0; color: var(--text-muted); font-size: .8125rem; }
.bmh-checkout__separator { display: flex; align-items: center; gap: .8rem; color: var(--text-muted); font-size: .8125rem; margin: 1.2rem 0; }
.bmh-checkout__separator::before, .bmh-checkout__separator::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.bmh-checkout__details { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: .65rem; }
.bmh-checkout__detail { display: flex; align-items: center; min-width: 0; gap: .7rem; padding: .8rem; border: 1px solid var(--border); border-radius: .7rem; }
.bmh-checkout__detail--important { grid-column: 1 / -1; background: var(--upgrade-highlight); border-color: var(--upgrade-border); }
.bmh-checkout__detail-icon { display: grid; place-items: center; flex: 0 0 2rem; height: 2rem; border-radius: .5rem; background: var(--upgrade-icon-surface); color: var(--upgrade-icon-ink); }
.bmh-checkout__detail > div { min-width: 0; flex: 1; }
.bmh-checkout__label { display: block; color: var(--text-muted); font-size: .8125rem; }
.bmh-checkout__detail strong { display: block; overflow-wrap: anywhere; font-size: .92rem; color: var(--text-strong); }
.bmh-checkout__detail .bmh-copy { flex: 0 0 44px; }
.bmh-checkout__notice { display: flex; gap: .6rem; margin: 1rem 0 0; color: var(--text-muted); font-size: .82rem; line-height: 1.45; }
.bmh-checkout__notice .bi { color: var(--upgrade-accent); }
.bmh-checkout__summary { background: var(--upgrade-surface); }
.bmh-checkout__summary-icon { display: grid; place-items: center; width: 2.6rem; height: 2.6rem; margin-bottom: 1rem; border-radius: .7rem; background: var(--upgrade-ink); color: var(--upgrade-on-ink); }
.bmh-checkout__summary h2 { font-size: 1.7rem; color: var(--text-strong); margin: .35rem 0 1.5rem; }
.bmh-checkout__summary-row, .bmh-checkout__summary-total { display: flex; justify-content: space-between; gap: 1rem; padding: .7rem 0; }
.bmh-checkout__summary-row { color: var(--text-muted); font-size: .9rem; }
.bmh-checkout__summary-row b { color: var(--text-strong); }
.bmh-checkout__summary-total { align-items: baseline; border-top: 1px solid var(--upgrade-border); margin-top: .7rem; padding-top: 1.2rem; color: var(--text-strong); }
.bmh-checkout__summary-total strong { font-size: 1.5rem; white-space: nowrap; }
.bmh-checkout__expiry { border-top: 1px solid var(--upgrade-border); padding-top: 1.2rem; margin: 1.2rem 0; font-size: .82rem; line-height: 1.6; }
.bmh-checkout__expiry .bi { color: var(--upgrade-accent); }
.bmh-checkout__secure { display: flex; gap: .5rem; font-size: .8125rem; color: var(--text-muted); margin: 0; }
.bmh-checkout__qr-link { display: none; }
.bmh-checkout__summary-actions { border-top: 1px solid var(--upgrade-border); margin-top: 1.25rem; padding-top: 1.25rem; }
.bmh-checkout__cancel-button {
  --bs-btn-color: var(--upgrade-cancel-ink);
  --bs-btn-border-color: var(--upgrade-cancel-ink);
  --bs-btn-hover-color: var(--surface-sunken);
  --bs-btn-hover-bg: var(--money-negative);
  --bs-btn-hover-border-color: var(--money-negative);
  --bs-btn-active-color: var(--surface-sunken);
  --bs-btn-active-bg: var(--money-negative);
  --bs-btn-active-border-color: var(--money-negative);
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  width: 100%; min-height: 2.9rem; font-weight: 600;
}
.bmh-checkout__cancel-button:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; }
.bmh-checkout__ended { margin: 1rem 0 2rem; }
.bmh-checkout__ended { display: flex; gap: .6rem; flex-wrap: wrap; }
@media (max-width: 767.98px) {
  .bmh-upgrade__intro { padding: .35rem .5rem 1rem; }
  .bmh-upgrade__intro h2 { font-size: 1.65rem; line-height: 1.2; margin: .4rem 0 .3rem; }
  .bmh-upgrade__intro p { font-size: .875rem; }
  .bmh-upgrade__plans, .bmh-checkout__layout { grid-template-columns: 1fr; }
  .bmh-upgrade .bmh-plan { min-height: 0; }
  .bmh-checkout__layout { gap: .75rem; margin-top: .75rem; }
  .bmh-checkout__summary { grid-row: 1; padding: 1rem; }
  .bmh-checkout__summary-icon, .bmh-checkout__summary-row--method, .bmh-checkout__secure { display: none; }
  .bmh-checkout__summary h2 { font-size: 1.125rem; margin: .2rem 0 .3rem; }
  .bmh-checkout__summary-row { padding: .2rem 0; }
  .bmh-checkout__summary-total { padding: .6rem 0 0; margin-top: .3rem; }
  .bmh-checkout__summary-total strong { font-size: 1.35rem; }
  .bmh-checkout__expiry { border-top: 0; padding: 0; margin: .5rem 0 .2rem; }
  .bmh-checkout__qr-link { display: inline-flex; align-items: center; gap: .35rem; color: var(--bs-primary-text-emphasis); font-size: .8125rem; font-weight: 600; text-decoration: none; }
  .bmh-checkout__qr-link:hover { text-decoration: underline; }
  .bmh-checkout__qr-link:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; }
  .bmh-checkout__summary-actions { margin-top: .55rem; padding-top: .55rem; }
  .bmh-checkout__main { padding: 1rem; }
  .bmh-checkout__qr-wrap { scroll-margin-top: 1rem; margin-top: .8rem; }
}
@media (max-width: 575.98px) {
  .bmh-checkout__details { grid-template-columns: 1fr; }
  .bmh-checkout__detail--important { grid-column: auto; }
  .bmh-checkout__summary-total strong { font-size: 1.3rem; }
}

/* Màn báo thanh toán thành công (BR-PAY-13).
   📱 Dấu tick to hẳn vì đó là thứ người vừa chuyển khoản cần thấy TRƯỚC cả chữ. Cỡ bằng `rem` chứ
   không px cứng (MobileScaleTest gác) nên nó co giãn theo cỡ chữ hệ thống.
   Ba dòng đối chiếu dùng grid tự co: một cột ở điện thoại, ba cột từ máy tính bảng trở lên — không
   cần media query riêng, và không hardcode bề rộng cột. */
.bmh-pay-done { text-align: center; }
.bmh-pay-done__icon { font-size: 3.5rem; line-height: 1; color: var(--money-positive); }
.bmh-pay-done__info {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-md);
  margin: var(--space-lg) 0 0;
  padding: var(--space-md) 0 0;
  border-top: 1px solid var(--border);
}

/* ============================================================
   `.bmh-confirm` — hộp thoại xác nhận dùng chung (fragments/confirm.html). Phần không phụ thuộc
   kích thước màn; các override cho điện thoại nằm ở tầng mobile phía trên.
   ============================================================ */
/* 🔴 Dòng ô tích luôn màu ĐỎ (người dùng yêu cầu 07/09/2026), kể cả trong hộp thoại không đánh dấu
   `danger` như "Tắt Lịch tập". Đúng về nghĩa chứ không chỉ về màu: ô tích trong khuôn này luôn là
   thứ MỞ RỘNG hậu quả phá huỷ của hành động — ở màn Tắt nó biến một thao tác tạm ngừng (bật lại
   được) thành một thao tác có xoá dữ liệu. Chính vì nó không giống phần còn lại của hộp thoại nên
   nó phải trông khác.
   `--money-negative` chứ không `--bs-danger`: token này đã có bản riêng cho theme tối (#F87171),
   nên chữ vẫn đọc được ở cả hai theme. */
.bmh-confirm .form-check-label {
  color: var(--money-negative);
  font-weight: 600;
}

/* 🔴 #modalSetDau (soạn set / xác nhận xóa set — BR-LINEUP-07..10): nối lại chuỗi flex mà
   `modal-dialog-scrollable` dựa vào.

   Bootstrap chỉ cho `.modal-body` cuộn khi nó là CON TRỰC TIẾP của `.modal-content`: content là
   flex column có `max-height:100%; overflow:hidden`, body là flex item co được. Thân modal này do
   htmx nạp vào và luôn có một lớp ở giữa — `<form>` với soan-set, `.bmh-confirm > form` với xác
   nhận xóa — nên lớp ở giữa nhận NGUYÊN chiều cao ruột (min-height:auto ⇒ không co), body không
   còn gì để cuộn, và `overflow:hidden` của content XÉN phần thừa.

   ĐO THẬT trên repro dựng từ chính bootstrap 5.3.3 + app.css (22/09/2026), viewport 740×420 —
   điện thoại xoay ngang, cỡ thật, không phải trường hợp bịa: content clientH 362 / scrollH 680 ⇒
   mất 318px, nút "Lưu set" ở y=649 NGOÀI màn hình và không cuộn tới được. Sau luật này: content
   scrollH == clientH, body cuộn 540→~250px, nút Lưu luôn thấy.

   Vì sao không đặt trong @media: mắt xích đứt là chuyện cấu trúc DOM, không phải bề rộng màn —
   một cửa sổ máy tính thấp (1000×500) vỡ y hệt. Phần xác nhận xóa set có thêm lớp
   `.bmh-confirm` nên vẫn cần selector riêng; form trực tiếp của các modal khác dùng luật chung
   ngay bên dưới. */
#modalSetDau .modal-content > form,
#modalSetDau .modal-content > .bmh-confirm,
#modalSetDau .modal-content > .bmh-confirm > form {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  max-height: 100%;
  min-height: 0; /* mặc định min-height:auto chặn đúng phép co mà cả luật này dựa vào */
  overflow: hidden;
}

#modalSetDau .modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

#modalSetDau .modal-header,
#modalSetDau .modal-footer { flex: 0 0 auto; }

/* Modal có form bọc header/body/footer cần nối chuỗi flex của Bootstrap. Nếu form giữ
   min-height:auto, modal-dialog-scrollable cắt phần cuối form và nút Lưu không thể bấm.
   Áp cho cả form Giải đấu và các modal cùng cấu trúc trong app. */
.modal-dialog-scrollable .modal-content > form {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  max-height: 100%;
  min-height: 0;
  overflow: hidden;
}
.modal-dialog-scrollable .modal-content > form > .modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.modal-dialog-scrollable .modal-content > form > .modal-header,
.modal-dialog-scrollable .modal-content > form > .modal-footer { flex: 0 0 auto; }

/* 🔴 Ép CĂN TRÁI, không phải thừa: modal ở màn Lịch tập nằm trong `<td class="text-end">` (ô Thao
   tác), và `text-align` KẾ THỪA xuống mọi con — kể cả một `.modal` position:fixed phủ toàn màn.
   Người dùng báo 07/09/2026: chữ trong hộp thoại căn phải. Chữa ở chính component chứ không ở chỗ
   đặt nó: đây là khuôn dùng chung, lần sau ai đó thả nó vào một ô `text-center`/`text-end` khác thì
   lỗi lặp lại y hệt mà không ai nhớ vì sao. */
.bmh-confirm .modal-content { text-align: left; }

/* Ô tích to hơn hẳn mặc định 1em của Bootstrap (người dùng yêu cầu): đây là ô quyết định có xoá
   hàng chục buổi tập hay không, bấm bằng ngón tay ngoài sân thì 16px là quá nhỏ. Nhãn đi kèm đã
   phủ trọn dòng (nối bằng for/id) nên vùng chạm thật còn rộng hơn con số này.
   `padding-left` của `.form-check` và `margin-left` âm của input phải đổi CÙNG NHAU — Bootstrap
   dùng cặp 1.5em/-1.5em để kéo ô ra lề; giữ nguyên một vế thì ô đè lên chữ. */
.bmh-confirm .form-check { padding-left: 2.25rem; min-height: 1.75rem; }

.bmh-confirm .form-check-input {
  width: 1.5rem;
  height: 1.5rem;
  margin-left: -2.25rem;
  margin-top: .0625rem;
}

/* ══════════════════════════════════════════════════════════════════════════
   Thí điểm điều hướng nhanh (07/09/2026) — xem static/js/fast-nav.js.
   Chỉ có tác dụng ở trang khai #page-content[data-bmh-fast-nav]; JS không bao
   giờ bỏ `hidden` ở các trang khác nên khối này im lặng ở toàn bộ phần còn lại.
   ══════════════════════════════════════════════════════════════════════════ */

/* Vạch tiến trình mảnh dán đỉnh màn hình. `position: fixed` + `top: 0` chứ không
   dính theo thanh trên: ở mobile thanh trên cuộn đi mất, mà chỉ báo "đang tải"
   thì phải thấy được ở mọi vị trí cuộn.
   z-index cao hơn .modal (1055) và .offcanvas (1045) của Bootstrap — điều hướng
   vẫn xảy ra khi đang mở offcanvas Menu, và lúc đó thanh bị che là vô dụng. */
#bmh-nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  z-index: 2000;
  background: var(--action-primary);
  /* Biến đổi từ mép TRÁI: mặc định scaleX nở đều hai bên, nhìn ra thành một vạch
     phình ở giữa màn chứ không phải thanh tiến trình. */
  transform-origin: 0 50%;
  transform: scaleX(0);
  /* Chỉ animate `transform` (chạy trên compositor, không đụng layout/paint) — đây
     là lý do vạch này không tốn main thread, tức là nó vẫn mượt ĐÚNG LÚC main
     thread đang bận swap DOM, thời điểm duy nhất nó cần mượt. */
  transition: transform .16s linear;
  pointer-events: none;
}

/* Cú chạm cuối: chạy nốt sang 100% rồi mờ đi. `transition` dài hơn nhịp bò để
   đoạn kết thúc trông là một cú trượt dứt khoát, không phải một nhịp bò nữa. */
#bmh-nav-progress.is-xong {
  transition: transform .18s ease-out, opacity .22s ease-in .06s;
  opacity: 0;
}

/* Người đã bật "giảm chuyển động" ở hệ điều hành: giữ chỉ báo (nó truyền thông
   tin, không phải trang trí) nhưng bỏ hoạt cảnh bò — vạch nhảy thẳng tới vị trí. */
@media (prefers-reduced-motion: reduce) {
  #bmh-nav-progress { transition: none; }
  #bmh-nav-progress.is-xong { transition: opacity .01s; }
}

/* View Transition khi swap nội dung tab (hx-swap="... transition:true").
   🔴 Rút xuống .18s: mặc định của trình duyệt là .25s, đủ để một thao tác vốn
   tức thì bị CHẬM ĐI thấy rõ. Hoạt cảnh chuyển trang chỉ đáng tiền khi nó ngắn
   hơn thời gian người dùng cần để nhận ra trang đã đổi.
   Trình duyệt không hỗ trợ View Transitions API thì htmx tự rơi về swap thường —
   không cần fallback ở đây. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .18s;
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

/* ============================================================
   GIẢI ĐẤU (ADR-48, spec 2026-09-25 § 7–8, U1–U8)
   ============================================================ */
.giai-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--space-md); }
.giai-card { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-card); box-shadow: var(--elevation-1); overflow: hidden; color: inherit; text-decoration: none;
  transition: transform var(--motion-base) var(--ease-out), box-shadow var(--motion-base); }
/* Hover chỉ dành cho thiết bị CÓ con trỏ. Trên cảm ứng, trình duyệt giữ :hover ở phần tử vừa chạm
   cho tới khi chạm chỗ khác ⇒ thẻ/nút "dính" ở trạng thái nhấc lên sau khi đã buông tay. */
@media (hover: hover) {
  .giai-card:hover { transform: translateY(-2px); box-shadow: var(--elevation-2); color: inherit; }
}
.giai-card__cover { position: relative; height: 88px; background: linear-gradient(135deg, var(--court-650), var(--court-500) 60%, var(--court-400)); }
.giai-card__cover.is-muted { background: linear-gradient(135deg, var(--slate-600), var(--slate-400)); }
/* Badge trạng thái nằm trong <h1> của pageHeader nên thừa hưởng letter-spacing/cỡ chữ tiêu đề — chữ bị khít. */
.giai-badge { letter-spacing: 0; font-size: .8125rem; padding: .25rem .7rem; }
.giai-card__cover .giai-badge { position: absolute; right: 12px; top: 12px; }
.giai-card__date { position: absolute; left: 16px; bottom: -22px; width: 3.625rem; text-align: center; padding: 6px 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--elevation-2); }
.giai-card__date b { display: block; font-size: 1.35rem; line-height: 1; color: var(--text-strong); }
.giai-card__date small { font-size: .8125rem; font-weight: 700; color: var(--action-primary); text-transform: uppercase; }
.giai-card__body { padding: 30px 16px 16px; display: grid; gap: 6px; }
.giai-card__ten { font-size: 1rem; font-weight: 700; margin: 0; }
.giai-card__meta, .giai-card__stats { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: .82rem; color: var(--text-muted); }
.giai-card__stats { border-top: 1px dashed var(--border); padding-top: 8px; }
.giai-card__cho { color: var(--status-warning-text); font-weight: 700; }
/* Khung điều hướng RIÊNG của một giải (tournament/nav.html): khối tên giải ở đầu thanh bên/offcanvas,
   chấm báo đơn chờ trên nút Menu của thanh dưới. */
.giai-side-dau { display: grid; gap: 4px; justify-items: start; margin: 0 var(--space-sm) var(--space-sm);
  padding: 10px 12px; border-radius: var(--radius-lg); background: rgba(255, 255, 255, .08); color: #fff; }
.giai-side-dau__nhan { font-size: .8125rem; font-weight: 600; opacity: .75; }
.giai-side-dau__ten { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  padding-block: 2px; margin-block: -2px; line-height: 1.35; overflow-wrap: anywhere; }
.offcanvas .giai-side-dau { margin: 0 0 var(--space-sm); background: var(--surface-muted); color: var(--text-strong); }
html.sidebar-collapsed .app-sidebar .giai-side-dau { display: none; }
/* Nhãn trạng thái là badge-soft (chữ màu trên nền nhạt) — trên nền xanh đậm của thanh bên nó chìm hẳn. */
.app-sidebar .giai-side-dau .giai-badge { background: #fff; }
.giai-side-lui { opacity: .85; }
.app-offcanvas-link.is-on { color: var(--action-primary); font-weight: 700; }
.giai-bottom-menu { position: relative; }
.giai-bottom-menu__cho { position: absolute; top: 6px; right: calc(50% - 16px); width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--status-warning-text, #b45309); }
/* U2/A6: vạch dưới tab mọc ra khi trang mới hiện. */
.giai-tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.giai-tabs::-webkit-scrollbar { display: none; }
.giai-tabs .nav-link { white-space: nowrap; min-height: 44px; }
.giai-tabs .nav-link.active { position: relative; }
.giai-tabs .nav-link.active::after { content: ""; position: absolute; left: 12px; right: 12px; bottom: -1px; height: 3px;
  border-radius: 3px 3px 0 0; background: var(--action-primary); transform-origin: left; animation: giaiInk var(--motion-base) var(--ease-out); }
@keyframes giaiInk { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.giai-tab-body { animation: giaiFade var(--motion-base) var(--ease-out); }
@keyframes giaiFade { from { opacity: .001; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.giai-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-sm); }
.giai-kpi { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 10px 14px; }
.giai-kpi small { color: var(--text-muted); font-weight: 600; font-size: .8125rem; }
.giai-kpi b { display: block; font-size: 1.35rem; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.giai-kpi__nho { font-size: 1rem !important; }
.giai-kpi.is-warn b { color: var(--status-warning-text); }
.giai-suat { padding: 10px 0; border-bottom: 1px solid var(--border); display: grid; gap: 6px; }
.giai-suat:last-child { border-bottom: 0; }
.giai-progress { height: 6px; }
.giai-noi-dung { display: grid; grid-template-columns: auto 1fr auto; gap: var(--space-sm); align-items: start; }
.giai-noi-dung__ico { width: 44px; height: 44px; border-radius: 14px; background: var(--court-100); color: var(--court-700);
  display: grid; place-items: center; font-weight: 800; font-size: .8125rem; }
.giai-noi-dung__nut { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.giai-noi-dung__nut form { display: inline; }
.giai-spec { display: flex; flex-wrap: wrap; gap: 6px; }
.giai-spec span { font-size: .8125rem; background: var(--surface-muted); border-radius: 8px; padding: 2px 8px; }
.giai-nguoi { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.giai-nguoi__ten { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-weight: 600; }
.giai-nguoi--thieu { color: var(--status-warning-text); }
.giai-nu { color: var(--badge-pink-fg); }
.giai-trinh-do { font-size: .8125rem; font-weight: 700; border: 1px solid var(--border-strong); border-radius: 6px; padding: 0 5px; color: var(--text-muted); }
.giai-dong { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); padding: 12px 16px; border-bottom: 1px solid var(--border); }
.giai-dong:last-child { border-bottom: 0; }
.giai-hep { max-width: var(--w-content); }
@media (max-width: 767.98px) {
  .giai-kpis { grid-template-columns: repeat(2, 1fr); }
  .giai-noi-dung { grid-template-columns: auto 1fr; }
  .giai-noi-dung__nut { grid-column: 1 / -1; justify-content: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
  .giai-card, .giai-tab-body, .giai-tabs .nav-link.active::after { animation: none; transition: none; }
}

.giai-chips { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.giai-chips::-webkit-scrollbar { display: none; }
.giai-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 6px 12px; white-space: nowrap;
  border: 1px solid var(--border); border-radius: var(--radius-pill); background: var(--surface);
  color: var(--text-muted); font-size: .85rem; font-weight: 600; text-decoration: none; }
.giai-chip.is-on { border-color: var(--action-primary); background: var(--court-100); color: var(--court-700); }
/* Mã QR link giải (Đợt 4B): cạnh theo % để không tràn ở 360px, trần 220px để quét từ màn hình dễ. */
.giai-qr { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.giai-qr__ma { width: min(220px, 60vw); height: auto; aspect-ratio: 1; border-radius: var(--radius-sm, 8px); border: 1px solid var(--border); flex: none; }
.giai-qr__chu { flex: 1 1 200px; min-width: 0; }
/* Tab Đơn: cột lưới ngầm định là 'auto' nên giãn theo hàng chip lọc (nowrap) ⇒ cả tab tràn ngang tới
   614px ở 390px. Khoá cột về minmax(0, 1fr) để hàng chip tự cuộn ngang bên trong như thiết kế. */
.giai-tab-don { grid-template-columns: minmax(0, 1fr); }
.giai-tab-don > .d-flex > .giai-chips { min-width: 0; max-width: 100%; }
/* Lộ trình tổ chức (TournamentRoadmap): bảy bước dọc, đường nối giữa các số; bước hiện tại nổi khối.
   Dọc ở MỌI cỡ màn — đọc từ trên xuống như danh sách việc, không vỡ ở 360px. */
.giai-lt { list-style: none; margin: var(--space-sm) 0 0; padding: 0; display: grid; }
.giai-lt__dong { position: relative; display: grid; grid-template-columns: 2rem minmax(0, 1fr); gap: 12px; padding-bottom: 12px; }
.giai-lt__dong:last-child { padding-bottom: 0; }
.giai-lt__dong:not(:last-child)::before { content: ""; position: absolute; left: calc(1rem - .0625rem); top: 2rem; bottom: 0;
  width: .125rem; background: var(--border); }
.giai-lt__dong.is-xong:not(:last-child)::before { background: var(--action-primary); }
.giai-lt__so { position: relative; z-index: 1; width: 2rem; height: 2rem; border-radius: 50%; display: grid; place-items: center;
  font-weight: 700; font-size: .875rem; border: 2px solid var(--border-strong); color: var(--text-muted); background: var(--surface); }
.giai-lt__dong.is-xong .giai-lt__so { background: var(--action-primary); border-color: var(--action-primary); color: #fff; }
.giai-lt__dong.is-hien-tai .giai-lt__so { border-color: var(--action-primary); color: var(--action-primary); box-shadow: 0 0 0 4px var(--court-100); }
.giai-lt__noi-dung { min-width: 0; }
.giai-lt__ten { min-height: 2rem; display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; font-weight: 600; color: var(--text); }
.giai-lt__dong.is-xong .giai-lt__ten { color: var(--text-muted); font-weight: 500; }
.giai-lt__dong.is-hien-tai .giai-lt__noi-dung { background: var(--surface-muted); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 4px 12px 12px; }
.giai-lt__dong.is-hien-tai .giai-lt__ten { color: var(--text-strong); font-weight: 700; }
.giai-lt__goi-y { margin: 0 0 8px; font-size: .875rem; color: var(--text-muted); }
.giai-lt__nut { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
/* Một dòng "Bước tiếp theo" ở các tab khác Tổng quan. */
.giai-buoc-tiep { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: var(--space-sm);
  padding: 10px 14px; background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--action-primary);
  border-radius: var(--radius-lg); }
.giai-buoc-tiep__nhan { font-size: .8125rem; font-weight: 700; color: var(--action-primary); white-space: nowrap; }
.giai-buoc-tiep__ten { flex: 1 1 14rem; min-width: 0; }
.giai-buoc-tiep__nut { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: center; }
/* Màn đầu tiên của module (chưa có giải): bốn chặng + nút tạo. */
.giai-bat-dau__chang { list-style: none; margin: 0 0 var(--space-md); padding: 0; display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--space-sm); }
.giai-bat-dau__chang li { display: flex; gap: 12px; align-items: flex-start; padding: 12px; border: 1px solid var(--border);
  border-radius: var(--radius-lg); background: var(--surface-muted); }
.giai-bat-dau__chang i { flex: none; width: 2.25rem; height: 2.25rem; border-radius: 12px; display: grid; place-items: center;
  background: var(--court-100); color: var(--court-700); font-size: 1.1rem; }
.giai-bat-dau__chang b { display: block; color: var(--text-strong); }
.giai-bat-dau__chang span { display: block; font-size: .875rem; color: var(--text-muted); }
/* Chip chọn nội dung/ngày là vùng chạm chính ở sân — điện thoại cần đủ 44px (MB-3). */
@media (max-width: 767.98px) { .giai-chip { min-height: 44px; } }
.giai-chip__n { font-size: .8125rem; background: var(--surface-muted); border-radius: var(--radius-pill); padding: 0 7px; color: var(--text); }
.giai-thanh-duyet { position: sticky; top: calc(var(--topbar-height) + 8px); z-index: 5; display: flex; align-items: center;
  justify-content: space-between; gap: 10px; flex-wrap: wrap; padding: 8px 8px 8px 16px; border-radius: 14px;
  background: var(--text-strong); color: var(--surface); box-shadow: var(--elevation-2); animation: giaiFade var(--motion-base) var(--ease-out); }
.giai-don { display: grid; grid-template-columns: 28px minmax(0, 1.6fr) minmax(0, 1fr) auto; gap: 12px; align-items: center;
  padding: 12px 16px; border-bottom: 1px solid var(--border); }
.giai-don:last-child { border-bottom: 0; }
.giai-don__chon { width: 1.375rem; height: 1.375rem; }
.giai-don__cap { display: grid; gap: 3px; min-width: 0; }
.giai-don__meta { display: grid; gap: 4px; font-size: .82rem; color: var(--text-muted); min-width: 0; }
.giai-don__nut { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.giai-ban-ghep { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-sm); }
.giai-cot-ghep { background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 12px; display: grid; gap: 8px; align-content: start; }
.giai-nguoi-le { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 48px; padding: 10px 12px; text-align: left;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px; transition: transform var(--motion-fast), border-color var(--motion-fast), opacity var(--motion-fast); }
.giai-nguoi-le[aria-pressed="true"] { border-color: var(--action-primary); box-shadow: inset 0 0 0 1px var(--action-primary); transform: scale(1.02); }
.giai-nguoi-le.is-mo { opacity: .4; }
.giai-kpis--3 { grid-template-columns: repeat(3, 1fr); }
.giai-phi { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; }
.giai-phi__tien { font-variant-numeric: tabular-nums; }
.giai-phi__nut { min-width: 9.375rem; }
.giai-phi__nut.is-paid { background: var(--badge-green-bg, rgba(22,163,74,.14)); color: var(--badge-green-fg); border-color: transparent;
  animation: giaiNay var(--motion-base) var(--ease-out); }
@keyframes giaiNay { 0% { transform: scale(.9); } 60% { transform: scale(1.06); } 100% { transform: none; } }
@media (max-width: 767.98px) {
  .giai-don { grid-template-columns: 28px minmax(0, 1fr); gap: 8px 10px; }
  .giai-don__meta, .giai-don__nut { grid-column: 2; }
  .giai-don__nut { justify-content: flex-start; }
  .giai-kpis--3 { grid-template-columns: 1fr 1fr; }
  .giai-kpis--3 .giai-kpi:first-child { grid-column: 1 / -1; }
  .giai-phi { grid-template-columns: minmax(0, 1fr) auto; }
  .giai-phi form, .giai-phi .giai-phi__nut { grid-column: 1 / -1; width: 100%; }
}
@media (prefers-reduced-motion: reduce) { .giai-thanh-duyet, .giai-phi__nut.is-paid, .giai-nguoi-le { animation: none; transition: none; } }

.giai-pub { background: var(--surface-sunken); }
.giai-hero { background: linear-gradient(135deg, var(--court-900), var(--court-500) 55%, var(--court-400)); color: #fff; padding: 28px 0 64px; }
.giai-hero__clb { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: .85rem; }
.giai-hero__ten { color: #fff; font-size: clamp(1.6rem, 5vw, 2.2rem); font-weight: 800; margin: 16px 0 6px; max-width: 18ch; text-wrap: balance; }
.giai-hero__meta { display: flex; flex-wrap: wrap; gap: 6px 18px; color: var(--nav-text); }
/* Thẻ đè lên hero (margin-top âm) PHẢI có nền riêng: .data-card không tự mang nền, nên thẻ trạng thái
   trong suốt và chữ tối "chìm" trên nền xanh của hero (chỉ còn thấy nửa dưới của badge). */
.giai-pub__noi-dung > .data-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); }
.giai-pub__noi-dung { max-width: 55rem; margin-top: -40px; display: grid; gap: var(--space-md); padding-bottom: var(--space-xl); }
.giai-cta { display: grid; grid-template-columns: 1fr auto; gap: var(--space-md); align-items: center; }
.giai-dem { display: flex; gap: 8px; }
.giai-dem div { min-width: 3.5rem; text-align: center; padding: 6px 4px; border-radius: 12px; background: var(--surface-muted); }
.giai-dem b { display: block; font-size: 1.35rem; line-height: 1.2; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.giai-dem small { font-size: .8125rem; color: var(--text-muted); font-weight: 600; }
.giai-pub-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-sm); }
.giai-pub-ev { display: grid; gap: 8px; padding: 14px; border: 1px solid var(--border); border-radius: var(--radius-lg); }
.giai-dieu-le__noi-dung { white-space: pre-line; }
.giai-acc { border-top: 1px solid var(--border); padding: 10px 0; }
.giai-acc summary { cursor: pointer; font-weight: 700; min-height: 36px; display: flex; justify-content: space-between; align-items: center; }
.giai-roster { margin: 8px 0 0; padding-left: 1.4rem; display: grid; gap: 4px; }
.giai-form { max-width: 40rem; padding-block: var(--space-lg) var(--space-xl); display: grid; gap: var(--space-md); }
.giai-form__dau { display: flex; gap: 12px; align-items: center; }
.giai-buoc { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; font-size: .8125rem; font-weight: 700; color: var(--text-muted); }
.giai-buoc__muc { display: grid; gap: 6px; }
.giai-buoc__muc i { height: 4px; border-radius: 999px; background: var(--border); }
.giai-buoc__muc.is-on { color: var(--action-primary); }
.giai-buoc__muc.is-on i { background: var(--action-primary); }
.giai-chon { border: 1px solid var(--border-strong); border-radius: var(--radius-lg); overflow: hidden; transition: border-color var(--motion-fast), box-shadow var(--motion-fast); }
.giai-chon.is-on { border-color: var(--action-primary); box-shadow: inset 0 0 0 1px var(--action-primary); }
.giai-chon__dau { display: grid; grid-template-columns: 24px 1fr auto; gap: 12px; align-items: center; padding: 14px; cursor: pointer; margin: 0; }
.giai-chon__dong-doi { border-top: 1px dashed var(--border); padding: 14px; display: grid; gap: 12px; background: var(--surface-sunken); animation: giaiFade var(--motion-base) var(--ease-out); }
.hp { position: absolute; left: -9999px; width: .0625rem; height: .0625rem; overflow: hidden; }
/* U7: con cầu bay vòng cung rồi vẽ dấu tích */
.giai-cau-bay { width: 11.25rem; height: 7.5rem; overflow: visible; color: var(--action-primary); }
.giai-cau-bay__nen { fill: var(--court-100); }
.giai-cau-bay__qua { fill: var(--action-primary); offset-path: path("M10 100 Q 90 -30 170 70"); animation: giaiBay 1.1s var(--ease-out) both; }
.giai-cau-bay__tich { stroke: var(--action-primary); stroke-dasharray: 40; stroke-dashoffset: 40; animation: giaiTich .5s .9s ease forwards; }
@keyframes giaiBay { from { offset-distance: 0%; } to { offset-distance: 100%; } }
@keyframes giaiTich { to { stroke-dashoffset: 0; } }
@media (max-width: 767.98px) {
  .giai-hero { padding: 20px 0 56px; }
  .giai-cta { grid-template-columns: 1fr; }
  .giai-cta .btn { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .giai-cau-bay__qua, .giai-cau-bay__tich, .giai-chon__dong-doi { animation: none; }
  .giai-cau-bay__tich { stroke-dashoffset: 0; }
}

/* ── Giải đấu · Thi đấu (Đợt 2) — màu bảng A–D theo sơ đồ đã duyệt; bảng sau dùng lại vòng màu ── */
:root, [data-bs-theme="light"] {
  --bang-A: var(--court-500); --bang-B: #2563EB; --bang-C: #EA580C; --bang-D: #7C3AED; --bang-N: var(--text-strong, #0F172A);
  --giai-vang: #B7791F; --giai-vang-nen: #FEF3C7;
}
[data-bs-theme="dark"] {
  --bang-A: var(--court-400); --bang-B: #60A5FA; --bang-C: #FB923C; --bang-D: #A78BFA; --bang-N: #F8FAFC;
  --giai-vang: #FBBF24; --giai-vang-nen: #3A3220;
}
.bang-A, .bang-E, .bang-I, .bang-M { --bang: var(--bang-A); }
.bang-B, .bang-F, .bang-J, .bang-N { --bang: var(--bang-B); }
.bang-C, .bang-G, .bang-K, .bang-O { --bang: var(--bang-C); }
.bang-D, .bang-H, .bang-L, .bang-P { --bang: var(--bang-D); }
.o.bang-N, .giai-doi.bang-N, .giai-bang-boc.bang-N { --bang: var(--bang-N); }
.giai-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); gap: var(--space-md); align-items: start; }
.giai-o-so { max-width: 8rem; }
.giai-cach-chia { font-weight: 600; background: var(--surface-sunken); border-radius: var(--radius-md); padding: 8px 12px; }
.giai-hat-giong { display: grid; gap: 2px; max-height: 26rem; overflow-y: auto; }
.giai-hat-giong__dong { display: grid; grid-template-columns: minmax(0, 1fr) auto 4.5rem; gap: 8px; align-items: center; padding: 4px 0; border-bottom: 1px solid var(--border); }
.giai-hat-giong__so { min-height: var(--tap-min, 44px); text-align: center; }
.giai-luoi-boc { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: var(--space-sm); }
.giai-bang-boc { border: 1px solid var(--border); border-top: 4px solid var(--bang); border-radius: var(--radius-lg); padding: 10px; display: grid; gap: 6px; }
.giai-bang-boc__ten { color: var(--bang); margin: 0; }
.giai-o-boc { display: flex; align-items: center; gap: 6px; min-height: 44px; width: 100%; text-align: left; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-sunken); padding: 6px 10px; font-size: .875rem; }
.giai-o-boc[aria-pressed="true"] { border-color: var(--action-primary); box-shadow: 0 0 0 3px rgba(14,122,95,.25); }
.giai-o-boc .giai-nguoi__ten { flex: 1; }
.giai-hg { font-size: .8125rem; font-weight: 800; color: var(--giai-vang); background: var(--giai-vang-nen); border-radius: 6px; padding: 0 5px; }
.giai-ma { font-weight: 800; color: var(--bang, var(--text-strong)); margin-right: 4px; }
.giai-cac-bang { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: var(--space-md); }
.giai-bang { border-top: 4px solid var(--bang); }
.giai-bang__ten { color: var(--bang); }
.giai-bxh td, .giai-bxh th { text-align: right; font-variant-numeric: tabular-nums; }
.giai-bxh td:nth-child(2), .giai-bxh th:nth-child(2) { text-align: left; }
.giai-bxh tr.is-q td:nth-child(2) { font-weight: 700; }
.giai-bxh tr.is-q td:first-child { box-shadow: inset 3px 0 0 var(--bang); }
.giai-bxh tr.is-tie td { background: var(--badge-amber-bg, rgba(245,158,11,.12)); }
.giai-bxh tr.is-rut td { color: var(--text-muted); text-decoration: line-through; }
.giai-ghi-chu { background: var(--surface-sunken); border-radius: var(--radius-md); padding: 4px 10px; }
.giai-phan-dinh, .giai-canh-bao { background: var(--giai-vang-nen); color: var(--giai-vang); border-radius: var(--radius-md); padding: 8px 12px; }
.giai-luot { display: grid; gap: 2px; margin-top: 6px; }
.giai-luot__ten { font-size: .8125rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); padding-block: 2px; margin-block: -2px; }
.giai-tran { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 8px; align-items: center; width: 100%; min-height: 44px; padding: 6px 8px; border: 1px solid transparent; border-radius: var(--radius-md); background: none; font-size: .875rem; text-align: left; }
button.giai-tran:hover { border-color: var(--border); background: var(--surface-sunken); }
.giai-tran__ten { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; color: var(--text-muted); }
.giai-tran__ten--b { text-align: left; }
.giai-tran__ten.is-w { color: var(--text-strong); font-weight: 700; }
.giai-tran__so { font-variant-numeric: tabular-nums; background: var(--surface-sunken); border-radius: 6px; padding: 0 8px; white-space: nowrap; }
.giai-tran.is-trong .giai-tran__so { background: none; border: 1px dashed var(--action-primary); color: var(--action-primary); }
.giai-tran.is-cho { opacity: .6; }
.giai-tag { font-size: .8125rem; font-weight: 800; color: var(--status-warning-text); margin-left: 4px; }
.giai-so-do { max-width: 36rem; width: 100%; margin: 0 auto; }
.giai-vong-tron { width: 100%; height: auto; display: block; overflow: visible; }
.giai-vong-tron .vong { fill: none; stroke: var(--border); stroke-dasharray: 2 5; }
.giai-vong-tron .cung { fill: none; stroke-width: 3; stroke-linecap: round; transition: stroke .3s, opacity .3s; }
.giai-vong-tron .cung.win { stroke: var(--action-primary); }
.giai-vong-tron .cung.lose, .giai-vong-tron .cung.tbd { stroke: var(--border-strong); }
.giai-vong-tron .cung.tbd { stroke-dasharray: 4 6; }
.giai-vong-tron .cung.bye { stroke: var(--border); stroke-dasharray: 2 5; }
.giai-vong-tron.has-focus .cung { opacity: .18; }
.giai-vong-tron.has-focus .cung.is-focus { opacity: 1; stroke: var(--giai-vang); }
.giai-vong-tron .nut { fill: var(--surface); stroke: var(--border-strong); stroke-width: 2; }
.giai-vong-tron .nut.done { fill: var(--action-primary); stroke: var(--action-primary); }
.giai-vong-tron .nut.ready { stroke: var(--action-primary); stroke-width: 2.5; }
.giai-vong-tron .nut.ready.can { animation: giaiNhip 1.8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.giai-vong-tron .nut.bye { fill: var(--surface-sunken); stroke: var(--border); stroke-dasharray: 2 2; }
.giai-vong-tron .nut-cham { fill: transparent; cursor: pointer; }
.giai-vong-tron .nut-cham:focus-visible { outline: none; stroke: var(--action-primary); stroke-width: 2; }
.giai-vong-tron .nut-ty-so { font-size: 13px; font-weight: 700; fill: var(--text-muted); font-variant-numeric: tabular-nums;
  paint-order: stroke; stroke: var(--surface-sunken, #fff); stroke-width: 4px; stroke-linejoin: round; }
.giai-vong-tron.is-16 .nut-ty-so { font-size: 13px; }
.giai-vong-tron .o { cursor: pointer; }
.giai-vong-tron .o text { font-size: 17px; font-weight: 800; fill: var(--bang); font-variant-numeric: tabular-nums; }
.giai-vong-tron.is-16 .o text { font-size: 13px; }
.giai-vong-tron.is-4 .o text { font-size: 19px; }
.giai-vong-tron .o-cham { fill: var(--bang); }
.giai-vong-tron .o.is-out { opacity: .5; }
.giai-vong-tron .o.is-bye { cursor: default; }
.giai-vong-tron .o.is-bye text { fill: var(--text-muted); font-weight: 600; }
.giai-vong-tron .o.is-champ text, .giai-vong-tron .o.is-focus text { fill: var(--giai-vang); }
.giai-vong-tron .o:focus-visible { outline: none; }
.giai-vong-tron .o:focus-visible text { text-decoration: underline; }
.giai-vong-tron .tam { fill: var(--surface-sunken); stroke: var(--border-strong); stroke-width: 2; }
.giai-vong-tron .tam.is-vo-dich { fill: var(--giai-vang-nen); stroke: var(--giai-vang); }
.giai-vong-tron .tam-cup { font-size: 14px; }
.giai-vong-tron .tam-ma { font-size: 22px; font-weight: 800; fill: var(--giai-vang); }
.giai-vong-tron .tam-phu { font-size: 13px; font-weight: 600; fill: var(--text-muted); }
@keyframes giaiNhip { 50% { stroke-width: 5; } }
.giai-cay { display: flex; gap: var(--space-md); overflow-x: auto; padding-bottom: 6px; }
.giai-cay__cot { flex: 0 0 16rem; display: grid; gap: 6px; align-content: center; }
.giai-duong-di { border: 1px solid var(--giai-vang); background: var(--giai-vang-nen); border-radius: var(--radius-lg); padding: 10px 14px; font-size: .875rem; }
.giai-doi { display: grid; grid-template-columns: 3rem minmax(0, 1fr) auto; gap: 12px; }
.giai-doi__hg { width: 2.4rem; height: 2.4rem; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: .8125rem; background: var(--surface-sunken); color: var(--text-muted); }
.giai-doi__hg.is-hg { background: var(--giai-vang-nen); color: var(--giai-vang); }
.giai-ty-so__ten { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 8px; text-align: center; align-items: center; }
.giai-set { display: grid; grid-template-columns: 3.5rem minmax(0, 1fr) 1rem minmax(0, 1fr); gap: 8px; align-items: center; }
.giai-set__nhan { font-size: .8125rem; font-weight: 700; color: var(--text-muted); }
.giai-set__o { min-height: 54px; text-align: center; font-size: 1.35rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.giai-set__loi { grid-column: 2 / -1; font-size: .8125rem; color: var(--status-danger); font-weight: 600; }
.giai-set__loi:empty { display: none; }
.giai-thu-tu { display: grid; gap: 6px; margin: 0; padding-left: 1.5rem; }
.giai-thu-tu__dong { display: flex; align-items: center; gap: 8px; min-height: 44px; }
.giai-thu-tu__nut { margin-left: auto; display: flex; gap: 4px; }
.giai-thu-tu__nut .btn { min-width: 44px; min-height: 44px; font-size: 1.15rem; line-height: 1; }
.giai-tabs--phu { font-size: .9rem; }
.justify-self-end { justify-self: end; }
.justify-self-start { justify-self: start; }
@media (max-width: 767.98px) {
  .giai-split { grid-template-columns: 1fr; }
  .giai-cac-bang { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
  .giai-vong-tron .nut.ready.can, .giai-vong-tron .cung { animation: none; transition: none; }
}

/* ── Giải đấu · Lịch & Sân (Đợt 3) — màu nội dung lặp theo vòng 6 màu; rem, không px cứng ── */
.lich-mau-0 { --lich: var(--bang-A); }
.lich-mau-1 { --lich: var(--bang-B); }
.lich-mau-2 { --lich: var(--bang-C); }
.lich-mau-3 { --lich: var(--bang-D); }
.lich-mau-4 { --lich: #DB2777; }
.lich-mau-5 { --lich: #0891B2; }
[data-bs-theme="dark"] .lich-mau-4 { --lich: #F472B6; }
[data-bs-theme="dark"] .lich-mau-5 { --lich: #22D3EE; }
.giai-chip[class*="lich-mau-"] { border-left: 4px solid var(--lich); }
.giai-chip[aria-pressed="true"] { border-color: var(--action-primary); background: var(--court-100); color: var(--court-700); }
.giai-lich-thanh { display: flex; flex-wrap: wrap; gap: var(--space-xs); align-items: center; }
.giai-lich-loc { display: flex; flex-wrap: wrap; gap: var(--space-xs); align-items: center; }
.giai-lich-loc .form-control { flex: 1 1 100%; min-height: var(--tap-min, 44px); }
.giai-lich-luoi { width: 100%; border-collapse: separate; border-spacing: .25rem; }
.giai-lich-luoi th { font-size: .8125rem; color: var(--text-muted); font-weight: 700; text-align: left; white-space: nowrap; background: var(--surface); }
.giai-lich-luoi thead th { position: sticky; top: 0; z-index: 1; }
.giai-lich-luoi tbody th { position: sticky; left: 0; z-index: 1; font-variant-numeric: tabular-nums; }
.giai-lich-luoi td { min-width: 10rem; height: 3.5rem; vertical-align: top; border-radius: var(--radius-md); background: var(--surface-sunken); }
.giai-lich-luoi td[data-lich-o] { cursor: pointer; }
.giai-lich-the { display: grid; gap: .125rem; width: 100%; min-height: var(--tap-min, 44px); padding: .375rem .5rem; text-align: left; border: 1px solid var(--border); border-left: 4px solid var(--lich, var(--border)); border-radius: var(--radius-md); background: var(--surface); font-size: .8125rem; color: var(--text); }
.giai-lich-the[aria-pressed="true"] { border-color: var(--action-primary); box-shadow: 0 0 0 3px rgba(14,122,95,.25); }
.giai-lich-the__dau { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted); }
.giai-lich-the__cap { min-width: 0; font-weight: 600; }
.giai-lich-the__trang-thai { font-weight: 700; color: var(--court-700); }
.giai-lich-the__du-bao { font-weight: 700; color: var(--status-warning-text); }
.giai-lich-the.is-on_court { box-shadow: inset 0 0 0 2px var(--court-500); }
.giai-lich-the.is-done { opacity: .7; }
.giai-lich-chua-o { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: var(--space-xs); }
.giai-lich-ds { display: grid; gap: var(--space-sm); }
.giai-lich-ds__gio { font-size: .8125rem; font-weight: 800; color: var(--text-muted); margin: 0 0 .25rem; font-variant-numeric: tabular-nums; }
.giai-lich-ds__dong { display: grid; grid-template-columns: 4.5rem minmax(0, 1fr); gap: .5rem; align-items: start; margin-bottom: .375rem; }
.giai-lich-san { font-size: .8125rem; font-weight: 700; background: var(--surface-muted); border-radius: var(--radius-pill); padding: .25rem .5rem; text-align: center; }
.giai-lich-chon { position: sticky; bottom: env(safe-area-inset-bottom, 0px); z-index: 5; background: var(--court-100); color: var(--court-700); border-radius: var(--radius-md); padding: .5rem .75rem; font-size: .875rem; }
.giai-lich-ngay { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-sm); display: grid; gap: var(--space-xs); }
.giai-lich-ngay legend { float: none; width: auto; margin: 0; padding: 0 .25rem; font-size: 1rem; font-weight: 700; }
.giai-san-luoi { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); gap: var(--space-sm); }
.giai-san { display: grid; gap: var(--space-xs); padding: var(--space-sm); border-top: 4px solid var(--border); }
.giai-san.is-dang-danh { border-top-color: var(--court-500); }
.giai-san__dau { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); }
.giai-san .btn { min-height: var(--tap-min, 44px); }
.giai-san__khac summary { min-height: var(--tap-min, 44px); display: flex; align-items: center; cursor: pointer; font-weight: 600; }
.giai-san__lua-chon { margin-top: .375rem; }
.giai-tran__lich { grid-column: 1 / -1; font-size: .8125rem; color: var(--text-muted); }

/* ── Giải đấu · Tiền & Kết quả (Đợt 4A) — rem, không px cứng cho bề ngang; MF-4: không in số âm ── */
.giai-ket-du { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-sm);
  padding: 10px 14px; border-radius: var(--radius-lg); background: var(--court-100); color: var(--court-700); }
.giai-ket-du b { font-size: 1.5rem; font-variant-numeric: tabular-nums; }
.giai-ket-du.is-lo { background: var(--badge-red-bg, rgba(220,38,38,.14)); color: var(--badge-red-fg); }
.giai-theo-loai { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px var(--space-sm); margin: var(--space-sm) 0 0; }
.giai-theo-loai dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
.giai-phi__nhom { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
.giai-phi.is-goi-y { background: var(--badge-amber-bg, rgba(249,115,22,.08)); }
.giai-so { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; }
.giai-so__loai { margin-inline: 4px; }
.giai-so__ghi-chu { overflow-wrap: anywhere; }
.giai-so__tien { font-variant-numeric: tabular-nums; white-space: nowrap; }
.giai-so.is-huy .giai-so__tien, .giai-so.is-huy .giai-so__loai { text-decoration: line-through; color: var(--text-muted); }
.giai-chon { display: flex; flex-wrap: wrap; gap: 6px; }
.giai-chon .btn { min-height: 44px; }
.giai-kq__dong { display: grid; gap: 6px; padding-block: 10px; border-top: 1px solid var(--border); }
.giai-kq > h3 + .giai-kq__dong { border-top: 0; }
.giai-kq__dau { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--space-sm); }
.giai-kq__hang { font-weight: 700; }
.giai-kq__hang.is-vo-dich { color: var(--status-warning-text); }
.giai-kq__tien { font-weight: 600; font-variant-numeric: tabular-nums; }
.giai-kq__nguoi { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
.giai-kq__nguoi li { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.giai-kq__ten { flex: 1 1 12rem; min-width: 0; overflow-wrap: anywhere; }
.giai-kq__dau > .btn-link:first-of-type { margin-left: auto; }
.giai-kq__dau .btn-link { padding-block: 0; }
.giai-kq__nguoi li.is-lech { color: var(--status-warning-text); }
.giai-kq__trao, .giai-kq__chon { min-height: 44px; }
.giai-kq__chon { justify-self: start; }
@media (max-width: 575.98px) {
  .giai-phi__nhom { grid-column: 1 / -1; }
  .giai-phi__nhom > * { flex: 1 1 auto; }
  .giai-so { grid-template-columns: minmax(0, 1fr) auto; }
  .giai-so > .btn { grid-column: 1 / -1; min-height: 44px; }
}

/* ---- Tóm tắt cài đặt CLB (BR-ONB-01..04) ---------------------------------------------
   Nút mở hướng dẫn ở sidebar/offcanvas dùng cùng dáng với mục menu dạng <a>. */
.app-sidebar-nav button.nav-link,
button.app-offcanvas-link {
  width: 100%; text-align: left; background: none; border: 0; font: inherit;
}
button.app-offcanvas-link { width: calc(100% + 1rem); }
.tour-cai-dat[hidden] { display: none; }
body.bmh-tour-open { overflow: hidden; }
.tour-cai-dat {
  position: fixed; inset: 0; z-index: calc(var(--bs-modal-zindex, 1055) + 1);
  display: grid; place-items: center; padding: .75rem;
}
.tour-cai-dat__backdrop { position: absolute; inset: 0; background: rgb(0 0 0 / .56); }
.tour-cai-dat__panel {
  position: relative; display: flex; flex-direction: column;
  width: min(var(--w-narrow), 100%); max-height: calc(100dvh - 1.5rem);
  overflow: hidden; border: 1px solid var(--border);
  border-radius: var(--radius-card); background: var(--surface); color: var(--text);
  box-shadow: var(--elevation-3);
}
.tour-cai-dat__panel:focus-visible { outline: 2px solid var(--action-primary); outline-offset: -3px; }
.tour-cai-dat__header {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg) var(--space-xs);
}
.tour-cai-dat__header .btn {
  display: inline-grid; place-items: center; min-width: 44px; min-height: 44px;
  padding: 0; color: var(--text-muted); border-radius: var(--radius-md);
}
.tour-cai-dat__header .btn:hover { background: var(--surface-muted); color: var(--text-strong); }
.tour-cai-dat__identity {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  color: var(--action-primary); font-size: .875rem; font-weight: 700;
}
.tour-cai-dat__identity .bi { font-size: 1.25rem; line-height: 1; }
.tour-cai-dat__progress {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--space-xs);
  padding-inline: var(--space-lg);
}
.tour-cai-dat__progress span {
  height: 4px; border-radius: var(--radius-pill); background: var(--border);
}
.tour-cai-dat__progress span.is-done,
.tour-cai-dat__progress span.is-current { background: var(--action-primary); }
.tour-cai-dat__body {
  min-height: 0; overflow-y: auto; padding: var(--space-lg);
  scrollbar-gutter: stable;
}
.tour-cai-dat__meta {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}
.tour-cai-dat__count { color: var(--text-muted); font-size: .875rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.tour-cai-dat__chang {
  color: var(--action-primary); background: var(--bs-primary-bg-subtle);
  border-radius: var(--radius-pill); padding: .25rem .75rem;
  font-size: .8125rem; font-weight: 700; white-space: nowrap;
}
.tour-cai-dat__lead { display: flex; align-items: center; gap: var(--space-md); }
.tour-cai-dat__step-icon {
  flex: none; display: inline-grid; place-items: center;
  width: 3.5rem; height: 3.5rem; border-radius: var(--radius-lg);
  background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis);
  font-size: 1.5rem; line-height: 1;
}
.tour-cai-dat__title {
  min-width: 0; margin: 0; color: var(--text-strong);
  font-size: 1.375rem; font-weight: 700; line-height: 1.3;
}
.tour-cai-dat__title:focus { outline: 0; }
.tour-cai-dat__title:focus-visible { outline: 2px solid var(--action-primary); outline-offset: 4px; border-radius: 2px; }
.tour-cai-dat__body > p { margin: var(--space-lg) 0 0; line-height: 1.65; }
.tour-cai-dat__links { margin-top: var(--space-lg); }
.tour-cai-dat__links [hidden] { display: none; }
.tour-cai-dat__destinations { display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.tour-cai-dat__destinations .btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-xs);
  min-height: 48px; padding-inline: var(--space-md); font-weight: 600;
}
.tour-cai-dat__guide {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  min-height: 44px; margin-top: var(--space-xs);
  color: var(--action-primary); font-size: .875rem; font-weight: 600; text-decoration: none;
}
.tour-cai-dat__guide:hover { color: var(--action-primary-hover); text-decoration: underline; }
.tour-cai-dat__nut {
  flex: none; display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs);
  padding: var(--space-sm) var(--space-lg); border-top: 1px solid var(--border); background: var(--surface);
}
.tour-cai-dat__nut .btn { min-width: 44px; min-height: 44px; font-weight: 600; }
.tour-cai-dat__skip { padding-inline: var(--space-xs); color: var(--text-muted); white-space: nowrap; }
.tour-cai-dat__skip:hover { background: var(--surface-muted); color: var(--text-strong); }
.tour-cai-dat__pager { display: flex; align-items: center; gap: var(--space-xs); }
.tour-cai-dat__pager .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-xs); }
@media (max-width: 575.98px) {
  .tour-cai-dat__header { padding: var(--space-xs) var(--space-md); }
  .tour-cai-dat__progress { padding-inline: var(--space-md); gap: .375rem; }
  .tour-cai-dat__body { padding: var(--space-md); }
  .tour-cai-dat__meta { margin-bottom: var(--space-md); }
  .tour-cai-dat__lead { gap: var(--space-sm); }
  .tour-cai-dat__step-icon { width: 3rem; height: 3rem; font-size: 1.25rem; border-radius: var(--radius-md); }
  .tour-cai-dat__title { font-size: 1.1875rem; }
  .tour-cai-dat__body > p { margin-top: var(--space-md); font-size: .9375rem; }
  .tour-cai-dat__links { margin-top: var(--space-md); }
  .tour-cai-dat__destinations { flex-direction: column; }
  .tour-cai-dat__destinations .btn { width: 100%; }
  .tour-cai-dat__nut { padding: var(--space-sm) var(--space-md) calc(var(--space-sm) + env(safe-area-inset-bottom, 0px)); }
  .tour-cai-dat__pager .btn { padding-inline: var(--space-sm); }
}
