/* =========================================================
   SOLAR GUARD — Web UI
   Ngôn ngữ thiết kế: bento grid + sidebar, tông indigo–cyan,
   hỗ trợ 2 chế độ Sáng / Tối (đổi qua [data-theme] trên <html>).
   ========================================================= */

/* ---------- 1. DESIGN TOKENS ---------- */
:root {
  --bg:        #eef1f8;
  --bg-grad-1: #e5e9fb;
  --bg-grad-2: #eaf6fb;
  --surface:   #ffffff;
  --surface-2: #f6f8fd;
  --surface-3: #eef2fa;
  --border:    #e2e7f3;
  --border-strong:#cfd7ea;

  --text:  #0d1424;
  --muted: #5f6b85;
  --faint: #93a0bb;

  --brand:   #4f46e5;
  --brand-2: #7c3aed;
  --accent:  #0891b2;
  --brand-soft: rgba(79,70,229,.10);
  --accent-soft: rgba(8,145,178,.10);

  --ok:     #059669;
  --warn:   #d97706;
  --danger: #dc2626;
  --info:   #2563eb;
  --ok-soft:     rgba(5,150,105,.12);
  --warn-soft:   rgba(217,119,6,.12);
  --danger-soft: rgba(220,38,38,.12);
  --info-soft:   rgba(37,99,235,.12);

  --shadow-sm: 0 1px 2px rgba(13,20,36,.05);
  --shadow:    0 1px 2px rgba(13,20,36,.04), 0 10px 30px -18px rgba(13,20,36,.35);
  --shadow-lg: 0 24px 60px -28px rgba(13,20,36,.45);

  --r-sm: 10px; --r: 16px; --r-lg: 22px; --r-xl: 28px;
  --sidebar-w: 248px;
  --font: "Inter", ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "JetBrains Mono", "SF Mono", Consolas, "Liberation Mono", monospace;
}

html[data-theme="dark"] {
  --bg:        #070a12;
  --bg-grad-1: #0b1020;
  --bg-grad-2: #08131c;
  --surface:   #101728;
  --surface-2: #0c1322;
  --surface-3: #16203a;
  --border:    #1e2a44;
  --border-strong:#2c3a5c;

  --text:  #e8edf8;
  --muted: #93a2c0;
  --faint: #63718f;

  --brand:   #818cf8;
  --brand-2: #a78bfa;
  --accent:  #22d3ee;
  --brand-soft: rgba(129,140,248,.14);
  --accent-soft: rgba(34,211,238,.12);

  --ok:     #34d399;
  --warn:   #fbbf24;
  --danger: #f87171;
  --info:   #60a5fa;
  --ok-soft:     rgba(52,211,153,.14);
  --warn-soft:   rgba(251,191,36,.14);
  --danger-soft: rgba(248,113,113,.14);
  --info-soft:   rgba(96,165,250,.14);

  --shadow-sm: 0 1px 2px rgba(0,0,0,.4);
  --shadow:    0 1px 2px rgba(0,0,0,.35), 0 14px 34px -20px rgba(0,0,0,.9);
  --shadow-lg: 0 28px 70px -30px rgba(0,0,0,.95);
}

