/* =============================================================
   VCI360 STS — Giao diện xác thực "Command Console"

   Ngôn ngữ hình khối lấy NGUYÊN từ kit2 của vci-sun-pms-fe:
     - HudFrame  (src/shared/ui/box/HudFrame.tsx): vát góc TL+BR bằng clip-path,
       bracket [ ] ở TR+BL, chấm khoá góc, viền gradient 150deg, top accent
       stripe, scanline overlay, glow 2 lớp.
     - HudCorners: 4 góc chữ L có quầng teal.
   Kỹ thuật hiệu ứng tham chiếu ui-smart (DataV-React / BigDataView 003·020·025):
     - borderBox8   -> đốm sáng chạy vòng quanh viền  (ở đây: conic-gradient quay
                       + mask ring, không cần JS đo kích thước như bản SVG gốc)
     - decoration10 -> đường mạch + node sáng tuần tự
     - decoration3  -> ma trận chấm nhấp nháy
     - .boxall/025  -> viền 1px mờ + bracket 2px ở góc

   Màu / typography: design/colors-theme.json + design-system-style-guide.md
     - primary (#22C55E) LÀ XANH LÁ  -> brand + hành động
     - teal / sky                    -> chrome HUD (khung, bracket, lưới)
   Nạp SAU css-ant/* để đè Ant Design.
   ============================================================= */

/* Cho phép animate góc của conic-gradient (đốm sáng chạy viền).
   Trình duyệt không hỗ trợ @property sẽ bỏ qua khối này -> gradient đứng yên,
   viền vẫn đẹp. Đây là lý do KHÔNG dựa vào chuyển động để tạo ra đường viền. */
@property --hud-angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
}

:root {
    /* --- Surface (dark) --- */
    --bg-base: #04162A;
    --bg-1: #0A1D30;
    --bg-2: #122A42;
    --bg-3: #1B3B5C;
    --bg-input: #0F2438;

    /* --- Text --- */
    --fg-1: #FFFFFF;
    --fg-2: #EDF2F8;
    --fg-3: #D8E1EE;
    --fg-4: #BCC8DC;
    --fg-5: #96A6BE;

    /* --- Border --- */
    --border-1: #345677;
    --border-2: #4C7BA3;

    /* --- Brand: primary LÀ XANH LÁ, không phải cyan --- */
    --primary-4: #4EDE82;
    --primary-5: #22C55E;
    --primary-6: #16A34A;
    --secondary-5: #22B5D8;
    --link: #4EDE82;

    /* --- GẠCH DỌC NHẤN TIÊU ĐỀ — MỘT mô-típ, dùng ở CẢ HAI khoang ---
       Khoang trái nhấn "NỀN TẢNG SỐ / QUẢN LÝ DỰ ÁN", khoang phải nhấn "Đăng
       nhập". Cùng hình dạng, nên BẮT BUỘC cùng màu, cùng bề dày, cùng khe.

       ⛔ VÌ SAO PHẢI KHAI Ở ĐÂY chứ không viết thẳng vào từng rule: trước đây
       hai chỗ khai riêng và đã LỆCH NHAU 4 điểm — bên trái mở đầu bằng cyan,
       bên phải bằng xanh lá; quầng 12px cyan vs 10px xanh; điểm tan 46% vs 62%;
       khe tới chữ 18px vs 13px. Mắt nhận ra "hai cái này là cùng một thứ" từ
       hình dạng, rồi bị màu và khe phản lại — đó đúng là cái đọc ra "rối", chứ
       không phải vì bản thân cái gạch. Comment cũ còn ghi "hai bên nói cùng một
       ngôn ngữ" trong khi code thì không. Khai một chỗ thì không lệch được nữa.

       Màu lấy gốc XANH LÁ (--primary-4) chứ không cyan: hai gạch này nhấn NỘI
       DUNG (tiêu đề), không phải khung HUD. Token --cyan-5 có ghi rõ chỉ dành
       cho hoạ tiết sci-fi; để nó mở đầu một gạch nhấn tiêu đề thì nó tranh vai
       với màu thương hiệu. */
    --tbar-w: 2px;
    /* Khe QUANG HỌC từ mép phải gạch tới mép chữ (không tính bề dày gạch).
       Hai chỗ tiêu thụ qua `calc(--tbar-gap + --tbar-w)` nên đổi số này là cả
       hai dịch cùng nhau. Bậc màn hẹp ghi đè CHÍNH biến này, ở `:root`, để phép
       khoá đó còn hiệu lực ở mọi bề rộng. */
    --tbar-gap: 16px;
    --tbar-fill: linear-gradient(180deg,
        var(--primary-4) 0%,
        var(--teal-5) 58%,
        rgba(45, 212, 191, .10) 100%);
    /* Quầng SIẾT còn 8px (trước 10–12px). Gạch chỉ 2px mà quầng 12px thì vùng
       sáng rộng gấp 6 lần vật thể — đọc ra một vệt mờ, không ra một nét. */
    --tbar-glow: 0 0 8px -1px rgba(34, 197, 94, .50);

    /* --- Chrome HUD: teal/sky. Tách khỏi brand để khung không tranh
           sự chú ý với nút hành động (HudCorners dùng teal-5). --- */
    --teal-5: #2DD4BF;
    --sky-5: #38BDF8;
    /* Cyan HUD thuần — CHỈ cho họa tiết sci-fi ở khoang giới thiệu (ngoặc [ ],
       crosshair, viền thẻ phân hệ). ⛔ ĐỪNG dùng cho nút hoặc link: brand của
       VCI360 là XANH LÁ (--primary-5), cyan mà leo lên nút hành động thì hai
       màu tranh nhau và người dùng không biết đâu là chỗ bấm. */
    --cyan-5: #00F2FE;
    --azure-6: #0369A1;

    /* --- Status --- */
    --error-5: #FF424E;
    --warning-5: #FC820A;
    --success-5: #00E676;

    /* --- Type scale --- */
    --text-h3: 24px;
    --text-h4: 20px;
    --text-body-16: 16px;
    --text-body-14: 14px;
    --text-caption-12: 12px;
    --text-caption-10: 10px;

    /* Khớp --font-sans của FE (styles/globals.css): PingFang SC + Segoe UI Variable
       để render CJK/Windows 11 đẹp, Noto Sans giữ dấu tiếng Việt khi Inter chưa tải. */
    --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Segoe UI Variable',
                 'Microsoft YaHei', 'Hiragino Sans', 'Noto Sans', ui-sans-serif, system-ui,
                 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

    /* Mono CHỈ cho chuỗi kỹ thuật (mã hệ thống, đồng hồ) — KHÔNG cho chữ thường.
       Khớp --font-mono của FE. */
    --font-mono: 'JetBrains Mono', 'Roboto Mono', 'SF Mono', ui-monospace, 'Menlo',
                 'Consolas', 'Liberation Mono', monospace;

    /* Alias giữ tương thích: các rule cũ đang đọc `var(--radius)`. Trỏ thẳng
       vào `--auth-radius` để cả trang chỉ còn MỘT chỗ chỉnh bán kính. */
    --radius: var(--auth-radius);

    /* Chiều cao chuẩn - khớp Button.tsx (xl/lg/md) và FIELD_HEIGHT của FE */
    --h-xl: 44px;
    --h-lg: 40px;
    --h-md: 36px;

    /* --- Đường cong chuyển động, dùng CHUNG cho cả trang ---
       `settle` là chuẩn sheet iOS/Material: vọt tới rồi dừng êm, không nảy
       ngược. Lấy đúng hằng `SETTLE` của vci-sun-mobile (LoginBrandHeader) để
       hai sản phẩm có cùng một "tính cách" chuyển động.
       `snap` có nảy nhẹ — CHỈ cho micro-interaction nhỏ (icon, tick), đừng
       dùng cho khối lớn: khối to mà nảy thì đọc ra lỗi layout. */
    --ease-settle: cubic-bezier(.16, 1, .3, 1);
    --ease-out: cubic-bezier(.22, .61, .36, 1);
    --ease-snap: cubic-bezier(.34, 1.4, .64, 1);

    /* --- ⛔ VÌ SAO THẺ FORM KHÔNG CÒN VÁT GÓC (bỏ 2026-08-23) ---
       Bản trước vát góc trên-trái + dưới-phải theo `HudFrame` của kit2. Hệ quả:
       chỉ còn HAI góc vuông để đặt dấu ngoặc, nên thẻ chỉ có 2 dấu góc — trong
       khi thiết bị trang trí chủ đạo của ảnh mẫu (`images/login.jpg`) là ĐỦ
       BỐN dấu ngoặc lớn ôm lấy thẻ, hai bên hai màu.
       Đổi lại: bỏ vát ở MÀN XÁC THỰC để lấy đủ 4 góc. `HudFrame` trong app
       KHÔNG đụng tới — vát góc vẫn là ngôn ngữ của panel dashboard.

       --auth-radius: bán kính bo góc của ô nhập và nút TRÊN MÀN NÀY.
       Giữ 0 vì hệ thiết kế chốt vuông góc (`tailwind-conventions.md`, và
       vci-sun-mobile §14 "radius toàn app đã ép về 0"). Ảnh mẫu dùng ô bo
       tròn hoàn toàn; muốn theo ảnh mẫu thì đổi ĐÚNG dòng này thành `999px`,
       mọi ô nhập và nút sẽ đổi theo — không phải sửa chỗ nào khác. */
    --auth-radius: 0px;
}

/* =============================================================
   1. Nền trang — 4 lớp dựng bằng CSS, không dùng ảnh
      gradient navy -> lưới kỹ thuật trôi -> quét radar -> vignette
   ============================================================= */

html, body {
    height: 100%;
}