/* ---------- 2. BASE ---------- */
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: var(--font);
  color: var(--text);
  background: var(--bg);
  background-image:
    radial-gradient(1100px 600px at 12% -10%, var(--bg-grad-1), transparent 60%),
    radial-gradient(900px 500px at 100% 0%, var(--bg-grad-2), transparent 55%);
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
  transition: background-color .25s ease, color .25s ease;
}
h1,h2,h3,h4,p { margin: 0; }
button { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
svg { stroke: currentColor; fill: none; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.muted { color: var(--muted); }
.small { font-size: 12.5px; }
.mono  { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.is-hidden { display: none !important; }
/* Mọi khai báo `display` của tác giả (vd .sheet{display:grid}, .chips{display:flex})
   đều đè mất [hidden]{display:none} của trình duyệt — vì luật của UA stylesheet
   luôn thua luật tác giả. Không có dòng này, phần tử đặt el.hidden = true vẫn
   hiện nguyên (lớp phủ hộp thoại nằm đè giữa màn hình là do vậy). */
[hidden] { display: none !important; }
::selection { background: var(--brand); color: #fff; }

/* scrollbar */
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 99px; border: 3px solid transparent; background-clip: content-box; }

/* ---------- 2b. MÀN ĐĂNG NHẬP ----------
   Đứng trên mọi thứ (splash z-index 90) vì nó là cổng: chưa qua thì không có
   gì phía sau được nhìn thấy. Cùng nền chuyển sắc với splash để hai màn nối
   nhau liền mạch chứ không nhảy màu. */
#authScreen {
  position: fixed; inset: 0; z-index: 120;
  display: grid; place-items: center;
  padding: 20px; overflow-y: auto;
  background: var(--bg);
  background-image:
    radial-gradient(700px 420px at 50% 18%, var(--brand-soft), transparent 70%),
    radial-gradient(600px 400px at 50% 92%, var(--accent-soft), transparent 70%);
}
#authScreen[hidden] { display: none; }

/* ---------- MÀN CHẶN HẾT HẠN / BỊ KHOÁ (lock.js) ----------
   z-index cao hơn cả #authScreen (120) lẫn .splash: màn này là lớp cuối cùng,
   không lớp nào được phép nằm đè lên nó. Nền đỏ nhạt thay cho nền tím của màn
   đăng nhập để khách nhìn một cái là biết đây không phải chuyện đăng nhập. */
.lk {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  padding: 20px; overflow-y: auto;
  background: var(--bg);
  background-image:
    radial-gradient(700px 420px at 50% 15%, var(--danger-soft), transparent 70%),
    radial-gradient(600px 400px at 50% 95%, var(--surface-2), transparent 70%);
}
.lk[hidden] { display: none; }
.lk-card {
  width: 100%; max-width: 420px; margin: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-lg);
  padding: 28px 24px; text-align: center;
}
.lk-ico {
  width: 56px; height: 56px; margin: 0 auto 16px;
  border-radius: 18px; display: grid; place-items: center;
  background: var(--danger-soft); color: var(--danger);
}
.lk-ico svg { width: 28px; height: 28px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.lk-card h1 { font-size: 20px; font-weight: 800; margin-bottom: 8px; }
.lk-sub { font-size: 13px; color: var(--muted); line-height: 1.55; }

.lk-list { list-style: none; margin: 18px 0 4px; display: grid; gap: 8px; text-align: left; }
.lk-list li {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 13px; border-radius: var(--r-sm);
  background: var(--surface-2); border: 1px solid var(--border);
}
.lk-dev { min-width: 0; }
.lk-dev b { display: block; font-size: 13.5px; font-weight: 700; }
.lk-dev small { font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums; }
.lk-why { text-align: right; }

/* Tên thiết bị do khách tự đặt nên dài ngắn tuỳ ý, mà nhãn lý do tiếng Việt
   ("Chưa tới ngày kích hoạt") cũng không ngắn. Hai cột cạnh nhau trên màn hẹp
   là tràn mép. Xuống dòng thành hai hàng, căn trái cả hai. */
@media (max-width: 420px) {
  .lk-list li { display: block; }
  .lk-why { text-align: left; margin-top: 6px; }
}
.lk-why span { display: block; font-size: 12px; font-weight: 700; color: var(--danger); }
.lk-why small { font-size: 11px; color: var(--muted); }

.lk-help { margin-top: 20px; display: grid; gap: 9px; }
.lk-help p { font-size: 12.5px; color: var(--muted); }
.lk-help .btn { justify-content: center; padding: 13px 16px; }

/* Hai nút phụ chia đôi hàng. Xuống một cột dưới 340px (iPhone SE nằm dọc còn
   hẹp hơn thế sau khi trừ padding) để chữ trong nút không bị xuống dòng lẻ. */
.lk-foot { margin-top: 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
.lk-foot .btn { justify-content: center; }
@media (max-width: 380px) { .lk-foot { grid-template-columns: 1fr; } }

.lk-company { margin-top: 18px; font-size: 11px; color: var(--faint); }
.auth-card {
  width: 100%; max-width: 400px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-lg);
  padding: 26px 24px;
  /* Căn giữa theo chiều dọc, nhưng thẻ cao hơn màn (màn đăng ký, điện thoại
     nằm ngang) thì phải cuộn được chứ không bị cắt cụt hai đầu. */
  margin: auto;
}
.auth-brand { display: flex; align-items: center; gap: 11px; margin-bottom: 22px; }
.auth-brand img { width: 38px; height: 38px; flex: none; }
.auth-brand b { font-size: 15px; font-weight: 800; letter-spacing: .05em; display: block; }
.auth-brand b span { background: linear-gradient(100deg, var(--brand), var(--accent)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.auth-brand small { font-size: 9px; letter-spacing: .2em; color: var(--faint); font-weight: 700; }
.auth-view { display: grid; gap: 13px; }
.auth-view[hidden] { display: none; }
.auth-view h1 { font-size: 21px; font-weight: 800; }
.auth-view h1 + .auth-sub { margin-top: -9px; }
.auth-sub { font-size: 13px; color: var(--muted); line-height: 1.5; }
.auth-view .btn { padding: 13px 16px; }
.auth-back {
  justify-self: start; display: inline-flex; align-items: center; gap: 4px;
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 13px; font-weight: 700;
}
.auth-back:hover { color: var(--brand); }
.auth-back svg { width: 15px; height: 15px; }
.auth-link {
  background: none; border: 0; padding: 2px; cursor: pointer; font: inherit;
  font-size: 13px; font-weight: 700; color: var(--brand); justify-self: center;
}
.auth-link:hover { text-decoration: underline; }
.auth-sep { display: flex; align-items: center; gap: 10px; color: var(--faint); font-size: 11.5px; }
.auth-sep::before, .auth-sep::after { content: ""; flex: 1; height: 1px; background: var(--border); }

/* Ô mật khẩu + con mắt hiện/ẩn. Con mắt nằm ĐÈ lên ô chứ không đứng cạnh, để
   mọi ô nhập trong thẻ rộng bằng nhau. */
.auth-pw { position: relative; display: grid; min-width: 0; }
.auth-pw input { padding-right: 44px; }
.auth-eye {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 34px; height: 34px; display: grid; place-items: center;
  border: 0; background: none; border-radius: 9px;
  color: var(--faint); cursor: pointer;
}
.auth-eye:hover { color: var(--brand); background: var(--surface-3); }
.auth-eye svg { width: 17px; height: 17px; }
.auth-eye.on { color: var(--brand); }

/* Edge và Chrome trên Windows tự chèn thêm một con mắt "hiện mật khẩu" của
   riêng trình duyệt (::-ms-reveal). Cạnh con mắt của trang thành ra hai cái
   nằm sát nhau, bấm cái nào cũng ra kết quả khác nhau. Bỏ cái của trình duyệt
   đi, giữ lại cái mình dựng — chỉ nó mới đổi được cả icon lẫn nhãn trợ năng.

   Hai dòng RIÊNG chứ không gộp bằng dấu phẩy: chỉ cần một bộ chọn trong danh
   sách không hợp lệ là trình duyệt bỏ cả luật, kể cả phần nó vốn hiểu được. */
input[type="password"]::-ms-reveal { display: none; width: 0; height: 0; }
input[type="password"]::-ms-clear  { display: none; width: 0; height: 0; }

/* Dòng báo lỗi / báo thành công. Rỗng thì KHÔNG chiếm chỗ, nếu không mỗi màn
   luôn thừa một khoảng trống lửng giữa ô nhập và nút. */
.auth-msg { font-size: 12.5px; line-height: 1.5; }
.auth-msg:empty { display: none; }
.auth-msg.err {
  color: var(--danger); background: var(--danger-soft);
  border-radius: 10px; padding: 9px 11px;
}
.auth-msg.ok {
  color: var(--ok); background: var(--ok-soft);
  border-radius: 10px; padding: 9px 11px;
}

/* Ô mật khẩu nằm trong .field (cả màn đăng nhập lẫn hai trang bảo mật ở phần
   Hồ sơ). Không có dòng này thì `.field input { padding: 12px 13px }` — đứng
   sau trong tệp, cùng độ ưu tiên — nuốt mất padding-right của .auth-pw, và con
   mắt đè lên đúng chỗ chữ đang gõ tới. */
.field .auth-pw input { padding-right: 44px; }

/* Ô 6 chữ số của mã PIN. Chữ to, giãn đều, canh giữa — nhìn là đếm được đang
   gõ tới chữ số thứ mấy, thứ mà một hàng chấm tròn sát nhau không nói ra được.
   text-indent bù lại khoảng giãn thừa sau chữ số cuối, nếu không cả cụm lệch
   trái đúng nửa chữ. Đối xứng hai bên để con mắt không đè lên chữ số. */
.field .pin-box {
  font-family: var(--mono); font-size: 19px; font-weight: 700;
  letter-spacing: .48em; text-indent: .48em; text-align: center;
  padding-left: 44px;
}

@media (max-width: 420px) {
  #authScreen { padding: 14px; }
  .auth-card { padding: 20px 17px; border-radius: var(--r); }
  .auth-view h1 { font-size: 19px; }
}

/* ---------- 3. SPLASH ---------- */
.splash {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-items: center;
  background: var(--bg);
  background-image:
    radial-gradient(700px 420px at 50% 22%, var(--brand-soft), transparent 70%),
    radial-gradient(600px 400px at 50% 90%, var(--accent-soft), transparent 70%);
  transition: opacity .45s ease, visibility .45s;
}
.splash.out { opacity: 0; visibility: hidden; }
.splash[hidden] { display: none; }
.splash-inner { text-align: center; padding: 24px; }
.splash-logo img { width: 92px; height: 92px; animation: pop .7s cubic-bezier(.2,1.2,.3,1) both; }
.wordmark { font-size: clamp(26px, 6vw, 36px); font-weight: 800; letter-spacing: .06em; margin-top: 14px; }
.wordmark span { background: linear-gradient(100deg, var(--brand), var(--accent)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.tagline { margin-top: 4px; font-size: 11.5px; letter-spacing: .28em; color: var(--faint); font-weight: 600; }
.splash-greet { margin: 34px 0 22px; }
.splash-greet p:first-child { font-size: 19px; font-weight: 700; }
.splash-bar { width: 190px; height: 4px; border-radius: 99px; background: var(--surface-3); margin: 0 auto 14px; overflow: hidden; }
.splash-bar i { display: block; height: 100%; width: 40%; border-radius: 99px; background: linear-gradient(90deg, var(--brand), var(--accent)); animation: slide 1.15s ease-in-out infinite; }
@keyframes slide { 0% { transform: translateX(-110%);} 100% { transform: translateX(320%);} }
@keyframes pop { from { transform: scale(.7) rotate(-8deg); opacity: 0; } }

/* ---------- 4. SHELL ---------- */
.app { min-height: 100%; }
@media (min-width: 1024px) {
  .app { display: grid; grid-template-columns: var(--sidebar-w) minmax(0,1fr); }
}

.sidebar { display: none; }
@media (min-width: 1024px) {
  .sidebar {
    display: flex; flex-direction: column; gap: 8px;
    position: sticky; top: 0; height: 100vh;
    padding: 22px 16px;
    border-right: 1px solid var(--border);
    background: color-mix(in srgb, var(--surface) 72%, transparent);
    backdrop-filter: blur(14px);
  }
}
.brand { display: flex; align-items: center; gap: 11px; padding: 6px 8px 18px; }
.brand img { width: 36px; height: 36px; }
.brand-text b { display: block; font-size: 15px; letter-spacing: .04em; font-weight: 800; }
.brand-text b span { background: linear-gradient(100deg, var(--brand), var(--accent)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.brand-text small { display: block; font-size: 9px; letter-spacing: .18em; color: var(--faint); font-weight: 600; }

.nav { display: flex; flex-direction: column; gap: 4px; }
.nav-item {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 12px; border-radius: 12px;
  border: 1px solid transparent; background: none; cursor: pointer;
  font-size: 14.5px; font-weight: 600; color: var(--muted);
  transition: background .18s, color .18s, transform .18s;
  text-align: left; width: 100%;
}
.nav-item svg { width: 19px; height: 19px; flex: none; }
.nav-item:hover { background: var(--surface-2); color: var(--text); }
.nav-item.active { background: var(--brand-soft); color: var(--brand); border-color: color-mix(in srgb, var(--brand) 24%, transparent); }
.nav-item .badge { margin-left: auto; }

.sidebar-foot { margin-top: auto; display: grid; gap: 10px; }
.sys-pill {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px; border-radius: 12px;
  background: var(--surface-2); border: 1px solid var(--border);
  font-size: 12.5px; font-weight: 600; color: var(--muted);
}

/* theme switch (sidebar) */
.theme-switch { display: flex; gap: 4px; padding: 4px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border); }
.theme-switch button {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px; border: 0; border-radius: 9px; background: none; cursor: pointer;
  font-size: 12.5px; font-weight: 700; color: var(--muted); transition: .18s;
}
.theme-switch button svg { width: 15px; height: 15px; }
.theme-switch button.on { background: var(--surface); color: var(--brand); box-shadow: var(--shadow-sm); }

/* ---------- 4b. DẢI "ADMIN ĐANG TRUY CẬP" ----------
   Hiện khi trang được mở từ nút trong trang quản trị (index.html?adminView=1).
   Dán CỨNG lên đỉnh màn hình chứ không cuộn theo trang: mục đích của nó là để
   người đang xem không có lúc nào quên mất mình đang nhìn dữ liệu của người
   khác — cuộn xuống là mất tác dụng.

   Đỏ đặc, không phải đỏ nhạt như .badge: dải này phải khác hẳn mọi thứ khác
   trên giao diện, kể cả huy hiệu cảnh báo. */
.admin-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 95;
  display: flex; justify-content: center; padding: 7px 12px;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}
.admin-bar[hidden] { display: none; }
.admin-pill {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  padding: 7px 15px; border-radius: 99px;
  background: var(--danger); color: #fff;
  font-size: 12.5px; font-weight: 700; line-height: 1.35;
  box-shadow: 0 10px 24px -12px var(--danger);
}
/* Tab không đăng nhập bằng tài khoản quản trị: vẫn cảnh báo, nhưng màu cam để
   phân biệt ngay từ xa với dải đỏ "đúng là admin". */
.admin-pill.canhbao { background: var(--warn); box-shadow: 0 10px 24px -12px var(--warn); }
.admin-pill b { font-weight: 800; letter-spacing: .06em; white-space: nowrap; }
/* Email dài thì cắt bớt chứ không kéo dải rộng ra ngoài màn hình. */
.admin-pill span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: .92; }
.admin-pill i {
  width: 8px; height: 8px; border-radius: 99px; flex: none;
  background: #fff; animation: adminBlink 1.6s ease-in-out infinite;
}
@keyframes adminBlink { 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .admin-pill i { animation: none; } }

/* Đẩy cả app xuống đúng chiều cao dải. Thanh tiêu đề dính (sticky, top:0) tính
   mốc theo khung nhìn chứ không theo phần đệm của .app, nên phải dời riêng —
   thiếu dòng dưới thì cuộn trang là thanh tiêu đề chui lên dưới dải đỏ.

   Chiều cao lấy từ `--admin-bar-h`, do app.js đo dải thật rồi đặt vào. Viết
   cứng một con số thì chỉ cần người dùng phóng to chữ hệ thống là lệch đi vài
   pixel và thanh tiêu đề ló ra dưới dải. */
html.admin-view .app    { padding-top: var(--admin-bar-h, 46px); }
html.admin-view .topbar { top: var(--admin-bar-h, 46px); }

/* ---------- 4c. DẢI HƯỚNG DẪN NỐI WIFI ----------
   Nằm giữa thanh tiêu đề và nội dung trang, không phải cửa sổ đè lên: người
   dùng phải vừa đọc được nó vừa thao tác trên điện thoại (đổi sóng WiFi, quay
   lại ứng dụng), mà một hộp thoại chặn thì mỗi lần quay lại là một lần phải
   tìm cách mở nó ra. */
.wifi-call {
  margin: 16px 18px 0; padding: 16px 18px;
  border-radius: var(--r); border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
  background: var(--warn-soft);
}
@media (min-width: 1024px) { .wifi-call { margin: 18px 30px 0; } }
.wifi-call-head { display: flex; align-items: center; gap: 12px; }
.wifi-call-ico {
  width: 42px; height: 42px; border-radius: 13px; flex: none;
  display: grid; place-items: center; background: var(--warn); color: #fff;
}
.wifi-call-ico svg { width: 21px; height: 21px; stroke-width: 2; }
.wifi-call-head b { display: block; font-size: 15px; line-height: 1.4; }
.wifi-call-head p { font-size: 12px; color: var(--muted); margin-top: 2px; }
.wifi-call-ap {
  display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 12px 0 4px;
  padding: 10px 12px; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--border);
  font-size: 12.5px; color: var(--muted);
}
.wifi-call .btn { margin-top: 14px; }

/* Danh sách các bước — dùng chung cho dải này và trang Reset WiFi. */
.steps { margin: 0; padding-left: 20px; display: grid; gap: 8px; }
.steps li { font-size: 13.5px; line-height: 1.6; }
.steps li::marker { font-weight: 800; color: var(--brand); }
.steps b { font-weight: 700; color: var(--text); }

/* ---------- 5. TOPBAR ---------- */
.main { min-width: 0; display: flex; flex-direction: column; min-height: 100vh; }
.topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 12px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px);
}
.topbar-title h2 { font-size: 19px; font-weight: 800; letter-spacing: -.01em; }
.topbar-actions { margin-left: auto; display: flex; gap: 8px; }
.icon-btn {
  width: 40px; height: 40px; display: grid; place-items: center;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface); cursor: pointer; position: relative;
  transition: .18s; color: var(--muted);
}
.icon-btn:hover { color: var(--brand); border-color: color-mix(in srgb, var(--brand) 30%, transparent); transform: translateY(-1px); }
.icon-btn svg { width: 19px; height: 19px; }
.icon-btn .dot { position: absolute; top: 8px; right: 9px; width: 8px; height: 8px; border-radius: 99px; background: var(--danger); border: 2px solid var(--surface); }
/* Bản nhỏ, dùng cho nút bút chì đứng cạnh tiêu đề trang chi tiết thiết bị */
.icon-btn-sm { width: 32px; height: 32px; border-radius: 10px; }
.icon-btn-sm svg { width: 16px; height: 16px; }
.ico-moon { display: none; }
html[data-theme="dark"] .ico-sun { display: none; }
html[data-theme="dark"] .ico-moon { display: block; }
.only-mobile { display: grid; }
@media (min-width: 1024px) { .only-mobile { display: none; } }

.view { flex: 1; padding: 20px 18px 96px; max-width: 1320px; width: 100%; margin: 0 auto; }
@media (min-width: 1024px) { .view { padding: 26px 30px 40px; } }
.main-foot { padding: 0 18px 96px; text-align: center; }
@media (min-width: 1024px) { .main-foot { padding: 10px 30px 26px; } }

/* ---------- 6. TABBAR (mobile) ---------- */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: grid; grid-auto-flow: column;
  padding: 8px 8px calc(8px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(18px);
}
@media (min-width: 1024px) { .tabbar { display: none; } }
.tab {
  display: grid; justify-items: center; gap: 3px;
  padding: 7px 4px; border: 0; background: none; cursor: pointer;
  color: var(--faint); font-size: 10.5px; font-weight: 700; border-radius: 12px;
  position: relative;
}
.tab svg { width: 21px; height: 21px; }
.tab.active { color: var(--brand); }
.tab.active::before { content:""; position: absolute; top: -8px; width: 26px; height: 3px; border-radius: 99px; background: linear-gradient(90deg, var(--brand), var(--accent)); }

/* ---------- 7. LAYOUT PRIMITIVES ---------- */
.grid { display: grid; gap: 14px; grid-template-columns: repeat(12, minmax(0,1fr)); }
.c12 { grid-column: span 12; } .c6 { grid-column: span 6; }
.c4  { grid-column: span 6; }  .c3 { grid-column: span 6; }
.c8  { grid-column: span 12; }
/* Như .c4 nhưng trải hết hàng trên điện thoại — dùng cho cột thẻ bên phải
   của Tổng quan, để nó không nằm co một nửa với nửa còn lại bỏ trống. */
.c4w { grid-column: span 12; }
/* Điện thoại: đưa cụm "Tổng hợp cảnh báo / Trạng thái hệ thống" LÊN TRƯỚC
   biểu đồ công suất. Trên máy tính hai khối nằm cạnh nhau nên thứ tự trong
   mã nguồn là hợp lý; trên màn hẹp chúng xếp dọc, mà tình trạng hệ thống là
   thứ cần liếc thấy đầu tiên chứ không phải biểu đồ. */
@media (max-width: 759px) { .ov-side { order: -1; } }
@media (min-width: 760px) {
  .c4 { grid-column: span 4; } .c3 { grid-column: span 3; } .c8 { grid-column: span 8; }
  .c4w { grid-column: span 4; }
}
.stack { display: grid; gap: 14px; }
.row { display: flex; align-items: center; gap: 10px; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.spacer { flex: 1; }

.section-title {
  display: flex; align-items: center; gap: 10px;
  font-size: 11.5px; letter-spacing: .16em; font-weight: 800;
  color: var(--faint); text-transform: uppercase; margin: 22px 2px 12px;
}
.section-title::after { content: ""; flex: 1; height: 1px; background: var(--border); }

/* ---------- 8. CARD ---------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 16px;
  box-shadow: var(--shadow);
  position: relative; overflow: hidden;
}
.card-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.card-head h3 { font-size: 14px; font-weight: 800; letter-spacing: -.005em; }
.card-head .card-ico { width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; background: var(--brand-soft); color: var(--brand); flex: none; }
.card-head .card-ico svg { width: 16px; height: 16px; }
.card-link { margin-left: auto; font-size: 12.5px; font-weight: 700; color: var(--brand); cursor: pointer; background: none; border: 0; display: flex; align-items: center; gap: 4px; }
.card-link svg { width: 14px; height: 14px; }

/* stat tile */
.stat {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  padding: 14px; box-shadow: var(--shadow-sm); position: relative; overflow: hidden;
}
.stat::after { content: ""; position: absolute; inset: auto 0 0 0; height: 3px; background: var(--tone, var(--brand)); opacity: .8; }
/* Ô nằm trong lưới grid: mặc định min-width của một ô grid là auto, tức nó
   KHÔNG chịu co nhỏ hơn nội dung. Nhãn dài ("SẢN LƯỢNG TRUNG BÌNH") vì thế đẩy
   cả hàng rộng ra và trang tràn ngang trên điện thoại. */
.stat { min-width: 0; }
.stat .stat-label, .stat .small { overflow-wrap: anywhere; }
.stat .stat-ico { width: 32px; height: 32px; border-radius: 10px; display: grid; place-items: center; background: var(--tone-soft, var(--brand-soft)); color: var(--tone, var(--brand)); margin-bottom: 10px; }
.stat .stat-ico svg { width: 17px; height: 17px; }
.stat .stat-label { font-size: 10.5px; letter-spacing: .14em; font-weight: 800; color: var(--faint); text-transform: uppercase; }
.stat .stat-value { font-size: 26px; font-weight: 800; font-family: var(--mono); letter-spacing: -.02em; margin-top: 2px; }
.stat .stat-value small { font-size: 13px; color: var(--muted); font-weight: 600; }

/* banner */
.banner {
  display: flex; align-items: center; gap: 14px;
  padding: 16px; border-radius: var(--r-lg);
  border: 1px solid color-mix(in srgb, var(--tone) 30%, transparent);
  background: linear-gradient(120deg, var(--tone-soft), transparent 70%), var(--surface);
  box-shadow: var(--shadow);
}
.banner .banner-ico { width: 46px; height: 46px; border-radius: 14px; display: grid; place-items: center; flex: none; background: var(--tone); color: #fff; }
.banner .banner-ico svg { width: 23px; height: 23px; stroke-width: 2.2; }
/* Chỉ TIÊU ĐỀ của dải mới xuống dòng riêng. Trước đây luật này bắt MỌI <b>
   trong dải thành khối, nên một chữ in đậm giữa câu ở đoạn mô tả cũng bị đá
   xuống dòng — câu văn vỡ thành từng mẩu xếp dọc. */
.banner-text > b { display: block; font-size: 15.5px; }
.banner p { font-size: 13px; color: var(--muted); margin-top: 2px; }
/* basis 0 chứ không phải auto: có flex-wrap rồi nên nếu lấy bề rộng theo nội dung,
   khối chữ sẽ tự nhảy xuống hàng riêng, bỏ icon đứng trơ một mình. */
.banner-text { flex: 1 1 0; min-width: 0; }
.banner > .btn { flex: none; }
/* Điện thoại: nút tụt xuống hàng riêng, nhờ vậy phần chữ được cả chiều ngang
   và không bị bóp thành cột hẹp vỡ dòng. */
@media (max-width: 559px) {
  .banner { flex-wrap: wrap; }
  .banner > .btn { flex: 1 0 100%; justify-content: center; }
}

/* badge / pill */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: 99px;
  font-size: 11px; font-weight: 800; letter-spacing: .02em;
  background: var(--tone-soft, var(--brand-soft)); color: var(--tone, var(--brand));
}
.badge i { width: 6px; height: 6px; border-radius: 99px; background: currentColor; }
.badge.live i { animation: blink 1.6s ease-in-out infinite; }
@keyframes blink { 50% { opacity: .25; } }

/* buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 11px 16px; border-radius: 12px; cursor: pointer;
  border: 1px solid var(--border); background: var(--surface);
  font-size: 14px; font-weight: 700; transition: .18s;
}
.btn svg { width: 17px; height: 17px; }
.btn:hover { border-color: var(--border-strong); transform: translateY(-1px); }
.btn-primary { background: linear-gradient(100deg, var(--brand), var(--brand-2)); border-color: transparent; color: #fff; box-shadow: 0 8px 22px -12px var(--brand); }
.btn-primary:hover { filter: brightness(1.06); }
.btn-ghost { background: var(--surface-2); }
.btn-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 32%, transparent); background: var(--danger-soft); }
.btn-block { width: 100%; }
.btn-dashed { border-style: dashed; border-width: 1.5px; color: var(--muted); background: none; padding: 16px; }
.btn-dashed:hover { color: var(--brand); border-color: var(--brand); background: var(--brand-soft); }

/* segmented control */
.seg { display: inline-flex; padding: 4px; gap: 3px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 13px; }
.seg button { padding: 8px 15px; border: 0; border-radius: 10px; background: none; cursor: pointer; font-size: 13px; font-weight: 700; color: var(--muted); transition: .16s; }
.seg button.on { background: var(--surface); color: var(--brand); box-shadow: var(--shadow-sm); }

/* chips */
.chips { display: flex; gap: 7px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }
.chips::-webkit-scrollbar { display: none; }
.chip { flex: none; padding: 7px 13px; border-radius: 99px; border: 1px solid var(--border); background: var(--surface-2); cursor: pointer; font-size: 12.5px; font-weight: 700; color: var(--muted); transition: .16s; }
.chip.on { background: var(--brand); border-color: var(--brand); color: #fff; }

/* list row */
.list { display: grid; }
.list-row {
  display: flex; align-items: center; gap: 13px; width: 100%;
  padding: 13px 4px; border: 0; background: none; cursor: pointer; text-align: left;
  border-bottom: 1px solid var(--border);
}
.list .list-row:last-child { border-bottom: 0; }
/* Hàng có thể là <button> (điều hướng trong app) hoặc <a> (mailto:/tel: ở mục
   Liên hệ hỗ trợ). Thẻ <a> mang sẵn gạch chân và màu xanh liên kết của trình
   duyệt, phải gỡ để hai loại hàng trông giống hệt nhau. */
a.list-row { color: inherit; text-decoration: none; }
.list-row:hover { background: var(--surface-2); }
.list-row .li-ico { width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center; flex: none; background: var(--tone-soft, var(--brand-soft)); color: var(--tone, var(--brand)); }
.list-row .li-ico svg { width: 18px; height: 18px; }
.list-row b { display: block; font-size: 14.5px; font-weight: 700; }
.list-row p { font-size: 12.5px; color: var(--muted); margin-top: 1px; }
.list-row .chev { margin-left: auto; color: var(--faint); }
.list-row .chev svg { width: 17px; height: 17px; }

/* kv */
.kv { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 11px 0; border-bottom: 1px dashed var(--border); font-size: 13.5px; }
.kv:last-child { border-bottom: 0; }
.kv span { color: var(--muted); }
.kv b { font-weight: 700; font-family: var(--mono); }

/* temp-table — thẻ "Nhiệt độ": 1 cột nhãn + 3 cột Thấp nhất/Hiện tại/Cao nhất,
   mỗi hàng một cảm biến (MOSFET/Cảm biến 1/Cảm biến 2). */
.temp-table { display: flex; flex-direction: column; }
.temp-row {
  display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; align-items: center;
  gap: 6px; padding: 10px 0; border-bottom: 1px dashed var(--border); font-size: 13.5px;
}
.temp-row:last-child { border-bottom: 0; }
.temp-row span { color: var(--muted); }
.temp-row b { font-weight: 700; font-family: var(--mono); text-align: right; }
.temp-head { padding-top: 0; padding-bottom: 8px; }
.temp-head span { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; text-align: right; }
.temp-head span:first-child { text-align: left; }

/* toggle */
.tgl { width: 46px; height: 27px; border-radius: 99px; border: 1px solid var(--border); background: var(--surface-3); position: relative; cursor: pointer; flex: none; transition: .2s; }
.tgl::after { content: ""; position: absolute; top: 2px; left: 2px; width: 21px; height: 21px; border-radius: 99px; background: var(--surface); box-shadow: var(--shadow-sm); transition: .2s; }
.tgl.on { background: var(--brand); border-color: var(--brand); }
.tgl.on::after { transform: translateX(19px); background: #fff; }

/* input */
/* min-width:0 ở CẢ BA cấp là bắt buộc, không phải phòng xa. Ô <input> mặc định
   rộng 20 ký tự (thuộc tính `size`), và trong lưới/flex thì bề rộng tối thiểu
   tự động của một ô CHÍNH LÀ con số đó — nó không chịu co lại. Với ô mã PIN
   (chữ 19px, giãn .48em, đệm 44px mỗi bên) thì 20 ký tự ra ~500px, vượt cả bề
   ngang điện thoại, và thẻ .card bị đẩy tràn khỏi màn hình. */
.field { display: grid; gap: 6px; min-width: 0; }
.field label { font-size: 12px; font-weight: 700; color: var(--muted); }
.field input, .field select {
  padding: 12px 13px; border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text); font: inherit; font-size: 14px; outline: none;
  min-width: 0; width: 100%;
}
.field input:focus, .field select:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }

/* Ô nhập nhiều dòng — .field chỉ tạo kiểu cho input/select nên textarea phải
   khai riêng, nếu không nó rơi về kiểu mặc định của trình duyệt (viền vuông,
   phông serif) và lệch hẳn khỏi các ô còn lại. */
.field textarea {
  padding: 12px 13px; border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text); font: inherit; font-size: 14px;
  outline: none; min-width: 0; width: 100%; resize: vertical; line-height: 1.55;
}
.field textarea:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }

/* ---------- hàng sao đánh giá ---------- */
.stars { display: flex; justify-content: center; gap: 6px; }
.star-btn {
  border: 0; background: none; cursor: pointer; padding: 4px;
  color: var(--faint); transition: transform .15s ease, color .15s ease;
}
/* pointer-events:none để `e.target` của cú bấm LUÔN là chính cái nút, không
   phải <path> bên trong. Bớt một đường cho lỗi vặt kiểu bấm trúng nét vẽ. */
.star-btn svg { width: 34px; height: 34px; stroke-width: 1.6; pointer-events: none; }
.star-btn.on { color: var(--warn); }
/* Tô đặc ngôi sao đã chấm. Nhắm thẳng vào <path> chứ không vào <svg>: quy tắc
   chung `svg { fill: none }` truyền xuống path theo kế thừa, mà khai báo nhắm
   đúng phần tử thì thắng giá trị kế thừa. */
.star-btn.on svg path { fill: currentColor; }
.star-btn:hover { transform: scale(1.12); }
@media (prefers-reduced-motion: reduce) { .star-btn:hover { transform: none; } }

/* empty state */
.empty { text-align: center; padding: 40px 20px; }
.empty .empty-ico { width: 76px; height: 76px; border-radius: 50%; display: grid; place-items: center; margin: 0 auto 16px; background: var(--tone-soft, var(--ok-soft)); color: var(--tone, var(--ok)); }
.empty .empty-ico svg { width: 34px; height: 34px; stroke-width: 1.8; }
.empty h3 { font-size: 18px; font-weight: 800; }
.empty p { color: var(--muted); font-size: 13.5px; margin-top: 6px; max-width: 380px; margin-inline: auto; line-height: 1.55; }