body.auth-body {
    margin: 0;
    min-height: 100%;
    /* ⛔ BẮT BUỘC. Quả cầu dữ liệu rộng tới 600px nằm trong cột trái chỉ 500px,
       nên nó THÒ RA khoảng 50px mỗi bên; hai lưới phối cảnh cũng trải rộng hơn
       màn. Không chặn thì trang mọc thanh cuộn NGANG — trên màn đăng nhập đó là
       lỗi nhìn thấy ngay.
       Khai `hidden` trước rồi `clip` sau: trình duyệt hiểu `clip` sẽ lấy dòng
       sau (không tạo vùng cuộn mới, không phá `position: sticky`), trình duyệt
       cũ giữ `hidden` — vẫn chặn được, chỉ kém tinh một bậc. */
    overflow-x: hidden;
    overflow-x: clip;
    font-family: var(--font-sans);
    font-size: var(--text-body-14);
    line-height: 20px;
    color: var(--fg-3);
    /* Trên nền tối con trỏ nhập gần như không thấy nếu để mặc định
       (nguyên tắc 4 của field.theme.ts) */
    caret-color: var(--primary-5);
    background-color: var(--bg-base);
    background-image:
        radial-gradient(900px 520px at 78% 16%, rgba(34, 197, 94, .16), transparent 62%),
        radial-gradient(760px 480px at 12% 88%, rgba(45, 212, 191, .13), transparent 60%),
        radial-gradient(680px 420px at 22% 34%, rgba(56, 189, 248, .07), transparent 64%),
        linear-gradient(160deg, #04162A 0%, #071B2E 45%, #0A1D30 100%);
    background-attachment: fixed;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
}

/* Bọc toàn bộ lớp nền. Dùng phần tử thật (không phải ::before/::after của body)
   vì cần 4 lớp độc lập và mỗi lớp có animation riêng. */
.auth-bg {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
}

    .auth-bg > span {
        position: absolute;
        inset: 0;
        display: block;
    }

/* --- LƯỚI PHỐI CẢNH — sàn lùi về chân trời.
       Kỹ thuật synthwave chuẩn: lưới phẳng + `perspective()` + `rotateX()`.
       `perspective` tạo lớp hợp thành riêng nên phần vẽ lại của animation bị
       giam trong lớp đó, không đụng tới phần còn lại của trang.
       Mask tan dần về phía chân trời để lưới không cắt ngang màn bằng một mép
       cứng — đó là chỗ ảnh mẫu làm tinh nhất. --- */
.auth-bg-floor,
.auth-bg-ceil {
    left: -60% !important;
    right: -60% !important;
    height: 62% !important;
    overflow: hidden;
    background-image:
        linear-gradient(rgba(45, 212, 191, .44) 1px, transparent 1px),
        linear-gradient(90deg, rgba(45, 212, 191, .30) 1px, transparent 1px);
    background-size: 68px 46px;
    animation: hudFloorRun 7s linear infinite;
}

.auth-bg-floor {
    top: auto !important;
    bottom: -6% !important;
    transform: perspective(300px) rotateX(64deg);
    transform-origin: center bottom;
    /* HAI mask giao nhau: tan về chân trời (dọc) + tan về phía panel form
       (ngang). Ảnh mẫu chỉ để lưới ở nửa trái; nửa phải là nền trơn để thẻ
       form không phải cạnh tranh với hoa văn nào.
       ⚠️ Không hỗ trợ `mask-composite` thì mặc định là `add` (hợp) -> lưới hiện
       cả sang phải. Xấu hơn một bậc chứ không hỏng. */
    mask-image: linear-gradient(to top, #000 4%, transparent 78%),
                linear-gradient(to right, #000 34%, transparent 66%);
    -webkit-mask-image: linear-gradient(to top, #000 4%, transparent 78%),
                        linear-gradient(to right, #000 34%, transparent 66%);
    mask-composite: intersect;
    -webkit-mask-composite: source-in;
}

/* Bản lật ngược ở đỉnh màn — ảnh mẫu bọc lưới thành một đường hầm chứ không
   chỉ có sàn. Tông lục để hai mặt không cùng một màu (đọc ra chiều sâu). */
.auth-bg-ceil {
    bottom: auto !important;
    top: -6% !important;
    background-image:
        linear-gradient(rgba(34, 197, 94, .32) 1px, transparent 1px),
        linear-gradient(90deg, rgba(34, 197, 94, .22) 1px, transparent 1px);
    transform: perspective(300px) rotateX(-64deg);
    transform-origin: center top;
    mask-image: linear-gradient(to bottom, #000 4%, transparent 78%),
                linear-gradient(to right, #000 34%, transparent 66%);
    -webkit-mask-image: linear-gradient(to bottom, #000 4%, transparent 78%),
                        linear-gradient(to right, #000 34%, transparent 66%);
    mask-composite: intersect;
    -webkit-mask-composite: source-in;
    animation-direction: reverse;
}

/* --- VỆT CHÂN TRỜI.
   Hai lưới phối cảnh gặp nhau ở giữa màn, nhưng vì cả hai đều tan dần nên chỗ
   gặp trước đây chỉ là một vùng mờ — đường hầm mất tiêu điểm, và cả nền đọc ra
   "có hoa văn" chứ không ra "có chiều sâu". Bộ mẫu TQ luôn ghim chỗ đó bằng
   một vệt sáng ngang: nó là ĐƯỜNG CHÂN TRỜI, thứ cho mắt biết hai mặt lưới
   đang lùi về đâu.
   Đặt ở 50% và chỉ sáng ở nửa trái, đúng nơi hai lưới còn đậm. --- */
.auth-bg-horizon {
    top: 50% !important;
    bottom: auto !important;
    height: 220px !important;
    margin-top: -110px;
    background:
        /* nét sáng mảnh — chính là chân trời */
        linear-gradient(90deg, transparent 2%, rgba(120, 245, 205, .50) 26%,
                        rgba(45, 212, 191, .22) 52%, transparent 72%)
            center center / 100% 1px no-repeat,
        /* quầng toả quanh nét, cho chân trời "có khí quyển" */
        radial-gradient(60% 100% at 32% 50%, rgba(45, 212, 191, .16), transparent 70%);
}

/* --- Đốm sao tĩnh + ma trận chấm nhấp nháy (decoration3 rút gọn).
       Nhấp nháy bằng opacity của cả lớp, KHÔNG per-dot: rẻ hơn nhiều lần
       mà mắt vẫn đọc ra "dữ liệu đang chạy". --- */
.auth-bg-dots {
    opacity: .55;
    background-image:
        radial-gradient(1.5px 1.5px at 12% 22%, rgba(255,255,255,.55), transparent),
        radial-gradient(1.5px 1.5px at 68% 12%, rgba(78,222,130,.55), transparent),
        radial-gradient(1px 1px at 34% 62%, rgba(255,255,255,.40), transparent),
        radial-gradient(1.5px 1.5px at 88% 48%, rgba(45,212,191,.50), transparent),
        radial-gradient(1px 1px at 52% 84%, rgba(34,181,216,.45), transparent),
        radial-gradient(1px 1px at 22% 76%, rgba(255,255,255,.30), transparent),
        radial-gradient(1px 1px at 76% 72%, rgba(78,222,130,.40), transparent),
        radial-gradient(1px 1px at 44% 34%, rgba(255,255,255,.28), transparent);
    animation: hudTwinkle 5.5s ease-in-out infinite;
}

/* --- Vignette: tối 4 mép để mắt bị kéo vào giữa. --- */
.auth-bg-vignette {
    background: radial-gradient(ellipse 90% 80% at 50% 45%, transparent 40%, rgba(2, 10, 20, .68) 100%);
}

/* =============================================================
   2. Chrome HUD toàn màn — 4 góc + thanh trạng thái trên/dưới
   Chuyển thể HudCorners (kit2) từ 4 góc PANEL lên 4 góc MÀN HÌNH.
   ============================================================= */

.screen-corner {
    position: fixed;
    z-index: 2;
    width: 22px;
    height: 22px;
    pointer-events: none;
    /* drop-shadow bám theo NÉT vẽ; box-shadow sẽ toả quanh cả hộp 22x22
       khiến dấu góc trông như ô vuông đặc. */
    filter: drop-shadow(0 0 5px rgba(45, 212, 191, .75));
}

.screen-corner--tl {
    top: 14px;
    left: 14px;
    border-top: 2px solid var(--teal-5);
    border-left: 2px solid var(--teal-5);
}

.screen-corner--tr {
    top: 14px;
    right: 14px;
    border-top: 2px solid var(--teal-5);
    border-right: 2px solid var(--teal-5);
}

.screen-corner--bl {
    bottom: 14px;
    left: 14px;
    border-bottom: 2px solid var(--teal-5);
    border-left: 2px solid var(--teal-5);
}

.screen-corner--br {
    bottom: 14px;
    right: 14px;
    border-bottom: 2px solid var(--teal-5);
    border-right: 2px solid var(--teal-5);
}

/* =============================================================
   3. Shell 2 cột: trái thương hiệu, phải form
   ============================================================= */

/* ⛔ HAI LỚP, KHÔNG PHẢI MỘT — đây là chỗ bản trước hỏng.

   Bản trước gộp làm một: `.auth-layout` vừa `min-height:100vh` vừa
   `align-items:stretch` và hai panel là con trực tiếp của nó. Dòng flex vì thế
   nhận chiều cao của KHUNG NHÌN, nên trên màn cao 1000px panel Đăng nhập (nội
   dung ~600px) bị kéo giãn thành hộp 880px: thừa gần 300px trống phát sáng lục
   ở đáy — đúng lỗi "vỡ, cái hộp xanh to đùng".

   Tách ra thì mỗi lớp làm đúng một việc:
     · `.auth-layout` — CĂN GIỮA khối trong khung nhìn. Không đụng chiều cao con.
     · `.auth-shell`  — dòng flex THẬT, cao theo nội dung. `stretch` ở ĐÂY mới
       có nghĩa "hai panel cao bằng nhau", tức bằng panel CAO HƠN.

   Kiểm chứng nhanh khi sửa: mở trang Đăng nhập ở màn 1440×1080, đáy panel phải
   phải cách nút "Đăng ký" đúng phần chân panel — nếu còn một mảng tối rỗng thì
   `stretch` đã leo ngược lên `.auth-layout`. */
.auth-layout {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    /* Chừa chỗ cho 2 thanh HUD (34px + 10px mỗi đầu) để form không chui xuống dưới. */
    padding: 58px 32px;
    box-sizing: border-box;
}

/* =============================================================
   MỘT TẤM, HAI KHOANG

   ⛔ TRƯỚC ĐÂY LÀ HAI TẤM RIÊNG — đã gộp (2026-08-23), đừng tách lại.

   Bản trước: mỗi cột là một `.hud-panel` có khung riêng, đặt cạnh nhau cách
   26px. Cộng lại thành HAI bộ ngoặc góc + HAI bộ ray dọc + HAI viền trong +
   HAI thanh tiêu đề chữ. Mắt phải đọc "hai thiết bị" rồi mới suy ra chúng
   thuộc cùng một màn — nhiều nét hơn mà thông tin thì ít hơn.

   Nay: `.auth-shell` MANG khung, hai khoang nằm trong cùng một mặt kính, ngăn
   nhau bằng đúng một nét dọc (`.auth-seam`). Một bộ ngoặc, một viền trong, một
   đốm sáng chạy vòng. Ít nét hơn hẳn mà đọc ra "một tấm thiết bị liền".

   `.hud-panel` giữ nguyên tên vì nó vẫn là CÙNG một công thức khung (viền
   gradient dựng bằng 1px padding) — chỉ khác là nay chỉ còn một chỗ dùng.
   ============================================================= */

.auth-shell {
    position: relative;
    width: 100%;
    /* Kẹp bề ngang: hai khoang cộng lại ~1070px. Không kẹp thì trên màn 2560px
       tấm thẻ kéo dài thành một dải, khoang trái loãng ra không đọc được.
       Nới từ 1030 -> 1070 cùng lúc với khoang form (452 -> 492): nới riêng form
       mà giữ nguyên tấm thì khoang trái bị bóp lại 40px. */
    max-width: 1070px;

    /* ⛔ ĐÃ BỎ `view-transition-name: auth-shell` (2026-08-24). ĐỪNG GẮN LẠI.

       Gắn tên cho tấm thẻ nghĩa là bảo trình duyệt NỘI SUY KÍCH THƯỚC của nó
       giữa hai trang. Nhưng ảnh chụp cũ/mới của một nhóm được vẽ ở khổ
       `inline-size: 100%; block-size: auto`, nên trong suốt cú chuyển tấm thẻ
       bị KÉO GIÃN theo chiều dọc — ngoặc góc, ray dọc, viền, vạch chia đều méo
       theo. Đó chính là "lỗi hiển thị khi chuyển giữa hai form".

       Cách chữa đúng KHÔNG phải là nội suy cho mượt, mà là ĐỪNG ĐỂ NÓ ĐỔI CỠ —
       xem `min-height` ngay dưới. Khung không đổi cỡ thì không cần tên, không
       cần nội suy, và không có gì để méo. Nó cũng trả lại đúng cơ chế Day_13
       mà §13b mô tả: KHUNG ĐỨNG YÊN, chỉ RUỘT form trượt. */

    /* ⚠️ CHIỀU CAO ÉP SÀN nằm ở hai khối @media ngay dưới — xem giải thích ở đó.
       Đó là thứ làm tấm thẻ đứng im khi đổi màn, tức là thứ làm cú chuyển mượt. */
}

/* =============================================================
   ÉP HAI TRANG CAO BẰNG NHAU — điều kiện đủ để cú chuyển màn mượt

   Tấm Đăng ký cao hơn tấm Đăng nhập. Chênh lệch đó là TOÀN BỘ lý do khiến cú
   chuyển trước đây phải nội suy kích thước, và nội suy kích thước là thứ kéo
   ngoặc góc / ray dọc / viền méo theo (xem `.auth-shell`). Ép hai trang bằng
   nhau thì không còn gì để nội suy: khung đứng im tuyệt đối, chỉ ruột form
   trượt — đúng cơ chế Day_13 mà §13b mô tả.

   ⚠️ HAI SỐ, KHÔNG PHẢI MỘT, vì §14 có hai nhịp dọc khác nhau. Đã đo thật
   (Chrome, 1440px ngang, đổi chiều cao khung nhìn, đọc
   `document.querySelector('.auth-shell').getBoundingClientRect().height`):

       khung nhìn   Đăng nhập   Đăng ký   lệch
       ≤ 820px        623,3px    691px     68px   <- nhịp bị §14 siết
       ≥ 821px        691px      819px    128px   <- nhịp đầy đủ

   Nên sàn của mỗi dải phải bằng ĐÚNG chiều cao trang Đăng ký của DẢI ĐÓ.

   ⚠️ CHẶN DƯỚI CỦA MỖI @media LÀ CHỖ VỪA ĐỦ CHỖ, không phải số cho đẹp. Ép sàn
   mà khung nhìn không chứa nổi thì tự tay dựng lại thanh cuộn trên màn đăng
   nhập:
     · dải thấp: `.auth-layout` chừa 34px trên + 22px dưới -> cần 691+56 = 747px
     · dải cao : chừa 46px mỗi bên                          -> cần 819+92 = 911px
   Giữa 821 và 910 thì cố tình KHÔNG ép: ở đó ngay cả trang Đăng ký cũng đã phải
   cuộn, ép thêm chỉ làm trang Đăng nhập cuộn theo. Dải đó cú chuyển suy giảm
   thành mờ chéo của `root` — chấp nhận được, và không ai mất gì thêm.

   ⚠️ NẾU THÊM/BỚT Ô Ở FORM ĐĂNG KÝ THÌ PHẢI ĐO LẠI CẢ HAI SỐ, bằng đúng cách
   trên. Sàn nhỏ hơn chiều cao thật = hai trang lại lệch nhau và lỗi cũ quay về,
   mà lỗi đó nhìn ra rất khó nếu chỉ xem từng trang một.
   ============================================================= */
@media (min-height: 747px) and (max-height: 820px) {
    .auth-shell {
        min-height: 691px;
    }
}

@media (min-height: 911px) {
    .auth-shell {
        min-height: 819px;
    }
}

.hud-panel {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    /* 1px padding = bề dày viền; nền gradient của lớp này CHÍNH LÀ đường viền.
       Dốc gradient đi từ teal (góc trên-trái, phía khoang giới thiệu) sang lục
       brand (góc dưới-phải, phía khoang hành động) — cùng luật hai tông với bộ
       ngoặc góc, nên viền và ngoặc kể cùng một câu chuyện. */
    padding: 1px;
    box-sizing: border-box;
    background: linear-gradient(142deg,
        rgba(45, 212, 191, .78) 0%,
        rgba(45, 212, 191, .26) 26%,
        var(--border-1) 58%,
        rgba(34, 197, 94, .55) 100%);
    /* Ba lớp bóng, mỗi lớp một việc: quầng lục lan xa (đây là vật thể chính) ·
       quầng teal ôm sát (nối với chrome HUD) · bóng đen đổ xuống (nhấc thẻ khỏi
       nền — thiếu nó thì thẻ dán bẹp vào background). */
    /* BỐN tầng, mờ dần ra — quầng sáng thật không có mép, nó tắt dần. Một lớp
       bóng duy nhất bao giờ cũng đọc ra cái vòng cứng quanh thẻ. Thứ tự từ SÁT
       ra XA, mỗi tầng nhạt hơn tầng trước:
         1. vành lục sát mép, gần như một nét sáng   (10px)
         2. quầng teal ôm quanh                       (26px)
         3. quầng lục lan xa — tầng cho cảm giác "phát sáng vào không gian" (72px)
         4. bóng đen đổ xuống, nhấc thẻ khỏi nền     (64px, lệch xuống 28px)
       ⚠️ Bán kính tăng thì alpha PHẢI giảm. Giữ nguyên alpha ở tầng xa sẽ ra
       một mảng lục mờ to bằng nửa màn — đúng lỗi "hộp xanh" đã sửa trước đây. */
    box-shadow: 0 0 10px -2px rgba(34, 197, 94, .28),
                0 0 26px -8px rgba(45, 212, 191, .30),
                0 0 72px -26px rgba(34, 197, 94, .55),
                0 28px 64px -34px rgba(0, 0, 0, .95);
}

/* Mặt kính. `row` chứ không `column`: hai khoang nằm CẠNH nhau trong cùng một
   mặt, và `stretch` mặc định làm chúng cao bằng nhau — đó chính là thứ khiến
   tấm thẻ đọc ra liền một khối. */
.hud-panel-surface {
    position: relative;
    z-index: 2;
    display: flex;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    background: linear-gradient(180deg, rgba(18, 42, 66, .94) 0%, rgba(10, 29, 48, .96) 100%);
}

/* ⛔ ĐỘ ĐỤC ≥ .88 — số đo, không phải thẩm mỹ.

   Bản trước để .62/.70. Mặt kính trong tới mức đó thì quầng neon lục nằm SAU
   panel (`.auth-panel-wrap::before`) lọt thẳng qua và nhuộm cả tấm panel thành
   một mảng lục phẳng — cộng thêm lỗi kéo giãn ở trên, ra đúng "cái hộp xanh to
   đùng" mà chủ dự án nhìn thấy. Kính phải ĐỤC thì quầng mới đọng lại ở ngoài
   mép và đọc ra HÀO QUANG; trong quá thì nó thành NƯỚC SƠN.
   Muốn nhìn rõ nền hơn thì tăng blur, đừng hạ alpha. */

/* ⛔ ĐÃ BỎ `backdrop-filter: blur(28px)` TRÊN MẶT KÍNH (2026-08-24, theo yêu cầu
   "chữ nét hơn"). ĐỪNG DỰNG LẠI mà không đọc hết đoạn này.

   `backdrop-filter` buộc phần tử phải nằm trên một LỚP HỢP THÀNH riêng, và
   trình duyệt không biết trước nền đục nằm sau lớp đó tại thời điểm raster.
   Hệ quả: Chrome TẮT khử răng cưa theo kênh phụ (LCD/subpixel) cho TOÀN BỘ chữ
   nằm trong lớp — tức mọi chữ trong tấm thẻ, cả hai khoang, rơi xuống khử răng
   cưa thang xám. Trên Windows đó là một bậc nét nhìn thấy rõ.

   Cái giá phải trả để đổi lấy gì? Nền của mặt kính vốn đã đục .94/.96, tức chỉ
   4-6% hậu cảnh lọt qua. Đã chụp đối chứng hai bản (có/không blur) ở
   `.auth-brand`: phần lưới nền hiện thêm khi bỏ blur gần như không nhìn ra.
   Trả toàn bộ chữ của màn đăng nhập để lấy 4% hoa văn là một cuộc đổi tồi.

   Bỏ `@supports` đi thì rule gốc ở trên (alpha .94/.96, KHÔNG blur) tự nhận
   việc — mặt kính vẫn đục, quầng neon vẫn đọng ngoài mép, và chữ được raster
   thẳng vào lớp chính nên giữ subpixel-antialias.

   ⚠️ Ngưỡng alpha ≥ .88 ở ghi chú trên VẪN CÒN HIỆU LỰC: nay không còn blur
   che nữa nên hạ alpha xuống là hậu cảnh lọt qua SẮC NÉT, còn tệ hơn trước. */

/* --- Ánh sáng viền trên mặt kính.
   Kính thật bắt sáng ở MÉP TRÊN (nguồn sáng ở trên) rồi tối dần xuống. Thiếu
   nét này thì mặt kính đọc ra một tấm phẳng tô màu; có nó thì nó có BỀ DÀY.
   Cùng thủ pháp với gờ lõm của ô nhập, chỉ đảo chiều: ô nhập lõm vào nên sáng
   ở đáy, tấm thẻ nổi lên nên sáng ở đỉnh. --- */
.hud-panel-surface::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    box-shadow: inset 0 1px 0 rgba(190, 240, 255, .10),
                inset 0 -1px 0 rgba(0, 0, 0, .28);
}

/* --- VIỀN TRONG (双层边框) — mô-típ hai lớp viền của `DataV borderBox2`:
   nét ngoài ở inset 2, nét trong ở inset 6, tức KHE HỞ ĐÚNG 4px. Đây là thứ
   làm khung đọc ra "có chế tác" thay vì một hình chữ nhật kẻ viền.
   ⚠️ Khe 4px là mức nhỏ nhất mắt còn tách được thành HAI nét; 3px thì hai nét
   dính lại thành một vệt dày nhoè (đo lại từ nguồn, ghi ở `FIELD_INNER_FRAME`
   của vci-sun-mobile). --- */
.hud-inner-frame {
    position: absolute;
    inset: 5px;
    z-index: 4;
    pointer-events: none;
    border: 1px solid rgba(45, 212, 191, .13);
}

/* --- GÓC VÁT KẺ SỌC CHÉO (斜纹角标) ---

   Chữ ký dễ nhận nhất của bộ mẫu command-center TQ: mọi panel trong
   `BigDataView/web/003` đều có một mẩu kẻ sọc chéo ghim ở góc trên-trái thanh
   tiêu đề. Nó là thứ khiến khung đọc ra "thiết bị công nghiệp" chứ không phải
   "cái div có border" — và là chi tiết bản trước thiếu.

   Dựng bằng MỘT `repeating-linear-gradient` + `clip-path` cắt vát mép phải, nên
   không tốn thẻ ảnh nào. Sọc nghiêng -20° khớp đúng độ nghiêng của 3 vạch chéo
   `.hud-slashes` ngay bên cạnh — hai chi tiết phải cùng một góc, lệch góc thì
   mắt đọc ra lỗi in. */
.hud-tab {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 4;
    width: 62px;
    height: 7px;
    pointer-events: none;
    background: repeating-linear-gradient(-70deg,
        var(--teal-5) 0 2px,
        transparent 2px 6px);
    /* Vát mép phải để mẩu sọc "chạy hết đà" thay vì cụt ngang */
    clip-path: polygon(0 0, 100% 0, calc(100% - 7px) 100%, 0 100%);
    filter: drop-shadow(0 0 4px rgba(45, 212, 191, .8));
    opacity: .95;
}

/* --- VẠCH CHIA (刻度) dọc hai mép tấm.
   Bộ mẫu TQ luôn có một dải vạch chia ở mép khung — dấu hiệu "đây là thiết bị
   ĐO". Nay tấm chỉ còn MỘT nên đặt đối xứng cả hai mép; trước đây mỗi panel
   một vạch nên chúng lệch nhau. --- */
.hud-ticks {
    position: absolute;
    top: 64px;
    bottom: 64px;
    z-index: 5;
    width: 6px;
    pointer-events: none;
    background-image: repeating-linear-gradient(180deg,
        rgba(45, 212, 191, .55) 0 1px,
        transparent 1px 11px);
    /* Tan dần hai đầu: vạch chia cắt cụt ở hai đầu đọc ra như bị xén */
    mask-image: linear-gradient(180deg, transparent, #000 22%, #000 78%, transparent);
    -webkit-mask-image: linear-gradient(180deg, transparent, #000 22%, #000 78%, transparent);
}

.hud-ticks--left  { left: -11px; }
.hud-ticks--right { right: -11px; }

/* =============================================================
   4. Cột trái — panel giới thiệu sản phẩm
   ============================================================= */

/* Khoang trái. Không còn khung/bóng riêng — nay nó chỉ là NỬA của một tấm.

   ⛔ `display: flex; flex-direction: column` BẮT BUỘC, đừng bỏ.
   `.brand-body` bên trong mang `flex: 1` để SƠ ĐỒ hút được chiều cao dôi. Nếu
   khoang này là block thường thì `.brand-body` cao theo NỘI DUNG chứ không theo
   khoang, `flex: 1` mất tác dụng, và phần dôi rơi xuống thành một mảng trống ở
   đáy khoang trái — đúng lỗi đã tốn ba lượt để sửa. (Bản hai-panel không lộ ra
   vì `.hud-panel` vốn đã là flex column; gộp khung xong thì mất tính chất đó.)

   `min-width: 0` bắt buộc cho flex item chứa chữ: thiếu nó thì `min-width:auto`
   khiến khoang không co được dưới min-content và tấm thẻ tràn ngang. */
.auth-brand {
    display: flex;
    flex-direction: column;
    flex: 1 1 578px;
    min-width: 0;
}

/* --- NÉT NGĂN HAI KHOANG.
   Toàn bộ thứ chia đôi tấm thẻ chỉ là nét này. Trước đây chỗ này là hai mép
   panel + một khe 26px + hai bộ ray dọc: sáu nét cho một việc.
   Tan dần hai đầu để nó không đâm vào ngoặc góc trên/dưới — nét chạy hết chiều
   cao sẽ CẮT tấm thẻ làm đôi thật, đúng thứ vừa bỏ công gộp lại. --- */
.auth-seam {
    flex: 0 0 1px;
    align-self: stretch;
    background: linear-gradient(180deg,
        transparent 0%,
        rgba(45, 212, 191, .30) 18%,
        rgba(45, 212, 191, .30) 82%,
        transparent 100%);
}

/* ⛔ CHIỀU CAO DÔI PHẢI CÓ CHỖ ĐI — bài học của hai lần sửa trước.

   Panel này bị `stretch` cao bằng panel form, mà form Đăng ký cao hơn form Đăng
   nhập ~230px. Đã thử và ĐÃ LOẠI hai cách chia phần dôi đó:

     · `center`         -> dôi chia đôi thành hai mảng trống ở đỉnh và đáy
     · `space-between`  -> dôi dồn vào khe giữa, đọc ra một CÁI LỖ giữa panel

   Cả hai đều là cùng một lỗi "panel rỗng", chỉ đổi chỗ. Cách đúng: cho phần dôi
   một phần tử để CHẢY VÀO — `.brand-canvas` mang `flex: 1` nên nó nở/co theo
   chỗ trống, còn bốn khối chữ giữ nguyên nhịp. Nhờ vậy `flex-start` ở đây là
   đủ, không cần trò canh chỉnh nào. */
.brand-body {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 20px;
    /* Nới lề trong (28/32 -> 34/44): chữ "QUẢN LÝ DỰ ÁN" trước đây nằm khá sát
       viền phát sáng bên trái, đọc ra bí. 44px cho cạnh trái là mức mà tiêu đề
       vẫn còn neo vào mép mà không dính vào nó. */
    padding: 34px 44px 30px;
}

/* Khối giữa — tiêu đề hero + vạch ngăn + câu dẫn. Ba thứ này phải đi LIỀN nhau
   (chúng là một câu chuyện), nên gom vào một khối rồi mới để `space-between`
   dàn khoảng cách giữa các KHỐI. */
.brand-lead {
    display: flex;
    flex-direction: column;
    /* Khai qua biến để `.brand-kicker` trừ lại được đúng lượng này — xem chú
       thích "khe hai bậc tiêu đề" ở đó. Đổi số ở đây thì khe kicker↔tiêu đề
       KHÔNG đổi theo, vì bên kia trừ theo biến chứ không theo số. */
    --lead-gap: 16px;
    gap: var(--lead-gap);
    /* ⛔ MARGIN ÂM CÓ CHỦ ĐÍCH — kéo khối chữ lên sát logo.

       Logo và tiêu đề là MỘT khối nhận diện, phải đọc ra liền nhau; nhưng
       `.brand-body` dùng `gap: 20px` chung cho mọi khối con, mà 20px đó lại
       đúng cho hai khe kia (chữ -> sơ đồ -> danh sách). Hạ `gap` là ba khe cùng
       hẹp lại, cả khoang trái dồn cục.

       Nên chỉ siết ĐÚNG khe này: -13px, cộng với ~3px nửa khoảng dòng của
       `.brand-kicker` (line-height 26 trên chữ 20) còn lại ~10px hở thật.
       ⚠️ Phải trả về 0 ở bậc ≤900px: ở đó `.brand-body` xoay sang HÀNG NGANG,
       logo nằm CẠNH chữ — margin âm dọc sẽ đẩy chữ lệch lên khỏi trục logo. */
    margin-top: -13px;
    /* ⛔ ĐÃ BỎ `view-transition-name: brand-lead` — xem `.brand-head`. */
}

/* =============================================================
   4c. SƠ ĐỒ PHÂN LỚP — phần tử hút chiều cao dôi

   Nhiệm vụ 1 (quan trọng nhất): `flex: 1`. Xem chú thích `.brand-body` — không
   có phần tử co giãn ở đây thì chiều cao dôi giữa trang Đăng nhập và Đăng ký
   biến thành một cái lỗ giữa panel.

   Nhiệm vụ 2: nói đúng sản phẩm. Bản trước chỗ này là vòng quét radar; nhìn thì
   "công nghệ" nhưng radar không liên quan gì tới quản lý dự án xây dựng, nên
   panel vẫn đọc ra rỗng ruột dù đã kín chỗ. Ba mặt lưới chồng lớp thì nói đúng
   thứ VCI360 làm — hiện trường ở dưới, mô hình BIM ở giữa, lớp dữ liệu ở trên,
   nối nhau bằng hai trụ đứng.

   ⛔ MỌI KÍCH THƯỚC ĐO THEO CHIỀU CAO (`%` của height + `aspect-ratio`), KHÔNG
   theo bề ngang. Khối này rộng ~450px nhưng cao từ 92px tới 296px tuỳ trang;
   dùng `%` bề ngang thì trang Đăng nhập nhận một hình to gấp năm khung chứa và
   bị xén thành mấy vệt — đúng lỗi đã mắc ở bản vòng tròn trước. Đã trả giá một
   lần rồi, đừng đổi lại.

   HÌNH HỌC (H = chiều cao khối, mọi số dưới đây suy ra từ đó):
     · cạnh hình vuông trước khi xoay  S = 46%H
     · xoay 45° + nén dọc 0,55 -> hình thoi rộng 1,414·S = 65%H, cao 0,778·S = 35,8%H
     · tâm 3 lớp: 34%H · 49,5%H · 65%H  =>  `top` = tâm - S/2 = 11% · 26,5% · 42%
     · trụ đứng nối tâm lớp trên tới tâm lớp dưới: cao 31%H, rộng 65%H
   Sửa một số thì phải sửa cả cụm — chúng khớp nhau bằng phép tính, không phải
   bằng mắt.
   ============================================================= */

.brand-canvas {
    position: relative;
    flex: 1;
    /* Sàn 128px: dưới mức đó ba lớp dính vào nhau thành một vệt, không còn đọc
       ra "phân lớp". Trần 296px: quá cỡ đó hình tranh chỗ với tiêu đề hero. */
    min-height: 150px;
    max-height: 296px;
    margin: -2px 0;
    overflow: hidden;
    pointer-events: none;
    /* Khối này NỞ RA giữa hai trang (nó là chỗ hút chiều cao dôi), nên nó là
       khối DUY NHẤT bên khoang trái được phép đổi kích thước trong lúc chuyển —
       và vì nó là hình trừu tượng nên phóng to mượt trông đúng chứ không sai.
       Xem chú thích ở `.brand-head`. */
    view-transition-name: brand-canvas;
}

    /* ⛔ ĐÃ BỎ mask tan dần bốn phía (bản trước có). Hai lý do:
       · Không cần: hình cao 0,668·H và rộng 0,65·H, tức KHÔNG BAO GIỜ chạm mép
         khối — chẳng có mép cứng nào để phải làm mềm. Mask chỉ là phòng xa thừa.
       · Có hại: mask đo theo cạnh gần nhất cho bán kính = H/2, mà ba nhãn lớp
         nằm cách tâm ~0,4·H nên chúng bị làm mờ mất một nửa. Trang trí không
         được phép ăn vào chữ.

       `left: 44%` chứ không 50%: cụm hình gồm SƠ ĐỒ (đối xứng) + BA NHÃN nằm hẳn
       về bên phải. Canh sơ đồ vào giữa thì cả cụm lệch phải; dịch sang trái vài
       phần trăm thì cụm mới cân. 44% là mức cân ở cả hai cỡ khối (150px và
       296px) — nhãn rộng cố định còn sơ đồ thì co giãn, nên không có con số nào
       đúng tuyệt đối cho mọi cỡ. */
    .brand-canvas > span {
        position: absolute;
        left: 44%;
    }

/* --- Quầng nền + bóng đổ dưới chân.
   Quầng cho khối lớp một "khí quyển" để nó không lơ lửng trên nền phẳng; vệt
   tối ở đáy là BÓNG — thiếu nó thì chồng lớp trôi giữa panel, có nó thì nó
   đứng trên một mặt. Gộp hai gradient vào MỘT phần tử: cả hai đều là nền
   không tương tác, tách ra chỉ thêm một thẻ mà không thêm khả năng nào. --- */
.iso-glow {
    top: 50%;
    height: 96%;
    aspect-ratio: 1.7;
    transform: translate(-50%, -50%);
    background:
        radial-gradient(ellipse 30% 7% at 50% 84%, rgba(2, 12, 24, .85), transparent 70%),
        radial-gradient(ellipse at 50% 46%, rgba(34, 197, 94, .17), transparent 66%);
    /* --- NHỊP SỐNG của cả sơ đồ nằm ở ĐÂY, và chỉ ở đây.

       Trước kia cả `.brand-canvas` trôi lên xuống 5px (`isoFloat`) — đã bỏ, đó
       chính là nguồn giật; số đo đầy đủ ở `@keyframes isoGlowBreathe`.

       ⛔ VÌ SAO ĐẶT LÊN QUẦNG chứ không lên ba mặt phẳng: bất cứ animation nào
       cũng ép phần tử mang nó thành một lớp hợp thành riêng. Với mặt phẳng lưới
       (nét 1px + `rotate(45deg)`) thì tách lớp = nội suy lúc vẽ = nhoè vĩnh
       viễn, đo được mất 11% độ sắc. Quầng thì NGƯỢC LẠI: nó chỉ có hai gradient
       mềm, không có một nét nào để nhoè — đo lại sau khi tách lớp, độ sắc của
       cả khung KHÔNG giảm (2,2967 so với 2,2422 khi không có animation nào).
       Đây là phần tử DUY NHẤT trong sơ đồ động được mà không phải trả giá.

       Chu kỳ 7,2s = đúng HAI lần chu kỳ xung trên trụ (3,6s), nên cả hình chỉ
       có một nhịp: hai xung dâng lên trong một lần quầng thở. Bắt đầu ở 1 (đúng
       giá trị tĩnh) nên không có bước nhảy nào lúc animation khởi động. --- */
    animation: isoGlowBreathe 7.2s ease-in-out infinite;
}

/* --- Ba mặt phẳng lưới.
   `aspect-ratio: 1` + `height` => hình vuông đo theo chiều cao khối. Xoay 45°
   rồi nén dọc 0,55 ra phối cảnh trục đo — cùng góc nhìn với lưới nền của trang.
   ⚠️ `scaleY` nén CẢ nét viền: viền 1px thành 0,55px ở cạnh ngang. Đó là lý do
   viền khai 1px mà vẫn đọc ra mảnh — muốn dày hơn phải tăng ở đây, đừng tăng
   `border-width` của riêng một lớp rồi tưởng ba lớp cùng độ dày. --- */
.iso-plane {
    height: 46%;
    aspect-ratio: 1;
    /* ⛔ THỨ TỰ: `scaleY` phải đứng TRƯỚC `rotate` trong chuỗi khai báo.
       CSS áp transform từ PHẢI sang TRÁI, nên viết `rotate(45deg) scaleY(.55)`
       là nén dọc hình vuông thành hình chữ nhật RỒI mới xoay — ra một cái thẻ
       nghiêng 45°, không ra mặt phẳng nằm. Phải xoay trước (được hình thoi) rồi
       mới nén cả hình thoi đó theo trục màn hình. Đã mắc đúng lỗi này một lần. */
    transform: translateX(-50%) scaleY(.55) rotate(45deg);
    border: 1px solid rgba(45, 212, 191, .38);
    /* Lưới 4x4 theo % nên co giãn cùng mặt phẳng, không cần media query nào */
    background-image:
        linear-gradient(rgba(45, 212, 191, .16) 1px, transparent 1px),
        linear-gradient(90deg, rgba(45, 212, 191, .16) 1px, transparent 1px);
    background-size: 25% 25%;
    /* ⛔ BA MẶT PHẲNG PHẢI ĐỨNG YÊN VÀ KHÔNG ĐƯỢC CÓ ANIMATION NÀO — kể cả
       `opacity`, dù luật §12b cho phép. Đã thử và đã ĐO:

       Gắn một nhịp sáng `opacity` lên đây là ép trình duyệt tách mỗi mặt phẳng
       thành MỘT LỚP HỢP THÀNH riêng. Lúc đó phép `rotate(45deg) scaleY(.55)` ở
       trên không còn được nướng thẳng vào lúc raster nữa mà bị áp lúc VẼ, tức
       nội suy song tuyến — mọi nét 1px mềm đi VĨNH VIỄN. Số đo trên ảnh chụp
       thật: năng lượng biên 2,2422 (không animation) tụt còn 1,9902, mất 11%,
       và đỉnh dốc 161,8 -> 156,9. Nhìn bằng mắt cũng thấy lưới nhạt hẳn đi.
       Đổi cái nhoè NHẤP NHÁY của `isoFloat` lấy một cái nhoè ĐỨNG YÊN thì
       không phải là sửa.

       Nhịp sống của sơ đồ vì thế nằm ở `.iso-glow` (quầng nền, toàn gradient
       mềm — hợp thành riêng vẫn sắc nét: đo được 2,2967) và ở hai xung chạy
       trên trụ. Cấu trúc thì đứng im và sắc. */
}

/* Lớp đáy — HIỆN TRƯỜNG. Mờ nhất: nó ở xa nhất và là lớp thô nhất. */
.iso-plane--site {
    top: 42%;
    border-color: rgba(45, 212, 191, .36);
    background-image:
        linear-gradient(rgba(45, 212, 191, .13) 1px, transparent 1px),
        linear-gradient(90deg, rgba(45, 212, 191, .13) 1px, transparent 1px);
}

/* Lớp giữa — MÔ HÌNH BIM */
.iso-plane--model {
    top: 26.5%;
    border-color: rgba(45, 212, 191, .48);
}

/* Lớp đỉnh — DỮ LIỆU. Sáng nhất và là lớp DUY NHẤT có quầng: đây là thứ sản
   phẩm bán, và trong một chồng lớp thì mắt phải biết đọc từ đâu trước. */
.iso-plane--data {
    top: 11%;
    border-color: rgba(78, 222, 130, .85);
    background-image:
        linear-gradient(rgba(78, 222, 130, .20) 1px, transparent 1px),
        linear-gradient(90deg, rgba(78, 222, 130, .20) 1px, transparent 1px);
    box-shadow: 0 0 22px -4px rgba(34, 197, 94, .55);
}

/* --- Hai trụ đứng nối lớp đỉnh xuống lớp đáy.
   Không có chúng thì ba hình thoi đọc ra ba vật rời nhau nằm chồng tình cờ;
   có chúng thì cả cụm đọc ra MỘT khối có chiều cao.
   Chiều cao 31% = khoảng cách tâm lớp đỉnh tới tâm lớp đáy (65% − 34%).
   Bề ngang 65%H = đúng bề ngang hình thoi, ép bằng `aspect-ratio` 65/31. --- */
.iso-risers {
    top: 34%;
    height: 31%;
    aspect-ratio: 2.097;
    transform: translateX(-50%);
    border-left: 1px solid rgba(45, 212, 191, .30);
    border-right: 1px solid rgba(45, 212, 191, .30);
}

/* --- Xung dữ liệu CHẠY NGƯỢC LÊN theo hai trụ.

   ⛔ Bản đầu ở đây là một vạch sáng NGANG quét từ trên xuống. Bỏ vì sai phối
   cảnh: hình này là trục đo, mọi cạnh của nó đều nghiêng 45°, nên một vạch
   nằm ngang tuyệt đối không thuộc về mặt phẳng nào — nó đọc ra như cái gạch
   lạc trôi qua, chứ không ra tia quét.
   Trụ đứng thì CÓ THẬT trong hình, nên xung chạy dọc theo trụ đọc ra đúng thứ
   cần nói: dữ liệu từ hiện trường dâng lên lớp mô hình rồi lên lớp dữ liệu.
   Hai trụ lệch pha nửa chu kỳ để hình không "đập" một nhịp như đèn nháy.

   ⛔ Chỉ động vào `transform`/`opacity` (luật §12b cho hiệu ứng lặp vô hạn).
   `translateY(%)` tính theo chiều cao CHÍNH phần tử: phần tử cao 34% khung trụ
   nên đi hết trụ = 66/34 = 194% chiều cao của nó. Quãng đường vì thế co giãn
   cùng hình, không có một con số px nào phải chỉnh tay. --- */
.iso-risers::before,
.iso-risers::after {
    content: "";
    position: absolute;
    top: 0;
    width: 1px;
    height: 34%;
    background: linear-gradient(to top,
        transparent, rgba(160, 255, 214, .95) 55%, transparent);
    animation: isoRise 3.6s ease-in-out infinite;
}

/* --- Ba nhãn lớp. ĐÂY là thứ biến hình trang trí thành một SƠ ĐỒ.

   Không có nhãn, ba mặt phẳng chỉ là hoa văn "trông có vẻ công nghệ" — đúng
   loại hình mà bản vòng quét radar trước đó mắc phải, và là lý do panel trái bị
   chê. Có nhãn, người xem đọc được hệ thống làm gì ngay ở màn đăng nhập:
   hiện trường -> mô hình BIM -> lớp dữ liệu.

   `top: 0 / 50% / 100%` rơi đúng tâm ba mặt phẳng vì hộp cha trải đúng từ tâm
   lớp đỉnh tới tâm lớp đáy (xem `.iso-risers`). Không có số canh tay nào ở đây,
   nên đổi hình học của sơ đồ thì nhãn tự đi theo. --- */
.iso-tag {
    position: absolute;
    left: 100%;
    margin-left: 15px;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    gap: 7px;
    font-family: var(--font-mono);
    font-size: var(--text-caption-10);
    font-style: normal;
    line-height: 13px;
    letter-spacing: .15em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--fg-5);
}

    /* Nét dẫn nối nhãn về phía mặt phẳng của nó — thiếu nó thì ba nhãn đọc ra
       một danh sách đứng cạnh hình, không ra chú thích CỦA hình. */
    .iso-tag::before {
        content: "";
        width: 12px;
        height: 1px;
        background: linear-gradient(90deg, transparent, currentColor);
        opacity: .7;
    }

/* Lớp đỉnh sáng nhất trong hình thì nhãn của nó cũng phải sáng nhất — nếu không
   mắt đọc hình theo một thứ tự còn đọc chữ theo một thứ tự khác. */
.iso-tag--data  { top: 0;    color: var(--primary-4); }
.iso-tag--model { top: 50%;  color: var(--fg-4); }
.iso-tag--site  { top: 100%; }

.iso-risers::before { left: -1px; }

.iso-risers::after {
    right: -1px;
    animation-delay: 1.8s;
}

/* Logo + dòng nhãn nằm NGANG. Trước đây logo đứng riêng một dòng rồi tới nhãn,
   nên khối đầu panel cao 130px chỉ để nói đúng một việc. Xếp ngang lấy lại
   ~44px chiều cao và cho dòng nhãn một mỏ neo — chữ nhỏ đứng trơ giữa panel
   rộng thì trôi. */
.brand-head {
    display: flex;
    align-items: center;

    /* ⛔ ĐÃ BỎ `view-transition-name: brand-head` (2026-08-24), cùng lượt với
       `brand-lead` và `brand-modules`. ĐỪNG GẮN LẠI — và đây là lý do, vì ghi
       chú cũ ở đây nói ngược lại.

       Ghi chú cũ đúng VỚI TIỀN ĐỀ CỦA NÓ: hồi đó `.auth-shell` có tên riêng và
       đổi cỡ giữa hai trang, nên mọi khối con không có tên đều bị kéo giãn theo
       ảnh chụp của tấm. Đặt tên cho từng khối là cách gỡ khối đó ra khỏi ảnh
       chụp bị giãn.

       Nay tiền đề đã mất: tấm thẻ bị ép `min-height` nên KHÔNG còn đổi cỡ, và
       `.auth-shell` cũng không còn tên. Không có gì giãn thì không có gì để gỡ.

       Mà đặt tên thừa thì không hề miễn phí. Mỗi tên sinh ra một nhóm chuyển
       cảnh RIÊNG, nằm phẳng hàng cùng cấp với các nhóm khác chứ không lồng vào
       nhau, và bay nổi trên mọi thứ trong lớp phủ chuyển cảnh. Bảy nhóm cùng
       chạy (đã đếm được đúng bảy trong lúc chuyển: root · auth-shell ·
       auth-form · brand-head · brand-lead · brand-canvas · brand-modules) là
       bảy vật thể rời nhau, mỗi vật một quỹ đạo — mắt đọc ra tấm thẻ bị vỡ ra
       từng mảnh rồi lắp lại. Ít tên hơn thì cú chuyển liền hơn. */
}

/* --- Logo ---
   `logo-vci.svg` — logo VCI360 dạng VUÔNG (viewBox 507×508.79), vòng tròn hai
   cung lục/đỏ ôm chữ "VCI 360", nền trong suốt nên đặt thẳng lên nền tối được.

   ⛔ ĐỔI CỠ Ở ĐÂY THÌ PHẢI ĐỔI CẢ `width`/`height` trên thẻ <img> trong
   _LayoutBase.cshtml — xem chú thích ở đó.

   Bản PNG trước đây phải giữ tối thiểu 88px vì nét vòng tròn của mark mỏng
   dưới 1 pixel ở cỡ nhỏ rồi bị khử răng cưa thành vệt mờ ("logo bé và vỡ").
   SVG không có ràng buộc đó — nhưng 120px vẫn là cỡ ĐỌC ĐƯỢC chữ "360" bên
   trong vòng; nhỏ hơn nữa thì mark còn rõ mà chữ thành vệt.

   Ba lệnh cấm của chủ dự án vẫn nguyên: KHÔNG khung bọc, KHÔNG quầng sáng sau
   logo, KHÔNG wordmark "VCI360" cỡ lớn bằng chữ. --- */
.brand-logo {
    display: block;
    width: 120px;
    height: 120px;
}

/* =============================================================
   4a. KHỐI TIÊU ĐỀ HERO — "NỀN TẢNG SỐ" / "QUẢN LÝ DỰ ÁN"

   ⛔ HÌNH THỨC HIỆN TẠI (2026-08-24). Khối này ĐÃ QUA HAI LẦN GIẢM: bỏ ngoặc
   vuông [ ], rồi bỏ đường đứt nét + crosshair — cả hai đều theo yêu cầu chủ dự
   án, lý do chung là "rối". Nay còn HAI họa tiết, mỗi cái làm MỘT việc:

     1. VẠCH SÁNG DỌC ôm CẢ HAI dòng (`.brand-titleblock::before`).
        Bản trước vạch này nằm ở `.brand-kicker::before` nên nó chỉ cao 20px và
        chỉ neo dòng nhỏ; dòng "QUẢN LÝ DỰ ÁN" — dòng NẶNG nhất trang — thì
        không neo vào gì cả. Nay vạch chạy hết chiều cao khối, cả hai bậc tiêu
        đề cùng treo trên một trục.
     2. CHỮ ĐƠN CÁCH (`--font-mono`) cho dòng kicker.
        Chất "kỹ thuật" nói bằng MẶT CHỮ chứ không bằng họa tiết bao quanh: chữ
        đơn cách + giãn chữ + chữ hoa đọc ra nhãn thiết bị/terminal, mà không
        thêm vật thể nào vào khung — đây cũng là mặt chữ đang dùng cho nhãn HUD
        của sơ đồ ba lớp bên dưới, nên hai chỗ đọc ra cùng một hệ.

   ⛔ ĐỪNG THÊM HỌA TIẾT THỨ BA. Hai thứ đã bỏ (`[ ]` và đường đứt nét) đều là
   họa tiết ĐẶT CẠNH chữ, và cả hai đều bị gỡ vì cùng một lý do: khoang trái đã
   có logo + hai bậc tiêu đề + mô tả + sơ đồ ba lớp + ba thẻ đánh số, thêm vật
   thể vào dòng kicker là dòng đó tranh chú ý với chính tiêu đề nó đang dẫn.
   Nếu cần "kỹ thuật" hơn thì chỉnh MẶT CHỮ hoặc màu, đừng thêm hình.
   ⛔ Đừng dựng lại ngoặc bằng ký tự "[" "]" trong markup: trình đọc màn hình sẽ
   đọc thành "dấu ngoặc vuông", và bề dày nét lại đổi theo font.

   ⛔ KHÔNG dùng `padding` trên `.brand-titleblock` để chừa chỗ cho vạch dọc:
   padding đẩy chữ xuống và phá nhịp `margin-top: -13px` mà `.brand-lead` đã đo
   với logo. Mọi họa tiết đặt bằng toạ độ ÂM — chúng mượn chỗ trong lề 44px của
   `.brand-body`, không lấy chỗ của chữ.
   ⚠️ `--tb-out` phải NHỎ HƠN padding trái của `.brand-body` (44px, và 24px ở
   bậc ≤1180px) — nếu không vạch dọc chui ra ngoài khung và dính vào
   `.hud-inner-frame` (cách mép 4px). Đổi padding thì đổi cả biến này.
   ============================================================= */
.brand-titleblock {
    position: relative;
    /* Khoảng lùi RA NGOÀI của gạch = khe quang học + bề dày gạch, nên mép phải
       gạch cách chữ đúng --tbar-gap y như bên khoang phải. */
    --tb-out: calc(var(--tbar-gap) + var(--tbar-w));
}

    /* Vạch sáng dọc — CÙNG mô-típ với gạch dọc của tiêu đề form bên khoang
       phải; bề dày / màu / quầng đều lấy từ token `--tbar-*` (§1) nên hai bên
       không thể lệch nhau. Tan dần xuống dưới để nó KHÔNG cắt ngang vạch
       `.brand-divider` ngay phía dưới. */
    .brand-titleblock::before {
        content: "";
        position: absolute;
        left: calc(-1 * var(--tb-out));
        top: 2px;
        bottom: 4px;
        width: var(--tbar-w);
        background: var(--tbar-fill);
        box-shadow: var(--tbar-glow);
    }

/* --- BẬC 1 CỦA TIÊU ĐỀ: "NỀN TẢNG SỐ".

   Dòng nhỏ IN HOA giãn chữ RỘNG đặt trên dòng lớn nén chữ — chênh lệch cỡ cộng
   chênh lệch giãn chữ mới tạo ra nhịp; chỉ đổi màu thì hai dòng đọc ra một khối.

   `.32em` là mức giãn CAO (gấp ~2,5 lần chữ thường). Ở cỡ 20px chữ hoa tiếng
   Việt còn đọc tốt tới ~.36em; trên mức đó dấu thanh bắt đầu tách rời khỏi chữ.

   ⚠️ `display: flex` để đường đứt nét ăn hết chỗ thừa bên phải (`flex: 1`).
   `align-items: center` canh đường nét vào giữa THÂN CHỮ HOA chứ không vào giữa
   hộp dòng — hộp dòng cao 26px trên chữ 20px nên hai mốc lệch nhau ~1px, mắt
   không thấy nhưng nét 1px thì thấy. --- */
.brand-kicker {
    position: relative;
    display: flex;
    align-items: center;
    /* ⛔ KHÔNG còn `gap`. Sau khi bỏ đường đứt nét, `.brand-kicker` chỉ còn MỘT
       flex item (`.brand-kicker-text`) nên `gap` không có gì để chen vào —
       để lại là một trị số chết, người sau đọc sẽ tưởng nó đang có tác dụng. */
    /* KHE GIỮA HAI BẬC TIÊU ĐỀ = 4px. Muốn hai bậc đọc ra MỘT khối thì khe phải
       nhỏ hơn hẳn khe ngăn nó với khối kế (16px), không thì chúng rời ra thành
       hai dòng chữ không liên quan. Trị số thật nằm ở đây và ở rule
       `.brand-lead > .brand-kicker` ngay dưới — xem giải thích ở đó. */
    margin: 0 0 4px;
    /* CHỮ ĐƠN CÁCH — đây là thứ tạo ra chất "công nghệ" sau khi bỏ ngoặc [ ].
       Dùng đúng biến --font-mono mà nhãn HUD của sơ đồ ba lớp đang dùng, để hai
       chỗ đọc ra cùng một hệ chữ thay vì hai phông lạ nhau. */
    font-family: var(--font-mono);
    font-size: 19px;
    /* 600 chứ không 700: JetBrains/Roboto Mono ở 700 nặng hơn hẳn bản sans cùng
       số, chữ hoa giãn chữ mà nét quá dày thì đọc ra chữ in đậm chứ không ra
       nhãn kỹ thuật. */
    font-weight: 600;
    line-height: 26px;
    /* .18em chứ không .32em: chữ đơn cách đã có bước tiến đều và rộng sẵn, cộng
       thêm .32em thì các chữ cái rời hẳn ra thành sáu vật thể lẻ, đọc không ra
       một từ. .18em giữ được nhịp "nhãn thiết bị" mà cụm chữ vẫn liền. */
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--teal-5);
    /* ⚠️ SIẾT TỪ 22px XUỐNG 9px (yêu cầu "chữ nét hơn"). Quầng bán kính 22px
       toả quanh chữ hoa cỡ 20px giãn .32em thì lấp đầy luôn khe giữa các chữ
       cái — cả dòng đọc ra một dải sương lục chứ không ra sáu chữ rời. 9px chỉ
       ôm sát nét, đủ để dòng chữ vẫn "phát sáng" mà mép chữ còn sắc. */
    /* ⚠️ Quầng SIẾT tiếp còn 7px (trước 9px). Chữ đơn cách có nét đều và mảnh
       hơn chữ sans cùng cỡ, quầng 9px bắt đầu làm mép nét nhoè — mà "nét công
       nghệ" thì phải SẮC. Đủ để dòng chữ vẫn hắt sáng trên nền tối. */
    text-shadow: 0 0 7px rgba(45, 212, 191, .38);
}

/* ⛔ BÙ TRỪ `gap` — CHỈ khi kicker là CON TRỰC TIẾP của `.brand-lead`.
   ĐỪNG gộp trị số này vào rule trên, và đừng bỏ bộ chọn con trực tiếp `>`.

   Markup của khối tiêu đề có HAI dạng, và khe 4px phải đúng ở cả hai:

     dạng A — có thẻ bọc:  .brand-lead > .brand-titleblock > (kicker + tiêu đề)
       Kicker và tiêu đề nằm trong `.brand-titleblock`, tức BLOCK-FLOW bình
       thường. `gap: 16px` của `.brand-lead` áp giữa titleblock ↔ vạch ngăn ↔
       câu mô tả, KHÔNG chen vào giữa hai bậc tiêu đề. Nên `margin-bottom: 4px`
       ở rule trên đã là khe thật, không phải bù gì cả.

     dạng B — không thẻ bọc: .brand-lead > (kicker + tiêu đề + vạch + mô tả)
       Nay kicker và tiêu đề là hai flex item anh em, nên `gap: 16px` CHEN VÀO
       giữa chúng. Trong flexbox `gap` KHÔNG gộp với margin như block-flow — nó
       CỘNG THÊM. Khe thật thành 16 + 4 = 20px, và đo trên bản dựng ra 25,65px
       tính từ mép chữ tới mép dấu: hai bậc rời hẳn ra.
       Rule dưới đây trừ trọn `--lead-gap` rồi cộng lại đúng 4px muốn có.

   ⚠️ Đã dính đúng cái bẫy này một lần: viết công thức bù cho dạng B rồi markup
   đổi sang dạng A, thế là tiêu đề bị KÉO CHỒNG LÊN dòng kicker 12px. Bộ chọn
   con trực tiếp làm phần bù tự tắt khi có thẻ bọc, nên đổi qua đổi lại dạng nào
   cũng ra 4px.
   ⚠️ Trừ theo BIẾN chứ không theo số: đổi `gap` của `.brand-lead` ở bậc
   responsive nào thì nhớ sửa `--lead-gap`, đừng ghi đè thẳng `gap`. */
.brand-lead > .brand-kicker {
    margin-bottom: calc(4px - var(--lead-gap));
}

    /* --- CHỮ KICKER ---
       ⛔ ĐÃ BỎ ngoặc vuông [ ] (hai pseudo-element vẽ bằng border) và cùng với
       chúng là `padding-left`/`padding-right` — hai lề đó CHỈ tồn tại để chừa
       chỗ cho ngoặc; giữ lại thì chữ thụt vào 14px so với vạch sáng dọc và dòng
       "QUẢN LÝ DỰ ÁN" bên dưới, hai bậc tiêu đề lệch trục ngay.

       ⛔ ĐÃ BỎ luôn `margin-right: -.18em`. Nó là bù trừ cho khe tới ĐƯỜNG ĐỨT
       NÉT (letter-spacing cộng khoảng trống sau cả chữ cuối). Đường đứt nét đã
       bỏ nên không còn khe nào để bù — giữ lại thì nó chỉ co hộp chữ lại 3,4px
       một cách vô nghĩa. Nếu sau này dựng lại đường nét thì phải đặt lại số
       này, và phải khớp với `letter-spacing` đang khai ở `.brand-kicker`. */
    .brand-kicker-text {
        position: relative;
        flex: 0 0 auto;
    }

/* --- Tiêu đề hero ---
   Thang chữ dựng theo lối biên tập hiện đại: một dòng NHỎ có giãn chữ rộng đặt
   trên một dòng LỚN nén chữ. Chênh lệch cỡ + chênh lệch giãn chữ là thứ tạo ra
   nhịp, không phải chỉ đổi màu. --- */
/* --- BẬC 2 CỦA TIÊU ĐỀ: "QUẢN LÝ DỰ ÁN".
   Dòng NÉN chữ, cỡ lớn, đối trọng với dòng giãn chữ ở trên. `clamp` để nó tự co
   trên laptop hẹp thay vì ngắt dòng giữa chừng — tiêu đề hai chữ mà ngắt dòng
   thì mất hết sức nặng. --- */
.brand-title {
    margin: 0;
    font-size: clamp(30px, 2.9vw, 41px);
    /* ⛔ 1.3, KHÔNG PHẢI 1.12 — số đo, không phải thẩm mỹ. Đừng nén lại.

       Bản trước để 1.12. Ở cỡ 41px, hộp dòng cao 45,92px trong khi font cần
       ascent 43 + descent 11 = 54px, nên đường chân chữ rơi ở 38,96px tính từ
       đỉnh hộp. Chữ HOA TIẾNG VIỆT có dấu (Ả, Ý, Ự, Á) vươn cao 41px trên
       đường chân chữ => 2,04px dấu nằm NGOÀI hộp, và 2,04px chân chữ nằm dưới
       đáy hộp.

       Với chữ thường điều đó chỉ là tràn vô hại. Ở ĐÂY THÌ KHÔNG: dòng này tô
       bằng `background-clip: text` (xem `.brand-title-accent`), mà vùng tô của
       nền bị CẮT theo hộp viền của phần tử. Phần dấu nằm ngoài hộp không được
       tô gì cả, cộng `-webkit-text-fill-color: transparent` => dấu BIẾN MẤT.
       Đó chính là lỗi "chữ QUẢN mất dấu" — cả bốn dấu bị xén phẳng theo một
       đường ngang, đúng dấu vân tay của lỗi cắt hộp.

       1.3 => hộp 53,3px, chân chữ ở 42,65px, dư 1,65px trên và 1,65px dưới.
       Cộng thêm `padding-block` ở `.brand-title-accent` làm lớp bảo hiểm cho
       trường hợp font dự phòng có metric khác. */
    line-height: 1.3;
    font-weight: 800;
    text-transform: uppercase;
    /* ⛔ GIÃN DƯƠNG cho chữ HOA, đừng bê tracking âm của chữ thường sang.
       Chữ thường có phần trên/dưới thân chữ nên tự tạo khoảng thở, nén lại mới
       chặt chẽ; chữ hoa thì mọi glyph đều là khối chữ nhật đặc, nén vào là dính
       thành một mảng. Chữ hoa TIẾNG VIỆT còn cần thêm chỗ cho dấu trên đỉnh
       (Ả, Ự, Á) — .015em là mức thấp nhất mà dấu không chạm chữ bên cạnh. */
    letter-spacing: .015em;
    color: var(--fg-1);
    text-wrap: balance;
    /* Quầng rất mờ sau chữ. Trên nền tối, chữ đậm không có quầng đọc ra
       "phẳng như in giấy"; quầng .18 đủ để nét chữ có ánh mà chưa nhoè viền
       glyph (trên .3 là bắt đầu nhoè, đã thử). */
    text-shadow: 0 0 32px rgba(216, 236, 255, .18);
}

/* Đổi CHẤT LIỆU chứ không chỉ đổi màu.
   ⛔ Fallback `color` đặt TRƯỚC `background-clip: text`: trình duyệt không hỗ
   trợ sẽ giữ `color`; thiếu nó thì chữ trong suốt trên nền tối = tiêu đề biến
   mất hẳn.
   ⚠️ Dốc gradient bắt đầu bằng TRẮNG rồi mới ngả lục: đây là dòng chữ NẶNG nhất
   trang, nó cần độ tương phản của trắng ở phần đầu — gradient lục suốt cả dòng
   sẽ hạ tương phản xuống dưới mức đọc thoải mái trên nền navy. */
.brand-title-accent {
    display: inline-block;
    color: var(--fg-1);
    /* ⛔ CẶP padding/margin NÀY LÀ THUỐC CHỮA "MẤT DẤU", ĐỪNG DỌN ĐI.

       `background-clip: text` tô nền trong phần GIAO giữa hộp viền và hình
       glyph. Dấu thanh của chữ hoa tiếng Việt vươn cao hơn hộp dòng, nên nếu
       hộp không được nới ra thì phần dấu không có nền để tô => mất dấu.

       `padding-block` nới vùng tô lên trên và xuống dưới; `margin-block` âm
       cùng trị số trả lại đúng chiều cao cũ cho bố cục (với inline-block, hộp
       LỀ mới là thứ tính vào chiều cao dòng, nên hai vế triệt tiêu nhau).
       .18em ≈ 7,4px ở cỡ 41px — thừa sức cho mọi font trong danh sách dự
       phòng, kể cả khi máy không có Inter và rơi về Segoe UI Variable. */
    padding-block: .18em;
    margin-block: -.18em;
    background: linear-gradient(98deg,
        #FFFFFF 0%,
        #EAFFF3 30%,
        var(--primary-4) 62%,
        var(--teal-5) 84%,
        var(--cyan-5) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    /* ⚠️ HAI LỚP QUẦNG, KHÔNG PHẢI MỘT — và đừng gộp lại thành một lớp to.

       Lớp 1 (8px, .30) là quầng NÉT: nó ôm sát glyph nên chữ vẫn "phát sáng"
       mà mép còn sắc. Đây chính là mức đã siết xuống theo yêu cầu "chữ nét
       hơn" (từ 26px), và nó vẫn giữ nguyên tinh thần đó.

       Lớp 2 (26px, .13) là quầng KHÍ QUYỂN: ở độ đục .13 nó không đủ tương
       phản để làm mờ mép glyph — nó chỉ thắp một vùng sáng nhạt sau chữ, đúng
       thứ tạo cảm giác neon trên nền navy. Ghi chú cũ loại bỏ bán kính 26px là
       đúng Ở ĐỘ ĐỤC .26; ở .13 thì kết luận đó không còn áp dụng.
       ⛔ Đừng nâng lớp 2 quá .16: trên mức đó nó bắt đầu ăn vào mép chữ thật. */
    filter:
        drop-shadow(0 0 8px rgba(45, 212, 191, .30))
        drop-shadow(0 0 26px rgba(0, 242, 254, .13));
}

/* --- Vạch ngăn có viên kim cương ở giữa + xung sáng chạy qua --- */
.brand-divider {
    position: relative;
    height: 9px;
    overflow: hidden;
    /* Vạch ngang tan dần hai đầu — đặt ở CHÍNH phần tử để hai pseudo-element
       còn trống cho viên kim cương và xung sáng. */
    background: linear-gradient(90deg,
        transparent, rgba(45, 212, 191, .45) 22%,
        rgba(45, 212, 191, .45) 78%, transparent) center center / 100% 1px no-repeat;
}

    /* Viên kim cương ở giữa = ô vuông xoay 45° (mô-típ bullet của dashboard TQ) */
    .brand-divider::before {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        width: 5px;
        height: 5px;
        margin: -2.5px 0 0 -2.5px;
        background: var(--teal-5);
        box-shadow: 0 0 7px rgba(45, 212, 191, .9);
        transform: rotate(45deg);
    }

    /* Xung sáng chạy ngang — cùng nhịp với xung trên mạch của bản trước */
    .brand-divider::after {
        content: "";
        position: absolute;
        top: 50%;
        left: 0;
        width: 34%;
        height: 1px;
        margin-top: -.5px;
        background: linear-gradient(90deg, transparent, rgba(160, 255, 210, .95), transparent);
        animation: dividerSweep 3.8s ease-in-out infinite;
    }

/* --- Câu mô tả sản phẩm.
   ⛔ ĐÃ BỎ cặp dấu ngoặc kép `::before`/`::after` của bản trước: chỗ này từng là
   một câu TRÍCH DẪN, nay là câu MÔ TẢ. Để nguyên dấu nháy thì người đọc hiểu
   nhầm là đang trích lời ai đó.
   `max-width` theo `ch` chứ không `px`: giữ đúng ~52 ký tự mỗi dòng — vùng dễ
   đọc — bất kể cỡ chữ có được chỉnh về sau. --- */
.brand-desc {
    margin: 0;
    max-width: 52ch;
    font-size: 15px;
    line-height: 24px;
    color: var(--fg-4);
}

    /* Tên sản phẩm là thứ duy nhất được nhấn trong câu này — nhấn thêm chỗ nào
       nữa thì câu mất trọng tâm. */
    .brand-desc b {
        font-weight: 700;
        color: var(--fg-1);
        letter-spacing: .01em;
    }

/* =============================================================
   4b. DANH SÁCH PHÂN HỆ (đánh số) + DẢI ĐẶC TẢ

   Lý do tồn tại: bộ mẫu command-center TQ (BigDataView 003/009/025) KHÔNG BAO
   GIỜ để một khoang panel rỗng — mỗi khoang luôn kín nội dung tới sát khung.
   Panel trái bản trước chỉ có logo + tiêu đề + một câu trích, nên nửa dưới
   trống hoác, và vì hai panel bị `stretch` cao bằng nhau nên khoảng trống đó
   càng phình ra ở trang Đăng ký.

   ⛔ VÌ SAO ĐÁNH SỐ CHỨ KHÔNG GẠCH ĐẦU DÒNG (đổi 2026-08-23)
   Bản trước là ba dấu kim cương GIỐNG HỆT NHAU. Ba dấu giống nhau không cho mắt
   một mỏ neo nào, nên cả khối đọc ra một mảng chữ chứ không ra ba mục — đó là
   phần lớn cảm giác "panel trái xấu". Bảng 热门话题榜 trong `BigDataView/web/009`
   đánh số mọi danh sách, và số làm được đúng hai việc mà dấu chấm không làm
   được: cho mắt điểm bám khi quét, và nói "đây là một HỆ có nhiều phân hệ".

   Số sinh bằng CSS counter, KHÔNG gõ vào markup — xem chú thích ở
   `_LayoutBase.cshtml`.

   Nội dung KHÔNG phải chữ mẫu: ba phân hệ là ba phân hệ có thật của sản phẩm,
   ba đặc tả là năng lực có thật của chính cổng này. Điền số liệu bịa kiểu
   "1.200 dự án" vào đây thì đẹp hơn thật, nhưng đó là nói dối trên màn đăng
   nhập của một hệ thống quản lý hồ sơ — nơi tuyệt đối không được đùa.
   ============================================================= */

/* ⛔ HÌNH THỨC MỚI (2026-08-24, theo yêu cầu "mỗi mục thành một Tech Panel"):
   BA THẺ KÍNH MỜ VÁT GÓC, mỗi thẻ mang một ô số kỹ thuật. Đừng quay về bản cũ.

   Bản trước: ba hàng chữ có ô số 30px, nối nhau bằng MỘT nét dọc chạy từ đáy ô
   này xuống đỉnh ô kế. Nét nối đó làm đúng việc của nó — nó gom ba hàng thành
   một chuỗi — nhưng ba hàng vẫn KHÔNG CÓ THÂN: không nền, không viền, nên nửa
   dưới khoang trái đọc ra "chữ rơi trên nền" trong khi nửa trên đã có sơ đồ có
   khối. Cả khoang vì thế mất thăng bằng từ giữa xuống.

   Bản này cho mỗi mục một THÂN THẬT:
     · nền kính mờ tối (`backdrop-filter` + hai lớp gradient trong suốt),
     · viền neon 1px chạy đủ BỐN cạnh KỂ CẢ hai cạnh vát,
     · vát góc 45° ở trên-trái và dưới-phải (`clip-path`) — đúng mô-típ
       `HudFrame` của kit2 (`vci-sun-pms-fe/src/shared/ui/box/HudFrame.tsx`),
     · ô số vát góc đặt gọn ở góc trên-trái của thẻ.
   Nét nối dọc BỎ HẲN: ba thẻ xếp cách nhau 9px đã đọc ra một chuỗi rồi, thêm
   nét nối chỉ là kẻ thêm một đường giữa hai vật vốn đã thẳng hàng.

   ⛔ VÌ SAO VIỀN PHẢI LÀM BẰNG HAI LỚP CLIP-PATH, KHÔNG PHẢI `border`
   `clip-path` cắt CẢ `border`. Cho `li` một `border: 1px` rồi vát góc thì hai
   cạnh vát lộ ra KHÔNG có nét nào — thẻ trông như bị xé góc chứ không như được
   vát. Cách đúng (và là cách duy nhất bằng CSS thuần): `li` tô nguyên khối bằng
   gradient MÀU VIỀN rồi vát; `li::after` phủ lên, thụt vào 1px, vát lại, tô màu
   MẶT THẺ. Phần 1px lòi ra quanh mép chính là đường viền — và nó chạy đủ cả
   cạnh vát.

   ⚠️ `--mod-notch-in` KHÔNG bằng `--mod-notch`. Gọi n là cỡ vát lớp ngoài, ni
   là cỡ vát lớp trong. Theo toạ độ hộp `li`, cạnh vát ngoài là đường
   `x + y = n`, cạnh vát trong là `x + y = 2 + ni` (lớp trong đã thụt 1px theo
   cả hai trục). Muốn hai đường cách nhau đúng 1px: `(2 + ni − n) / √2 = 1`, tức
   `ni = n − 2 + √2 = n − 0,586`. Để nguyên n thì cạnh vát dày 1,41px trong khi
   bốn cạnh thẳng dày 1px — lệch 41%, đủ để mắt thấy góc vát "béo" hơn.

   ⚠️ MỌI SỐ ĐO KHÁC SUY RA TỪ 5 BIẾN ở đầu `li`. Sửa cỡ ô số hay lề trong thì
   CHỈ sửa biến, đừng sửa từng con số — `padding-left` và `top` của ô số đều là
   `calc` bám vào chúng.
   ============================================================= */
.brand-modules {
    counter-reset: mod;
    display: flex;
    flex-direction: column;
    /* 9px: đủ để ba thẻ tách hẳn nhau mà cụm vẫn đọc ra MỘT khối. Trên 14px là
       ba thẻ rời ra thành ba vật không liên quan. */
    gap: 9px;
    margin: 0;
    padding: 0;
    list-style: none;
}

    .brand-modules li {
        counter-increment: mod;
        position: relative;
        /* Bản thân `backdrop-filter` đã tạo stacking context, nhưng khai thêm
           `isolation` để trình duyệt KHÔNG hỗ trợ `backdrop-filter` cũng vẫn
           giữ được `z-index: -1` của mặt thẻ nằm trong lòng `li` — thiếu nó thì
           ở những máy đó mặt thẻ tụt ra sau nền của cả khoang và biến mất. */
        isolation: isolate;

        --mod-pad: 13px;  /* lề trong trên/dưới */
        --mod-sq: 34px;   /* cạnh ô số */
        --mod-x: 14px;    /* ô số cách mép trái */
        --mod-line: 22px; /* chiều cao hộp dòng chữ đầu: 14px × 1.55 */
        --mod-notch: 13px;
        --mod-notch-in: calc(var(--mod-notch) - .586px);

        padding:
            var(--mod-pad)
            16px
            var(--mod-pad)
            calc(var(--mod-x) + var(--mod-sq) + 14px);

        clip-path: polygon(
            0 var(--mod-notch),
            var(--mod-notch) 0,
            100% 0,
            100% calc(100% - var(--mod-notch)),
            calc(100% - var(--mod-notch)) 100%,
            0 100%);

        /* Đây là MÀU VIỀN, không phải màu nền — mặt thẻ do `::after` phủ lên.
           Gradient chéo để viền sáng dần từ góc vát trên-trái (nơi có ô số,
           tức nơi mắt vào) xuống góc vát dưới-phải. */
        background: linear-gradient(135deg,
            rgba(0, 242, 254, .62) 0%,
            rgba(45, 212, 191, .24) 46%,
            rgba(34, 197, 94, .38) 100%);

        -webkit-backdrop-filter: blur(9px) saturate(120%);
                backdrop-filter: blur(9px) saturate(120%);
    }

        /* --- MẶT THẺ KÍNH MỜ.
           Hai lớp gradient: lớp dưới là thân kính (navy trong suốt, sáng hơn ở
           giữa để mặt kính có độ cong), lớp trên là ánh hắt cyan từ mép trái —
           thiếu nó thì ba thẻ đọc ra ba ô nhựa phẳng.
           ⚠️ `z-index: -2` để mặt thẻ nằm TRÊN nền của `li` (tức lớp màu viền)
           nhưng DƯỚI chữ. Đổi sang 0 thì nó phủ luôn lên chữ.
           ⚠️ -2 chứ không phải -1 nữa (đổi cùng lượt thêm hiệu ứng chạy sáng):
           lớp sáng `.brand-modules b::after` nằm ở -1, tức TRÊN mặt thẻ này và
           DƯỚI chữ. Hai lớp âm xếp theo TRỊ SỐ trước, cây DOM sau — nếu để cả
           hai cùng -1 thì mặt thẻ (đứng sau trong cây) sẽ đè lên lớp sáng và
           hiệu ứng biến mất sạch. */
        .brand-modules li::after {
            content: "";
            position: absolute;
            inset: 1px;
            z-index: -2;
            clip-path: polygon(
                0 var(--mod-notch-in),
                var(--mod-notch-in) 0,
                100% 0,
                100% calc(100% - var(--mod-notch-in)),
                calc(100% - var(--mod-notch-in)) 100%,
                0 100%);
            background:
                linear-gradient(96deg,
                    rgba(0, 242, 254, .10) 0%,
                    rgba(0, 242, 254, .02) 34%,
                    transparent 60%),
                linear-gradient(150deg,
                    rgba(7, 26, 45, .86) 0%,
                    rgba(12, 38, 60, .66) 52%,
                    rgba(7, 26, 45, .84) 100%);
        }

        /* --- Ô SỐ.
           `decimal-leading-zero` cho "01" chứ không "1": hai chữ số làm ô cân
           đối và cặp số đọc ra "mã phân hệ" thay vì "số đếm" — đúng giọng bảng
           điều khiển. `tabular-nums` phòng khi font dự phòng có chữ số rộng
           hẹp khác nhau.
           ⛔ KHÔNG gõ "01"/"02"/"03" vào markup — xem `_LayoutBase.cshtml`.

           ⚠️ `top` là CÔNG THỨC, không phải số canh mắt: ô phải ngang hàng với
           DÒNG CHỮ ĐẦU chứ không ngang hàng với cả khối chữ (mục nào xuống hai
           dòng thì ô vẫn phải nằm cạnh dòng một). Tâm dòng đầu ở
           `--mod-pad + --mod-line/2`; tâm ô ở `top + --mod-sq/2`; cho hai vế
           bằng nhau ra đúng biểu thức dưới đây.
           ⚠️ Kết quả ≈ 7px, và góc trên-trái của ô rơi ở (14, 7) — tổng 21 > 13
           nên ô nằm NGOÀI tam giác bị cắt của `--mod-notch`. Nâng `--mod-notch`
           quá 21px thì ô bắt đầu bị xén góc. */
        .brand-modules li::before {
            content: counter(mod, decimal-leading-zero);
            position: absolute;
            left: var(--mod-x);
            top: calc(var(--mod-pad) + (var(--mod-line) - var(--mod-sq)) / 2);
            display: grid;
            place-items: center;
            box-sizing: border-box;
            width: var(--mod-sq);
            height: var(--mod-sq);
            font-family: var(--font-mono);
            font-size: 15px;
            font-weight: 700;
            line-height: 1;
            letter-spacing: .02em;
            font-variant-numeric: tabular-nums;
            color: #A8FFE0;
            border: 1px solid rgba(0, 242, 254, .42);
            background: rgba(0, 242, 254, .09);
            /* ⛔ Ô SỐ KHÔNG VÁT GÓC — đã thử và đã loại.
               Vát bằng `clip-path` thì hai cạnh vát mất luôn `border` (cùng cái
               bẫy đã ghi ở đầu mục), mà ô chỉ 34px nên không có chỗ dựng hai
               lớp như thẻ ngoài: nét viền đứt ở hai góc, ô đọc ra như hộp bị
               hở chứ như hộp được vát. Góc vuông sắc là đúng hệ (`--auth-radius: 0`)
               và nó tương phản có chủ đích với thẻ vát bao quanh. */
            text-shadow: 0 0 10px rgba(0, 242, 254, .5);
        }

    /* ⛔ ĐÃ BỎ THANG ĐẬM-NHẠT TĨNH (100% / 72% / 42% ở viền và ô số) khi thêm
       đèn chạy. ĐỪNG DỰNG LẠI — hai thứ này loại trừ nhau, không cộng được.

       Thang tĩnh sinh ra để làm mỏ neo cho mắt khi cả ba thẻ đứng yên: thẻ đầu
       sáng nhất nên mắt rơi vào đó trước. Nay đèn chạy làm đúng việc đó, nhưng
       nó DI CHUYỂN. Để cả hai cùng chạy thì có lúc thẻ 03 đang được thắp sáng
       mà thẻ 01 vẫn là thẻ nổi nhất màn hình — đã dựng thử và chụp lại đúng cảnh
       đó: mắt vẫn dính vào 01, cú chuyền đèn đọc ra như một vệt sáng lạc chứ
       không ra "đang giới thiệu mục này".

       Nay tách bạch hai tín hiệu, mỗi tín hiệu một việc:
         · THỨ TỰ  -> do chính con số 01/02/03 nói, không cần đến độ sáng;
         · ĐANG NHẤN -> do đèn chạy nói, và là thứ DUY NHẤT thay đổi độ sáng.
       Ba thẻ vì thế nghỉ ở cùng một mức, xem `.brand-modules li`.

       ⛔ KHÔNG dùng `box-shadow` để thắp quầng quanh thẻ: `clip-path` cắt cả
       `box-shadow`, nên quầng biến mất sạch. Muốn quầng thì phải là
       `filter: drop-shadow` — mà `filter` đặt cùng `backdrop-filter` trên một
       phần tử thì Safari dựng sai lớp. Đó là lý do đèn chạy được vẽ bằng
       GRADIENT trong lòng thẻ (`b::after`) chứ không bằng quầng hắt ra ngoài. */

    /* Chữ ĐƯỢC NHẤN: trắng, đậm, giãn nhẹ. Đây là dòng mắt quét trước — mô tả
       bên dưới chỉ đọc khi đã quan tâm.

       ⛔ THỤT LỀ TREO nay do `padding-left` của `li` lo (ô số đã tách ra thành
       phần tử ĐỊNH VỊ TUYỆT ĐỐI, không còn chiếm cột trong lưới). Nhờ vậy dòng
       thứ hai của một mục dài tự thẳng hàng với dòng đầu. Đừng bỏ `padding-left`
       rồi thụt bằng `text-indent`: cách đó chỉ thụt ĐÚNG DÒNG ĐẦU.

       line-height 1.55 (≈22px ở cỡ 14px) — cũng chính là `--mod-line` ở trên.
       Đổi số này thì phải đổi cả biến, không thì ô số lệch khỏi dòng đầu. */
    .brand-modules b {
        display: block;
        font-size: var(--text-body-14);
        font-weight: 600;
        line-height: 1.55;
        letter-spacing: .005em;
        color: var(--fg-1);
    }

/* =============================================================
   ĐÈN CHẠY QUA BA THẺ — mỗi thẻ sáng lên 3,5s rồi chuyền cho thẻ kế

   ⛔ VÌ SAO LỚP SÁNG NẰM Ở `b::after` — nghe vô lý nhưng có lý do.

   `li::before` đang là Ô SỐ, `li::after` đang là MẶT THẺ KÍNH. Hết chỗ. Cần một
   lớp thứ ba nằm giữa mặt thẻ và chữ, mà thêm một <span> trang trí vào markup
   thì phải sửa `_LayoutBase.cshtml` cho một thứ thuần trang trí. `b::after` là
   ô còn trống duy nhất, và vì `b` không được định vị nên nó neo thẳng vào `li`
   (`position: relative`) — tức phủ đúng cả thẻ chứ không chỉ phần chữ.

   ⛔ CHỈ ĐỘNG VÀO `opacity` (luật §12b cho hiệu ứng lặp vô hạn). Mọi thứ tạo ra
   cảm giác "được chọn" — nền sáng hơn, vệt hắt bên trái, quầng sau ô số — đều
   được VẼ SẴN Ở TRẠNG THÁI SÁNG rồi chỉ mờ đi/hiện ra. Đừng chuyển sang chạy
   animation trên `background`/`border-color`/`box-shadow`: đó là thuộc tính vẽ
   lại, và bài học của `isoFloat` ở §4c còn nguyên — animation lặp vô hạn chạm
   vào lớp có nét mảnh + chữ thì mỗi khung hình phải khử răng cưa lại, mắt đọc
   ra nhấp nháy chứ không ra chuyển động.

   ⚠️ QUẦNG SAU Ô SỐ hoạt động được là nhờ nền ô số vốn gần như trong suốt
   (`rgba(0, 242, 254, .09)`), và ô số là phần tử ĐỊNH VỊ nên nằm trên mọi lớp
   z âm. Quầng thắp phía sau nên nó ánh xuyên qua ô — không cần đụng gì vào ô.
   Tăng độ đục nền ô lên thì quầng tắt dần theo, đó không phải lỗi.

   ⚠️ `clip-path` phải TRÙNG KHỚP với `li::after`, dùng chung `--mod-notch-in`.
   Thiếu nó thì lớp sáng thò ra hai góc vát và thẻ đọc ra như bị hở.
   ============================================================= */
    .brand-modules b::after {
        content: "";
        position: absolute;
        inset: 1px;
        z-index: -1;
        pointer-events: none;
        clip-path: polygon(
            0 var(--mod-notch-in),
            var(--mod-notch-in) 0,
            100% 0,
            100% calc(100% - var(--mod-notch-in)),
            calc(100% - var(--mod-notch-in)) 100%,
            0 100%);
        /* BA LỚP, mỗi lớp một việc — xếp từ trên xuống dưới:

           1. THANH SÁNG MÉP TRÁI (2px). Đây là tín hiệu "đang chọn" MẠNH nhất và
              rẻ nhất: một nét dọc dứt khoát ở mép, đúng mô-típ hàng-được-chọn
              của bảng điều khiển. Nó bị `clip-path` vát cụt ở hai góc trên-trái
              và dưới-trái — cố ý, nhờ vậy nét bám theo hình thẻ chứ không cắt
              ngang qua chỗ vát.
           2. QUẦNG SAU Ô SỐ. Tâm suy ra từ chính các biến dựng ô số (trừ 1px vì
              lớp này `inset: 1px`), nên đổi cỡ hay vị trí ô thì quầng tự đi
              theo — không có số canh tay nào ở đây.
           3. ÁNH HẮT ngang thân thẻ, tan dần về phải. Lớp này một mình thì quá
              nhẹ để đọc ra "được chọn", nhưng thiếu nó thì hai lớp trên đọc ra
              hai vật rời nhau dán lên một thẻ tối. */
        background:
            linear-gradient(90deg,
                rgba(120, 250, 255, .85) 0 2px,
                transparent 2px),
            radial-gradient(circle 48px
                at calc(var(--mod-x) + var(--mod-sq) / 2 - 1px)
                   calc(var(--mod-pad) + var(--mod-line) / 2 - 1px),
                rgba(0, 242, 254, .46), transparent 74%),
            linear-gradient(96deg,
                rgba(0, 242, 254, .20) 0%,
                rgba(0, 242, 254, .07) 42%,
                transparent 78%);
        opacity: 0;
    }

@media (prefers-reduced-motion: no-preference) {
    /* ⚠️ TRỄ = ĐÚNG MỘT PHẦN BA CHU KỲ, không phải số gõ tay: 10,5 / 3 = 3,5s.
       Lệch đi thì hoặc có lúc hai thẻ cùng sáng, hoặc có quãng cả ba cùng tối. */
    .brand-modules li:nth-child(2) b::after { animation-delay: 3.5s; }
    .brand-modules li:nth-child(3) b::after { animation-delay: 7s; }

    .brand-modules b::after {
        animation: modFlare 10.5s linear infinite;
    }
}

/* Người đã tắt hiệu ứng: KHÔNG để cả ba thẻ tối thui — như thế mất luôn mỏ neo
   cho mắt. Ghim sáng thẻ đầu, tức quay về đúng thiết kế tĩnh trước khi có đèn
   chạy. */
@media (prefers-reduced-motion: reduce) {
    .brand-modules li:first-child b::after {
        opacity: 1;
    }
}

    .brand-modules em {
        display: block;
        margin-top: 2px;
        font-size: var(--text-caption-12);
        font-style: normal;
        line-height: 1.55;
        color: var(--fg-5);
    }

/* =============================================================
   5. Cột phải — panel form

   Dùng CHUNG khung `.hud-panel` với cột trái (xem mục trên), chỉ thêm ba thứ
   riêng: quầng neon ngoài, đốm sáng chạy vòng viền, và tông viền lệch sang lục
   (đây là panel HÀNH ĐỘNG, cột trái là panel THÔNG TIN).
   ============================================================= */

/* --- Khoang phải.
   Bề rộng CỐ ĐỊNH 492px: form phải giữ đúng độ dài dòng dù màn rộng bao nhiêu.
   (Nới từ 452 theo yêu cầu — ô nhập rộng thêm 40px, thoáng hơn mà vẫn dưới
   ngưỡng ~75 ký tự/dòng của nhãn và mô tả.)
   Khoang trái mới là bên co giãn (`flex: 1 1 578px`).

   Nền sáng hơn khoang trái một bậc rất mỏng — đây là bên HÀNH ĐỘNG, và trong
   một tấm liền thì phân vai bằng SẮC ĐỘ, không bằng đường kẻ. Chênh lệch cố ý
   giữ nhỏ: đậm hơn nữa thì hai khoang lại tách ra thành hai tấm, đúng thứ vừa
   bỏ công gộp. --- */
.auth-panel-wrap {
    position: relative;
    display: flex;
    flex: 0 0 492px;
    width: 492px;
    max-width: 100%;
    background: linear-gradient(200deg,
        rgba(34, 197, 94, .07) 0%,
        rgba(45, 212, 191, .03) 42%,
        transparent 78%);
}

/* --- Đốm sáng chạy vòng quanh viền (borderBox8, bản CSS thuần).
       conic-gradient quay + mask chỉ chừa lại vành 1px -> không cần đo
       kích thước bằng JS như bản SVG animateMotion gốc.
       Nay chạy quanh CẢ TẤM chứ không quanh riêng khoang form. --- */
.auth-shell-ring {
    position: absolute;
    inset: 0;
    z-index: 1;
    padding: 1px;
    box-sizing: border-box;
    pointer-events: none;
    background: conic-gradient(from var(--hud-angle),
        transparent 0 60%,
        rgba(45, 212, 191, .55) 74%,
        var(--primary-4) 85%,
        #EAFFF3 89%,
        var(--primary-4) 93%,
        transparent 100%);
    /* Cắt bỏ phần giữa, chỉ giữ lại vành padding 1px */
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
    animation: hudRing 7s linear infinite;
}

/* Thân form — nằm TRONG `.hud-panel-surface`, dưới thanh tiêu đề.
   `position: relative` là bắt buộc: scanline, vệt nhấn và ánh kính đều
   `position: absolute` neo vào chính khối này. */
/* ⛔ `justify-content: center` — chống lỗi "panel rỗng" ở CHIỀU NGƯỢC LẠI.

   `_LayoutBase` này còn phục vụ ~25 màn khác (Lockout, LoggedOut, ConfirmEmail,
   RegisterConfirmation…) và phần lớn chỉ có một đoạn chữ + một nút. Ở những màn
   đó panel TRÁI mới là panel cao hơn (nó luôn ~530px), nên `stretch` kéo panel
   FORM cao theo — và nếu nội dung neo ở đỉnh thì phần dôi dồn xuống đáy thành
   đúng cái mảng trống vừa sửa, chỉ là đổi panel.

   Canh giữa dọc thì trang ngắn đọc ra "một thông báo đặt giữa khoang", trang
   dài (Đăng ký, Consent) không đổi gì vì đã hết chỗ dôi.
   ⚠️ Scanline và vệt nhấn đều `position:absolute` nên KHÔNG bị flex đụng tới —
   đó là lý do đổi được `.auth-panel-inner` sang flex mà không phải sửa chúng. */
.auth-panel-inner {
    position: relative;
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: center;
    /* Nới lề trong (30 -> 40 hai bên): mép phải của form trước đây gần như chạm
       viền phát sáng. Khoang form rộng 492px nên 40px mỗi bên vẫn để lại 412px
       cho ô nhập — thừa sức. */
    padding: 26px 40px 28px;
}

/* --- Scanline CRT (HudFrame scanlines). opacity thấp để không cản đọc. --- */
.auth-panel-scanlines {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    opacity: .5;
    background: repeating-linear-gradient(
        180deg,
        transparent 0,
        transparent 3px,
        rgba(45, 212, 191, .055) 3px,
        rgba(45, 212, 191, .055) 4px
    );
}

/* --- Top accent stripe (HudFrame intensity="cyber") --- */
.auth-panel-accent {
    position: absolute;
    top: 0;
    left: 6%;
    right: 34%;
    z-index: 3;
    height: 1px;
    pointer-events: none;
    background: linear-gradient(90deg,
        transparent 0%,
        rgba(78, 222, 130, .85) 30%,
        rgba(78, 222, 130, .85) 70%,
        transparent 100%);
    box-shadow: 0 0 8px rgba(34, 197, 94, .6);
}

/* Nội dung phải nằm TRÊN scanline, và là phần tử TRƯỢT trong hiệu ứng chuyển
   màn (xem §13b) — nên nó mang `view-transition-name` riêng.
   Cố ý nhắm vào .auth-panel-body chứ KHÔNG phải `.auth-panel-inner > *`:
   selector `> *` cũng trúng luôn hai lớp trang trí ở trên (scanline z-0,
   accent z-3) và vì nó đứng sau trong file nên sẽ đè z-index của chúng
   -> scanline nhảy lên che mất chữ. */
.auth-panel-body {
    position: relative;
    z-index: 4;
    view-transition-name: auth-form;
}

/* =============================================================
   NGOẶC GÓC CỦA THẺ FORM — 4 góc, HAI TÔNG, phát sáng

   Đây là thiết bị trang trí CHỦ ĐẠO của ảnh mẫu `images/login.jpg`: bốn dấu
   ngoặc lớn ôm lấy thẻ, dày, có quầng sáng rõ, và HAI BÊN HAI MÀU (ảnh mẫu:
   lam bên trái, hồng bên phải). Chuyển sang trục màu VCI360: teal bên trái
   (tông "thiết bị đo"), lục brand bên phải (tông "dữ liệu & hành động") — đúng
   luật hai tông của HudFrame, và tình cờ cũng khớp logo vốn là vòng hai cung
   hai màu.

   ⚠️ Bản trước chỉ có 2 ngoặc (TR + BL) vì thẻ bị VÁT góc TL + BR nên hai góc
   kia không đặt ngoặc được. Nay bỏ vát để lấy đủ 4 góc như ảnh mẫu — chi tiết
   lý do ở chú thích `.auth-panel`.
   ⚠️ Quầng dùng `drop-shadow` chứ không `box-shadow`: `drop-shadow` bám theo
   NÉT VẼ (hai cạnh chữ L), còn `box-shadow` toả quanh cả hộp 28×28 nên dấu góc
   trông như một ô vuông đặc đang phát sáng.
   ============================================================= */

.hud-bracket {
    position: absolute;
    z-index: 5;
    width: 28px;
    height: 28px;
    pointer-events: none;
    transition: width .3s var(--ease-out), height .3s var(--ease-out);
}

/* Nhịp thở gắn ở lớp CHUNG của cả 4 ngoặc, lệch pha 2 cặp để tấm thẻ không
   "đập" một nhịp như đèn xi-nhan — hai cặp so le đọc ra một vật đang hoạt động. */
@media (prefers-reduced-motion: no-preference) {
    .hud-bracket {
        animation: hudBreathe 4.2s ease-in-out infinite;
    }

    .hud-bracket--tr,
    .hud-bracket--bl {
        animation-delay: 2.1s;
    }
}

.hud-bracket--tl,
.hud-bracket--bl {
    border-color: var(--teal-5);
    filter: drop-shadow(0 0 6px rgba(45, 212, 191, .95));
}

.hud-bracket--tr,
.hud-bracket--br {
    border-color: var(--primary-4);
    filter: drop-shadow(0 0 6px rgba(78, 222, 130, .95));
}

.hud-bracket--tl { top: -1px;    left: -1px;  border-top: 2px solid;    border-left: 2px solid; }
.hud-bracket--tr { top: -1px;    right: -1px; border-top: 2px solid;    border-right: 2px solid; }
.hud-bracket--bl { bottom: -1px; left: -1px;  border-bottom: 2px solid; border-left: 2px solid; }
.hud-bracket--br { bottom: -1px; right: -1px; border-bottom: 2px solid; border-right: 2px solid; }

/* --- Ray dọc hai mép thẻ.
   Ảnh mẫu có hai sợi sáng mảnh chạy dọc mép trái/phải, nối hai đầu ngoặc lại.
   Nó là thứ biến bốn dấu góc rời rạc thành MỘT cái khung — thiếu nó thì bốn
   ngoặc đọc ra như bốn mảnh vỡ. Tan về 0 ở hai đầu để không đâm vào ngoặc. --- */
.hud-rail {
    position: absolute;
    top: 34px;
    bottom: 34px;
    z-index: 5;
    width: 1px;
    pointer-events: none;
}

.hud-rail--left {
    left: -1px;
    background: linear-gradient(180deg,
        transparent 0%, rgba(45, 212, 191, .55) 18%,
        rgba(45, 212, 191, .55) 82%, transparent 100%);
}

.hud-rail--right {
    right: -1px;
    background: linear-gradient(180deg,
        transparent 0%, rgba(78, 222, 130, .55) 18%,
        rgba(78, 222, 130, .55) 82%, transparent 100%);
}

/* =============================================================
   6. Tiêu đề form
   ============================================================= */

.form-title-wrapper {
    position: relative;
    text-align: left !important;
    margin-bottom: 20px !important;
    /* Chừa chỗ cho gạch dọc + khe. Tính từ token, KHÔNG gõ số: gõ số là chỗ này
       lệch khỏi khoang trái ngay lần ai đó đổi --tbar-gap. */
    padding-left: calc(var(--tbar-gap) + var(--tbar-w));
}

    /* Gạch dọc nhấn đầu tiêu đề — mô-típ tiêu đề panel của dashboard TQ.
       Chạy từ đỉnh dòng tiêu đề xuống hết dòng tiêu đề (chừa dòng mô tả) nên nó
       ôm đúng phần ĐƯỢC NHẤN, không ôm cả khối.
       Bề dày / màu / quầng lấy từ token dùng chung với khoang trái — xem chú
       thích ở khối token `--tbar-*` trong §1. */
    .form-title-wrapper::before {
        content: "";
        position: absolute;
        left: 0;
        top: 2px;
        bottom: 24px;
        width: var(--tbar-w);
        background: var(--tbar-fill);
        box-shadow: var(--tbar-glow);
    }

/* ⛔ ĐÃ BỎ: dòng mã trên tiêu đề ("// XÁC THỰC NGƯỜI DÙNG").
   Nó chú thích cho <h1> nằm ngay dưới — mà <h1> đã ghi "Đăng nhập" rồi. Hai
   dòng nói cùng một việc thì dòng nhỏ chỉ là tiếng ồn. Cùng lý do với hai thanh
   tiêu đề panel và dải chân, đều bỏ trong cùng một lượt.
   Bậc thang dẫn vào tiêu đề nay do GẠCH DỌC bên trái đảm nhiệm (`::before` của
   `.form-title-wrapper`) — một dấu hiệu, không phải một dòng chữ. */

/* --- Tiêu đề form.
   ⛔ `background-clip: text` LUÔN phải có `color` khai TRƯỚC làm bản dự phòng:
   trình duyệt không hỗ trợ sẽ giữ `color`, còn thiếu nó thì chữ trong suốt trên
   nền tối = tiêu đề biến mất.
   Dốc gradient rất ngắn (trắng → trắng ở 58% rồi mới ngả teal): chữ phải đọc ra
   là TRẮNG có ánh kim ở đuôi, không phải chữ hai màu — chữ hai màu ở cỡ 30px
   đọc như lỗi render. --- */
.form-title,
h1.form-title {
    margin: 0 0 7px;
    font-size: 30px;
    line-height: 37px;
    font-weight: 700;
    letter-spacing: -.02em;
    color: var(--fg-1);
    background: linear-gradient(103deg, #FFFFFF 0%, #FFFFFF 58%, #C8F5DF 82%, var(--teal-5) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    /* ⚠️ SIẾT TỪ 18px XUỐNG 8px, cùng lý do với `.brand-title-accent`: quầng
       nhoè KHÔNG bù lại được phần nét mà `background-clip:text` làm mất, nó chỉ
       chồng thêm sương. Muốn nét thì phải bớt nhoè, không phải thêm. */
    filter: drop-shadow(0 0 8px rgba(200, 245, 223, .20));
    /* ⚠️ Dòng này CŨNG tô bằng `background-clip: text`, tức cũng dính được cái
       bẫy cắt dấu của `.brand-title` (xem giải thích dài ở đó). Đã đo: ở cỡ
       26px/32px hộp còn dư 7,5px trên đường dấu nên hiện KHÔNG bị cắt.
       Nếu về sau có ai siết `line-height` xuống sát 1.0, hoặc đổi font, thì
       kiểm lại chỗ này TRƯỚC khi đi tìm lỗi ở nơi khác. */
}

/* Dòng mô tả dưới tiêu đề — kéo rộng tối đa 300px để nó ngắt dòng ở chỗ đọc
   được, thay vì chạy hết bề ngang panel rồi ngắt tuỳ ý. */
.form-title-wrapper .ant-typography-secondary {
    display: block;
    max-width: 320px;
    font-size: var(--text-body-14);
    line-height: 21px;
}

/* =============================================================
   7. Ghi đè Ant Design — typography & form item
   ============================================================= */

.ant-typography {
    color: var(--fg-3);
}

.ant-typography-secondary {
    color: var(--fg-5) !important;
}

.ant-form-item {
    /* 15px chứ không 18px: trang Đăng ký có 5 hàng, mỗi hàng dôi 3px là panel
       cao thêm 15px — đủ để nó vượt khung nhìn laptop 1080p và sinh thanh cuộn
       trên một màn đăng nhập. */
    margin-bottom: 15px !important;
}

/* Ant đặt màu chữ tối qua selector 2 class (.css-var-xxx.ant-form-css-var .ant-form-item-label > label)
   nên rule thường thua -> nhãn thành đen trên nền tối. Phải !important. */
.ant-form-item-label > label,
.form-item-label label,
.ant-form-vertical .ant-form-item-label > label,
form label {
    display: inline-block;
    margin-bottom: 6px;
    font-size: var(--text-body-14);
    font-weight: 500;
    letter-spacing: .005em;
    color: var(--fg-2) !important;
}

/* =============================================================
   DẤU BẮT BUỘC — chuyển từ ĐẦU nhãn xuống CUỐI nhãn

   Ant Design chèn dấu sao bằng `::before` (xem umi.css:553
   `.ant-form-item .ant-form-item-label>label.ant-form-item-required:before`,
   specificity 0-3-1). Nên phải TẮT nó rồi tự dựng lại ở `::after`.

   ⚠️ `visibility: visible` KHÔNG thừa. Chính antd đặt `visibility: hidden` lên
   `label::after` ở mấy bộ chọn layout dọc / cột 24 (đó là cách nó giấu dấu hai
   chấm). Thiếu dòng này thì dấu sao có `content` nhưng vô hình — lỗi im lặng,
   nhìn ra như "rule không chạy".
   ⚠️ `font-family: SimSun` là của antd, giữ lại cho dấu sao đúng hình dạng cũ
   (SimSun vẽ `*` nằm ngang giữa dòng thay vì treo lên đỉnh như font Latin).
   ============================================================= */

.ant-form-item-required::before {
    display: none !important;
}

.ant-form-item-label > label.ant-form-item-required::after,
.form-item-label label.ant-form-item-required::after,
form label.ant-form-item-required::after {
    content: "*" !important;
    visibility: visible !important;
    display: inline-block !important;
    margin: 0 0 0 4px !important;
    color: var(--error-5) !important;
    font-family: SimSun, sans-serif;
    font-size: var(--text-body-14);
    line-height: 1;
}

/* =============================================================
   8. Ô nhập
   ============================================================= */

/* Bọc ô nhập để đặt icon trái và vạch quét đáy.
   Ô mật khẩu dùng lại .password-wrapper vốn có -> gộp selector. */
.field-shell,
.password-wrapper {
    position: relative;
}

/* Icon trái: vẽ bằng mask nên MÀU theo currentColor -> đổi màu khi focus
   mà không phải nạp 2 file ảnh. */
.field-icon {
    position: absolute;
    left: 12px;
    top: 50%;
    width: 16px;
    height: 16px;
    margin-top: -8px;
    pointer-events: none;

    /* ⚠️⚠️ z-index BẮT BUỘC — đây là thứ đã làm icon mờ, và khi ô được trình
       duyệt tự điền thì MẤT HẲN.

       umi.css:10471 khai `.ant-input { position: relative }`. Thế là cái
       <input> thành phần tử ĐƯỢC ĐỊNH VỊ với z-index:auto, ngang hàng với
       .field-icon — hai phần tử cùng hạng thì vẽ theo THỨ TỰ DOM, mà <input>
       lại đứng SAU <span class="field-icon">. Kết quả: input vẽ ĐÈ LÊN icon.

       Icon không biến mất ngay vì nền ô là rgba(4,20,38,.72) — chỉ đục 72%,
       nên icon còn lọt qua đúng 28%: #D8E1EE bị kéo xuống còn cỡ #3F4D5E,
       gần như trùng màu nền ô. Đó là cái "icon không rõ".

       Còn lúc Chrome autofill, rule ở §autofill phủ lòng ô bằng
       `box-shadow: 0 0 0 1000px var(--bg-input) inset` — ĐỤC 100%. Icon nằm
       dưới nên bị che sạch: đó là cái "có trường hợp mất luôn".

       z-index:2 xếp icon ngang nút hiện/ẩn mật khẩu (cũng z-2, và cũng chính
       vì có z-index mà con mắt chưa bao giờ bị mờ trong khi icon trái thì bị).
       Hai cái không bao giờ chồng chỗ nhau (icon ở trái 12px, nút ở phải 8px).
       ⛔ Đừng hạ xuống 0/auto, và đừng gỡ vì "trông thừa" — bỏ ra là icon chìm
       lại y như cũ, mà lỗi chỉ lộ rõ ở trạng thái autofill nên rất dễ lọt QA. */
    z-index: 2;

    /* Màu LÚC NGHỈ. --fg-3 chứ không phải --fg-5 (bậc mờ nhất, vốn dành cho
       chữ phụ) — icon nét mảnh 1,5px cần bậc sáng hơn mới đọc rõ trên nền ô.
       :focus-within vẫn đổi sang --primary-4 nên tín hiệu "ô đang gõ" còn nguyên. */
    background-color: var(--fg-3);
    transition: background-color .15s ease-out;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
}

/* ⚠️ stroke-width 2.25, KHÔNG phải 1.7. viewBox 24 vẽ vào ô 16px = tỉ lệ .667,
   nên nét 1.7 xuống màn chỉ còn 1.13px. Mảnh hơn 1px thì trình duyệt phải trải
   nét qua 2 hàng pixel, mỗi hàng chỉ ăn ~60% màu -> icon đọc ra mờ hẳn dù token
   màu vẫn đủ sáng. 2.25 * 16/24 = 1.5px, nét ăn trọn pixel.
   Nút con mắt (.password-suffix::before) dùng ô 18px nên để 2.0 (2.0*18/24 =
   1.5px) — CÙNG độ đậm quang học. Hai cỡ ô khác nhau thì stroke phải khác số;
   đặt chung một con số là một trong hai cái sẽ lệch. */
.field-icon--mail {
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><rect x='2.5' y='4.8' width='19' height='14.4'/><path d='m3 6 9 6.6L21 6'/></svg>");
    mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><rect x='2.5' y='4.8' width='19' height='14.4'/><path d='m3 6 9 6.6L21 6'/></svg>");
}

.field-icon--lock {
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><rect x='4' y='10.2' width='16' height='10.3'/><path d='M8 10.2V7a4 4 0 0 1 8 0v3.2'/></svg>");
    mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><rect x='4' y='10.2' width='16' height='10.3'/><path d='M8 10.2V7a4 4 0 0 1 8 0v3.2'/></svg>");
}

.field-icon--user {
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='8' r='3.6'/><path d='M4.6 20.2a7.4 7.4 0 0 1 14.8 0'/></svg>");
    mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='8' r='3.6'/><path d='M4.6 20.2a7.4 7.4 0 0 1 14.8 0'/></svg>");
}

.field-icon--shield {
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3 5 5.8v6c0 4.2 3 7.9 7 9.2 4-1.3 7-5 7-9.2v-6z'/><path d='m9 12 2.2 2.2L15.4 10'/></svg>");
    mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3 5 5.8v6c0 4.2 3 7.9 7 9.2 4-1.3 7-5 7-9.2v-6z'/><path d='m9 12 2.2 2.2L15.4 10'/></svg>");
}

/* Chừa chỗ cho icon trái.
   ⛔ Cố ý KHÔNG dùng `:has(.field-icon)`: `:has()` chỉ có từ Firefox 121, và
   nếu nó không chạy thì chữ trong ô nằm ĐÈ lên icon — hỏng nhìn thấy được,
   không phải suy giảm êm. Dùng anh-em kề `+` thay cho `:has()`: chạy ở mọi
   trình duyệt, và vẫn CHỈ bắt đúng ô nào thật sự có icon đứng ngay trước
   (⚠️ hệ quả: ô nào cần icon thì <span class="field-icon"> phải là anh em
   ĐỨNG NGAY TRƯỚC <input>. Chèn thêm phần tử vào giữa là mất 38px padding và
   chữ đè lên icon — đúng lỗi placeholder chồng icon đã gặp. Ngược lại, ô không
   khai icon thì không bị chừa 38px trống vô cớ).

   ⚠️ Bắt buộc có tên lớp bọc ở đầu selector (độ đặc hiệu 0-2-1). Rule nền
   `.ant-input, input[type="email"]…` bên dưới đặt padding VIẾT TẮT
   (`padding: 0 12px`) và có độ đặc hiệu 0-1-1 — bằng `.field-shell input` mà
   lại đứng SAU nên thắng, kéo chữ về đúng chỗ icon (12px) và đè lên icon.
   Đây chính là lỗi placeholder chồng icon đã gặp; đừng hạ độ đặc hiệu lại. */
.field-shell .field-icon + input,
.password-wrapper .field-icon + input {
    padding-left: 38px;
}

.field-shell:focus-within .field-icon,
.password-wrapper:focus-within .field-icon {
    background-color: var(--primary-4);
}

/* Vạch quét chạy dưới đáy ô khi đang nhập — tín hiệu "ô này đang hoạt động",
   cộng thêm vào ring chứ không thay ring (ring vẫn là kênh chính). */
.field-scan {
    position: absolute;
    /* ⚠️ Thụt 14px mỗi bên, KHÔNG chạy hết chiều ngang: lúc focus ngoặc góc nở
       ra `inset: 0` và hai cánh ngang dưới (10px) chiếm đúng hai đầu cạnh đáy.
       Vạch quét chạy hết cạnh sẽ trườn qua bên dưới ngoặc — hai nét cùng màu
       chồng nhau ở góc, đọc ra như nét bị dày lên chứ không ra hai vật thể. */
    left: 14px;
    right: 14px;
    bottom: 0;
    z-index: 1;
    height: 1px;
    overflow: hidden;
    pointer-events: none;
    opacity: 0;
    transition: opacity .15s ease-out;
}

    .field-scan::before {
        content: "";
        position: absolute;
        top: 0;
        left: 0;
        width: 45%;
        height: 100%;
        background: linear-gradient(90deg, transparent, var(--primary-4), transparent);
        animation: hudScanX 1.9s ease-in-out infinite;
    }

.field-shell:focus-within .field-scan,
.password-wrapper:focus-within .field-scan {
    opacity: 1;
}

.ant-input,
input[type="text"],
input[type="email"],
input[type="password"] {
    width: 100%;
    box-sizing: border-box;
    height: var(--h-lg);
    padding: 0 12px;
    font-family: inherit;
    font-size: var(--text-body-14);
    color: var(--fg-1);
    /* Đục hơn panel để đọc thành "ô lõm"; nếu cùng độ trong suốt thì ô nhập
       biến mất vào nền kính và chữ gõ vào bị chìm. */
    background: rgba(4, 20, 38, .72) !important;
    border: 1px solid rgba(52, 86, 119, .85) !important;
    border-radius: var(--radius) !important;
    outline: none;
    /* GỜ LÕM (port `HudBevel` của mobile). Lòng ô đọc ra là cái GIẾNG khoét vào
       mặt panel chứ không phải hình chữ nhật dán lên — đây chính là lớp thiếu
       khiến ô trước đây trông "thô" (nhận xét của chủ dự án trên bản mobile).

       Nguồn: bộ mẫu command-center TQ luôn dựng ô nhập bằng BA lớp — chu vi mờ
       ít bão hoà, màu sáng chỉ ăn vài đoạn ở góc, và lòng ô lõm bằng
       `box-shadow: inset 0 1px 5px`. Web có `inset` thật nên chép được thẳng;
       bản RN phải vẽ lại bằng SVG gradient vì RN không có inset shadow.

       Hai nét, CÙNG MỘT câu chuyện ánh sáng — nguồn sáng ở TRÊN:
         · vệt tone hắt từ mép trên xuống, tắt dần   (alpha .10 = `peak`)
         · hairline sáng sát đáy: mặt đáy giếng bắt sáng nên có mép (alpha .07)
       ⚠️ Đáy LUÔN nhạt hơn đỉnh, nếu không nguồn sáng đọc ra ở dưới. */
    box-shadow: inset 0 1px 5px rgba(45, 212, 191, .10),
                inset 0 -1px 0 rgba(45, 212, 191, .07);
    /* 240ms thay cho 150ms: ô nhập là thứ người dùng nhìn lâu nhất, nét chuyển
       chậm hơn một nhịp đọc ra "êm" chứ chưa tới mức trễ (trên ~300ms là bắt
       đầu thấy ô phản hồi chậm hơn ngón tay).
       ⛔ KHÔNG dùng `transition: all`. `all` bắt trình duyệt theo dõi mọi thuộc
       tính, kể cả những thứ ảnh hưởng bố cục (width, padding) — một lần đổi
       nhầm là cả ô co giãn có hiệu ứng. Liệt kê đúng 4 thứ cần đổi. */
    transition: color .24s ease-out, border-color .24s ease-out, background-color .24s ease-out, box-shadow .24s ease-out;
}

    /* Selection màu riêng - nguyên tắc 4 của field.theme.ts */
    .ant-input::selection,
    input::selection {
        background: rgba(34, 197, 94, .30);
        color: var(--fg-1);
    }

    /* Chữ gợi ý phải MỜ hơn hẳn chữ thật, nếu không lúc gõ vào người dùng không
       thấy ngay là ô đã có nội dung. .72 vẫn còn ~3,1:1 trên nền ô — trên sàn 3:1
       của WCAG cho chữ phụ trợ, đừng hạ tiếp.
       ⚠️ `opacity: 1` khai TRƯỚC là bắt buộc: Firefox mặc định cho placeholder
       một opacity riêng, không ghi đè thì hai trình duyệt ra hai độ mờ khác nhau. */
    .ant-input::placeholder,
    input::placeholder {
        color: var(--fg-5);
        opacity: 1;
        opacity: .72;
    }

    /* Đang gõ thì chữ gợi ý của các ô KHÁC không cần tranh chú ý nữa */
    .field-shell:focus-within input::placeholder,
    .password-wrapper:focus-within input::placeholder {
        opacity: .5;
    }

    .ant-input:hover,
    input[type="text"]:hover,
    input[type="email"]:hover,
    input[type="password"]:hover {
        border-color: var(--border-2) !important;
    }

    /* Hai kênh cùng lúc: đổi viền + ring bên ngoài (nguyên tắc 1 của field.theme.ts).
       Hover chỉ đậm viền, KHÔNG có ring - để phân biệt "chuột đang trên" với
       "con trỏ đang trong ô" (nguyên tắc 2). */
    /* ⛔ VIỀN FOCUS LÀ 60% CHỨ KHÔNG ĐẶC — số đo, không phải thẩm mỹ.
       Bản mobile từng để `#22C55E` đặc chạy đều quanh chu vi và chủ dự án gọi
       đúng một chữ: "thô". Bệnh không ở MÀU mà ở chỗ ô chỉ có ĐÚNG MỘT nét bão
       hoà 100%. Trong `ui-smart` (119 mẫu BigDataView + borderBox1-13) chu vi
       LUÔN là nét mờ ít bão hoà (#235fa7, #2e6099), màu sáng (#4fd2dd, #2cf7fe)
       chỉ ăn vài ĐOẠN NGẮN ở góc: nét sáng là thứ hiếm nên mới đọc ra tín hiệu,
       rải đều cả chu vi thì nó thành cái khung.

       60% là mức THẤP NHẤT còn đạt 3,39:1 trên nền ô — sàn 3:1 của WCAG 1.4.11
       cho ranh giới control. `/45` chỉ còn 2,48:1. ⛔ ĐỪNG hạ tiếp.
       Độ "nổi" không mất đi đâu: nó chuyển sang ngoặc góc đặc + ring + gờ lõm. */
    .ant-input:focus,
    input[type="text"]:focus,
    input[type="email"]:focus,
    input[type="password"]:focus {
        border-color: rgba(34, 197, 94, .60) !important;
        background: rgba(4, 20, 38, .88) !important;
        /* gờ lõm (đổi sang tông lục) + ring 2px + quầng neon toả ra ngoài.
           ⚠️ PHẢI gộp trong MỘT khai báo — tách ra là cái sau xoá cái trước,
           mất gờ lõm đúng lúc ô đang được gõ (bẫy mobile đã dính, xem
           `FIELD_SURFACE_CLASS`). */
        box-shadow: inset 0 1px 5px rgba(34, 197, 94, .12),
                    inset 0 -1px 0 rgba(34, 197, 94, .08),
                    0 0 0 2px rgba(34, 197, 94, .34),
                    0 0 18px -2px rgba(34, 197, 94, .42) !important;
    }

    /* Ô lỗi GIỮ ring đỏ khi focus - chuyển xanh sẽ khiến user tưởng đã sửa xong
       (nguyên tắc 3 của field.theme.ts).
       ⛔ ĐỎ GIỮ NGUYÊN ĐẶC, không hạ xuống 60% như viền lục: mobile đã tính thử
       `/70` → 2,80:1, TRƯỢT. Và đây là trạng thái DUY NHẤT mà "thô" là đúng —
       ô sai thì phải chặn mắt lại. Nét tinh của trạng thái lỗi đến từ gờ lõm +
       ngoặc góc đỏ, không đến từ việc làm nhạt viền. */
    .ant-input.input-validation-error,
    input.input-validation-error {
        border-color: var(--error-5) !important;
        box-shadow: inset 0 1px 5px rgba(255, 66, 78, .12),
                    inset 0 -1px 0 rgba(255, 66, 78, .08) !important;
    }

        .ant-input.input-validation-error:focus,
        input.input-validation-error:focus {
            border-color: var(--error-5) !important;
            box-shadow: inset 0 1px 5px rgba(255, 66, 78, .14),
                        inset 0 -1px 0 rgba(255, 66, 78, .09),
                        0 0 0 2px rgba(255, 66, 78, .38) !important;
        }

/* Ô lỗi: vạch quét và icon cũng phải đỏ, nếu không ô báo lỗi mà vẫn
   nhấp nháy xanh -> tín hiệu mâu thuẫn. */
.field-shell:focus-within:has(.input-validation-error) .field-icon,
.password-wrapper:focus-within:has(.input-validation-error) .field-icon {
    background-color: var(--error-5);
}

.field-shell:has(.input-validation-error) .field-scan::before,
.password-wrapper:has(.input-validation-error) .field-scan::before {
    background: linear-gradient(90deg, transparent, var(--error-5), transparent);
}

/* =============================================================
   Trình duyệt tự điền (autofill)
   Nền base của ô là rgba(...,.72) - TRONG SUỐT, nên nền autofill của UA
   lộ xuyên qua. Phải phủ bằng inset shadow dày, không phải bằng background.
   ============================================================= */

/* Chromium + Safari. Phủ đủ 4 state: thiếu :hover/:active thì Chrome
   vẽ lại nền khi đổi state. */
.ant-input:-webkit-autofill,
.ant-input:-webkit-autofill:hover,
.ant-input:-webkit-autofill:active,
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:active {
    -webkit-text-fill-color: var(--fg-1) !important;
    box-shadow: 0 0 0 1000px var(--bg-input) inset !important;
    caret-color: var(--primary-5) !important;
    /* Chrome hoãn áp nền UA qua transition; kéo dài vô hạn = không bao giờ tới */
    transition: background-color 100000s ease-in-out 0s !important;
}

/* Focus PHẢI gộp inset + ring trong CÙNG một khai báo.
   Tách thành hai rule thì cái sau xoá cái trước -> mất nền hoặc mất ring. */
.ant-input:-webkit-autofill:focus,
input:-webkit-autofill:focus {
    box-shadow: 0 0 0 1000px var(--bg-input) inset,
                0 0 0 2px rgba(34, 197, 94, .40),
                0 0 18px -2px rgba(34, 197, 94, .45) !important;
    border-color: var(--primary-4) !important;
}

/* Ô lỗi đã autofill: giữ ring ĐỎ khi focus - chuyển xanh sẽ khiến người dùng
   tưởng đã sửa xong (nguyên tắc 3 của field.theme.ts) */
.ant-input.input-validation-error:-webkit-autofill:focus,
input.input-validation-error:-webkit-autofill:focus {
    box-shadow: 0 0 0 1000px var(--bg-input) inset,
                0 0 0 2px rgba(255, 66, 78, .40) !important;
    border-color: var(--error-5) !important;
}

/* Firefox: KHÔNG hiểu -webkit-autofill. Dùng pseudo-class chuẩn và gỡ
   filter mà UA tự áp lên ô vừa được điền. */
.ant-input:autofill,
input:autofill {
    filter: none;
    background-color: var(--bg-input) !important;
    color: var(--fg-1) !important;
}

/* --- Nút hiện/ẩn mật khẩu ---
   Là <button> thật (trước đây là <span> có onclick) nên bàn phím Tab tới được
   và trình đọc màn hình đọc ra "nút". Phải reset dáng button của UA. */
.password-suffix {
    position: absolute;
    top: 50%;
    z-index: 2;
    right: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    margin: 0;
    padding: 0;
    transform: translateY(-50%);
    /* Cùng lý do với .field-icon: nét mảnh không bao giờ đạt tới màu đã khai,
       nên --fg-5 đọc ra mờ hơn hẳn con số trên giấy. Nút này còn là thứ BẤM
       ĐƯỢC nên không được nhạt hơn cái icon trang trí bên trái. Sáng lên
       cùng bậc --fg-3; khác biệt "bấm được" vẫn nói bằng ĐỔI MÀU lúc
       hover/focus (sang --primary-4) chứ không bằng độ mờ lúc nghỉ. */
    color: var(--fg-3);
    background: none;
    border: 0;
    border-radius: var(--radius);
    cursor: pointer;
    transition: color .15s ease-out, background-color .15s ease-out;
}

    /* Icon vẽ bằng mask -> đổi màu theo currentColor, không cần 2 file ảnh */
    .password-suffix::before {
        content: "";
        width: 18px;
        height: 18px;
        background-color: currentColor;
        transition: transform .18s cubic-bezier(.34, 1.4, .64, 1);
        -webkit-mask-repeat: no-repeat;
        mask-repeat: no-repeat;
        -webkit-mask-position: center;
        mask-position: center;
        -webkit-mask-size: contain;
        mask-size: contain;
        -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M1.8 12S5.8 5 12 5s10.2 7 10.2 7-4 7-10.2 7S1.8 12 1.8 12Z'/><circle cx='12' cy='12' r='3.2'/></svg>");
        mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M1.8 12S5.8 5 12 5s10.2 7 10.2 7-4 7-10.2 7S1.8 12 1.8 12Z'/><circle cx='12' cy='12' r='3.2'/></svg>");
    }

    /* Đang hiện mật khẩu -> mắt gạch chéo */
    .password-suffix.is-visible::before {
        -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4.5 20 19.5'/><path d='M9.6 6.2A9.6 9.6 0 0 1 12 5c6.2 0 10.2 7 10.2 7a18 18 0 0 1-3.3 4M6.7 8.2A18.4 18.4 0 0 0 1.8 12s4 7 10.2 7a9.7 9.7 0 0 0 3.6-.7'/><path d='M9.9 10a3.2 3.2 0 0 0 4.3 4.4'/></svg>");
        mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4.5 20 19.5'/><path d='M9.6 6.2A9.6 9.6 0 0 1 12 5c6.2 0 10.2 7 10.2 7a18 18 0 0 1-3.3 4M6.7 8.2A18.4 18.4 0 0 0 1.8 12s4 7 10.2 7a9.7 9.7 0 0 0 3.6-.7'/><path d='M9.9 10a3.2 3.2 0 0 0 4.3 4.4'/></svg>");
    }

    .password-suffix:hover {
        color: var(--primary-4);
        background: rgba(34, 197, 94, .10);
    }

        .password-suffix:hover::before {
            transform: scale(1.12);
        }

    .password-suffix:active::before {
        transform: scale(.92);
    }

    .password-suffix:focus-visible {
        outline: none;
        color: var(--primary-4);
        box-shadow: 0 0 0 2px rgba(34, 197, 94, .55);
    }

/* Chừa chỗ cho nút hiện/ẩn mật khẩu */
.password-wrapper input {
    padding-right: 44px;
}

/* --- Cảnh báo Caps Lock. Ẩn bằng display:none chứ không phải opacity:
       để opacity thì trình đọc màn hình vẫn đọc ra một cảnh báo không có thật. --- */
.caps-hint {
    display: none;
    align-items: center;
    gap: 6px;
    margin-top: 6px;
    font-size: var(--text-caption-12);
    line-height: 16px;
    color: var(--warning-5);
}

    .caps-hint.is-on {
        display: flex;
    }

    .caps-hint::before {
        content: "";
        flex: 0 0 auto;
        width: 13px;
        height: 13px;
        background-color: currentColor;
        -webkit-mask-repeat: no-repeat;
        mask-repeat: no-repeat;
        -webkit-mask-position: center;
        mask-position: center;
        -webkit-mask-size: contain;
        mask-size: contain;
        -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3 4 11h4v5h8v-5h4z'/><path d='M8 19h8'/></svg>");
        mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3 4 11h4v5h8v-5h4z'/><path d='M8 19h8'/></svg>");
    }

/* --- ⛔ CẢNH BÁO NẰM Ở HÀNG NHÃN, KHÔNG NẰM DƯỚI Ô — VÌ BỐ CỤC.

   Trước đây `<p class="caps-hint">` là phần tử thường đứng ngay DƯỚI ô mật khẩu.
   Nó `display: none` -> `display: flex`, tức lúc bật Caps Lock nó chiếm thêm 22px
   (16px dòng + 6px margin) và ĐẨY mọi thứ bên dưới xuống: ở trang Đăng nhập là
   hàng "Ghi nhớ đăng nhập" + nút Đăng nhập, ở Đăng ký là cả ba hàng còn lại.
   Người dùng đang gõ mật khẩu thì cái nút nhảy chỗ — đúng loại giật bố cục mà
   `.pw-meter` đã cố tránh bằng `opacity` thay cho `display` (xem chú thích ở đó).

   Ở đây không dùng được mẹo `opacity`: chú thích cũ của `.caps-hint` nói rõ lý do
   — để `opacity: 0` thì trình đọc màn hình vẫn đọc ra một cảnh báo không có thật.

   Lời giải: dời cảnh báo lên hàng nhãn và cho `position: absolute`. Hàng nhãn
   luôn có sẵn khoảng trống ở mép phải ("Mật khẩu" chỉ ăn ~65px trong ~380px), nên
   cảnh báo KHÔNG tốn thêm một pixel chiều cao nào và không bao giờ đẩy gì cả.
   Thêm một cái được: nó hiện ngay cạnh chữ "Mật khẩu" — chỗ mắt đang nhìn khi gõ
   — chứ không phải dưới đáy ô.

   ⚠️ `.ant-form-item-label` của antd khai `overflow: hidden` + `white-space:
   nowrap`. Cả hai đều thuận: badge nằm gọn trong hộp nhãn nên không bị cắt, và
   chữ trong badge không xuống dòng. Nhưng ĐỪNG nới badge ra ngoài mép hộp —
   `overflow: hidden` sẽ cắt phần thừa. --- */
.form-item-label {
    position: relative;
}

    .form-item-label .caps-hint {
        position: absolute;
        right: 0;
        top: 50%;
        transform: translateY(-50%);
        margin-top: 0;
        /* Nhãn có `color` đổi theo :focus-within (xem §form-item). Cảnh báo phải
           giữ màu cảnh báo của nó, nên khai lại sau. */
        color: var(--warning-5);
    }

/* Màn hẹp: hàng nhãn không còn đủ chỗ cho cả nhãn dài ("Nhập lại mật khẩu") và
   badge. Bỏ chữ, giữ icon — icon tam giác cảnh báo + `title` là đủ, và vẫn không
   tốn chiều cao. `font-size: 0` chứ không `display: none` để `::before` (icon)
   còn sống. */
@media (max-width: 400px) {
    .form-item-label .caps-hint {
        font-size: 0;
        gap: 0;
    }
}

/* --- Thanh đo độ mạnh mật khẩu (trang Đăng ký).
       4 vạch; JS bật lần lượt class .is-on và đặt data-level ở khối cha. --- */
.pw-meter {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 7px;
    /* Lúc ô còn trống, 4 vạch xám nằm dưới ô nhập đọc ra như một đường kẻ bị vỡ
       chứ không ra thanh đo. Hạ mờ ở mức nghỉ để nó lùi về nền, và vì `opacity`
       không đụng bố cục nên khi có mật khẩu thanh đo sáng lên TẠI CHỖ, không
       đẩy các hàng dưới nhảy xuống (đó là lý do không dùng `display:none`). */
    opacity: .5;
    transition: opacity .2s ease-out;
}

.pw-meter:not([data-level="0"]) {
    opacity: 1;
}

.pw-meter-track {
    display: flex;
    flex: 1;
    gap: 4px;
}

.pw-meter-seg {
    flex: 1;
    height: 3px;
    background: rgba(52, 86, 119, .8);
    transition: background-color .2s ease-out, box-shadow .2s ease-out;
}

.pw-meter-label {
    flex: 0 0 auto;
    min-width: 62px;
    text-align: right;
    font-size: var(--text-caption-10);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--fg-5);
}

.pw-meter[data-level="1"] .pw-meter-seg.is-on { background: var(--error-5);   box-shadow: 0 0 7px rgba(255, 66, 78, .6); }
.pw-meter[data-level="2"] .pw-meter-seg.is-on { background: var(--warning-5); box-shadow: 0 0 7px rgba(252, 130, 10, .6); }
.pw-meter[data-level="3"] .pw-meter-seg.is-on { background: var(--secondary-5); box-shadow: 0 0 7px rgba(34, 181, 216, .6); }
.pw-meter[data-level="4"] .pw-meter-seg.is-on { background: var(--primary-5); box-shadow: 0 0 7px rgba(34, 197, 94, .7); }

.pw-meter[data-level="1"] .pw-meter-label { color: var(--error-5); }
.pw-meter[data-level="2"] .pw-meter-label { color: var(--warning-5); }
.pw-meter[data-level="3"] .pw-meter-label { color: var(--secondary-5); }
.pw-meter[data-level="4"] .pw-meter-label { color: var(--primary-4); }

/* =============================================================
   9. Nút
   Khớp Button.tsx: h-10 (lg) mặc định, h-11 (xl) cho CTA, transition 200ms,
   disabled dùng opacity chứ không đổi màu nền.
   ============================================================= */

.ant-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: var(--h-lg);
    padding: 0 16px;
    overflow: hidden;
    font-family: inherit;
    font-size: var(--text-body-14);
    font-weight: 500;
    border-radius: var(--radius) !important;
    border: 1px solid transparent;
    cursor: pointer;
    transition: all .2s ease;
}

    .ant-btn:focus-visible {
        outline: none;
        box-shadow: 0 0 0 2px var(--bg-base), 0 0 0 4px var(--primary-5);
    }

    .ant-btn:disabled,
    .ant-btn[disabled] {
        opacity: .5;
        cursor: not-allowed;
        pointer-events: none;
    }

.ant-btn-lg {
    height: var(--h-xl);
    padding: 0 20px;
    letter-spacing: .02em;
}

/* Mũi tên trên nút chính — ảnh mẫu ghi "Log In →": mũi tên nói nút này ĐƯA ĐI
   đâu đó, khác hẳn một nút chỉ đổi trạng thái tại chỗ.
   Là phần tử thật trong markup chứ không pseudo-element: `::before` của
   `.ant-btn` đã dành cho spinner loading và `::after` cho vệt sáng quét. */
.btn-arrow {
    flex: 0 0 auto;
    width: 15px;
    height: 15px;
    background-color: currentColor;
    transition: transform .22s var(--ease-out);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 12h15'/><path d='m13 6 6 6-6 6'/></svg>");
    mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 12h15'/><path d='m13 6 6 6-6 6'/></svg>");
}

/* Mũi tên trượt sang phải khi rê chuột — nó nói nút này ĐƯA ĐI đâu đó.
   3px: đủ để mắt bắt được, chưa đủ để mũi tên rời khỏi cụm chữ. */
.ant-btn:hover .btn-arrow {
    transform: translateX(3px);
}

/* Đang gửi: mũi tên biến mất để không chạy song song với spinner — hai tín
   hiệu trái ngược (đang chờ / mời bấm tiếp) đứng cạnh nhau. */
.ant-btn.is-loading .btn-arrow {
    display: none;
}

.ant-btn-block {
    display: flex;
    width: 100%;
}

/* FE dùng primary-6 + chữ trắng, nhưng đo ra chỉ 3.3:1 - KHÔNG đạt WCAG AA (4.5).
   Chính Button.tsx của FE đã ghi nguyên tắc này cho nút accent: "nền sáng ->
   BẮT BUỘC text tối, chữ trắng FAIL WCAG". Áp cùng logic cho nút xanh:
   nền primary-5 sáng hơn (hợp tông neon) + chữ tối -> 7.43:1. */
.ant-btn-primary {
    color: #04220F !important;
    background: linear-gradient(135deg, var(--primary-4) 0%, var(--primary-5) 52%, #1FB894 100%) !important;
    border-color: var(--primary-4) !important;
    box-shadow: 0 0 26px -8px rgba(34, 197, 94, .95);
}

    /* Vệt sáng quét ngang khi rê chuột. Chỉ chạy KHI hover nên không tốn
       animation frame lúc trang đứng yên.
       ⛔ Chạy bằng transform, KHÔNG bằng `left`: `left` buộc trình duyệt tính
       lại bố cục 60 lần/giây cho một hiệu ứng thuần trang trí. */
    .ant-btn-primary::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        width: 42%;
        background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .55), transparent);
        transform: translateX(-180%) skewX(-18deg);
        opacity: 0;
        pointer-events: none;
    }

    .ant-btn-primary:hover {
        background: linear-gradient(135deg, #6BE99A 0%, var(--primary-4) 52%, #2DD4BF 100%) !important;
        border-color: var(--primary-4) !important;
        box-shadow: 0 0 36px -6px rgba(78, 222, 130, 1);
        /* Sáng thêm 8%. `filter` tác động lên CẢ nút (nền + chữ + mũi tên) nên
           không phải chỉnh từng màu; và nó chạy trên compositor nên không tốn
           một lần tính lại bố cục nào.
           ⚠️ Chỉ 1.08 chứ không 1.10: nền nút vốn đã là gradient lục sáng, đẩy
           thêm 10% là chữ tối trên nền bắt đầu tụt dưới ngưỡng tương phản. */
        filter: brightness(1.08);
        /* Nhấc 1px — đủ để tay cảm thấy nút "sống", chưa đủ để dòng bên cạnh
           trông như bị xô lệch. */
        transform: translateY(-1px);
    }

        .ant-btn-primary:hover::after {
            opacity: 1;
            animation: hudSheen .8s ease-out;
        }

    .ant-btn-primary:active {
        background: var(--primary-6) !important;
        border-color: var(--primary-5) !important;
        box-shadow: 0 0 18px -8px rgba(34, 197, 94, .8);
        transform: translateY(0);
    }

/* variant=stroke skin=neutral của FE: viền border-2, chữ fg-2, hover nền bg-2 */
.ant-btn-default,
.ant-btn-variant-outlined {
    color: var(--fg-2) !important;
    background: transparent !important;
    border: 1px solid var(--border-2) !important;
}

    /* ⛔ Viền ĐỔI MÀU khi rê chuột. Bản trước giữ nguyên `--border-2` nên nút
       mạng xã hội chỉ đổi nền — trên nền tối thì đổi nền là tín hiệu rất yếu,
       nút đọc ra như chết. Viền ngả teal + một quầng mảnh là đủ để nó "sống"
       mà vẫn không tranh chỗ với nút chính màu lục. */
    .ant-btn-default:hover,
    .ant-btn-variant-outlined:hover {
        color: var(--fg-1) !important;
        background: var(--bg-2) !important;
        border-color: rgba(45, 212, 191, .75) !important;
        box-shadow: 0 0 18px -8px rgba(45, 212, 191, .9);
    }

    .ant-btn-default:active,
    .ant-btn-variant-outlined:active {
        background: var(--bg-3) !important;
    }

/* =============================================================
   10. Link, checkbox, divider, lỗi
   ============================================================= */

/* Ant dùng selector 2 class (.css-var-xxx.ant-typography) nên rule 1 class thua.
   Không sửa được bundle Ant nên dùng !important; loại trừ .ant-btn để nút giữ màu riêng. */
a:not(.ant-btn) {
    color: var(--link) !important;
    text-decoration: none;
    transition: color .16s ease;
}

    a:not(.ant-btn):hover {
        color: var(--primary-5) !important;
        text-decoration: underline;
    }

.ant-checkbox-wrapper {
    display: inline-flex;
    align-items: flex-start;
    gap: 8px;
    font-size: var(--text-body-14);
    line-height: 20px;
    color: var(--fg-4);
    cursor: pointer;
}

    /* --- Checkbox thô của trang Đăng nhập ("Ghi nhớ đăng nhập").

       Trang Đăng ký dựng checkbox theo cấu trúc Ant (input thật bị ẩn, ô nhìn
       thấy là `.ant-checkbox-inner`), trang Đăng nhập thì dùng `<input>` trần.
       Bản trước chỉ đặt `accent-color` lên nó, nên lúc CHƯA TÍCH trình duyệt vẫn
       vẽ cái ô mặc định của hệ điều hành: bo tròn, viền xám sáng, nền trắng ngà —
       lạc hẳn khỏi cả trang, và đứng cạnh checkbox lục vuông của trang Đăng ký
       thì hai màn đọc ra như hai sản phẩm khác nhau.

       `appearance: none` để tự vẽ lại đúng bằng token của ô nhập, dấu tích là
       một `::before` xoay 45° (cùng thủ pháp với `.ant-checkbox-inner`).
       ⚠️ Giữ nguyên `<input>` thật, KHÔNG thay bằng span: bàn phím, form POST và
       trình đọc màn hình đều dựa vào nó. --- */
    .ant-checkbox-wrapper input[type="checkbox"]:not(.ant-checkbox-input) {
        position: relative;
        flex: 0 0 auto;
        width: 16px;
        height: 16px;
        margin: 2px 0 0;
        padding: 0;
        appearance: none;
        -webkit-appearance: none;
        background: rgba(4, 20, 38, .72);
        border: 1px solid rgba(52, 86, 119, .95);
        border-radius: var(--radius);
        cursor: pointer;
        transition: background-color .15s ease-out, border-color .15s ease-out, box-shadow .15s ease-out;
    }

        .ant-checkbox-wrapper input[type="checkbox"]:not(.ant-checkbox-input):hover {
            border-color: var(--primary-4);
        }

        .ant-checkbox-wrapper input[type="checkbox"]:not(.ant-checkbox-input):checked {
            background: linear-gradient(135deg, var(--primary-4), var(--primary-5));
            border-color: var(--primary-4);
            box-shadow: 0 0 12px -2px rgba(34, 197, 94, .75);
        }

        /* Dấu tích = hai cạnh của một ô vuông xoay 45°. Vẽ tay thay vì dùng ký
           tự "✓": ký tự tick đổi hình theo font hệ thống, còn hai cạnh border
           thì máy nào cũng ra đúng một hình. */
        .ant-checkbox-wrapper input[type="checkbox"]:not(.ant-checkbox-input):checked::before {
            content: "";
            position: absolute;
            top: 1px;
            left: 4.5px;
            width: 4px;
            height: 8px;
            border: solid #04220F;
            border-width: 0 2px 2px 0;
            transform: rotate(45deg);
        }

        .ant-checkbox-wrapper input[type="checkbox"]:not(.ant-checkbox-input):focus-visible {
            outline: none;
            border-color: var(--primary-4);
            box-shadow: 0 0 0 2px rgba(34, 197, 94, .45);
        }

/* Checkbox theo cấu trúc Ant (trang Đăng ký): input thật bị ẩn,
   ô nhìn thấy là .ant-checkbox-inner. Ant dùng selector 2 class nên cần !important. */
.ant-checkbox .ant-checkbox-inner {
    background-color: var(--bg-input) !important;
    border-color: var(--border-1) !important;
    border-radius: var(--radius) !important;
}

.ant-checkbox-checked .ant-checkbox-inner {
    background-color: var(--primary-5) !important;
    border-color: var(--primary-5) !important;
    box-shadow: 0 0 8px rgba(34, 197, 94, .5);
}

    /* Dấu tick: tối trên nền xanh lá cho đủ tương phản */
    .ant-checkbox-checked .ant-checkbox-inner::after {
        border-color: #04220F !important;
    }

.ant-checkbox-wrapper:hover .ant-checkbox-inner {
    border-color: var(--primary-5) !important;
}

/* ⛔ Ở đây KHÔNG có runtime React của antd để gắn/gỡ class `.ant-checkbox-checked`,
   nên ô tick sẽ đứng nguyên trạng thái ban đầu dù người dùng bấm bao nhiêu lần:
   bản trước hardcode sẵn class `checked` nên ô LÚC NÀO CŨNG trông như đã tick,
   kể cả khi input đang bỏ tick. Dựng lại từ trạng thái `:checked` THẬT.

   Dùng bộ chọn anh-em `+` chứ không `:has()` — `+` chạy ở mọi trình duyệt, và
   đây là trạng thái điều khiển chứ không phải trang trí, không được phép hỏng. */
.ant-checkbox .ant-checkbox-input:checked + .ant-checkbox-inner {
    background-color: var(--primary-5) !important;
    border-color: var(--primary-5) !important;
    box-shadow: 0 0 8px rgba(34, 197, 94, .5);
    transform: scale(1.06);
}

    /* Giá trị transform sao đúng theo antd (umi.css) để dấu tick nằm đúng chỗ */
    .ant-checkbox .ant-checkbox-input:checked + .ant-checkbox-inner::after {
        opacity: 1;
        border-color: #04220F !important;
        transform: rotate(45deg) scale(1) translate(-50%, -50%);
    }

/* Input thật bị antd ẩn đi, nên người dùng bàn phím sẽ không thấy ô nào đang
   được chọn nếu không vẽ ring lên phần tử nhìn thấy được. */
.ant-checkbox .ant-checkbox-input:focus-visible + .ant-checkbox-inner {
    outline: none;
    box-shadow: 0 0 0 2px rgba(34, 197, 94, .55);
}

.ant-divider {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 22px 0;
    color: var(--fg-5) !important;
    font-size: var(--text-caption-12);
}

    .ant-divider::before,
    .ant-divider::after {
        content: "";
        flex: 1;
        height: 1px;
        background: linear-gradient(90deg, transparent, var(--border-1), transparent);
    }

.ant-divider-inner-text {
    white-space: nowrap;
    color: var(--fg-5) !important;
}

.ant-form-item-explain-error,
.field-validation-error,
.text-danger,
.validation-summary-errors {
    color: var(--error-5) !important;
    font-size: var(--text-caption-12);
    line-height: 16px;
    margin-top: 6px;
}

.validation-summary-errors ul {
    margin: 0;
    padding-left: 18px;
}

/* =============================================================
   11. Khối riêng của từng trang
   ============================================================= */

.social-btn-group {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Nút đăng nhập mạng xã hội - là thẻ <a> nên phải tự dựng dáng nút */
.ant-btn-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    height: var(--h-lg);
    box-sizing: border-box;
    padding: 0 16px;
    font-size: var(--text-body-14);
    font-weight: 500;
    color: var(--fg-2) !important;
    background: rgba(255, 255, 255, .04);
    border: 1px solid rgba(76, 123, 163, .6);
    border-radius: var(--radius);
    text-decoration: none;
    transition: all .2s ease;
}

    .ant-btn-link:hover {
        color: var(--fg-1) !important;
        background: var(--bg-2);
        border-color: var(--teal-5);
        box-shadow: 0 0 16px -6px rgba(45, 212, 191, .8);
        text-decoration: none;
    }

    .ant-btn-link .btn-icon {
        display: flex;
        align-items: center;
        margin-right: 8px;
    }

    .ant-btn-link img,
    .social-btn-group .btn-icon img {
        display: block;
        width: 18px;
        height: 18px;
    }

.remember-forgot-wrapper {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
}

/* Khối thông báo dùng chung cho các trang trạng thái (lỗi / khoá / 2FA) */
.auth-note {
    padding: 14px 16px;
    margin-bottom: 20px;
    font-size: var(--text-body-14);
    line-height: 20px;
    color: var(--fg-3);
    background: rgba(34, 197, 94, .06);
    border-left: 2px solid var(--primary-5);
}

.auth-note--error {
    background: rgba(255, 66, 78, .08);
    border-left-color: var(--error-5);
}

.auth-note--warning {
    background: rgba(252, 130, 10, .08);
    border-left-color: var(--warning-5);
}

.auth-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 4px;
}

/* Link quay lại đăng nhập - có ở mọi trang trừ Login/Register */
.auth-back-link {
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid var(--border-1);
    text-align: center;
    font-size: var(--text-body-14);
}

/* Trạng thái chờ single sign-out ở trang đã đăng xuất */
.auth-signing-out {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 16px;
    font-size: var(--text-body-14);
    color: var(--fg-4);
    background: var(--bg-2);
    border-left: 2px solid var(--primary-5);
}

.auth-spinner {
    display: inline-block;
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    border: 2px solid var(--primary-5);
    border-top-color: transparent;
    border-radius: 9999px;
    animation: authSpin .6s linear infinite;
}

/* Khối thông tin ở trang xác nhận email */
.ant-confirm-email-info {
    padding: 14px 16px;
    background: rgba(34,197,94,.06);
    border-left: 2px solid var(--primary-5);
}

    .ant-confirm-email-info .ant-typography {
        color: var(--fg-4) !important;
    }

    .ant-confirm-email-info strong {
        color: var(--fg-1);
    }

/* =============================================================
   12. Trạng thái loading của nút
   Sao pattern Button.tsx: spinner THAY icon, chữ giữ nguyên, disabled + aria-busy.
   ============================================================= */

.ant-btn.is-loading,
.ant-btn-link.is-loading {
    opacity: .5;
    cursor: not-allowed;
    pointer-events: none;
}

    .ant-btn.is-loading::before,
    .ant-btn-link.is-loading::before {
        content: "";
        display: inline-block;
        flex: 0 0 auto;
        width: 16px;
        height: 16px;
        border: 2px solid currentColor;
        border-top-color: transparent;
        border-radius: 9999px;
        animation: authSpin .6s linear infinite;
    }

/* =============================================================
   12b. Lớp chi tiết & micro-interaction

   Kỷ luật áp dụng cho toàn bộ mục này:
     - Hiệu ứng LẶP VÔ HẠN chỉ được động vào `transform` / `opacity` (hai thuộc
       tính chạy thẳng trên compositor, không bắt trình duyệt tính lại bố cục).
     - `width` / `height` / `left` / `top` chỉ được động khi hiệu ứng CHỈ chạy
       lúc hover/focus, tức có điểm dừng.
     - Biên độ giữ nhỏ (1-3px, scale ≤ 1.12). Chi tiết nhiều nhưng mỗi chi tiết
       phải nhỏ, nếu không cả trang thành cái đèn nháy.
   ============================================================= */

/* =============================================================
   NGOẶC GÓC CỦA Ô NHẬP — port `HudFocusCorners` của vci-sun-mobile

   Bản trước ở đây là 2 dấu góc CHỈ hiện khi focus. Bản mobile đã đi qua đúng
   thiết kế đó rồi bỏ, lý do ghi trong `HudFocusCorners.tsx`:

     "ô nhập lúc NGHỈ trước đây là đúng một hình chữ nhật viền đều — thứ bộ mẫu
      command-center TQ không bao giờ vẽ. Ở đó góc LUÔN có nét, chỉ là nét mờ.
      Để `rest` ~0.4 thì ô nghỉ đã có hình, và lúc focus ngoặc **sáng lên** chứ
      không **hiện ra từ hư không**."

   Nên: 4 góc, LUÔN hiện ở opacity .4, focus thì lên 1.0 và nở ra ăn khớp mép.

   Số đo đo THẲNG từ nguồn (mẫu 009 bộ command-center TQ,
   `ui-smart/BigDataView/web/009 某公司大数据监控平台/css/style.css:40-51`):
   menu item cao 40px, KHÔNG viền bao, dựng bằng 4 ngoặc **10×5px dày 2px** ở
   **opacity .4**, hover đổi màu + opacity 1. Ô nhập ở đây cũng cao 40px nên
   lấy nguyên số đó, không phải quy đổi.

   ⛔ Ngoặc DẸT (10×5) chứ không vuông: ngoặc vuông đọc ra "bốn mảnh khung bị
   cắt rời", ngoặc dẹt thì mắt nối chúng thành MỘT đường bao đứt quãng — đó mới
   là hình "khoá mục tiêu". Ô rộng gấp ~8 lần cao thì ngoặc cũng phải dẹt theo.

   ⛔ Nở ra bằng `inset`, KHÔNG bằng `transform: scale()`. Mobile đã dính đúng
   bẫy này: `scale` giãn theo KÍCH THƯỚC nên trên ô rộng 300 thì 2% = 6px ngang
   mà chỉ 0,96px dọc — ngoặc bị đẩy lệch hẳn ra ngoài rồi bị cắt. `inset` dịch
   đúng 2px ở cả hai trục. Đây là 2px trên một phần tử `pointer-events:none`
   chỉ chạy lúc focus, không phải hiệu ứng lặp — chi phí layout không đáng kể.

   Một phần tử, 8 lớp background (mỗi góc 2 vạch) thay vì 4 thẻ span: ít DOM
   hơn hẳn mà vẫn đúng hình, vì cả 4 góc mờ/tỏ và trượt CÙNG NHAU. */

/* Tông của chrome ô nhập theo trạng thái.
   ⚠️ Theo LUẬT HAI TÔNG của HudFrame: cyan = THIẾT BỊ ĐO (khung, ô, tick),
   lục = DỮ LIỆU & HÀNH ĐỘNG. Ô chưa gõ vẫn chỉ là thiết bị đang chờ. */
.field-shell,
.password-wrapper {
    --fld-tone: 45, 212, 191; /* teal-5 — nghỉ */
}

.field-shell:focus-within,
.password-wrapper:focus-within {
    --fld-tone: 78, 222, 130; /* primary-4 — đang gõ */
}

/* ⚠️ Khai SAU `:focus-within` là có chủ đích: hai bộ chọn cùng specificity
   (0,2,0) nên thứ tự quyết thắng thua, và LỖI PHẢI THẮNG FOCUS — gõ lại vào ô
   đang sai thì chrome vẫn phải đỏ, nếu không người dùng tưởng đã sửa xong. */
.field-shell:has(.input-validation-error),
.password-wrapper:has(.input-validation-error) {
    --fld-tone: 255, 66, 78; /* error-5 */
}

.field-hud {
    position: absolute;
    inset: 2px;
    /* z-index thấp hơn nút hiện/ẩn mật khẩu (z-2). Cả hai đều là phần tử được
       định vị nên phần tử ĐỨNG SAU trong DOM sẽ vẽ đè — mà `.field-hud` lại
       nằm sau nút. Không ghim thứ tự thì hai cánh ngoặc góc phải vẽ chồng lên
       nền hover của nút. */
    z-index: 1;
    pointer-events: none;
    opacity: .4;
    background-repeat: no-repeat;
    background-image:
        linear-gradient(rgb(var(--fld-tone)), rgb(var(--fld-tone))), /* TL ngang */
        linear-gradient(rgb(var(--fld-tone)), rgb(var(--fld-tone))), /* TL dọc   */
        linear-gradient(rgb(var(--fld-tone)), rgb(var(--fld-tone))), /* TR ngang */
        linear-gradient(rgb(var(--fld-tone)), rgb(var(--fld-tone))), /* TR dọc   */
        linear-gradient(rgb(var(--fld-tone)), rgb(var(--fld-tone))), /* BL ngang */
        linear-gradient(rgb(var(--fld-tone)), rgb(var(--fld-tone))), /* BL dọc   */
        linear-gradient(rgb(var(--fld-tone)), rgb(var(--fld-tone))), /* BR ngang */
        linear-gradient(rgb(var(--fld-tone)), rgb(var(--fld-tone))); /* BR dọc   */
    background-size:
        10px 2px, 2px 5px,
        10px 2px, 2px 5px,
        10px 2px, 2px 5px,
        10px 2px, 2px 5px;
    background-position:
        left top, left top,
        right top, right top,
        left bottom, left bottom,
        right bottom, right bottom;
    /* 110ms là nhịp RA (mobile: OUT_DURATION) — nhịp VÀO đặt ở rule focus dưới */
    transition: inset 110ms ease-out, opacity 110ms ease-out;
}

.field-shell:focus-within .field-hud,
.password-wrapper:focus-within .field-hud {
    inset: 0;
    opacity: 1;
    /* 140ms là nhịp VÀO (mobile: IN_DURATION) — vào chậm hơn ra một chút để
       tín hiệu "đã bắt được ô" đọc rõ, còn lúc rời đi thì dứt khoát. */
    transition-duration: 140ms;
}

/* Nhãn sáng lên khi con trỏ vào ô — nối nhãn với ô đang nhập bằng mắt,
   hữu ích ở form Đăng ký có 4 ô liền nhau. */
.ant-form-item:focus-within .ant-form-item-label > label,
.ant-form-item:focus-within .form-item-label label {
    color: var(--primary-4) !important;
}

/* --- Link có mũi tên trượt ra khi hover --- */
.link-arrow {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0;
    white-space: nowrap;
}

    .link-arrow::after {
        content: "";
        width: 0;
        height: 9px;
        margin-left: 0;
        background-color: currentColor;
        opacity: 0;
        transform: translateX(-3px);
        transition: width .2s ease-out, margin-left .2s ease-out, opacity .2s ease-out, transform .2s ease-out;
        -webkit-mask-repeat: no-repeat;
        mask-repeat: no-repeat;
        -webkit-mask-position: center;
        mask-position: center;
        -webkit-mask-size: contain;
        mask-size: contain;
        -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='m9 5 7 7-7 7'/></svg>");
        mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='m9 5 7 7-7 7'/></svg>");
    }

    .link-arrow:hover::after,
    .link-arrow:focus-visible::after {
        width: 9px;
        margin-left: 5px;
        opacity: 1;
        transform: none;
    }

    /* Gạch chân vẽ dần từ trái thay vì bật ra nguyên dòng */
    .link-arrow::before {
        content: "";
        position: absolute;
        left: 0;
        bottom: -2px;
        width: 100%;
        height: 1px;
        background: currentColor;
        transform: scaleX(0);
        transform-origin: left center;
        transition: transform .22s cubic-bezier(.22, .61, .36, 1);
    }

    .link-arrow:hover::before,
    .link-arrow:focus-visible::before {
        transform: scaleX(1);
    }

/* text-decoration mặc định của link sẽ chồng lên gạch chân vừa dựng */
a.link-arrow:hover {
    text-decoration: none !important;
}

/* --- Biến thể "quay lại": mũi tên nằm bên TRÁI và chỉ sang trái.

   Dùng ở `.auth-back-link` của các trang Quên mật khẩu / Đặt lại mật khẩu.
   Trước đây mấy trang đó gõ thẳng ký tự "←" vào markup: bề dày nét đổi theo
   font (và trên máy thiếu font thì ra ô vuông), lại không hưởng được hiệu ứng
   trượt + gạch chân của `.link-arrow` như link ở Login/Register.

   ⛔ `row-reverse` chứ KHÔNG thêm một `::before` thứ hai: `::before` đang giữ
   gạch chân vẽ dần. `row-reverse` đưa `::after` (phần tử cuối hàng) về đầu hàng,
   nên vẫn đúng một pseudo-element cho mũi tên.

   ⚠️ Ba rule dưới đây phải ĐỨNG SAU khối `.link-arrow` gốc: độ đặc hiệu BẰNG
   nhau (0-2-1), chỉ hơn nhau ở thứ tự. Chuyển lên trên là mũi tên quay lại
   thành mũi tên chỉ sang phải. */
.link-arrow--back {
    flex-direction: row-reverse;
}

    .link-arrow--back::after {
        /* Nghỉ thì thu về bên PHẢI (hướng ngược lại với bản gốc) rồi trượt ra
           trái khi hover — mũi tên phải chạy cùng hướng nó chỉ. */
        transform: translateX(3px);
        -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='m15 5-7 7 7 7'/></svg>");
        mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='m15 5-7 7 7 7'/></svg>");
    }

    .link-arrow--back:hover::after,
    .link-arrow--back:focus-visible::after {
        /* Bản gốc đặt `margin-left: 5px` để đẩy mũi tên ra khỏi chữ. Ở đây mũi
           tên đứng TRƯỚC chữ nên phải trả margin-left về 0, khe hở chuyển sang
           margin-right — không trả thì chữ bị đẩy thêm 5px, cả dòng nhích. */
        margin-left: 0;
        margin-right: 5px;
        transform: none;
    }

/* --- Khối "đã có tài khoản / chưa có tài khoản" ở đáy panel --- */
.auth-alt-action {
    position: relative;
    margin-top: 15px;
    padding-top: 14px;
    text-align: center;
    font-size: var(--text-body-14);
}

    /* Vạch ngăn mờ dần hai đầu — không cắt ngang panel bằng một gạch thẳng đặc */
    .auth-alt-action::before {
        content: "";
        position: absolute;
        top: 0;
        left: 12%;
        right: 12%;
        height: 1px;
        background: linear-gradient(90deg, transparent, var(--border-1), transparent);
    }

/* ⛔ ĐÃ BỎ: gạch đứt trang trí cạnh tiêu đề (mô-típ `strokeDasharray="10,5"`
   của DataV borderBox13).

   Lý do bỏ chứ không chỉnh chỗ: mẩu gạch đứt thả lơ lửng cạnh dòng mô tả, không
   bám vào mép nào và không cùng lưới với thứ gì — đọc ra RÁC RENDER chứ không
   ra chi tiết, đúng nhận xét "chưa tinh tế". Khối tiêu đề đã có MỘT nét dẫn là
   gạch dọc bên trái; thế là đủ.
   Muốn dựng lại thì phải cho nó bám vào một mép thật, đừng thả giữa khoảng
   trắng. */

/* --- Nút mạng xã hội: mũi tên hiện ở mép phải + icon nảy nhẹ --- */
.ant-btn-link {
    transform: translateY(0);
    transition: color .2s ease, background-color .2s ease, border-color .2s ease,
                box-shadow .2s ease, transform .2s ease;
}

    .ant-btn-link::after {
        content: "";
        position: absolute;
        right: 14px;
        top: 50%;
        width: 9px;
        height: 9px;
        margin-top: -4.5px;
        background-color: currentColor;
        opacity: 0;
        transform: translateX(-6px);
        transition: opacity .2s ease-out, transform .2s ease-out;
        pointer-events: none;
        -webkit-mask-repeat: no-repeat;
        mask-repeat: no-repeat;
        -webkit-mask-position: center;
        mask-position: center;
        -webkit-mask-size: contain;
        mask-size: contain;
        -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='m9 5 7 7-7 7'/></svg>");
        mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='m9 5 7 7-7 7'/></svg>");
    }

    .ant-btn-link:hover {
        transform: translateY(-1px);
    }

        .ant-btn-link:hover::after {
            opacity: .85;
            transform: none;
        }

    .ant-btn-link .btn-icon img {
        transition: transform .22s cubic-bezier(.34, 1.4, .64, 1);
    }

    .ant-btn-link:hover .btn-icon img {
        transform: scale(1.12);
    }

/* Nút đang gửi: mũi tên phải tắt, nếu không nó chạy song song với spinner
   và đọc ra hai tín hiệu trái ngược (đang chờ / mời bấm tiếp). */
.ant-btn-link.is-loading::after {
    opacity: 0;
}

/* --- Checkbox: ô nảy nhẹ khi tick --- */
.ant-checkbox-inner {
    transition: background-color .18s ease-out, border-color .18s ease-out,
                box-shadow .18s ease-out, transform .18s cubic-bezier(.34, 1.4, .64, 1);
}

.ant-checkbox-checked .ant-checkbox-inner {
    transform: scale(1.06);
}

.ant-checkbox-wrapper {
    transition: color .18s ease-out;
}

    .ant-checkbox-wrapper:hover {
        color: var(--fg-3);
    }

/* --- Divider: hai viên kim cương nhỏ kẹp chữ "Hoặc" --- */
.ant-divider-inner-text {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 9px;
}

    .ant-divider-inner-text::before,
    .ant-divider-inner-text::after {
        content: "";
        width: 4px;
        height: 4px;
        background: var(--border-2);
        transform: rotate(45deg);
    }

/* --- Khối thông báo: dấu góc ở mép phải-trên, đồng bộ ngôn ngữ HUD --- */
.auth-note {
    position: relative;
    overflow: hidden;
}

    .auth-note::after {
        content: "";
        position: absolute;
        top: 6px;
        right: 6px;
        width: 8px;
        height: 8px;
        border-top: 1px solid currentColor;
        border-right: 1px solid currentColor;
        opacity: .35;
    }

/* --- Panel: ánh kính góc trên-trái + vạch nhấn đáy phải (đối xứng vạch đỉnh) --- */
.auth-panel-inner::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1;
    width: 62%;
    height: 46%;
    pointer-events: none;
    background: radial-gradient(ellipse at 0% 0%, rgba(255, 255, 255, .07), transparent 70%);
}

.auth-panel-inner::after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 40%;
    right: 8%;
    z-index: 3;
    height: 1px;
    pointer-events: none;
    background: linear-gradient(90deg,
        transparent 0%,
        rgba(45, 212, 191, .55) 35%,
        rgba(45, 212, 191, .55) 70%,
        transparent 100%);
}

/* --- Thanh đo mật khẩu: vạch bật lên có độ nảy --- */
.pw-meter-seg {
    transform-origin: center bottom;
    transition: background-color .22s ease-out, box-shadow .22s ease-out,
                transform .22s cubic-bezier(.34, 1.4, .64, 1);
}

    .pw-meter-seg.is-on {
        transform: scaleY(1.6);
    }

.pw-meter-label {
    transition: color .22s ease-out;
}

/* --- Ngoặc góc nở ra khi con trỏ vào thẻ, và nở THÊM nữa khi đang gõ trong
       form. Ba mức 28 → 36 → 42 đọc ra một thang trạng thái: nghỉ / đang nhìn /
       đang làm. Đây là chỗ dùng lại đúng ý "focus thì ngoặc SÁNG LÊN" của
       vci-sun-mobile, chỉ đổi kênh từ độ đục sang kích thước vì ở đây ngoặc vốn
       đã sáng sẵn.
       ⚠️ `transition` khai ở chính `.hud-bracket` (mục 5) nên cả ba mức đều
       chạy mượt theo cùng một nhịp. --- */
.auth-panel:hover .hud-bracket {
    width: 36px;
    height: 36px;
}

.auth-panel:focus-within .hud-bracket {
    width: 42px;
    height: 42px;
}

/* Ray dọc đậm lên theo — nếu chỉ ngoặc đổi mà ray đứng yên thì khung đọc ra
   như bị đứt ở đoạn giữa. */
.hud-rail {
    opacity: .7;
    transition: opacity .3s var(--ease-out);
}

.auth-panel:hover .hud-rail,
.auth-panel:focus-within .hud-rail {
    opacity: 1;
}

/* --- Góc màn hình: nở ra khi trang vừa tải xong (một lần, không lặp) --- */
@media (prefers-reduced-motion: no-preference) {
    .screen-corner {
        opacity: 0;
        animation: hudTickIn .5s cubic-bezier(.22, .61, .36, 1) .35s both;
    }

}

/* =============================================================
   13. Chuyển động
   ============================================================= */

@keyframes authSpin {
    to { transform: rotate(360deg); }
}

/* Lưới phối cảnh chạy về phía người xem. Bước bằng ĐÚNG một ô lưới (46px) nên
   vòng lặp khép kín — lệch một pixel là thấy lưới "giật" mỗi lần lặp lại. */
@keyframes hudFloorRun {
    to { background-position: 0 46px, 0 0; }
}

/* Xung sáng chạy qua vạch ngăn của cột trái. Dừng hẳn ở hai đầu (ease-in-out
   + quãng nghỉ trong keyframe) để không thành đèn chạy liên tục. */
@keyframes dividerSweep {
    0%        { transform: translateX(-40%); opacity: 0; }
    18%, 62%  { opacity: 1; }
    80%, 100% { transform: translateX(300%); opacity: 0; }
}

@keyframes hudTwinkle {
    0%, 100% { opacity: .38; }
    50%      { opacity: .70; }
}

@keyframes hudPulse {
    0%, 100% { opacity: 1;  transform: scale(1); }
    50%      { opacity: .45; transform: scale(.82); }
}

@keyframes hudRing {
    to { --hud-angle: 360deg; }
}

@keyframes hudScanX {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(320%); }
}

@keyframes hudSheen {
    to { transform: translateX(320%) skewX(-18deg); }
}

/* Vòng sóng lan từ chấm trạng thái — mượn keyframe `radar-ring` của
   vci-sun-pms-fe (src/styles/globals.css) để hai sản phẩm cùng một nhịp. */
/* Vệt quét chạy xuống qua chồng lớp rồi quay lại. `translateY` tính theo
   chiều cao CHÍNH phần tử, mà phần tử cao đúng 100% khung chứa -> quãng đường
   co giãn cùng khối, không cần một con số px nào. */
/* --- NHỊP THỞ của 4 ngoặc góc.
   Chu kỳ 4,2s — CỐ Ý rất chậm. Bảng điều khiển thật không nhấp nháy; nó chỉ
   "thở". Dưới ~3s là mắt bắt được nhịp và nó thành đèn báo, gây phân tâm khỏi
   form. Biên độ opacity .72 -> 1 cũng cố ý hẹp vì lý do đó.
   ⛔ Chỉ động vào `opacity` (luật §12b cho hiệu ứng lặp vô hạn): đây là 4 phần
   tử có `filter: drop-shadow`, đụng vào `transform`/kích thước là bắt trình
   duyệt vẽ lại quầng sáng 60 lần/giây. --- */
@keyframes hudBreathe {
    0%, 100% { opacity: .72; }
    50%      { opacity: 1; }
}

/* --- Quầng nền dưới chồng lớp thở nhẹ. Đây là chuyển động lặp DUY NHẤT còn
   lại của sơ đồ, ngoài hai xung chạy trên trụ.

   ⛔ THAY CHO `isoFloat` cũ (cả sơ đồ trôi lên xuống 5px/6s) — ĐÃ BỎ HẲN, và
   đây là số đo trên chính trang này, không phải cảm tính:
     · 5px trong 3s = 0,028px mỗi khung hình ở 60Hz. Đo thật: 197/199 khung có
       |dy| < 0,1px. Ở tốc độ đó mắt KHÔNG đọc ra chuyển động nào.
     · Nhưng khối bị trôi là khối đầy nét 1px (viền + lưới của ba mặt phẳng, hai
       trụ) và ba nhãn chữ 10px. Trình duyệt hợp thành lớp MỘT LẦN rồi vẽ lại ở
       toạ độ LẺ pixel, nên mọi nét 1px bị nội suy lại từng khung: đo năng lượng
       biên trên ảnh chụp thật, lệch 0,0px cho 2,32 — lệch 0,5px chỉ còn 1,84.
       Tức CẢ HÌNH nhoè đi 20% rồi nét lại, hai lần cho mỗi pixel đi được,
       ≈ 3,3 lần/giây. Đó đúng là cái "khá là giật" mà chủ dự án nhìn thấy.
     · ⚠️ Không có biên độ nào cứu được: muốn "trôi chậm" thì bước mỗi khung
       BẮT BUỘC dưới 1px, tức luôn đi qua các pha lẻ pixel. Tăng biên độ chỉ
       thêm chuyển động ĐÈ LÊN chỗ nhoè, không dọn được nó. Cách duy nhất là
       ĐỪNG TỊNH TIẾN khối chứa nét mảnh và chữ.

   ⛔ Và cũng ĐỪNG chuyển nhịp này sang ba mặt phẳng. Đã thử: `opacity` không
   nội suy hình học thật, nhưng việc CÓ animation lại ép mặt phẳng thành lớp
   hợp thành riêng, và khi đó `rotate(45deg)` của nó bị áp lúc vẽ thay vì lúc
   raster — nét 1px mềm đi vĩnh viễn, đo được 2,2422 -> 1,9902 (mất 11%).
   Quầng nền không dính lỗi đó vì nó chỉ có gradient mềm: tách lớp rồi đo lại
   vẫn 2,2967, tức KHÔNG mất gì. Xem chú thích ở `.iso-glow` (§4c).

   Chu kỳ 7,2s = đúng HAI chu kỳ xung trên trụ (`isoRise` 3,6s), cố ý: cả hình
   chỉ có MỘT nhịp tim, không phải hai chu kỳ vênh nhau đập vào nhau.
   Mở ở opacity 1 — đúng giá trị tĩnh của phần tử — nên không có bước nhảy nào
   lúc animation khởi động, và người bật "giảm chuyển động" (animation bị tắt)
   thấy đúng trạng thái SÁNG NHẤT chứ không phải một trạng thái nửa vời. --- */
@keyframes isoGlowBreathe {
    0%, 100% { opacity: 1; }
    50%      { opacity: .55; }
}

/* --- Đèn chạy qua ba thẻ phân hệ (xem `.brand-modules b::after`).

   MỌI MỐC ĐỀU LÀ 10,5s QUY RA PHẦN TRĂM, đừng chỉnh lẻ từng số:
     · 0 -> 4,8%      : sáng lên trong 0,5s
     · 4,8% -> 33,33% : giữ sáng (33,33% = đúng 1/3 chu kỳ = 3,5s)
     · 33,33% -> 38,1%: tối đi trong 0,5s
     · 38,1% -> 100%  : tắt, chờ hai thẻ kia
   Thẻ kế bắt đầu sáng ở đúng 33,33% (trễ 3,5s), tức nó dâng lên trong khi thẻ
   này lịm xuống — giao nhau 0,5s. Bỏ phần giao đó đi thì có nửa giây cả ba thẻ
   cùng tối, và cụm đọc ra như bị nháy chứ không ra "đèn chuyền".

   ⚠️ Đường cong khai TRONG keyframe vì mỗi chặng cần một đường riêng: chặng
   sáng lên đi nhanh rồi dừng êm (vật tới), chặng tối đi thì ngược lại (vật rời
   đi). Khai `animation-timing-function` ở rule thì cả hai chặng dùng chung một
   đường và cú chuyền đọc ra đều đều như đèn báo, không ra hơi thở. */
@keyframes modFlare {
    0%     { opacity: 0; animation-timing-function: cubic-bezier(.33, 0, .2, 1); }
    4.8%   { opacity: 1; }
    33.33% { opacity: 1; animation-timing-function: cubic-bezier(.4, 0, .7, 1); }
    38.1%  { opacity: 0; }
    100%   { opacity: 0; }
}

@keyframes isoRise {
    0%   { transform: translateY(194%); opacity: 0; }
    18%  { opacity: 1; }
    82%  { opacity: 1; }
    100% { transform: translateY(0);    opacity: 0; }
}

/* Dấu góc bật ra khi trang vừa tải */
@keyframes hudTickIn {
    from { opacity: 0; transform: scale(.55); }
    to   { opacity: 1; transform: none; }
}

/* Hàng trong panel + khối trong khu thông tin: TRỒI LÊN.
   14px là bản quy về web của 25dp mà mobile dùng — màn to hơn nhưng quãng trượt
   ngắn hơn, vì trên web nhiều hàng chạy cùng lúc trong tầm nhìn. */
/* --- ⛔ VÀO THEO TRỤC NGANG, KHÔNG PHẢI TRỤC DỌC (đổi 2026-08-23).

   Bản trước là `authRise` — mọi thứ trồi lên từ DƯỚI (`translateY(14px)`).
   Nay đổi sang trượt NGANG cho khớp với hiệu ứng chuyển màn Đăng nhập ⇄ Đăng ký
   (§13b cũng trượt ngang): hai chuyển động trên cùng một trang mà đi hai trục
   khác nhau thì đọc ra hai hệ chuyển động rời rạc.

   Hai chiều, chọn theo VỊ TRÍ của phần tử trong tấm thẻ:
     · khoang TRÁI  -> vào từ trái  (`authSlideL`)
     · khoang PHẢI  -> vào từ phải  (`authSlideR`)
   Mắt đọc ra "hai nửa nội dung chạy vào chỗ của mình". Đây KHÔNG mâu thuẫn với
   ghi chú "tấm vào làm một" ở dưới: cái vào một lượt là KHUNG; thứ trượt ngang
   là NỘI DUNG bên trong, và nó bị `overflow: hidden` của mặt kính cắt gọn nên
   đọc ra "chạy vào sau khung", không ra "tấm thẻ bị xé".

   Quãng 26px: đủ để nhận ra hướng, chưa tới mức phải chờ. --- */
@keyframes authSlideL {
    from { opacity: 0; transform: translateX(-26px); }
    to   { opacity: 1; transform: none; }
}

@keyframes authSlideR {
    from { opacity: 0; transform: translateX(26px); }
    to   { opacity: 1; transform: none; }
}

/* Riêng KHUNG vào bằng mờ + phóng rất nhẹ, KHÔNG trượt: khung là mốc đứng yên
   để mắt đo chuyển động của nội dung (cùng nguyên tắc với §13b). Khung mà cũng
   trượt thì không còn mốc nào, cả trang đọc ra "bị đẩy". */
@keyframes authShellIn {
    from { opacity: 0; transform: scale(.985); }
    to   { opacity: 1; transform: none; }
}

/*
   Hiệu ứng vào trang — CHỈ khai bên trong @media (prefers-reduced-motion:
   no-preference).

   Vì sao gói cả trạng thái ẩn (opacity:0) vào trong media query: dùng
   `animation-fill-mode: both` mà đặt opacity:0 ở ngoài thì người tắt hiệu ứng
   (hoặc trình duyệt không chạy animation) sẽ thấy TRANG TRẮNG — form vô hình.
   Gói vào đây thì nhóm đó không nhận rule nào cả, phần tử hiện bình thường.
*/
@media (prefers-reduced-motion: no-preference) {

    /* --- ⛔ TẤM VÀO LÀM MỘT, KHÔNG PHẢI HAI NỬA VÀO TỪ HAI PHÍA.

       Bản hai-panel cho cột trái trượt từ trái và cột phải trượt từ phải — hợp
       lý khi chúng là hai vật rời. Nay chúng là MỘT tấm: cho hai nửa đi ngược
       chiều nhau thì trong 0,5 giây đầu tấm thẻ đọc ra như bị XÉ ĐÔI rồi mới
       dính lại. Nên tấm vào một lượt, còn nhịp điệu dồn hết cho NỘI DUNG trồi
       lên so le bên trong.

       ⛔ Tấm có animation riêng, nội dung bên trong CŨNG có — nhưng nội dung chỉ
       chạy `opacity` từ 0, không lồng thêm một lớp fade nữa lên cả khối. Fade
       hai tầng làm mỗi phần tử mờ thêm một lần ngay sau khi vừa hiện xong
       (vci-sun-mobile đã dính lỗi này và đã bỏ). --- */
    .auth-shell {
        opacity: 0;
        animation: authShellIn .5s var(--ease-settle) both;
    }

    /* Khoang TRÁI: mọi thứ vào từ bên trái, so le theo thứ tự đọc. */
    .brand-logo     { opacity: 0; animation: authSlideL .42s var(--ease-settle) .16s both; }
    .brand-kicker   { opacity: 0; animation: authSlideL .34s var(--ease-out)   .24s both; }
    .brand-title    { opacity: 0; animation: authSlideL .42s var(--ease-settle) .30s both; }
    .brand-divider  { opacity: 0; animation: authSlideL .32s var(--ease-out)   .38s both; }
    .brand-desc     { opacity: 0; animation: authSlideL .34s var(--ease-out)   .44s both; }
    /* ⛔ CHỈ CÒN MỘT animation ở đây. Trước là hai: `authSlideL` (vào màn) nối
       đuôi `isoFloat` (cả sơ đồ trôi vô hạn theo trục Y). `isoFloat` ĐÃ BỎ vì
       nó chính là nguồn giật — nó tịnh tiến một khối đầy nét 1px và chữ 10px
       ở 0,028px/khung, tức không ra chuyển động mà chỉ làm mọi nét nhoè rồi
       nét lại ~3,3 lần/giây. Toàn bộ số đo ghi ở `@keyframes isoGlowBreathe`.
       ⛔ ĐỪNG gắn lại bất kỳ animation LẶP VÔ HẠN nào lên phần tử này — cả
       `transform` lẫn `opacity`: nó là hộp chứa cả ba mặt phẳng lưới lẫn ba
       nhãn chữ, nên mọi phép tịnh tiến dưới 1px/khung đều trả về đúng lỗi cũ,
       còn `opacity` ở đây thì kéo mờ luôn ba nhãn. "Sự sống" của sơ đồ nay nằm
       ở nhịp thở `opacity` của quầng nền `.iso-glow` (§4c) + hai xung chạy
       trên trụ — hai thứ toàn gradient mềm, động thế nào cũng không nhoè.

       ⚠️ Nhân tiện, giả thiết "bàn giao lúc 1s làm sơ đồ nhảy một cái" là SAI,
       đã đo: `authSlideL` kết ở `transform: none` còn `isoFloat` mở ở
       `translateY(0)` — hai giá trị bằng nhau, bước nhảy lớn nhất đo được trong
       cả 2,2s đầu là 0,043px. Khoảng hở 40ms không cứu gì cả; nó vô hại nhưng
       cũng vô ích. Ghi lại để lần sau đừng đi tìm lỗi ở chỗ này nữa. */
    .brand-canvas   {
        opacity: 0;
        animation: authSlideL .48s var(--ease-settle) .48s both;
    }
    .brand-modules  { opacity: 0; animation: authSlideL .34s var(--ease-out)   .54s both; }

    /* --- Cột phải: panel + các hàng trong panel ---
       `auth-stagger.ts` của mobile: BASE_DELAY 300 · STEP 60 · DURATION 340.
       ⚠️ STEP 60 là ngưỡng đọc được: dưới ~40 mắt gộp thành "hiện cùng lúc"
       (mất hẳn hiệu ứng), trên ~90 thì hàng cuối tới quá muộn — form Đăng ký có
       6 hàng, 90×5 = 450ms trễ cho hàng chót, người dùng đã kịp sốt ruột. */

    /* Khoang PHẢI: mọi thứ vào từ bên phải. Khối tiêu đề đi TRƯỚC các hàng —
       trước đây nó không có hiệu ứng nào nên nó đứng sẵn ở đó trong khi phần
       dưới còn đang chạy vào, đọc ra như hiệu ứng bị sót một mảng. */
    .auth-panel-inner .form-title-wrapper {
        opacity: 0;
        animation: authSlideR .38s var(--ease-settle) .22s both;
    }

    .auth-panel-inner .ant-form-item,
    .auth-panel-inner .ant-divider,
    .auth-panel-inner .social-btn-group,
    .auth-panel-inner .auth-alt-action {
        opacity: 0;
        animation: authSlideR .34s var(--ease-out) both;
    }

    .auth-panel-inner .ant-form-item:nth-of-type(1) { animation-delay: .30s; }
    .auth-panel-inner .ant-form-item:nth-of-type(2) { animation-delay: .36s; }
    .auth-panel-inner .ant-form-item:nth-of-type(3) { animation-delay: .42s; }
    .auth-panel-inner .ant-form-item:nth-of-type(4) { animation-delay: .48s; }
    .auth-panel-inner .ant-form-item:nth-of-type(5) { animation-delay: .54s; }
    .auth-panel-inner .ant-form-item:nth-of-type(6) { animation-delay: .60s; }
    .auth-panel-inner .ant-divider                  { animation-delay: .62s; }
    .auth-panel-inner .social-btn-group             { animation-delay: .68s; }
    .auth-panel-inner .auth-alt-action              { animation-delay: .74s; }
}

/* Người đã tắt hiệu ứng: dừng cả animation chạy vòng lặp của nền/HUD.
   Riêng spinner giữ lại (nó là chỉ báo trạng thái, mất đi là mất thông tin)
   nhưng quay chậm hẳn. */
@media (prefers-reduced-motion: reduce) {
    .auth-bg-floor,
    .auth-bg-ceil,
    .auth-bg-dots,
    .brand-divider::after,
    .auth-shell-ring,
    .iso-risers::before,
    .iso-risers::after,
    /* Quầng nền: `isoGlowBreathe` tắt là nó về đúng opacity 1 của thiết kế
       tĩnh — không cần ép `opacity` như `.hud-bracket` bên dưới, vì giá trị
       nghỉ của nó vốn đã là 1, .55 chỉ tồn tại giữa chu kỳ. */
    .iso-glow,
    .hud-bracket,
    .field-scan::before,
    .ant-btn-primary::after {
        animation: none !important;
    }

    /* Ngoặc góc đứng yên phải giữ độ đục ĐẦY, không phải mức .72 giữa nhịp thở */
    .hud-bracket {
        opacity: 1 !important;
    }

    /* Xung sáng đứng yên đọng thành một vệt sáng lệch giữa vạch ngăn — bỏ hẳn,
       vạch nền và viên kim cương vẫn còn nên hình không mất. */
    .brand-divider::after {
        display: none;
    }

    .auth-spinner,
    .ant-btn.is-loading::before,
    .ant-btn-link.is-loading::before {
        animation-duration: 1.6s;
    }
}

/* =============================================================
   13b. CHUYỂN MÀN ĐĂNG NHẬP ⇄ ĐĂNG KÝ

   Hai màn này là HAI TRANG do server dựng, không phải hai tab trong một trang —
   nên không có cách nào cho panel "trượt" bằng CSS thường: trình duyệt xoá sạch
   trang cũ trước khi vẽ trang mới.

   `@view-transition` là API sinh ra đúng cho ca này: trình duyệt tự chụp trang
   cũ và trang mới rồi nội suy giữa hai ảnh chụp. Panel form mang cùng một
   `view-transition-name` ở CẢ HAI trang, nên nó được nhận là CÙNG MỘT vật thể
   và tự biến hình từ khung Đăng nhập sang khung Đăng ký (cao hơn, nhiều ô hơn)
   thay vì biến mất rồi hiện lại.

   ⛔ ĐÚNG MỘT TÊN — `auth-form`. Bản trước có SÁU. Đừng thêm lại.

   Cơ chế lấy từ bài `homework/Day_13/ex01.css`: hai khối `.box-login` /
   `.box-register` xếp CHỒNG trong cùng một khung, khung đứng yên, chỉ RUỘT
   trượt ngang + mờ (`transform: translate(20%)` + `opacity`, `.3s ease-in-out`).
   Cái làm hiệu ứng đó "đọc được" chính là KHUNG KHÔNG ĐỘNG — mắt có một mốc
   đứng yên để đo chuyển động của ruột.

   ⛔ VÌ SAO BỎ NĂM TÊN KIA (sửa 2026-08-24) — đọc trước khi "tối ưu" lại.

   Bản trước gắn tên cho `auth-shell` · `brand-head` · `brand-lead` ·
   `brand-canvas` · `brand-modules` · `auth-form`. Đã đếm trong lúc chuyển thật:
   BẢY nhóm chuyển cảnh chạy đồng thời (sáu tên trên + `root`).

   Vấn đề nằm ở chỗ các nhóm chuyển cảnh KHÔNG LỒNG NHAU. Dù `.brand-lead` nằm
   trong `.auth-shell` trên DOM, hai ảnh chụp của chúng là hai anh em ngang hàng
   trong lớp phủ chuyển cảnh, mỗi cái định vị tuyệt đối theo toạ độ màn hình và
   nổi trên mọi thứ. Hệ quả:
     · tấm thẻ bị "khoét lỗ" đúng chỗ các khối con, rồi các khối con bay riêng;
     · ảnh chụp tấm thẻ bị kéo giãn dọc khi tấm đổi cỡ (xem `.auth-shell`);
     · ruột form trượt ra NGOÀI khung kính, vì không còn `overflow: hidden` nào
       cắt nó — đo được: trượt 14% × 436px = 61px, mà mép phải khung chỉ cách
       28px, tức lòi ra 33px giữa nền trang.
   Cộng lại, mắt đọc ra tấm thẻ vỡ thành nhiều mảnh rồi lắp lại — đúng thứ chủ
   dự án gọi là "lỗi hiển thị khi chuyển giữa hai form".

   Nay: tấm thẻ bị ép `min-height` nên KHÔNG đổi cỡ giữa hai trang, mà đã không
   đổi cỡ thì không cần tên. Bỏ hết tên đi, chỉ giữ `auth-form`. Khi đó ảnh chụp
   `root` của hai trang GIỐNG HỆT nhau ở mọi chỗ trừ vùng form, nên cú mờ chéo
   của `root` gần như vô hình, và thứ duy nhất người dùng thấy động là ruột
   form. Đó chính xác là Day_13: khung đứng yên, ruột trượt.

   ⚠️ Phần tử mang tên bị gỡ khỏi ảnh chụp của tổ tiên, nhưng chỉ gỡ PHẦN NÓ TỰ
   VẼ. Nền kính, viền, ngoặc góc phía sau form vẫn nằm trong ảnh `root` — nên
   không hề có "lỗ thủng" nào ở chỗ form.

   ⚠️ Chỉ chạy khi hai trang CÙNG NGUỒN GỐC và điều hướng bằng link thường —
   đúng trường hợp ở đây (`<a asp-action="Register">`).
   ⚠️ Firefox chưa hỗ trợ view transition xuyên trang. Trước đây chỗ này ghi
   "suy giảm êm, không cần bản dự phòng" — KHÔNG ĐÚNG NỮA: nay hiệu ứng chuyển
   màn là thứ được yêu cầu, nên có bản dự phòng chạy bằng JS ở `auth-forms.js`
   (`initPageSwap`). Nó tự TẮT khi trình duyệt có `startViewTransition`, để hai
   cơ chế không chạy chồng lên nhau.
   ⚠️ Tên phải DUY NHẤT trên mỗi trang. Mỗi trang chỉ có một `.auth-shell` và
   một `.auth-panel-body` nên an toàn — đừng gắn tên này cho phần tử lặp lại.
   ============================================================= */

@view-transition {
    navigation: auto;
}

/* ⛔ TẮT HIỆU ỨNG VÀO TRANG KHI TỚI BẰNG CHUYỂN MÀN.

   Lý do phải tắt (vẫn đúng như ghi chú cũ): với view transition XUYÊN TRANG,
   `::view-transition-new(...)` không phải ảnh chụp chết mà là nội dung SỐNG của
   trang mới. Trang mới vừa bị view transition trượt vào, vừa TỰ chạy hiệu ứng
   vào màn của §13 (mỗi hàng trượt 26px, so le tới .74s). Hai lớp chuyển động
   chồng lên nhau, mỗi lớp một hướng một nhịp -> mắt đọc ra rung.

   ⛔ NHƯNG `:root:active-view-transition` LÀ CÁCH TẮT SAI — và chính nó là thủ
   phạm to nhất của "lỗi hiển thị khi chuyển giữa hai form". ĐỪNG QUAY LẠI.

   `animation: none` không phải "bỏ qua animation", nó là "XOÁ animation". Bộ
   chọn `:active-view-transition` chỉ khớp TRONG LÚC chuyển; hễ cú chuyển kết
   thúc là khai báo rụng ra, animation được TẠO LẠI TỪ ĐẦU và chạy lại từ mốc 0.

   Đã đo bằng `document.getAnimations()` trong một cú chuyển thật:
     · 0 -> ~750ms : `:active-view-transition` bật, các nhóm chuyển cảnh chạy;
     · ~1000ms     : cú chuyển vừa xong -> 18 animation vào màn cùng hiện ra ở
                     `currentTime = 0ms, playState: running`;
     · ~1000 -> 2100ms : cả trang trượt vào LẦN THỨ HAI, so le tới 1080ms.
   Tức người dùng xem một cú chuyển 0,5s rồi bị xem thêm 1,1s cả trang tự dựng
   lại. Đó không phải "rung", đó là chạy hai lần.

   Cách đúng: một cái CỜ KHÔNG BAO GIỜ TẮT. `pagereveal` bắn trên trang MỚI
   trước khung hình đầu tiên và mang theo `event.viewTransition` nếu lần điều
   hướng này có chuyển màn; `auth-forms.js` bắt nó và gắn `.vt-nav` lên <html>.
   Class đó ở lại vĩnh viễn trong vòng đời trang, nên animation bị xoá là xoá
   hẳn — không có thời điểm nào để nó hồi sinh.

   ⚠️ Cờ này CHỈ gắn khi thực sự có chuyển màn. Vào thẳng bằng URL, F5, hay
   trình duyệt không hỗ trợ đều không có `.vt-nav`, nên hiệu ứng vào màn của §13
   vẫn chạy bình thường — nó chỉ nhường chỗ khi đã có cú chuyển làm thay việc. */
:root.vt-nav .auth-shell,
:root.vt-nav .brand-logo,
:root.vt-nav .brand-kicker,
:root.vt-nav .brand-title,
:root.vt-nav .brand-divider,
:root.vt-nav .brand-desc,
:root.vt-nav .brand-modules,
:root.vt-nav .auth-panel-inner .form-title-wrapper,
:root.vt-nav .auth-panel-inner .ant-form-item,
:root.vt-nav .auth-panel-inner .ant-divider,
:root.vt-nav .auth-panel-inner .social-btn-group,
:root.vt-nav .auth-panel-inner .auth-alt-action,
/* ⚠️ `.brand-canvas` NẰM THẲNG TRONG DANH SÁCH NÀY, không cần rule riêng.
   Bản đầu của khối này tách nó ra vì hồi đó nó mang HAI animation (vào màn +
   `isoFloat` trôi vô hạn), nên `animation: none` sẽ giết luôn cái thứ hai.
   `isoFloat` nay đã bỏ hẳn — sự sống của sơ đồ chuyển sang `.iso-glow`
   (`isoGlowBreathe`) và `.iso-risers::before/::after` (`isoRise`), là những
   phần tử KHÁC và không có tên trong danh sách này. Nên `.brand-canvas` giờ chỉ
   còn đúng animation vào màn, tức tắt thẳng là đúng, và sơ đồ vẫn thở bình
   thường sau khi đổi màn. */
:root.vt-nav .brand-canvas {
    animation: none !important;
    opacity: 1 !important;
}

@media (prefers-reduced-motion: no-preference) {
    /* Nền và 4 góc màn GIỮ NGUYÊN qua hai trang, chỉ mờ chéo rất nhanh — để
       mặc định thì cả trang trượt theo và người dùng đọc ra "tải lại trang",
       đúng thứ đang muốn giấu đi. */
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation-duration: .16s;
    }

    /* ⛔ ĐÃ BỎ TOÀN BỘ RULE CHO `auth-shell` VÀ `brand-*`.
       Không còn phần tử nào mang những tên đó nữa (xem `.auth-shell`,
       `.brand-head`) nên các nhóm này KHÔNG TỒN TẠI. Giữ lại rule chỉ để lại
       một bãi bộ chọn chết, và tệ hơn là gợi ý cho người sau rằng gắn tên lại
       là chuyện bình thường. Khung nay đứng yên vì nó KHÔNG ĐỔI CỠ, không phải
       vì được nội suy khéo. */

    /* --- RUỘT FORM: trượt ngang + mờ. Công thức Day_13. Đây là vật thể DUY
       NHẤT được phép động trong cú chuyển.

       ⚠️ Quãng trượt khai bằng `%` chứ không `px`: `%` trong `translateX` tính
       theo bề rộng CHÍNH ảnh chụp, nên quãng trượt tự co giãn khi khoang form
       đổi bề rộng (492px ở desktop, ~100% ở màn hẹp).

       ⛔ 7%, KHÔNG PHẢI 14% — đây là số đo, đừng nới ra cho "đã mắt".
       Ảnh chụp của nhóm chuyển cảnh nổi TRÊN lớp phủ, nên nó KHÔNG bị
       `overflow: hidden` của mặt kính cắt. Đã đo ở 1440×900: ruột form rộng
       436px, mép phải của nó cách mép phải khung kính đúng 28px. Trượt 14% =
       61px, tức lòi ra khỏi khung 33px và trôi giữa nền trang — đúng một phần
       của "lỗi hiển thị khi chuyển giữa hai form". 7% = 30,5px, vừa đủ nằm
       trong lòng khung. Muốn cú trượt "đã" hơn thì tăng độ mờ và chỉnh đường
       cong, đừng tăng quãng đường.

       ⚠️ Ra và vào CÙNG MỘT HƯỚNG (+7%), đúng như Day_13. Cho hai hướng ngược
       nhau thì đẹp ở một chiều nhưng SAI ở chiều còn lại (Đăng ký -> Đăng nhập
       sẽ trôi ngược trực giác), vì trang server không biết mình đang đi tới hay
       lùi.

       ⛔ NHỊP: RA rồi mới VÀO, chứ không chạy chồng nhau.
       Bản trước cho cả hai cùng bắt đầu ở t=0 (.30 và .42) nên suốt 0,30s đầu
       hai bản form nằm đè lên nhau, cùng trôi một hướng — đọc ra bóng ma nhoè
       chứ không ra đổi nội dung. Nay bản cũ đi trong .22s rồi bản mới mới vào ở
       .14s: chỉ còn 0,08s giao nhau, đủ để không hẫng mà không kịp thành bóng.

       ⛔ ĐƯỜNG CONG khác nhau cho hai chiều — đây là chỗ "mượt" đến từ:
         · RA  dùng ease-in  (chậm rồi nhanh dần) — vật đang rời đi thì phải
           TĂNG tốc, mắt mới đọc ra "nó đi mất" thay vì "nó bị kéo lê".
         · VÀO dùng ease-settle (vọt tới rồi dừng êm) — vật đang tới phải GIẢM
           tốc để đặt xuống, đó là toàn bộ cảm giác "mượt".
       Bản trước dùng `ease-in-out` cho cả hai: chiều nào cũng khởi động chậm,
       nên cú chuyển đọc ra ì.

       `scale` rất nhẹ (.985) cộng thêm chiều sâu; lớn hơn là thành hiệu ứng
       "pop" rẻ tiền. --- */
    ::view-transition-old(auth-form) {
        animation: vtFormOut .22s cubic-bezier(.4, 0, 1, 1) both;
    }

    ::view-transition-new(auth-form) {
        animation: vtFormIn .40s var(--ease-settle) .14s both;
    }
}

/* Người đã tắt hiệu ứng: bỏ hẳn phần chuyển màn, đổi trang thẳng. Không khai
   thì trình duyệt vẫn chạy phép mờ chéo mặc định. */
@media (prefers-reduced-motion: reduce) {
    ::view-transition-group(auth-form),
    ::view-transition-old(auth-form),
    ::view-transition-new(auth-form),
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation: none !important;
    }
}

/* ⚠️ ĐỘ MỜ TẮT SỚM HƠN CHUYỂN ĐỘNG (mốc 62% / 42%), không đi tuyến tính.
   Ruột form là một khối chữ đặc; nếu độ mờ giảm đều tay thì ở nửa cuối quãng
   trượt nó vẫn còn đọc được ~40% và mắt bám theo được từng chữ — cú chuyển đọc
   ra "trang bị kéo lê". Cho chữ tan gần hết khi mới đi được nửa đường thì phần
   đuôi chuyển động chỉ còn là một vệt mờ, và mắt đọc ra "nội dung đổi" chứ
   không ra "nội dung bị dời chỗ". Cùng lý do cho chiều vào, đảo lại. */
@keyframes vtFormOut {
    from { opacity: 1; transform: translateX(0) scale(1); }
    62%  { opacity: .12; }
    to   { opacity: 0; transform: translateX(7%) scale(.99); }
}

@keyframes vtFormIn {
    from { opacity: 0; transform: translateX(7%) scale(.99); }
    42%  { opacity: .9; }
    to   { opacity: 1; transform: translateX(0) scale(1); }
}

/* --- BẢN DỰ PHÒNG cho trình duyệt không có View Transitions (Firefox).
   `initPageSwap` trong `auth-forms.js` gắn class này rồi mới điều hướng, nên
   người dùng vẫn thấy đúng cú trượt ra. Cú trượt VÀO thì trang mới tự lo bằng
   hiệu ứng vào màn ở §13.
   Nhịp .26s phải KHỚP với hằng `SWAP_MS` trong `auth-forms.js` — lệch thì hoặc
   trang nhảy khi chưa trượt xong, hoặc đứng chờ một quãng trắng. --- */
.auth-panel-body.is-swapping {
    opacity: 0;
    /* Giữ ĐÚNG 7% như `vtFormOut` — hai đường (có/không View Transitions) phải
       cho ra cùng một cảm giác, lệch số là hai trình duyệt hai hiệu ứng. */
    transform: translateX(7%) scale(.99);
    /* ease-in (chậm rồi nhanh dần) chứ không `ease-in-out`: vật đang rời đi phải
       TĂNG tốc thì mắt mới đọc ra "nó đi mất". Cùng đường cong với
       `::view-transition-old(auth-form)` để hai đường (có/không View
       Transitions) cho ra cùng một cảm giác.
       Độ mờ đi NHANH hơn chuyển động (.20 vs .26): form biến mất sớm hơn một
       nhịp nên quãng chờ tải trang phía sau đỡ lộ. */
    transition: opacity .20s cubic-bezier(.4, 0, 1, 1),
                transform .26s cubic-bezier(.4, 0, 1, 1);
}

/* =============================================================
   14. Responsive
   ============================================================= */

/* Laptop hẹp: thu bề rộng cột trái trước khi phải xếp dọc */
@media (max-width: 1180px) {
    .auth-layout {
        padding: 54px 22px;
    }

    /* Khoang form giữ nguyên 492px (độ dài dòng của form là bất khả xâm phạm),
       khoang giới thiệu nhường chỗ. Tấm thẻ vì thế hẹp dần chứ không xuống hàng. */
    .auth-brand {
        flex: 1 1 400px;
    }

    .brand-body {
        padding: 24px 24px;
        gap: 15px;
    }

    /* ⚠️ Lề trong của khoang trái tụt 44px -> 24px, nên vạch sáng dọc của khối
       tiêu đề phải thu theo: để nguyên 20px thì nó còn cách mép panel 4px, tức
       chồng đúng lên `.hud-inner-frame`. 12px chừa lại 12px hở. */
    .brand-titleblock {
        --tb-out: 12px;
    }

    /* Vạch chia dọc mép cần 11px ngoài panel; ở bề rộng này chỗ đó đã bị lề ăn
       mất nên vạch dính vào mép màn -> bỏ, khung vẫn đủ chi tiết. */
    .hud-ticks {
        display: none;
    }
}

@media (max-width: 900px) {
    .auth-shell {
        max-width: min(492px, 100%);
    }

    .auth-layout {
        padding: 54px 16px;
    }

    /* Hai khoang xếp DỌC — vẫn trong CÙNG một tấm, chỉ đổi trục. Đó là cái lợi
       của việc gộp khung: bản hai-panel phải đổi cả `gap` lẫn bề rộng từng
       panel, còn ở đây chỉ đổi `flex-direction` là xong. */
    .hud-panel-surface {
        flex-direction: column;
    }

    /* Nét ngăn xoay theo trục: dọc -> ngang. Cùng một phần tử, không thêm thẻ. */
    .auth-seam {
        flex: 0 0 1px;
        background: linear-gradient(90deg,
            transparent 0%,
            rgba(45, 212, 191, .30) 14%,
            rgba(45, 212, 191, .30) 86%,
            transparent 100%);
    }

    .auth-brand,
    .auth-panel-wrap {
        flex: 0 0 auto;
        width: 100%;
        max-width: 100%;
    }

    /* Màn hẹp: khoang trái rút còn LOGO + TIÊU ĐỀ nằm NGANG — đó là thứ nói
       người dùng đang ở đúng hệ thống nào. Bỏ vạch ngăn, câu mô tả, sơ đồ,
       danh sách phân hệ và dải đặc tả: chúng là nội dung giới thiệu, ở đây chỉ
       đẩy form xuống dưới nếp gấp.
       ⛔ Bản trước bỏ luôn CẢ `.brand-lead`, nên khoang trái chỉ còn cái logo
       trơ trọi — không còn dòng chữ nào nói đây là hệ thống gì. */
    .brand-body {
        flex-direction: row;
        align-items: center;
        gap: 14px;
        padding: 14px 16px;
    }

    .brand-head {
        flex: 0 0 auto;
    }

    .brand-logo {
        /* Logo nằm CÙNG HÀNG với `.brand-lead` nên bề ngang phải nhường chữ:
           56px chừa lại ~258px cho hai dòng chữ trên màn 360px. Là SVG nên
           không vỡ ở cỡ này; chữ "360" bên trong vòng thành vệt, chấp nhận —
           ở đây logo chỉ còn làm dấu nhận diện, phần chữ đã có `.brand-lead`. */
        width: 56px;
        height: 56px;
    }

    .brand-lead {
        /* ⚠️ SỬA BIẾN, ĐỪNG SỬA `gap`. `.brand-kicker` trừ lại đúng
           `--lead-gap` để siết khe hai bậc tiêu đề; ghi đè thẳng `gap` thì bên
           kia vẫn trừ 16px trong khi khe thật là 0 -> tiêu đề bị kéo chồng lên
           dòng kicker. */
        --lead-gap: 0px;
        min-width: 0;
        /* Hàng ngang: logo cạnh chữ -> bỏ margin âm dọc (xem chú thích gốc). */
        margin-top: 0;
    }

    /* Ở bậc này `.brand-body` xoay sang HÀNG NGANG (logo cạnh chữ), nên vạch
       sáng dọc của khối tiêu đề rơi đúng vào khe 14px giữa logo và chữ — nó
       thành nét ngăn logo/chữ, đúng việc cần ở đây. `--tb-out: 9px` để nét nằm
       giữa khe chứ không dính vào chữ. */
    .brand-titleblock {
        --tb-out: 9px;
    }

        .brand-titleblock::before {
            /* ⛔ KHÔNG khai lại `width`: token --tbar-w đã là 2px. Khai trùng ở
               đây thì lần sau đổi token, bậc này âm thầm giữ số cũ. */
            top: 1px;
            bottom: 1px;
        }

    .brand-kicker {
        margin-bottom: 1px;
        font-size: var(--text-caption-12);
        line-height: 16px;
        /* Hạ .18em -> .14em: ở cỡ chữ 12px thì .18em chỉ còn ~2,2px, nhưng chữ
           đơn cách cỡ nhỏ đã thưa sẵn nên vẫn thừa. */
        letter-spacing: .14em;
    }

    .brand-title {
        font-size: 22px;
        line-height: 27px;
        letter-spacing: -.02em;
    }

    .brand-divider,
    .brand-desc,
    .brand-canvas,
    .brand-modules {
        display: none;
    }

    .auth-panel-inner {
        padding: 22px 20px 24px;
    }

    .screen-corner {
        width: 16px;
        height: 16px;
    }
}

/* --- Màn THẤP: trang Đăng ký (4 ô + điều khoản + 2 nút mạng xã hội) cao ~880px.
   Khung nhìn của laptop 1080p sau khi trừ thanh trình duyệt chỉ còn ~930px, nên
   chỉ cần lề 58px×2 là panel vượt khung và sinh thanh cuộn — trên MÀN ĐĂNG NHẬP
   thì đó là lỗi nhìn thấy ngay.
   Thu lề trước, vì lề là thứ rẻ nhất để cắt. --- */
@media (max-height: 940px) {
    .auth-layout {
        padding: 46px 32px;
    }

    .brand-body {
        padding: 22px 32px 20px;
    }
}

/* --- Thấp hơn nữa (laptop 1366×768, cửa sổ không toàn màn hình).
   Thanh HUD dưới bắt đầu che chân panel -> bỏ (nó là trang trí, form là việc
   chính), rồi siết nhịp dọc của form.

   Trọng tâm là trang ĐĂNG KÝ: 4 ô + điều khoản + nút chính + 2 nút mạng xã hội,
   cao ~880px ở nhịp thường. Mấy chỗ cắt dưới đây lấy lại ~150px, đủ để nó vừa
   khung nhìn ~800px thay vì bắt người dùng cuộn ngay trên màn đăng nhập.
   ⛔ KHÔNG hạ chiều cao ô nhập/nút xuống dưới 38px: đó là sàn vùng bấm được. --- */
@media (max-height: 820px) {
    .auth-layout {
        padding: 34px 32px 22px;
    }

    .brand-lead {
        /* Sửa BIẾN, không sửa `gap` — lý do ở bậc ≤900px. */
        --lead-gap: 12px;
    }

    /* Màn thấp: siết lề trong và ô số của THẺ phân hệ, không siết chữ.
       ⛔ Sửa BIẾN chứ đừng sửa `padding-top`/`padding-bottom` như bản cũ: ô số
       định vị tuyệt đối theo `--mod-pad` và `--mod-sq`, đổi lề mà không đổi
       biến thì ô rời khỏi dòng chữ đầu. (Kiểm lại: top = 9 + (22 − 30)/2 = 5px,
       góc ô ở (14, 5) — tổng 19 > 13 nên vẫn nằm ngoài phần vát.) */
    .brand-modules li {
        --mod-pad: 9px;
        --mod-sq: 30px;
    }

        .brand-modules li::before {
            font-size: 13px;
        }

    .auth-panel-inner {
        padding: 18px 28px 20px;
    }

    .form-title-wrapper {
        margin-bottom: 15px !important;
    }

    .form-title,
    h1.form-title {
        font-size: 26px;
        line-height: 32px;
    }

    .ant-form-item {
        margin-bottom: 11px !important;
    }

    .ant-form-item-label > label,
    .form-item-label label,
    form label {
        margin-bottom: 4px;
    }

    .ant-input,
    input[type="text"],
    input[type="email"],
    input[type="password"] {
        height: 38px;
    }

    .ant-btn-lg {
        height: 40px;
    }

    .ant-divider {
        margin: 14px 0;
    }

    /* Hai nút mạng xã hội XẾP NGANG: lấy lại đúng một chiều cao nút + gap (~50px)
       mà không bỏ lựa chọn nào. Chỉ làm ở màn thấp — ở nhịp thường, nút xếp dọc
       đọc ra danh sách rõ hơn và có chỗ cho tên nhà cung cấp dài. */
    .social-btn-group {
        flex-direction: row;
        gap: 8px;
    }

        /* ⛔ Ẩn phần dẫn "Tiếp tục với", GIỮ tên nhà cung cấp.
           Bản trước để nguyên cả cụm rồi cắt gọn bằng ellipsis, ra
           "Tiếp tục với Goo…" — mất đúng cái từ mang thông tin và đọc ra như
           lỗi hiển thị. Bỏ phần dẫn thì nút còn "Google" / "Microsoft": ngắn,
           đủ nghĩa, và icon bên trái đã nói đây là nút đăng nhập. */
        .btn-label-lead {
            display: none;
        }

        .btn-label {
            min-width: 0;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
        }

        /* Mũi tên hover cần 20px bên phải; ở nửa bề ngang chỗ đó là của chữ. */
        .social-btn-group .ant-btn-link::after {
            display: none;
        }

    .auth-alt-action {
        margin-top: 12px;
        padding-top: 11px;
    }

    .pw-meter {
        margin-top: 5px;
    }

    /* Quả cầu hút chiều cao dôi — ở màn thấp KHÔNG còn dôi để hút, nên sàn của
       nó chỉ còn là thứ đẩy panel cao thêm. Hạ sàn xuống mức vẫn đọc ra hình. */
    /* Sơ đồ co lại thì ba nhãn (13px mỗi dòng, cần ~39px) không còn đủ chỗ
       giữa lớp đỉnh và lớp đáy -> chúng chồng lên nhau. Ẩn nhãn, giữ hình. */
    .brand-canvas {
        min-height: 118px;
    }

    .iso-tag {
        display: none;
    }
}

/* Màn rất thấp (laptop 768px cao, hoặc xoay ngang điện thoại):
   4 góc màn ăn vào lề của thẻ -> bỏ hẳn. */
@media (max-height: 620px) {
    .screen-corner {
        display: none;
    }

    .auth-layout {
        padding: 24px 16px;
    }
}

/* =============================================================
   15. Danh sách quyền (Consent / Device) và thẻ thông tin (Grants)
   Class .info-card đặt tên trung tính có chủ đích: trang chọn dự án
   ở phase sau dùng lại chính nó, không phải .grant-card.
   ============================================================= */

.consent-client-logo {
    margin-bottom: 12px;
}

    .consent-client-logo img {
        max-height: 44px;
        max-width: 160px;
    }

.scope-group {
    margin-bottom: 20px;
}

.scope-group-title {
    margin-bottom: 8px;
    font-size: var(--text-caption-12);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--fg-5);
}