/* ---------- 9. DEVICE CARD (bố cục HÀNG NGANG) ---------- */
.dev-card {
  display: flex; align-items: center; flex-wrap: wrap; gap: 14px 18px; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 16px; box-shadow: var(--shadow);
  transition: .2s; text-align: left; width: 100%;
}
.dev-card:hover { transform: translateY(-2px); border-color: color-mix(in srgb, var(--brand) 35%, transparent); box-shadow: var(--shadow-lg); }
.dev-main { flex: 1 1 210px; min-width: 0; }
/* Hàng tiêu đề: tên thiết bị bên trái, sóng WiFi/BT dồn sát phải cùng hàng.
   align-items:flex-end để cặp sóng ngang với DÒNG TÊN chứ không phải mã máy. */
.dev-top { display: flex; align-items: flex-end; gap: 10px; }
.dev-title { min-width: 0; flex: 1 1 auto; }
.dev-id { font-size: 10.5px; letter-spacing: .12em; font-weight: 800; color: var(--faint); }
.dev-name { font-size: 17px; font-weight: 800; margin-top: 1px; }
.dev-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }

/* Hàng số liệu: 3 ô V/A/W + vòng SOC + mũi tên, tất cả trên MỘT hàng. */
.dev-row { display: flex; align-items: center; gap: 12px; width: 100%; }
.dev-metrics { flex: 1 1 auto; min-width: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.metric { background: var(--surface-2); border: 1px solid var(--border); border-radius: 13px; padding: 10px; min-width: 0; }
.metric span { font-size: 10px; letter-spacing: .12em; font-weight: 800; color: var(--faint); }
.metric b { display: block; font-family: var(--mono); font-size: 16px; font-weight: 700; margin-top: 2px; }
.dev-sig { flex: none; display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: var(--muted); }
.rssi { display: inline-flex; align-items: center; gap: 5px; font-family: var(--mono); }
.rssi svg { width: 14px; height: 14px; flex: none; }

/* Bút chì đổi tên trên thẻ thiết bị. Mờ sẵn, đậm lên khi rê chuột vào thẻ —
   để nó không tranh sự chú ý với mấy con số, nhưng trên cảm ứng (không có
   hover) vẫn phải nhìn thấy được, nên độ mờ nền chỉ hạ tới .55 chứ không ẩn. */
.dev-edit {
  display: grid; place-items: center; flex: none;
  width: 30px; height: 30px; border-radius: 9px;
  color: var(--faint); cursor: pointer; opacity: .55; transition: .18s;
}
.dev-edit svg { width: 15px; height: 15px; }
.dev-card:hover .dev-edit { opacity: 1; }
.dev-edit:hover { background: var(--surface-2); color: var(--brand); opacity: 1; }
.dev-edit:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; opacity: 1; }

/* Mũi tên phải có kích thước riêng: <svg> trong thẻ không tự co, để mặc định
   nó giãn hết chiều rộng thẻ. */
.dev-chev { width: 18px; height: 18px; flex: none; color: var(--faint); }

/* SOC ring */
.soc-ring { position: relative; width: 62px; height: 62px; flex: none; }
.soc-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.soc-ring .track { stroke: var(--surface-3); }
.soc-ring .val { stroke: var(--tone, var(--brand)); stroke-linecap: round; transition: stroke-dashoffset .6s ease; }
.soc-ring .lbl { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.soc-ring .lbl b { font-family: var(--mono); font-size: 16px; font-weight: 800; line-height: 1; }
.soc-ring .lbl small { font-size: 8px; letter-spacing: .1em; color: var(--faint); font-weight: 800; }

/* Màn hẹp: bóp nhẹ ô số liệu và vòng SOC để cả hàng vẫn đủ chỗ, không vỡ dòng */
@media (max-width: 419px) {
  .dev-card { padding: 14px; gap: 12px; }
  .dev-row { gap: 8px; }
  .dev-metrics { gap: 6px; }
  .metric { padding: 8px 7px; border-radius: 11px; }
  .metric b { font-size: 14px; }
  .soc-ring { width: 52px; height: 52px; }
  .soc-ring .lbl b { font-size: 14px; }
  .dev-chev { width: 14px; height: 14px; }
}

/* ---------- 10. DEVICE DETAIL ---------- */
.hero {
  border-radius: var(--r-xl); padding: 20px;
  border: 1px solid color-mix(in srgb, var(--brand) 22%, var(--border));
  background:
    radial-gradient(600px 220px at 88% 0%, var(--accent-soft), transparent 65%),
    linear-gradient(135deg, var(--brand-soft), transparent 55%), var(--surface);
  box-shadow: var(--shadow);
}
/* Bốn đồng hồ nửa vòng: 2 hàng × 2 cột, giữ nguyên cả trên điện thoại vì
   người dùng muốn nhìn thấy cùng lúc bốn đại lượng. */
.gauge-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px 12px; }
.gauge-cell {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 16px; padding: 12px 12px 11px;
  display: grid; justify-items: center; text-align: center;
  box-shadow: 0 1px 2px rgb(15 23 42 / .04);
}
.gauge-head {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 10.5px; letter-spacing: .1em; font-weight: 800; color: var(--faint);
  text-transform: uppercase;
}
.gauge-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 7px;
  background: var(--tone-soft); color: var(--tone);
}
.gauge-ico svg { width: 12px; height: 12px; }

/* Số đọc nằm chồng lên lòng cung. Đặt theo phần trăm chứ không theo pixel để
   khi ô co lại (màn hẹp) chữ vẫn rơi đúng giữa nửa vòng. */
.gauge-face { position: relative; width: 100%; max-width: 200px; margin-top: 6px; }
.gauge-svg svg { display: block; width: 100%; height: auto; overflow: visible; }
.gauge-read {
  position: absolute; left: 0; right: 0; bottom: 6%;
  display: flex; align-items: baseline; justify-content: center; gap: 3px;
  pointer-events: none;
}
.gauge-read b { font-size: 26px; font-weight: 800; line-height: 1; }
.gauge-read small { font-size: 11px; font-weight: 700; color: var(--muted); }

/* Nhãn hai mút thang, kéo sát mép cung */
.gauge-scale {
  display: flex; justify-content: space-between; width: 100%; max-width: 200px;
  font-family: var(--mono); font-size: 9.5px; color: var(--faint); margin-top: -2px;
}
.gauge-foot { font-size: 11px; color: var(--muted); margin-top: 7px; min-height: 18px; }
.gauge-foot .badge { font-size: 10px; padding: 3px 8px; }

@media (max-width: 520px) {
  .gauge-grid { gap: 9px 8px; }
  .gauge-cell { padding: 9px 6px 8px; border-radius: 13px; }
  .gauge-head { font-size: 8.5px; letter-spacing: .04em; gap: 4px; }
  .gauge-ico { width: 16px; height: 16px; border-radius: 6px; }
  .gauge-ico svg { width: 10px; height: 10px; }
  .gauge-read b { font-size: 19px; }
  .gauge-read small { font-size: 9px; }
  .gauge-scale { font-size: 8px; }
  .gauge-foot { font-size: 9.5px; margin-top: 5px; min-height: 15px; }
  .gauge-foot .badge { font-size: 8.5px; padding: 2px 6px; }
}

.sw-row { display: grid; grid-template-columns: repeat(3, 1fr); }
.sw { text-align: center; padding: 12px 6px; border-right: 1px solid var(--border); }
.sw:last-child { border-right: 0; }
.sw b { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 800; }
.sw b i { width: 8px; height: 8px; border-radius: 99px; background: currentColor; }
.sw p { font-size: 12px; color: var(--muted); margin-top: 3px; }
.sw.on b { color: var(--ok); }
.sw.off b { color: var(--faint); }
/* Mất đường RS485 thì không ai biết bộ nạp đang đóng hay cắt. Ô hiện dấu gạch
   và chấm tròn rỗng — KHÁC hẳn "Tắt", vốn là một khẳng định. */
.sw.unknown b { color: var(--muted); }
.sw.unknown b i { background: transparent; box-shadow: inset 0 0 0 2px var(--muted); }

.bar-split { height: 12px; border-radius: 99px; overflow: hidden; display: flex; background: var(--surface-3); }
.bar-split i { display: block; height: 100%; }

/* ---------- ĐIỆN ÁP TỪNG CELL ----------
   Mỗi cell = tên + thanh đo, cùng kiểu thanh với "Nạp & xả hôm nay".
   Xanh = bình thường · cam = cao nhất · đỏ = thấp nhất. */
.cells { display: grid; gap: 10px; }

.cells-peek { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px 16px; }

/* Hai cột 8 cell, cả hai canh lề trái giống hệt nhau. */
.cells-all { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px 18px; }
.cellcol { display: grid; gap: 9px; align-content: start; }

.cellrow-top { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.cellrow-name { font-size: 10px; font-weight: 800; color: var(--faint); letter-spacing: .1em; }
.cellrow-val { font-family: var(--mono); font-size: 12.5px; font-weight: 700; margin-left: auto; }

.cellbar { height: 8px; border-radius: 99px; background: var(--surface-3); display: flex; overflow: hidden; }
.cellbar i { display: block; height: 100%; border-radius: 99px; transition: width .35s ease; }

/* Nút mở rộng — xám, nhạt, không tranh sự chú ý với số liệu */
.cells-toggle {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; padding: 9px; margin-top: 2px;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface-2); color: var(--muted);
  font-size: 12.5px; font-weight: 700; cursor: pointer; transition: .16s;
}
.cells-toggle:hover { background: var(--surface-3); color: var(--text); }
.cells-toggle svg { width: 15px; height: 15px; transition: transform .25s ease; }

@media (max-width: 559px) {
  .cells-all, .cells-peek { gap: 9px 12px; }
  .cellrow-name { font-size: 9px; letter-spacing: .05em; }
  .cellrow-val { font-size: 11.5px; }
}

/* chart */
.chart-box { position: relative; width: 100%; }
/* stroke: none là BẮT BUỘC — quy tắc chung `svg { stroke: currentColor }` sẽ
   được các <rect> trong biểu đồ (vùng bắt chuột, cột dữ liệu) kế thừa và vẽ
   thành khung đen quanh đồ thị. Nét vẽ thật đều khai báo riêng từng phần tử. */
.chart-box svg { display: block; width: 100%; height: auto; overflow: visible; stroke: none; }
.chart-grid { stroke: var(--border); stroke-width: 1; stroke-dasharray: 2 6; }
.chart-axis { fill: var(--faint); font-size: 10px; font-family: var(--mono); stroke: none; }
/* Nhãn đơn vị ở đỉnh trục trái/phải của biểu đồ gộp hai đại lượng */
.chart-unit { fill: var(--muted); font-size: 10px; font-weight: 700; font-family: var(--mono); stroke: none; }
.chart-tip {
  position: absolute; z-index: 5; pointer-events: none;
  transform: translate(-50%, -128%);
  padding: 7px 11px; border-radius: 11px; white-space: nowrap;
  background: var(--surface); border: 1px solid var(--border);
  box-shadow: var(--shadow); font-size: 12.5px; line-height: 1.35;
}
.chart-tip b { display: block; font-family: var(--mono); font-size: 14px; font-weight: 800; }
.chart-tip span { color: var(--muted); font-size: 11px; font-family: var(--mono); }
.chart-legend { display: flex; gap: 16px; justify-content: center; margin-top: 10px; font-size: 12px; color: var(--muted); }
.chart-legend i { width: 9px; height: 9px; border-radius: 99px; display: inline-block; margin-right: 6px; }

/* Mục chú thích BẤM ĐƯỢC — bấm để tắt/bật đường tương ứng.
   Tắt rồi thì chấm màu rỗng ruột và cả mục mờ đi, để nhìn là biết đường đó
   đang ẩn chứ không phải biểu đồ mất dữ liệu. */
.leg-btn {
  display: inline-flex; align-items: center; gap: 0;
  border: 0; background: none; padding: 3px 7px; border-radius: 8px;
  font: inherit; color: inherit; cursor: pointer;
  transition: opacity .16s, background .16s;
}
.leg-btn:hover { background: var(--surface-2); }
.leg-btn i { transition: background .16s, box-shadow .16s; }
.leg-btn.off { opacity: .45; text-decoration: line-through; }
.leg-btn.off i { background: transparent !important; box-shadow: inset 0 0 0 2px var(--faint); }

/* Cặp ô đồ thị dùng chung một cụm Ngày/Tháng/Năm.
   Điện thoại: xếp dọc, ô Công suất & SOC nằm trước (đúng thứ tự trong DOM).
   Từ 1024px: hai cột bằng nhau. minmax(0,1fr) là bắt buộc — nếu không <svg>
   bên trong sẽ giữ bề ngang tự nhiên và làm tràn cột. */
.chart-pair { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr); margin-top: 14px; }
@media (min-width: 1024px) {
  .chart-pair { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; }
  .chart-pair.single { grid-template-columns: minmax(0, 1fr); }
}
.chart-pane { min-width: 0; }
.chart-pane + .chart-pane { border-top: 1px solid var(--border); padding-top: 14px; }
@media (min-width: 1024px) {
  .chart-pane + .chart-pane { border-top: 0; border-left: 1px solid var(--border); padding: 0 0 0 22px; }
}

/* Dòng ghi chú trong chú thích (vd "tổng 3 máy đang kết nối") — không phải nút
   bấm được như các mục kia nên để nhạt hơn và nghiêng, khỏi mời người ta bấm. */
.leg-note { font-size: 11.5px; color: var(--faint); font-style: italic; align-self: center; }
.date-nav { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; }
.date-nav button { width: 34px; height: 34px; border-radius: 10px; border: 1px solid var(--border); background: var(--surface); cursor: pointer; display: grid; place-items: center; color: var(--muted); flex: none; }
.date-nav button:disabled { opacity: .35; cursor: not-allowed; }
.date-nav button svg { width: 16px; height: 16px; }
.date-pill { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 99px; background: var(--surface-2); border: 1px solid var(--border); font-size: 13.5px; font-weight: 700; }

/* Nhãn nút kỳ hiện tại: bản dài cho màn rộng, bản ngắn cho điện thoại.
   Chỉ đổi chữ chứ không đổi cấu trúc, nên không phải vẽ lại khi xoay máy. */
.today-btn .lbl-short { display: none; }
@media (max-width: 559px) {
  .today-btn .lbl-full { display: none; }
  .today-btn .lbl-short { display: inline; }
}

/* nút "Hôm nay / Tháng này / Năm nay": sáng khi đang xem đúng kỳ hiện tại */
.date-nav .today-btn {
  width: auto; height: 34px; padding: 0 14px; border-radius: 10px;
  font-size: 13px; font-weight: 800; color: var(--muted);
}
.date-nav .today-btn.on {
  background: linear-gradient(100deg, var(--brand), var(--brand-2));
  border-color: transparent; color: #fff; box-shadow: 0 6px 16px -10px var(--brand);
}
.date-input {
  height: 34px; padding: 0 11px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
  font: inherit; font-size: 13.5px; font-weight: 700; outline: none;
}
.date-input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
.date-input.date-year { width: 92px; text-align: center; }
html[data-theme="dark"] .date-input::-webkit-calendar-picker-indicator { filter: invert(1) opacity(.6); }
/* Chế độ Tháng / Năm dùng thanh chọn gọn hơn */
.date-nav.compact button { width: 28px; height: 28px; }
.date-nav.compact button svg { width: 14px; height: 14px; }
.date-nav.compact .today-btn { width: auto; height: 28px; padding: 0 10px; font-size: 12px; }
.date-nav.compact .date-input { height: 28px; font-size: 12.5px; padding: 0 8px; }

/* Điện thoại: bóp gọn cụm điều khiển biểu đồ (Ngày/Tháng/Năm, chọn ngày, chọn
   đại lượng) để dành chỗ cho chính cái biểu đồ. */
@media (max-width: 559px) {
  .seg { padding: 3px; border-radius: 11px; }
  .seg button { padding: 6px 11px; font-size: 12px; border-radius: 9px; }

  /* Ép cả cụm nằm gọn MỘT hàng: ô chọn ngày co lại được, các nút thì không.
     Trước đây ô chọn giữ nguyên bề ngang nên nút ">" bị đẩy rớt xuống dưới. */
  .date-nav { gap: 5px; flex-wrap: nowrap; }
  .date-nav button { width: 28px; height: 30px; border-radius: 9px; flex: none; }
  .date-nav button svg { width: 14px; height: 14px; }
  .date-nav .today-btn { width: auto; height: 30px; padding: 0 9px; font-size: 11.5px; flex: none; }
  .date-input { height: 30px; font-size: 11.5px; padding: 0 6px; min-width: 0; flex: 1 1 auto; }
  .date-pill { padding: 6px 12px; font-size: 12px; }

  .chip { padding: 6px 11px; font-size: 11.5px; }
  .chart-legend { gap: 12px; font-size: 11px; margin-top: 7px; }
  .chart-hint { font-size: 10.5px; margin-top: 7px; }
}

/* Bộ ba ô số liệu đặt ngay trên biểu đồ (Tổng công suất / Sản lượng / SOC và
   Dung lượng còn / Số chu kỳ / Dung lượng). LUÔN 3 cột — trước đây dùng .c4
   nên trên điện thoại xuống thành 2+1, chiếm gần nửa màn hình và đẩy biểu đồ
   xuống dưới nếp gấp. */