.scope-list {
    margin: 0;
    padding: 0;
    list-style: none;
    border: 1px solid rgba(52, 86, 119, .85);
}

.scope-item + .scope-item {
    border-top: 1px solid rgba(52, 86, 119, .55);
}

.scope-item-main {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    margin: 0;
    padding: 12px;
    cursor: pointer;
}

/* Ô bắt buộc bị disabled - con trỏ phải nói rõ là không bấm được,
   nếu không người dùng bấm mãi mà tưởng trang hỏng. */
.scope-item-main:has(.scope-checkbox:disabled) {
    cursor: default;
}

.scope-checkbox {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    margin-top: 2px;
    accent-color: var(--primary-5);
}

    .scope-checkbox:disabled {
        opacity: .5;
    }

.scope-item-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.scope-item-name {
    color: var(--fg-2);
    font-size: var(--text-body-14);
}

.scope-item-required {
    margin-left: 4px;
    color: var(--fg-5);
    font-size: var(--text-caption-12);
}

.scope-item-desc {
    color: var(--fg-5);
    font-size: var(--text-caption-12);
    line-height: 1.5;
}

/* --- Thẻ thông tin --- */

.info-card {
    margin-bottom: 12px;
    padding: 14px;
    border: 1px solid rgba(52, 86, 119, .85);
    background: rgba(4, 20, 38, .45);
}