.stat-trio { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
/* Bốn ô đếm ở đầu trang Tổng quan: LUÔN 1 hàng 4 cột. Trước dùng .c3 nên trên
   điện thoại xuống thành 2x2, chiếm gần trọn màn hình đầu tiên. */
.stat-quad { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
@media (max-width: 559px) {
  .stat-trio, .stat-quad { gap: 7px; }
  .stat-trio .stat, .stat-quad .stat { padding: 9px 8px; border-radius: 13px; }
  /* Giữ lại icon (bỏ hẳn thì mất dấu nhận biết nhanh), chỉ thu nhỏ lại */
  .stat-trio .stat-ico, .stat-quad .stat-ico {
    width: 22px; height: 22px; border-radius: 7px; margin-bottom: 5px;
  }
  .stat-trio .stat-ico svg, .stat-quad .stat-ico svg { width: 13px; height: 13px; }
  .stat-trio .stat-label, .stat-quad .stat-label { font-size: 8.5px; letter-spacing: .06em; line-height: 1.25; }
  .stat-trio .stat-value, .stat-quad .stat-value { font-size: 17px; margin-top: 3px; }
  .stat-trio .stat-value small, .stat-quad .stat-value small { font-size: 10px; }
  /* 4 cột trên màn hẹp thì chữ nhãn phải nhỏ hơn nữa mới không vỡ dòng xấu */
  .stat-quad { gap: 6px; }
  .stat-quad .stat { padding: 8px 5px; }
  .stat-quad .stat-label { font-size: 7.5px; letter-spacing: .02em; }
  .stat-quad .stat-value { font-size: 19px; }
  /* Dòng chú thích dưới ô (CỤM PIN / ĐANG XẢ / SOH…) — giữ nguyên, chỉ nhỏ lại.
     .stat-trio cũng cần: ba ô Tổng quan nay có dòng mẫu số ("/ 3 thiết bị")
     bên dưới, để cỡ chữ mặc định là tràn ngang trên màn hẹp. */
  .stat-quad .stat .small,
  .stat-trio .stat .small { font-size: 8.5px; line-height: 1.2; margin-top: 2px; }
}

.chart-num { font-family: var(--mono); font-size: 9.5px; font-weight: 700; stroke: none; }
.chart-hint { margin-top: 10px; text-align: center; font-size: 11.5px; color: var(--faint); line-height: 1.5; }

/* Chú thích cách dùng: gấp lại, bấm mới mở. Trước đây luôn hiện, chiếm 2–3
   dòng chữ nhỏ ngay dưới biểu đồ mà hầu như chỉ đọc một lần rồi thôi. */
.chart-help { margin-top: 8px; }
.chart-help summary {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  list-style: none; cursor: pointer; user-select: none;
  padding: 6px; border-radius: 10px;
  font-size: 11.5px; font-weight: 600; color: var(--faint); transition: .16s;
}
.chart-help summary::-webkit-details-marker { display: none; }
.chart-help summary:hover { color: var(--muted); background: var(--surface-2); }
.chart-help summary svg { width: 14px; height: 14px; flex: none; }
.chart-help[open] summary { color: var(--muted); }
.chart-help .chart-hint { margin-top: 4px; }
.chart-box { touch-action: pan-y; cursor: crosshair; }
/* Lớp bắt chạm phủ kín biểu đồ. Nằm dưới bảng giá trị (z-index 5) nhưng trên
   phần SVG, nhờ vậy mọi cú chạm đều rơi vào một phần tử KHÔNG bao giờ bị vẽ
   lại — xem chú thích ở _host() trong charts.js. */
.chart-hit {
  position: absolute; inset: 0; z-index: 4;
  touch-action: pan-y; cursor: inherit; background: transparent;
}
.chart-box.grabbing { cursor: grabbing; }
/* Đang ở chế độ đọc số (sau khi nhấn giữ 1 giây): viền sáng lên để người dùng
   biết mình đã vào chế độ dò, rê ngón sẽ chạy theo chứ không kéo biểu đồ nữa. */
.chart-box.reading {
  border-radius: 12px;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 45%, transparent);
}
/* ---------- 11. ALERTS ---------- */
.alert-row { display: flex; gap: 12px; padding: 14px; border-radius: var(--r); border: 1px solid var(--border); background: var(--surface-2); border-left: 3px solid var(--tone); }
.alert-row .al-ico { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; flex: none; background: var(--tone-soft); color: var(--tone); }
.alert-row .al-ico svg { width: 17px; height: 17px; }
.alert-row b { font-size: 14px; }
.alert-row p { font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.5; }
.alert-row .al-time { font-size: 11px; color: var(--faint); font-family: var(--mono); margin-top: 6px; }

/* ---------- 12. TOAST / SHEET ---------- */
.toast {
  position: fixed; left: 50%; bottom: 90px; transform: translate(-50%, 20px);
  z-index: 80; padding: 12px 18px; border-radius: 14px;
  background: var(--text); color: var(--surface); font-size: 13.5px; font-weight: 600;
  box-shadow: var(--shadow-lg); opacity: 0; pointer-events: none; transition: .25s;
  max-width: calc(100vw - 40px); text-align: center;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@media (min-width: 1024px) { .toast { bottom: 30px; } }

.sheet { position: fixed; inset: 0; z-index: 70; display: grid; align-items: end; background: rgba(4,7,15,.5); backdrop-filter: blur(3px); animation: fade .2s; }
@media (min-width: 760px) { .sheet { place-items: center; } }
.sheet-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-xl) var(--r-xl) 0 0; padding: 20px; width: 100%; max-height: 88vh; overflow: auto; box-shadow: var(--shadow-lg); animation: up .25s cubic-bezier(.2,.9,.3,1); }
@media (min-width: 760px) { .sheet-card { max-width: 480px; border-radius: var(--r-xl); } }
@keyframes fade { from { opacity: 0; } }
@keyframes up { from { transform: translateY(24px); opacity: .6; } }
.sheet-handle { width: 42px; height: 4px; border-radius: 99px; background: var(--border-strong); margin: -6px auto 14px; }

/* ---------- 13. MISC ---------- */
.back-btn { display: inline-flex; align-items: center; gap: 7px; padding: 8px 13px 8px 10px; border-radius: 11px; border: 1px solid var(--border); background: var(--surface); cursor: pointer; font-size: 13.5px; font-weight: 700; color: var(--muted); }
.back-btn:hover { color: var(--brand); }
.back-btn svg { width: 16px; height: 16px; }
.wallet {
  border-radius: var(--r-xl); padding: 20px; color: #fff;
  background: linear-gradient(125deg, #4f46e5, #7c3aed 45%, #0891b2);
  box-shadow: 0 22px 44px -24px rgba(79,70,229,.9);
}
.wallet .w-label { font-size: 11px; letter-spacing: .18em; font-weight: 800; opacity: .8; }
.wallet .w-val { font-family: var(--mono); font-size: 32px; font-weight: 800; margin-top: 4px; }
.wallet .w-plan { display: inline-flex; align-items: center; gap: 6px; margin-top: 14px; padding: 5px 12px; border-radius: 99px; background: rgba(255,255,255,.18); font-size: 12px; font-weight: 800; }
.fade-in { animation: fadeUp .32s cubic-bezier(.2,.9,.3,1) both; }
@keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } }

/* ---------- TRANG CÀI ĐẶT BMS ----------
   Mỗi dòng = tên thông số + địa chỉ thanh ghi + ô nhập + nút OK.
   Địa chỉ hiện ngay cạnh tên: khi đối chiếu với tài liệu Modbus của JK, biết
   ô nào ứng với thanh ghi nào là thứ cần nhất. */
.cfg-device {
  width: 100%; padding: 11px 13px; border-radius: 12px;
  border: 1px solid var(--border); background: var(--surface-2);
  color: var(--text); font: inherit; font-size: 14px; cursor: pointer;
}
/* Mỗi nhóm mang màu riêng qua --tone (bmsconfig.js đặt).
   Ba chỗ ăn màu đó: dải dọc bên trái, ô icon, và nền lúc nhóm đang mở. Bảy
   thẻ xám giống hệt nhau thì cuộn qua 42 dòng chẳng còn mốc nào để bám; một
   dải màu 4px ở mép trái là đủ để mắt định vị mà không ồn như tô cả thẻ. */
.cfg-group {
  padding: 0; overflow: hidden;
  border-left: 4px solid var(--tone, var(--border));
}
.cfg-head {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 16px 18px; border: 0; background: none; cursor: pointer; color: inherit;
  text-align: left; font: inherit;
}
.cfg-head:hover { background: var(--tone-soft, var(--surface-2)); }
.cfg-head h3 { font-size: 15px; font-weight: 700; }
/* Đang mở thì đầu nhóm tô nền nhạt màu của nhóm — nhìn từ xa biết ngay mục
   nào đang bung ra, khỏi phải dò mũi tên bé tí ở cuối hàng. */
.cfg-head[aria-expanded="true"] { background: var(--tone-soft, var(--surface-2)); }
.cfg-head[aria-expanded="true"] h3 { color: var(--tone, var(--text)); }
.cfg-ico { background: var(--tone-soft, var(--surface-2)); color: var(--tone, var(--muted)); }

/* Số thông số trong nhóm. Trả lời sẵn câu "bung ra thì có gì trong đấy". */
.cfg-count {
  flex: none; min-width: 22px; height: 20px; padding: 0 6px;
  border-radius: 99px; background: var(--tone-soft, var(--surface-3));
  color: var(--tone, var(--muted));
  font-size: 11px; font-weight: 800; font-family: var(--mono);
  display: grid; place-items: center;
}
.cfg-arrow { color: var(--faint); display: grid; place-items: center; transition: transform .22s ease; }
.cfg-arrow svg { width: 18px; height: 18px; }
.cfg-head[aria-expanded="true"] .cfg-arrow { transform: rotate(90deg); color: var(--tone, var(--faint)); }
.cfg-body { border-top: 1px solid var(--border); }

/* Dấu "chưa đọc" cạnh tên ô chọn — thay cho địa chỉ thanh ghi đã bỏ đi. */
.cfg-note {
  font-size: 10.5px; font-weight: 700; color: var(--faint);
  background: var(--surface-3); border-radius: 99px; padding: 1px 7px;
  display: inline-block; margin-top: 3px;
}

/* ---------- KHỐI ĐIỀU KHIỂN (Bộ nạp / Bộ xả) ----------
   Đứng đầu trang Cài đặt, trên mọi nhóm thông số. Đây là hai thao tác nặng
   nhất trên trang nên được dựng to hơn hẳn một dòng thông số: mỗi công tắc là
   một ô riêng, có nền và viền đổi màu theo trạng thái — bật là xanh, tắt là
   xám. Nhìn lướt qua biết ngay BMS đang cho nạp / cho xả hay không. */