.info-card-head {
    display: flex;
    gap: 10px;
    align-items: center;
    margin-bottom: 8px;
}

.info-card-logo {
    max-height: 24px;
    max-width: 80px;
}

.info-card-title {
    color: var(--fg-1);
    font-size: var(--text-body-16);
    font-weight: 500;
}

.info-card-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 10px;
    color: var(--fg-5);
    font-size: var(--text-caption-12);
}

.info-card-section {
    margin-bottom: 10px;
}

.info-card-section-title {
    margin-bottom: 4px;
    color: var(--fg-4);
    font-size: var(--text-caption-12);
    font-weight: 600;
}

.info-card-list {
    margin: 0;
    padding-left: 18px;
    color: var(--fg-3);
    font-size: var(--text-caption-12);
}

.info-card-actions {
    margin-top: 12px;
}

/* Thu hồi quyền là hành động phá huỷ - phải đọc ra là nguy hiểm,
   nhưng không dùng nền đỏ đặc để khỏi tranh chỗ với nút chính. */
.info-card-revoke {
    border-color: rgba(255, 66, 78, .55) !important;
    color: var(--error-5) !important;
}

    .info-card-revoke:hover {
        border-color: var(--error-5) !important;
        background: rgba(255, 66, 78, .10) !important;
    }