.cfg-head-static { cursor: default; }
.cfg-head-static:hover { background: var(--tone-soft, var(--surface-2)); }
.ctl-grid {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px;
  padding: 14px 18px 4px;
}
@media (min-width: 620px) { .ctl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.ctl-item {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 14px; border-radius: 14px;
  border: 1px solid var(--border); background: var(--surface-2);
  transition: background .18s, border-color .18s;
}
.ctl-item.on { border-color: var(--ok); background: var(--ok-soft); }
.ctl-text { flex: 1; min-width: 0; }
.ctl-text b { font-size: 14.5px; font-weight: 800; display: block; }
.ctl-item.on .ctl-text b { color: var(--ok); }
.ctl-text p { font-size: 11.5px; color: var(--muted); margin-top: 2px; line-height: 1.35; }
.ctl-item .tgl.on { background: var(--ok); border-color: var(--ok); }
/* Đang chờ BMS trả lời: mờ đi và không nhận bấm nữa. Lệnh mất vài giây mới
   có xác nhận, không khoá lại thì rất dễ bấm dồn hai ba lần. */
.ctl-item .tgl.busy { opacity: .5; pointer-events: none; }
.ctl-item .tgl:disabled { opacity: .4; cursor: not-allowed; }
.ctl-note { padding: 4px 18px 16px; }

/* Dải nhắc khi hai công tắc bị ngưỡng an toàn khoá lại. */
.ctl-lock {
  display: flex; align-items: flex-start; gap: 9px;
  margin: 14px 18px 0; padding: 10px 12px; border-radius: 12px;
  background: var(--danger-soft); color: var(--danger);
  border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
  font-size: 12.5px; line-height: 1.55; font-weight: 600;
}
.ctl-lock-ico { display: grid; place-items: center; flex: none; margin-top: 1px; }
.ctl-lock-ico svg { width: 15px; height: 15px; }
.ctl-item.locked { opacity: .55; }

/* ---------- HÀNG CẦN GẠT MỚI: Emergency / LCD Always On / loại pin ----------
   Dùng LẠI đúng `.ctl-item`/`.tgl` của Bộ nạp — Bộ xả phía trên (cùng phong
   cách cần gạt, không dựng thẻ riêng).

   MỘT CỘT trên điện thoại, hai cột từ 620px — giống hệt `.ctl-grid` của Bộ
   nạp/Bộ xả ngay trên nó.

   Bản trước ép hai cột xuống tận 340px. Lúc đó khối này chỉ có 5 ô với nhãn
   ngắn (Emergency, LCD Always On, LFP, LTO, NCM/NCA) nên còn vừa. Giờ có 11
   ô, nhiều ô nhãn dài ("Tắt giới hạn dòng song song", "Tắt cảm biến nhiệt
   pin"), mà một cột ở màn 360px chỉ còn ~150px — trừ cần gạt và padding thì
   cột chữ còn chưa tới 90px, nên chữ bị bẻ giữa từ: "Emergenc/y",
   "Alway/s On". Đọc ra thành chữ khác hẳn.

   Một cột thì mỗi ô được trọn bề ngang, nhãn nằm đúng một dòng. Đổi lại phải
   cuộn dài hơn — đánh đổi đúng hướng: đây là các công tắc ghi thẳng xuống
   BMS, đọc nhầm nhãn tốn kém hơn nhiều so với cuộn thêm vài dòng. */
.ctl-grid2 {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px;
  padding: 4px 18px 4px;
}
@media (min-width: 620px) { .ctl-grid2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.ctl-span2 { grid-column: 1 / -1; }
/* `min-width: 0` vẫn cần: thiếu nó thì ô lưới không co lại được và một nhãn
   dài sẽ đẩy tràn cả hàng. Còn ngắt dòng thì dùng `break-word` chứ KHÔNG
   dùng `anywhere` — `anywhere` cho phép bẻ giữa từ ngay cả khi vẫn còn chỗ
   xuống dòng bình thường, đúng thứ đã đẻ ra "Emergenc/y". */
.ctl-grid2 .ctl-item { min-width: 0; max-width: 100%; }
.ctl-grid2 .ctl-text b, .ctl-grid2 .ctl-text p { overflow-wrap: break-word; }
/* Có hỗ trợ thật nhưng CHƯA đọc được thanh ghi (Emergency/LCD Always On lúc
   mới vào trang) — viền đứt nét để KHÔNG lẫn với trạng thái "tắt" (viền liền,
   biết chắc là đang tắt): đây là "chưa biết gì cả", nói khác đi là nói dối.
   Nhóm loại pin không bao giờ ở trạng thái này — xem NUT_TOGGLE. */
.ctl-grid2 .ctl-item.pending { border-style: dashed; }
.ctl-badge2 {
  margin-top: 4px; display: inline-block;
  font-size: 10px; font-weight: 800; letter-spacing: .02em;
  padding: 2px 7px; border-radius: 99px;
  background: var(--surface-3); color: var(--faint);
}
.ctl-badge2.ok { background: var(--ok-soft); color: var(--ok); }

/* Ba nút loại pin (LFP/LTO/NCM-NCA) — NÚT HÀNH ĐỘNG (`role="button"`), không
   phải cần gạt: bấm là gửi lệnh áp cấu hình, BMS không có gì để đọc lại nên
   không dùng `role="switch"` (ngụ ý một trạng thái bật/tắt đọc lại được).
   `.ctl-item.ctl-actbtn` chỉ là khung định vị trong lưới — bỏ hết
   padding/viền của `.ctl-item` gốc, để MỘT mình `.ctl-actbtn-inner` (chính
   là thẻ <button>) làm bề mặt bấm được, giữ đúng vùng chạm/focus là MỘT khối
   liền, không phải div bọc ngoài không tương tác + nút con lệch. */
.ctl-item.ctl-actbtn { padding: 0; border: 0; background: none; }
.ctl-actbtn-inner {
  width: 100%; display: flex; align-items: center; gap: 12px;
  padding: 13px 14px; border-radius: 14px;
  border: 1px solid var(--border); background: var(--surface-2);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  transition: background .18s, border-color .18s;
}
.ctl-item.ctl-actbtn.on .ctl-actbtn-inner { border-color: var(--ok); background: var(--ok-soft); }
.ctl-item.ctl-actbtn.on .ctl-actbtn-inner .ctl-text b { color: var(--ok); }
.ctl-actbtn-inner:disabled { opacity: .45; cursor: not-allowed; }
.ctl-actbtn-inner.busy { opacity: .6; pointer-events: none; }

/* Ba ô trạng thái MOS ở trang thiết bị KHÔNG còn là nút bấm — bỏ con trỏ tay
   và hiệu ứng rê chuột, để không ai tưởng bấm được. */
.sw-row .sw { cursor: default; }

/* KHÔNG xuống dòng: tên, ô nhập và nút OK luôn nằm chung một hàng ngang, kể
   cả trên điện thoại. Tên dài thì tự co lại (và xuống dòng BÊN TRONG cột tên),
   còn cụm điều khiển giữ nguyên bề ngang nên các ô nhập của mọi dòng thẳng
   thành một cột. */
.cfg-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: nowrap;
  padding: 12px 18px; border-bottom: 1px solid var(--border);
}
.cfg-row:last-child { border-bottom: 0; }
.cfg-row:hover { background: var(--surface-2); }
.cfg-name { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.cfg-name b { font-size: 13.5px; font-weight: 600; }
.cfg-reg { font-size: 10.5px; color: var(--faint); letter-spacing: .04em; }
.cfg-ctl { display: flex; align-items: center; gap: 8px; flex: none; }

/* Không còn cột "Hiện tại" và không còn hàng tiêu đề cột: số BMS đang giữ được
   điền THẲNG vào ô nhập, nên hai thứ đó chỉ lặp lại chính nó. */

/* Thanh cuối trang: mốc thời gian đọc + MỘT nút Cập nhật cho cả trang.
   Trước đây khối này lặp ở chân từng nhóm; nay là một thẻ đứng riêng nên tự có
   viền và bo góc của .card, không cần border-top giả làm vách ngăn nữa. */
.cfg-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 14px 18px;
}
.cfg-refresh { flex: none; }
.cfg-refresh svg { width: 15px; height: 15px; }
.cfg-refresh:disabled { opacity: .6; cursor: default; }

/* Hàng có DANH SÁCH CHỌN (giao thức truyền thông). Tên giao thức dài — ép vào
   bề ngang 104px của ô số thì chỉ đọc được vài chữ đầu — nên hàng này cho cụm
   điều khiển giãn ra và xuống dòng riêng trên màn hình hẹp. */
.cfg-row-sel { flex-wrap: wrap; }
.cfg-row-sel .cfg-name { flex: 1 1 100%; min-width: 0; }
/* min-width:0 ở CẢ hai cấp là bắt buộc. Bề rộng tối thiểu tự động của một
   <select> bằng đúng lựa chọn có chữ DÀI NHẤT — ở đây là
   "007 — Voltronic Inverter and BMS 485 (20200325)", khoảng 360px — nên thiếu
   nó thì cụm điều khiển phình ra rộng hơn cả hàng và nút OK văng khỏi màn hình
   điện thoại. */
.cfg-row-sel .cfg-ctl  { flex: 1 1 100%; min-width: 0; margin-top: 8px; }
.cfg-select {
  flex: 1 1 0; min-width: 0; width: 100%;
  height: 38px; padding: 0 10px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); color: var(--text);
  font: inherit; font-size: 13px; cursor: pointer;
  text-overflow: ellipsis;
}
.cfg-select:focus { border-color: var(--brand); outline: none; }
@media (min-width: 760px) {
  .cfg-row-sel .cfg-name { flex: 0 0 auto; }
  .cfg-row-sel .cfg-ctl  { flex: 1 1 320px; margin-top: 0; }
}

.cfg-input {
  display: flex; align-items: center; gap: 6px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); padding: 0 4px 0 10px; height: 38px;
}
.cfg-input:focus-within { border-color: var(--brand); }
.cfg-input input {
  width: 104px; border: 0; background: none; color: var(--text);
  font-family: var(--mono); font-size: 13.5px; outline: none; padding: 0;
  text-align: right;
}
/* Spinner mặc định của trình duyệt bị bỏ: nó chỉ hiện khi rê chuột vào ô, trên
   điện thoại thì không có, và bề rộng mỗi hãng một khác. Dùng cặp nút riêng
   bên dưới để lúc nào cũng thấy và bấm được bằng ngón tay. */
.cfg-input input::-webkit-outer-spin-button,
.cfg-input input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.cfg-input input[type=number] { -moz-appearance: textfield; }
.cfg-input input.bad { color: var(--danger); }

/* Cặp mũi tên lên/xuống, xếp chồng vừa đúng chiều cao ô nhập */
.cfg-spin { flex: none; display: flex; flex-direction: column; gap: 1px; }
.cfg-step {
  width: 22px; height: 16px; display: grid; place-items: center;
  border: 0; border-radius: 5px; background: none; padding: 0;
  color: var(--faint); cursor: pointer; transition: .14s;
}
.cfg-step:hover { background: var(--surface-3); color: var(--brand); }
.cfg-step:active { background: var(--brand-soft); }
/* Mượn luôn icon chevR rồi xoay, khỏi thêm hai icon gần như y hệt vào bộ icon */
.cfg-step svg { width: 13px; height: 13px; }
.cfg-step.up svg   { transform: rotate(-90deg); }
.cfg-step.down svg { transform: rotate(90deg); }

/* Hàng đang có số sửa dở, chưa bấm OK. Phải khác hẳn hàng thường: cùng một ô
   vừa hiện số BMS đang giữ vừa nhận số mới, nhìn nhầm là ghi nhầm thanh ghi. */
.cfg-row.dirty { background: var(--warn-soft); }
.cfg-row.dirty .cfg-input { border-color: var(--warn); }
.cfg-row.dirty .cfg-input input { color: var(--warn); font-weight: 700; }
.cfg-row.dirty .cfg-ok {
  background: var(--brand); border-color: var(--brand); color: #fff;
}

/* ---------- CÀI ĐẶT Ở CHẾ ĐỘ CHỈ XEM ----------
   Số vẫn đọc rõ, chỉ khung ô lùi lại: viền nhạt, nền chìm, con trỏ không đổi
   thành dấu nháy. Cố tình KHÔNG làm mờ chữ — người dùng vào đây là để đọc
   đúng mấy con số này. */
.cfg-locked .cfg-input {
  background: var(--surface-2); border-style: dashed;
}
.cfg-locked .cfg-input input,
.cfg-locked .cfg-select { cursor: default; }
.cfg-locked .cfg-select { opacity: 1; color: var(--text); background: var(--surface-2); }
.cfg-locked .cfg-row:hover { background: transparent; }

.cfg-unlock { border-color: color-mix(in srgb, var(--warn) 40%, var(--border)); }
.cfg-unlock .field { margin: 0; flex: 1 1 190px; min-width: 0; }
.cfg-unlock-row {
  display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin-top: 12px;
}
.cfg-unlock-row .btn { height: 44px; flex: none; }
/* Ô đơn vị rộng cố định: không thì "cell" và "°C" đẩy ô nhập lệch nhau vài
   pixel, 42 dòng nhìn xuống thấy cột số răng cưa. */