/* =============================================================
   16. Trang chọn dự án (PortalController)
   Một cột, rộng hơn hẳn panel đăng nhập - lưới card cần chỗ thở.
   ============================================================= */

.portal-layout {
    position: relative;
    z-index: 1;
    display: flex;
    justify-content: center;
    min-height: 100vh;
    padding: 60px 32px;
    box-sizing: border-box;
}

.portal-shell {
    width: 100%;
    max-width: 1120px;
}

.portal-head {
    margin-bottom: 28px;
}

.portal-title {
    margin: 0 0 6px;
    font-size: var(--text-h3);
    font-weight: 600;
    color: var(--fg-1);
}

/* auto-fill chứ không auto-fit: auto-fit kéo giãn 1 card lẻ ra hết chiều ngang,
   nhìn như lỗi layout. */
.project-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 16px;
    margin-bottom: 28px;
}

.project-card {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 20px;
    border: 1px solid rgba(52, 86, 119, .85);
    background: rgba(4, 20, 38, .45);
    text-decoration: none;
    transition: border-color .15s ease-out, background-color .15s ease-out, box-shadow .15s ease-out;
}

a.project-card:hover {
    border-color: var(--primary-4);
    background: rgba(4, 20, 38, .70);
    box-shadow: 0 0 18px -6px rgba(34, 197, 94, .45);
}

a.project-card:focus-visible {
    outline: none;
    border-color: var(--primary-4);
    box-shadow: 0 0 0 2px rgba(34, 197, 94, .40);
}

/* Chưa cấu hình khuôn URL -> card chỉ để đọc. Con trỏ phải nói rõ điều đó,
   nếu không người dùng bấm mãi và tưởng trang hỏng. */
.project-card--static {
    cursor: default;
}

.project-card-code {
    color: var(--fg-1);
    font-size: var(--text-body-16);
    font-weight: 500;
    word-break: break-word;
}

.project-card-go {
    color: var(--fg-5);
    font-size: var(--text-caption-12);
}

a.project-card:hover .project-card-go {
    color: var(--primary-4);
}

@media (max-width: 640px) {
    .portal-layout {
        padding: 28px 16px;
    }

    .project-grid {
        grid-template-columns: 1fr;
    }
}