.cfg-unit {
  font-size: 11.5px; color: var(--muted); font-family: var(--mono);
  flex: none; width: 30px; text-align: left;
}
.cfg-ok {
  min-width: 52px; height: 38px; border-radius: 10px; flex: none;
  border: 1px solid var(--border); background: var(--surface-2);
  color: var(--muted); font-size: 13px; font-weight: 700; cursor: pointer;
  transition: .16s;
}
.cfg-ok:hover:not(:disabled) { background: var(--brand); border-color: var(--brand); color: #fff; }
.cfg-ok:disabled { opacity: .6; cursor: default; }
.cfg-ok.ok   { background: var(--ok);     border-color: var(--ok);     color: #fff; opacity: 1; }
.cfg-ok.fail { background: var(--danger); border-color: var(--danger); color: #fff; opacity: 1; }

/* Màn hẹp chỉ thu nhỏ cụm điều khiển lại, tuyệt đối không cho nó rớt xuống
   hàng dưới — rớt xuống thì mỗi thông số chiếm hai hàng, cuộn mãi không hết. */
@media (max-width: 559px) {
  .cfg-head, .cfg-row { padding-left: 14px; padding-right: 14px; gap: 8px; }
  .cfg-input { padding: 0 3px 0 8px; height: 36px; gap: 4px; }
  .cfg-input input { width: 62px; font-size: 12.5px; }
  .cfg-unit { width: 22px; font-size: 10.5px; }
  /* Cặp mũi tên KHÔNG thu nhỏ theo màn: đây là chỗ bấm bằng ngón tay, thu lại
     là bấm trượt. Chỗ trống nhường ra lấy từ ô nhập bên cạnh. */
  .cfg-ok { min-width: 38px; height: 36px; font-size: 12px; }
  .cfg-name b { font-size: 12.5px; }
  .cfg-foot { padding: 12px 14px; }
}

/* =========================================================
   CẬP NHẬT FIRMWARE ESP32 (tab Cập nhật) — xem public/js/ota.js
   ========================================================= */

/* Hai cột trên màn rộng, một cột trên điện thoại. Dùng auto-fit + minmax
   chứ không phải một @media riêng: ô "Đang chạy / Bản mới nhất" chứa chuỗi
   phiên bản dài ngắn khác nhau, để trình duyệt tự quyết khi nào xuống dòng
   thì không bao giờ có chuyện chữ tràn ra ngoài thẻ. */
.ota-grid {
  display: grid; gap: 10px; margin-top: 12px;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.ota-grid > div {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 9px 11px; min-width: 0;
}
.ota-grid span { display: block; margin-bottom: 2px; }
/* Chuỗi phiên bản không có dấu cách nên phải cho phép ngắt giữa từ, nếu
   không nó đẩy rộng cả ô lưới ra và làm tràn ngang trên điện thoại. */
.ota-grid b { font-size: 13.5px; overflow-wrap: anywhere; }

.ota-note {
  margin-top: 12px; padding: 10px 12px;
  border-radius: var(--r-sm);
  background: var(--info-soft); border: 1px solid var(--border);
}

.ota-prog { margin-top: 12px; }

/* HÀNG TÊN PHIÊN BẢN: [tên · mũi tên] · huy hiệu kênh.
   ---------------------------------------------------------------------------
   Mũi tên BUỘC phải nằm cùng hàng với tên bản, nên nó nằm chung một cụm
   `.ota-ver-name` với tên và cụm đó không bao giờ ngắt. Ô lưới này hẹp (hai
   cột trên điện thoại, còn ~115px cho nội dung), nên khi chật thì HUY HIỆU
   kênh mới là thứ được phép xuống dòng — không phải mũi tên, và tuyệt đối
   không phải chuỗi phiên bản.

   Trước đây cả ba nằm chung một <b> với `overflow-wrap: anywhere`: mũi tên bị
   đẩy xuống dòng riêng, còn "sg-1.1.9" thì bị bẻ đôi thành "sg-" / "1.1.9" —
   một chuỗi phiên bản bẻ đôi thì đọc ra số khác hẳn. */
.ota-ver-line { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
.ota-ver-name { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.ota-ver-name > b { font-size: 13.5px; white-space: nowrap; }
.ota-ver-line .badge { flex: none; }

.ota-other-toggle {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px;
  padding: 0; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--muted); cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.ota-other-toggle:hover { background: var(--surface-2); color: var(--text); }
.ota-other-toggle svg { width: 14px; height: 14px; }

/* HỘP CHỌN PHIÊN BẢN (openSheet) — mỗi dòng: tên · dung lượng · ngày. */
.ota-pick-list { display: grid; gap: 6px; }
.ota-pick-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 11px 12px; margin: 0; text-align: left; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface); font: inherit; color: inherit;
}
.ota-pick-row:hover { background: var(--surface-2); }
.ota-pick-row.on { border-color: var(--ok); background: var(--ok-soft); }
/* Bản KHÔNG cài được vẫn CHỌN được — chỉ làm nhạt tên đi, không khoá nút.
   Khoá thì máy đang chạy bản X sẽ không chọn lại được chính bản X, và ai lỡ
   chọn sang bản khác là kẹt luôn. Xem chú thích ở moHopChonBan() trong
   views.js. */
.ota-pick-row.mo .ota-pick-main b { color: var(--muted); font-weight: 600; }
.ota-pick-main { flex: 1; min-width: 0; }
.ota-pick-main b { display: block; font-size: 14px; overflow-wrap: anywhere; }
.ota-pick-meta { display: block; margin-top: 2px; font-size: 12px; color: var(--muted); }
/* Lý do KHÔNG cài được — nói ngay tại dòng đó, không để người dùng chọn
   xong mới biết. */
.ota-pick-why { display: block; margin-top: 2px; font-size: 12px; color: var(--warn); }
.ota-pick-tick { flex: none; color: var(--ok); display: inline-flex; }
.ota-pick-tick svg { width: 18px; height: 18px; }

/* ---------------------------------------------------------
   HÀNG CẢNH BÁO GỌN — [icon nhỏ] [chữ]
   ---------------------------------------------------------
   `svg { width: 18px; height: 18px }` ở đây KHÔNG phải chuyện thẩm mỹ, nó
   sửa một lỗi hiển thị thật.

   `ic()` trong views.js sinh ra `<svg viewBox="0 0 24 24">` KHÔNG kèm
   width/height, và quy tắc chung `svg { … }` ở đầu tệp này cũng không đặt
   kích thước — nó chỉ đặt màu nét. Mọi icon khác trong dự án đều được một
   quy tắc RIÊNG cho kích thước: `.btn svg`, `.li-ico svg`, `.stat-ico svg`,
   `.nav-item svg`… Khối cảnh báo cũ của tab Cập nhật thì nằm trong một
   `<p class="small">` trần, không quy tắc nào khớp, nên trình duyệt rơi về
   kích thước mặc định của phần tử thay thế — 300×150 px. Đó là cái tam giác
   khổng lồ chiếm gần hết thẻ.

   Bài học đi kèm: THÊM ICON VÀO CHỖ MỚI THÌ PHẢI ĐẶT KÍCH THƯỚC CHO NÓ.

   `align-items: center` cho chữ căn giữa theo chiều dọc so với icon.
   `flex: none` giữ icon đúng 18px kể cả khi chữ dài — không có nó, flexbox
   co icon lại thành một vệt méo trên màn hẹp.
   Không `flex-wrap`: trên điện thoại vẫn đúng MỘT hàng, chữ tự xuống dòng
   bên trong cột của nó chứ không đẩy icon xuống dưới. */
.ota-alert {
  display: flex; align-items: center; gap: 9px;
  margin-top: 10px; padding: 8px 10px;
  border-radius: var(--r-sm);
  background: var(--tone-soft, var(--warn-soft));
  border: 1px solid var(--border);
}
.ota-alert svg {
  width: 18px; height: 18px; flex: none;
  stroke-width: 2; color: var(--tone, var(--warn));
}
/* Có dòng phụ thì icon đi cùng DÒNG TIÊU ĐỀ, không trôi xuống giữa khối.
   Dùng một class rõ ràng thay vì `:has(p)` — hành vi giống nhau trên mọi
   trình duyệt, và đọc mã cũng thấy ngay là có hai kiểu. */
.ota-alert.has-sub { align-items: flex-start; }
.ota-alert.has-sub svg { margin-top: 1px; }

.ota-alert-txt { min-width: 0; flex: 1; }
.ota-alert-txt b {
  display: block; font-size: 12.5px; line-height: 1.35;
  color: var(--tone, var(--warn)); overflow-wrap: anywhere;
}
/* Dòng chi tiết: nhỏ hơn, màu chữ phụ chứ không phải màu cảnh báo — để mắt
   đọc tiêu đề trước, và để hai dòng không chọi nhau về màu. */
.ota-alert-txt p {
  margin-top: 2px; font-size: 11.5px; line-height: 1.4;
  color: var(--muted); overflow-wrap: anywhere;
}

.ota-bar {
  height: 6px; border-radius: 99px; overflow: hidden;
  background: var(--surface-3); margin-top: 8px;
}
/* Chuyển mượt vì tiến độ nhảy từng bậc chứ không chạy liên tục — để nguyên
   thì thanh giật cục trông như đang treo.
   LƯU Ý: thanh CÓ phần trăm này KHÔNG dùng cho lúc tải. Trong lúc tải,
   firmware không gửi tiến độ lên Firebase (xem sgOtaNetworkBusy() trong
   sg_ota.h) nên web dùng `.ota-bar.indet` ở dưới. */
.ota-bar i { display: block; height: 100%; border-radius: 99px; transition: width .5s ease; }

/* ---------------------------------------------------------
   THANH KHÔNG XÁC ĐỊNH — dùng cho trạng thái `downloading`
   ---------------------------------------------------------
   Trong lúc tải, firmware giữ đúng MỘT kết nối HTTPS tới máy chủ phát hành
   và không mở thêm phiên Firebase nào, nên web không hề nhận được phần trăm.
   Vẽ một thanh 0% đứng yên ở đây là nói dối theo cách dễ hiểu lầm nhất —
   trông y như thiết bị treo ngay từ giây đầu tiên. Một vệt chạy qua lại nói
   đúng thứ ta thực sự biết: đang chạy, chưa rõ tới đâu. */
.ota-bar.indet i { width: 40%; transition: none; animation: ota-indet 1.4s ease-in-out infinite; }
@keyframes ota-indet {
  0%   { margin-left: -40%; }
  100% { margin-left: 100%; }
}
/* Tôn trọng người đã tắt hiệu ứng chuyển động: giữ một vệt tĩnh mờ, vẫn khác
   hẳn một thanh 0% rỗng nên không bị đọc nhầm thành "đang treo". */
@media (prefers-reduced-motion: reduce) {
  .ota-bar.indet i { animation: none; width: 100%; opacity: .45; margin-left: 0; }
}

/* Hai con số hai đầu: "đã tải / tổng" bên trái, phần trăm bên phải. */
.ota-prog-num {
  display: flex; justify-content: space-between; gap: 8px;
  margin-top: 4px; font-size: 11.5px; color: var(--muted);
}
