/* XME Console — thành phần giao diện. Không Tailwind, không icon font.
   Chuyển động: chỉ một hiệu ứng mờ dần cho toast/hộp thoại. Không nâng khi rê
   chuột, không đổ bóng động, không skeleton nhấp nháy. */

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font);
  font-size: var(--fs-base);
  line-height: 1.5;
  color: var(--text);
  background: var(--page);
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3 { margin: 0; font-weight: 600; }
h1 { font-size: var(--fs-title); }
h2 { font-size: var(--fs-head); }
/* Vòng focus.
   Ô nhập / dropdown dùng `box-shadow` chứ KHÔNG dùng `outline`. Lý do: outline
   vẽ bán kính bằng border-radius + outline-offset, nên khi có offset thì vòng
   bo góc rộng hơn control và trông như một khung rời lơ lửng bên ngoài — đúng
   thứ vừa thấy. box-shadow bám chính xác border-radius nên không bao giờ lệch.
   Đổi luôn màu viền để vòng đọc ra là một phần của control.

   Nút và link vẫn dùng outline + `:focus-visible`: chúng có thể không có viền,
   và bấm chuột xong mà còn đọng vòng thì rối.

   Khai tường minh cho `<select>` là bắt buộc: dù đã `appearance: none`, Chrome
   vẫn tự vẽ vòng focus mặc định của hệ điều hành đè lên — dày, màu accent của
   máy, mỗi máy một kiểu. */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

.input:focus, .select:focus, textarea:focus,
input[type="date"]:focus, input[type="number"]:focus {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px var(--focus-ring);
}
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible {
  outline: 2px solid var(--focus); outline-offset: 1px;
}

.mono { font-family: var(--font-mono); font-size: var(--fs-small); }
.num  { font-variant-numeric: tabular-nums; }
.muted { color: var(--text-3); }
.sec   { color: var(--text-2); }
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* ── Vỏ ───────────────────────────────────────────────────────────────── */
.shell { display: flex; min-height: 100vh; min-height: 100dvh; }

.sidebar {
  flex: 0 0 216px; background: var(--surface); border-right: 1px solid var(--border);
  display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; height: 100dvh;
}
.sidebar-brand {
  display: flex; align-items: center; gap: 9px;
  padding: 14px 16px; border-bottom: 1px solid var(--rule);
  font-weight: 600; letter-spacing: -0.01em;
}
/* ── Nhãn môi trường ───────────────────────────────────────────────────────
   Không chỉ là màu: mỗi nhãn có CHỮ riêng, để người mù màu và ảnh chụp màn hình
   đen trắng vẫn đọc được. Bản PROD thêm một vạch đỏ mảnh trên đầu sidebar — nhìn
   lướt qua từ xa là biết mình đang ở đâu, chưa cần đọc chữ. */
.env-badge {
  font-size: 10px; font-weight: 600; letter-spacing: .02em;
  padding: 2px 6px; border-radius: 4px; white-space: nowrap;
  border: 1px solid transparent;
}
.env-badge.env-test    { background: var(--warn-bg); color: var(--warn-text); border-color: var(--warn-text); }
.env-badge.env-prod    { background: var(--bad-bg);  color: var(--bad-text);  border-color: var(--bad-text); }
.env-badge.env-unknown { background: var(--surface-3); color: var(--text-2); border-color: var(--border-ctrl); }

/* Nút nhảy sang console còn lại. Cỡ nhỏ và nằm sát nhãn môi trường: nó nói VỀ cái
   nhãn đó, không phải một mục điều hướng ngang hàng với sidebar.
   Chiều sang máy thật tô đỏ như chính nhãn PROD — màu ở đây mang nghĩa, không phải
   trang trí. */
.env-switch {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; flex: 0 0 auto; padding: 0;
  color: var(--text-3); background: transparent;
  border: 1px solid var(--border-ctrl); border-radius: 5px; cursor: pointer;
}
.env-switch:hover { color: var(--text); background: var(--surface-2); }
.env-switch.to-prod { color: var(--bad-text); border-color: var(--bad-text); }
.env-switch.to-prod:hover { background: var(--bad-bg); }

/* Vạch cảnh báo chỉ ở bản PROD — chỗ duy nhất bấm nhầm là trả giá thật. */
.shell.env-prod .sidebar { box-shadow: inset 0 3px 0 0 var(--bad-text); }

.sidebar-brand { flex-wrap: wrap; }
/* Sidebar thu hẹp (<1150px) giấu chữ trong brand, nhưng nhãn môi trường PHẢI ở
   lại — xem `:not(.env-badge)` trong khối media ở cuối file. */

/* Ô vuông bo góc chứa dấu hiệu XME. Trước đây là chữ "XME"; đổi sang chính dấu
   hiệu thương hiệu cho khớp favicon và các sản phẩm khác. Kích thước cố định để
   không nhảy theo cỡ chữ của tiêu đề bên cạnh. */
.sidebar-brand .tag, .login-brand .tag {
  width: 22px; height: 22px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 6px;
  background: var(--accent); color: var(--on-accent);
}
.nav { padding: 8px; overflow-y: auto; flex: 1; }
.nav-group {
  padding: 12px 8px 5px; font-size: var(--fs-label); font-weight: 600;
  color: var(--text-3); letter-spacing: .02em;
}
.nav a {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 9px; margin: 1px 0; border-radius: var(--r-ctrl);
  color: var(--text-2); text-decoration: none; font-size: var(--fs-table);
}
.nav a:hover { background: var(--surface-2); color: var(--text); }
.nav a.active { background: var(--surface-3); color: var(--text); font-weight: 500; }
.nav a svg { flex: none; width: 17px; height: 17px; }
.sidebar-foot { padding: 9px 12px; border-top: 1px solid var(--rule); }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.main-inner { width: 100%; max-width: var(--shell-max); margin-inline: auto; padding: 18px 22px 40px; }

.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 14px; flex-wrap: wrap; margin-bottom: 16px;
}
.page-head p { margin: 3px 0 0; color: var(--text-3); font-size: var(--fs-small); }

/* Biến thể: hàng trên là H1 + một cụm điều khiển canh phải, mô tả xuống hàng dưới
   chạy hết bề ngang.
   Cần biến thể riêng vì `.page-head` mặc định coi (tiêu đề + mô tả) là MỘT khối:
   mô tả của trang Nhật ký rộng 504px, hàng tab 648px, cộng lại vượt 1020px nên
   hàng tab bị đẩy xuống dòng và mất luôn chỗ canh phải. Tách mô tả ra khỏi hàng
   trên thì chỉ còn H1 (~90px) đứng cùng, thừa chỗ. */
.page-head.split { flex-direction: column; align-items: stretch; gap: 6px; }
.page-head.split .ph-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
}
.page-head.split p { margin: 0; }

/* ── Điều khiển ───────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: var(--h-ctrl); padding: 0 13px;
  font-family: inherit; font-size: var(--fs-table); font-weight: 500;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--border-ctrl); border-radius: var(--r-ctrl);
  cursor: pointer; white-space: nowrap;
}
.btn:hover { background: var(--surface-2); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn svg { width: 16px; height: 16px; flex: none; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-danger { color: var(--bad-text); border-color: var(--bad-text); background: var(--surface); }
.btn-danger:hover { background: var(--bad-bg); }
/* Lớp điều khiển nhỏ thống nhất 30px: .btn-sm, .select-sm, .pgbtn và các ô nhập
   nội dòng đều dùng chung con số này. Trước đó .btn-sm để 28px nên nút đứng cạnh
   ô nhập bị lệch 2px — nhìn thì tưởng nút cao hơn, thực ra là ô nhập cao hơn. */
.btn-sm { height: 30px; padding: 0 10px; font-size: var(--fs-small); }
.btn-icon { width: var(--h-ctrl); padding: 0; }
/* Nút chỉ có icon, không viền — dùng cho thao tác phụ nằm cạnh nội dung (sửa,
   copy). Có viền thì mỗi dòng lại thêm một hộp, bảng nhìn rối. */
.btn-plain {
  border-color: transparent; background: transparent;
  width: 26px; height: 26px; padding: 0; flex: none; color: var(--text-3);
}
.btn-plain:hover { background: var(--surface-2); color: var(--text); }
.btn-plain svg { width: 15px; height: 15px; }
.btn-plain-xs { width: 22px; height: 22px; }
.btn-plain-xs svg { width: 13px; height: 13px; }

.input, .select {
  height: var(--h-ctrl); padding: 0 10px;
  font-family: inherit; font-size: var(--fs-table); color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-ctrl); border-radius: var(--r-ctrl);
  min-width: 0;
}
.input::placeholder { color: var(--text-3); }
/* Ô bị khoá vẫn phải ĐỌC được — chỉ giảm nhẹ và đổi nền, không làm mờ tới mức
   không thấy giá trị. Người dùng cần biết con số đang là bao nhiêu trước khi
   quyết định có bật lên sửa hay không. */
.input:disabled, .select:disabled {
  background: var(--surface-2); color: var(--text-3); cursor: not-allowed;
}
.select { padding-right: 26px; cursor: pointer;
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 12px center, right 7px center;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}
.field { display: flex; flex-direction: column; gap: 5px; }
.field > label { font-size: var(--fs-label); font-weight: 500; color: var(--text-2); }

/* Cả dòng nhãn là vùng bấm, không phải mỗi ô 16px */
.check {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 8px; margin: -6px -8px; border-radius: var(--r-ctrl);
  cursor: pointer; font-size: var(--fs-table); user-select: none;
}
.check:hover { background: var(--surface-2); }
.check input { width: 16px; height: 16px; margin: 0; accent-color: var(--accent); cursor: pointer; flex: none; }

.toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
.grow { flex: 1 1 200px; }
/* Ô tìm firmware nằm chung hàng với tab nên KHÔNG nuốt hết chỗ trống như `.grow`.
   Rộng vừa đủ chữ gợi ý: đo thật bằng canvas ở 14px system-ui = 300px chữ + 22px
   padding/viền = 322px. Rộng hơn chỉ là khoảng trắng, hẹp hơn thì chữ gợi ý bị
   cắt — mà nó chính là thứ nói cho người dùng biết ô này tìm được những gì.
   Vẫn cho co (flex-shrink 1) để màn hẹp không đẩy nút xuống hàng. */
.fw-search { flex: 0 1 322px; min-width: 0; }
/* Màn hẹp: hàng tab không đủ chỗ cho cả ba, cho thanh công cụ xuống hàng RIÊNG
   nguyên khối thay vì để nó vỡ vụn từng nút một. */
@media (max-width: 1100px) {
  .card-head > .toolbar { flex: 1 1 100%; justify-content: flex-start !important; }
}

/* ── Thẻ & ô số ───────────────────────────────────────────────────────── */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-card); overflow: hidden;
}
.card-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 14px; border-bottom: 1px solid var(--rule); flex-wrap: wrap;
}
.card-body { padding: 14px; }

.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(142px, 1fr)); gap: 10px; margin-bottom: 16px; }
.tile {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-card); padding: 8px 11px;
}
.tile .k { font-size: var(--fs-label); color: var(--text-2); line-height: 1.35; }
.tile .v { font-size: 21px; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums; }
/* Nhãn phụ giữ trên MỘT dòng: một ô xuống dòng là cả hàng cao lên theo, nhìn
   so le. Dài quá thì cắt bằng ellipsis, chữ đầy đủ nằm ở title. */
.tile .s {
  font-size: var(--fs-label); color: var(--text-3); line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tile.is-warn { border-color: var(--warn-text); }
.tile.is-bad  { border-color: var(--bad-text); }
.tile.is-warn .v { color: var(--warn-text); }
.tile.is-bad  .v { color: var(--bad-text); }

/* ── Bảng ─────────────────────────────────────────────────────────────── */
.tw { overflow-x: auto; }
table.tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-table); }
table.tbl th {
  position: sticky; top: 0; z-index: 1;
  text-align: left; padding: 8px 12px; white-space: nowrap;
  font-size: var(--fs-label); font-weight: 600; color: var(--text-2);
  background: var(--surface-2); border-bottom: 1px solid var(--border);
}
table.tbl th.sortable { cursor: pointer; user-select: none; }
table.tbl th.sortable:hover { color: var(--text); }
table.tbl th .ind { display: inline-block; width: 10px; color: var(--text-3); }
table.tbl td { padding: 0 12px; height: 42px; border-bottom: 1px solid var(--rule); vertical-align: middle; white-space: nowrap; }
table.tbl tbody tr:last-child td { border-bottom: none; }
table.tbl tbody tr.clickable { cursor: pointer; }
table.tbl tbody tr.clickable:hover td { background: var(--surface-2); }
table.tbl tbody tr.sel td { background: var(--surface-3); }
.r, table.tbl th.r { text-align: right; }
.empty { padding: 26px 12px; text-align: center; color: var(--text-3); font-size: var(--fs-table); }

/* ── Luồng nhiều bước ────────────────────────────────────────────────── */
.steps { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
.step {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--r-ctrl);
  background: var(--surface); font-size: var(--fs-small); color: var(--text-3);
  flex: 1 1 150px; min-width: 0;
}
.step-n {
  flex: none; width: 20px; height: 20px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 600;
  background: var(--surface-3); color: var(--text-2);
}
.step.cur { border-color: var(--accent); color: var(--text); }
.step.cur .step-n { background: var(--accent); color: var(--on-accent); }
.step.done { color: var(--ok-text); border-color: var(--ok-text); }
.step.done .step-n { background: var(--ok-bg); color: var(--ok-text); }
.step-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 767px) { .step { flex-basis: 100%; } }

/* ── Tab ─────────────────────────────────────────────────────────────── */
.tabs { display: flex; gap: 4px; flex-wrap: wrap; }
.tab {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 11px;
  font-family: inherit; font-size: var(--fs-table); color: var(--text-2);
  background: transparent; border: 1px solid transparent; border-radius: var(--r-ctrl); cursor: pointer;
}
.tab:hover { background: var(--surface-2); color: var(--text); }
.tab.active { background: var(--surface-3); border-color: var(--border); color: var(--text); font-weight: 500; }
.tab-n { font-size: var(--fs-label); color: var(--text-3); font-variant-numeric: tabular-nums; }
.tab.active .tab-n { color: var(--text-2); }

/* ── Thanh mức + đồ thị nhỏ ──────────────────────────────────────────── */
.spark { color: var(--text-3); }

.hosts { display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); gap: 12px; margin-bottom: 16px; }

/* Ruột thẻ máy chủ: các con số bên trái, vòng mức bên phải.
   `auto` cho cột phải chứ không phải 1fr — vòng mức có bề ngang cố định, để nó
   co giãn chỉ tạo khoảng trắng giữa hai vòng. Cột trái nuốt phần dư.
   `minmax(0,…)` là bắt buộc: mặc định min-width của một track grid là `auto`,
   nên nội dung dài (chuỗi commit, tên file backup) sẽ đẩy giãn thay vì xuống dòng. */
/* Ruột thẻ máy chủ. Mốc chuyển bố cục phải đo THẺ, không đo cửa sổ — thẻ nằm
   trong lưới `.hosts` nên bề ngang của nó phụ thuộc SỐ MÁY: một máy thì ~1130px,
   hai máy thì ~565px, mà cửa sổ vẫn 1440 trong cả hai trường hợp. Media query
   theo viewport giữ nguyên ba cột ở mức 565px và ba cột chen nát nhau — đã vấp
   thật. `@container` là công cụ đúng cho việc này. */
.hosts > .card { container-type: inline-size; }

.host-cols {
  /* BỐN vùng bằng nhau: số liệu · RAM · tải CPU · đĩa. `minmax(0, 1fr)` chứ
     không phải `1fr`: min-width mặc định của track lưới là `auto`, nên nội dung
     dài (chuỗi commit, tên file backup) sẽ đẩy cột phình ra và phá thế cân bằng. */
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px 20px;
  /* stretch chứ không start: cột đồ thị phải cao bằng hàng để thanh ăn hết chỗ,
     nếu không nó co về chiều cao nội dung và chừa một dải trắng dưới đáy. */
  align-items: stretch;
}
/* Số liệu và vòng mức canh GIỮA theo chiều dọc. Hai đồ thị bên cạnh chiếm hết
   chiều cao hàng, nên bám mép trên làm hai khối kia treo lơ lửng phía trên và
   hàng trông lệch hẳn về một bên. */
.host-cols > .kv, .host-cols > .host-rings { align-self: center; }
/* Thẻ vừa (hai máy cạnh nhau): số liệu + vòng mức ở hàng trên, đồ thị xuống
   hàng dưới chiếm hết bề ngang — nó là thứ cần bề ngang nhất.
   Phải đặt Ô TƯỜNG MINH, không dựa vào tự xếp: trong DOM đồ thị nằm GIỮA số liệu
   và vòng mức, nên chỉ cho nó `grid-column: 1/-1` thì nó chiếm nguyên hàng 2 và
   đẩy vòng mức xuống hàng 3 — thành ba hàng chồng nhau thay vì hai. */
@container (max-width: 860px) {
  .host-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .host-cols > .kv         { grid-column: 1; grid-row: 1; }
  .host-cols > .host-rings { grid-column: 2; grid-row: 1; }
  /* Chỉ ghim HÀNG, để lưới tự xếp cột — như vậy vẫn đúng cả khi máy không có
     RAM (macOS không có /proc) và chỉ còn một đồ thị. */
  .host-cols > .daychart   { grid-row: 2; }
}
@container (max-width: 420px) {
  .host-cols { grid-template-columns: 1fr; }
  .host-cols > .kv, .host-cols > .host-rings, .host-cols > .daychart {
    grid-column: auto; grid-row: auto;
  }
}

/* Đồ thị 24 thanh. Chiều cao ĂN THEO Ô CHỨA, không đặt cứng: hàng lưới đã cao
   bằng cột cao nhất rồi, đặt cứng 46px chỉ tạo khoảng trắng dưới đáy. `flex: 1`
   trên vùng thanh + `height: 100%` trên cả khối là đủ; `min-height: 0` là bắt
   buộc, thiếu nó thì flex item không co nhỏ hơn nội dung và tràn ra ngoài. */
.daychart {
  min-width: 0; color: var(--text-2);
  display: flex; flex-direction: column;
  /* Cao bằng ô chứa, sàn 72px để vùng thanh không mỏng tới mức không đọc được.
     `height: 100%` ở đây an toàn vì hai đồ thị nằm CẠNH nhau (mỗi cái một ô
     lưới riêng), khác hẳn lúc xếp chồng — khi đó mỗi cái xin 100% chiều cao cha
     thì tổng thành 200% và tràn. */
  height: 100%; min-height: 96px;
}
.dc-top {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  font-size: var(--fs-small); margin-bottom: 4px; flex: 0 0 auto;
}
.dc-top .dc-right { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.dc-top b { color: var(--text); font-variant-numeric: tabular-nums; font-weight: 600; }
.dc-delta {
  color: var(--text-3); font-variant-numeric: tabular-nums; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}

/* Vùng vẽ = cột nhãn trục dọc + vùng thanh. Nhãn để RIÊNG một cột chứ không
   chồng lên thanh: chồng lên thì cột giờ 0h luôn bị chữ đè, mà đó lại là cột
   người ta đọc đầu tiên. */
.dc-plot { flex: 1 1 auto; min-height: 44px; display: flex; gap: 5px; }
.dc-y {
  flex: 0 0 auto; display: flex; flex-direction: column; justify-content: space-between;
  font-size: 9px; line-height: 1; color: var(--text-3);
  font-variant-numeric: tabular-nums; text-align: right; min-width: 15px;
  padding-bottom: 1px;
}

.dc-canvas {
  position: relative; flex: 1 1 auto; min-width: 0;
  display: flex; align-items: flex-end;
  border-bottom: 1px solid var(--border);
}
/* Vạch chia mỗi 10%. `pointer-events: none` để không nuốt tooltip của ô giờ. */
.dc-grid {
  position: absolute; left: 0; right: 0; height: 0;
  border-top: 1px solid var(--rule); pointer-events: none; opacity: .55;
}
.dc-grid.major { opacity: 1; }
.dc-grid[style*="bottom:0%"] { border-top-color: transparent; }
/* Ô giờ giờ chỉ còn là vùng bắt chuột cho tooltip — không vẽ gì. */
.dc-slot { flex: 1 1 0; min-width: 0; height: 100%; }
/* Đường: `preserveAspectRatio="none"` kéo giãn cả nét, `non-scaling-stroke` giữ
   độ dày thật. `pointer-events: none` để tooltip của ô giờ bên dưới vẫn ăn. */
.dc-line { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.dc-stroke {
  fill: none; stroke: currentColor; stroke-width: 1.5px;
  stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke;
}
/* Vùng dưới đường: nhạt, chỉ để mắt thấy đường đang cách đáy 0 bao xa. */
.dc-area { fill: currentColor; opacity: .1; stroke: none; }
/* Chấm: điểm cuối (mức hiện tại) và những giờ lẻ đứng một mình. Đặt bằng DOM chứ
   không phải <circle> — trong SVG bị kéo giãn thì hình tròn ra hình bầu dục. */
.dc-dot {
  position: absolute; width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px;
  border-radius: 50%; background: currentColor; pointer-events: none;
}

.dc-axis {
  /* Thụt trái đúng bằng cột nhãn trục dọc để số giờ thẳng cột với điểm của nó. */
  flex: 0 0 auto; display: flex; margin-top: 3px; padding-left: 20px;
  font-size: 9px; color: var(--text-3); font-variant-numeric: tabular-nums;
}
/* Canh GIỮA ô: điểm trên đường cũng đặt ở giữa ô giờ, canh trái là lệch nửa ô. */
.dc-tick { flex: 1 1 0; min-width: 0; text-align: center; }
.dc-empty { font-size: var(--fs-small); color: var(--text-3); margin-top: 4px; }

/* Nút "i" trong tiêu đề: nằm cùng dòng chữ, nhạt khi nghỉ để không tranh chỗ
   với chính tiêu đề, đậm lên khi rê hoặc khi đang mở. */
.info-btn {
  color: var(--text-3); vertical-align: -3px; margin-left: 3px;
  padding: 2px; border-radius: 4px;
}
.info-btn:hover { color: var(--text); background: var(--surface-2); }
.info-btn[aria-expanded="true"] { color: var(--text); background: var(--surface-3); }

/* Vòng bên trái, chữ bên phải, chữ ăn hết chỗ còn lại — vòng đứng một mình canh
   trái để thừa gần 190px trong ô rộng 273px. */
.host-rings { display: flex; gap: 14px; align-items: center; min-width: 0; }
.host-rings > .kv { flex: 1 1 auto; min-width: 0; }

.donut { text-align: center; width: 84px; flex: 0 0 auto; }
.donut.bare { width: auto; }
.donut-label { font-size: var(--fs-label); color: var(--text-2); margin-top: 2px; }
.donut-value {
  font-size: var(--fs-small); color: var(--text-3);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}


/* ── Phân trang ──────────────────────────────────────────────────────── */
.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding: 9px 14px; border-top: 1px solid var(--rule);
}
.pager-left { display: flex; align-items: center; gap: 10px; font-size: var(--fs-small); color: var(--text-2); }
.pager-right { display: flex; align-items: center; gap: 4px; }
.select-sm { height: 30px; font-size: var(--fs-small); padding: 0 24px 0 9px; }
.pgbtn {
  min-width: 30px; height: 30px; padding: 0 7px;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: inherit; font-size: var(--fs-small); font-variant-numeric: tabular-nums;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--border-ctrl); border-radius: var(--r-ctrl); cursor: pointer;
}
.pgbtn:hover:not(:disabled) { background: var(--surface-2); }
.pgbtn:disabled { opacity: .4; cursor: not-allowed; }
.pgbtn.cur { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 500; }
.pgdots { padding: 0 3px; color: var(--text-3); font-size: var(--fs-small); }
@media (pointer: coarse) { .pgbtn, .select-sm { min-width: 38px; height: 38px; } }

/* ── Nhãn trạng thái: luôn icon + chữ + màu, không bao giờ chỉ màu ────── */
.badge {
  display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; flex: none;
  font-size: var(--fs-label); font-weight: 500;
  padding: 2px 7px; border-radius: var(--r-ctrl);
  background: var(--neutral-bg); color: var(--neutral-text);
}
.badge svg { width: 13px; height: 13px; flex: none; }
.badge.ok   { background: var(--ok-bg);   color: var(--ok-text); }
.badge.warn { background: var(--warn-bg); color: var(--warn-text); }
.badge.bad  { background: var(--bad-bg);  color: var(--bad-text); }

.alert-row {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 14px; border-bottom: 1px solid var(--rule);
  font-size: var(--fs-table); cursor: pointer;
}
.alert-row:last-child { border-bottom: none; }
.alert-row:hover { background: var(--surface-2); }
.alert-row .who { color: var(--text-3); font-size: var(--fs-small); margin-left: auto; white-space: nowrap; }

/* ── Hộp thoại ───────────────────────────────────────────────────────── */
.modal {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(460px, calc(100vw - 32px)); max-height: calc(100vh - 48px);
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-card); z-index: 50; animation: fade var(--dur) ease-out;
}
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 13px 16px; border-bottom: 1px solid var(--rule); }
.modal-body { padding: 16px; overflow-y: auto; font-size: var(--fs-table); }
.modal-body p { margin: 0 0 8px; }
.modal-foot { display: flex; justify-content: flex-end; gap: 8px;
  padding: 12px 16px; border-top: 1px solid var(--rule); }

/* Khối nhắc nhở trong hộp thoại — dùng cho ràng buộc an toàn phải đọc kỹ */
.note { border-radius: var(--r-ctrl); padding: 10px 12px; font-size: var(--fs-small); }
.note.warn { background: var(--warn-bg); color: var(--warn-text); }
.note.bad  { background: var(--bad-bg);  color: var(--bad-text); }
.note.ok   { background: var(--ok-bg);   color: var(--ok-text); }

/* ── Ngăn chi tiết + lớp phủ ──────────────────────────────────────────── */
.scrim {
  position: fixed; inset: 0; background: rgba(10,12,16,.42); z-index: 40;
  animation: fade var(--dur) ease-out;
}
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(520px, 100%);
  background: var(--surface); border-left: 1px solid var(--border);
  z-index: 41; display: flex; flex-direction: column;
  animation: fade var(--dur) ease-out;
}
.drawer-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
  padding: 14px 16px; border-bottom: 1px solid var(--rule);
}
.drawer-body { padding: 16px; overflow-y: auto; flex: 1; }
/* Chân ngăn — nơi đặt nút hành động. Nằm ngoài vùng cuộn để nút luôn thấy được
   dù nội dung dài. */
.drawer-foot {
  display: flex; justify-content: flex-end; gap: 8px; flex: none;
  padding: 12px 16px; border-top: 1px solid var(--rule);
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
}
/* margin: 0 là bắt buộc — trình duyệt mặc định cho <dl> margin 1em trên dưới,
   nên khối này bị đẩy xuống thêm ~15px và lệch hẳn với padding của khối tiêu đề
   ngay trên nó. */
/* Ô nhập trong ngăn kéo. `.check.off` = lựa chọn bị khoá vì server không nhận —
   phải nhìn ra là KHOÁ chứ không phải chưa chọn, nếu không người dùng bấm mãi
   không hiểu vì sao không ăn. */
/* Hàng cấu hình: nhãn bên trái, ô điều khiển bên phải. Cột nhãn = 20% bề ngang
   khối chứa — theo tỉ lệ chứ không px cố định, để ngăn kéo rộng hẹp khác nhau vẫn
   giữ đúng một dáng. Các hàng dùng chung một giá trị nên luôn thẳng cột.
   Sàn 112px là BẮT BUỘC: cùng bảng này còn nằm trong modal rộng 460px, ở đó 20%
   chỉ còn 85px trong khi "Tên đăng nhập" cần 96px — chữ xuống dòng dù hàng còn
   thừa cả trăm pixel. minmax() với max nhỏ hơn min thì min thắng, nên ngăn kéo
   620px vẫn giữ nguyên 117px như cũ. */
.cfg-row { display: grid; grid-template-columns: minmax(112px, 20%) minmax(0, 1fr); gap: 10px 14px;
  align-items: start; padding: 10px 0; border-bottom: 1px solid var(--rule); }
.cfg-row:last-of-type { border-bottom: 0; }
/* Hàng đầu GIỮ NGUYÊN padding 10px như mọi hàng — bỏ padding đi thì hàng đó
   thành trên 3 dưới 14, tức là chữa lệch chỗ này bằng cách đẻ lệch chỗ khác.
   Thay vào đó kéo cả hàng lên đúng bằng lề trên của .drawer-body (16px), để
   khoảng hở "đường kẻ → chữ" bằng y hệt "đường kẻ giữa hai hàng → chữ".
   Con số 16 buộc phải khớp padding của .drawer-body; đổi bên kia thì đổi cả ở đây. */
.cfg-row:first-of-type { margin-top: -16px; }
.cfg-label { font-size: var(--fs-table); color: var(--text-2); }
.cfg-ctl { min-width: 0; }
/* Ô nhập kéo hết bề ngang cột. Không có dòng này thì `<input>` giữ bề rộng mặc
   định theo thuộc tính `size` (~20 ký tự) và để thừa cả trăm pixel bên phải —
   nhìn ra như form bị canh lệch chứ không phải ô nhập bị hụt.
   Trừ ô SỐ và ô chọn giờ: giá trị ngắn, kéo dài chỉ làm khó đọc. */
.cfg-ctl > .input, .cfg-ctl > textarea { width: 100%; }
.cfg-ctl > .num-in, .cfg-ctl .num-wrap > .num-in { width: auto; max-width: 120px; }
.cfg-ctl > select.select { width: auto; min-width: 140px; }
/* Trường đã có trong DB nhưng CHƯA phép tính nào đọc. Phải nhìn ra ngay, nếu
   không người ta chỉnh số rồi ngồi đợi một cảnh báo không bao giờ tới. */
/* Hàng sắp bị bỏ khỏi dự án: gạch ngang + mờ, và vẫn còn đó cho tới khi bấm Lưu
   — thấy được mình sắp bỏ ai, và bấm "Giữ lại" là xong, không phải gán lại. */
/* Nhóm nút cấp tài khoản. Nút xoá đẩy sang phải, tách khỏi hai nút lành tính —
   cạnh nhau thì bấm nhầm chỉ là lệch một con chuột. */
.acct-ops { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.acct-ops .btn-danger { margin-left: auto; }

.row-drop { opacity: .5; }
.row-drop td:first-child { text-decoration: line-through; }

.tag-todo { font-size: 10px; font-weight: 600; padding: 1px 5px; border-radius: 4px;
  background: var(--warn-bg); color: var(--warn-text); white-space: nowrap; }
/* "chỉ đọc" KHÔNG dùng màu cảnh báo: đó là sự thật về API, không phải chuyện
   hỏng. Nhuộm vàng thì mỗi lần mở tab Nhiệt độ lại tưởng có lỗi. */
.tag-ro { background: var(--surface-3); color: var(--text-3); }

/* Chip phụ trong ô WiFi: xuống dòng chứ không kéo giãn cột. */
.cfg-chips { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 4px; }
/* Ô của bảng cấu hình cao THEO NỘI DUNG. `.tbl td` chốt cứng `height: 42px` và
   `padding: 0 12px` cho bảng một dòng; bảng này hàng nào cũng hai dòng nên nội
   dung cao hơn thế, và không có đệm dọc thì nhãn cảnh báo ở dòng dưới nằm sát
   rạt vạch kẻ hàng.
   ⚠ Phải viết đủ `table.tbl.tbl-cfg` — `.tbl-cfg td` (0,1,1) thua `table.tbl td`
   (0,1,2), luật sẽ bị bỏ qua im lặng mà nhìn vào CSS vẫn tưởng đã đặt. */
table.tbl.tbl-cfg td { height: auto; padding-top: 10px; padding-bottom: 10px; }

/* Chế độ XEM: giá trị là chữ, nút bút chì nằm cuối hàng. Nút mờ khi nghỉ và chỉ
   rõ lên khi rê vào hàng — sáu tab mà nút nào cũng đậm thì mắt không biết nhìn
   đâu; nhưng KHÔNG ẩn hẳn, vì ẩn thì trên màn cảm ứng không có cách nào biết
   hàng đó sửa được. */
.cfg-read { display: flex; align-items: center; gap: 10px; }
.cfg-val { min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-table); }
.cfg-val.muted { color: var(--text-3); }
/* Nút bút chì đúng bằng chiều cao MỘT DÒNG CHỮ. To hơn thì nó kéo cao khối
   `.cfg-read`, đẩy chữ giá trị tụt xuống vài px so với nhãn bên trái — và không
   sửa được bằng padding mà không đẻ ra lệch chỗ khác. */
.cfg-edit { margin-left: auto; flex: 0 0 auto; color: var(--text-3);
  width: 22px; height: 22px; padding: 0; border-radius: 4px; }
.cfg-row:hover .cfg-edit { color: var(--text-2); }
.cfg-edit:hover { color: var(--text); background: var(--surface-2); }
/* Hàng đang sửa: nền nhạt để phân biệt với các hàng chỉ xem. Chỉ Ở ĐÂY mới cần
   bám mép trên + đẩy nhãn xuống 7px — ô nhập cao ~35px và có thể kèm 2-3 dòng
   chú thích, canh giữa thì nhãn trôi xuống giữa khối chữ.
   Ở chế độ xem thì KHÔNG đặt padding thủ công: canh giữa là đủ, và mỗi chỗ tự
   cộng thêm vài px là thứ đã làm hàng lệch trên-dưới 4px. */
.cfg-row.editing { background: var(--surface-2); border-radius: var(--r-ctrl);
  padding-left: 10px; padding-right: 10px; margin: 0 -10px; }

/* Hàng CÓ ô nhập: cho ô nhãn CAO ĐÚNG BẰNG ô nhập rồi canh giữa chữ trong đó.
   Không bù bằng một con số padding — mỗi loại điều khiển một chiều cao (ô chữ
   37px, ô tick ~21px) nên một con số vừa cho cái này thì quá tay cho cái kia;
   đã thử 7px và lệch −4 / +4. Cách này không phụ thuộc số đo phông chữ.
   Bắt theo NỘI DUNG hàng (`:has`) chứ không theo class `.editing`: form tạo tài
   khoản cũng đầy ô nhập mà không phải "đang sửa", và đó chính là chỗ đã lệch. */
.cfg-row:has(.cfg-ctl > .input, .cfg-ctl > select, .cfg-ctl > textarea,
             .cfg-ctl > .num-wrap, .cfg-ctl > .check) > .cfg-label {
  min-height: var(--h-ctrl); display: flex; align-items: center;
}
/* Ô tick trong bảng cấu hình, ba thứ phải sửa cùng lúc:
     · `inline-flex` → `flex`: để inline thì nó nằm trên đường cơ sở dòng chữ
     · `margin: -6px -8px` của `.check` (cho vùng bấm tràn ra) phải BỎ theo trục
       dọc — chính nó kéo khối lên đúng 6px, đo ra nhãn 542→578 / tick 536→572
     · nâng chiều cao cho bằng ô chữ
   Giữ margin ngang để vùng bấm vẫn tràn sang trái như các chỗ khác. */
.cfg-ctl > .check {
  display: flex; min-height: var(--h-ctrl); margin-top: 0; margin-bottom: 0;
}
/* Textarea cao nhiều dòng: nhãn bám dòng ĐẦU, không trôi xuống giữa khối. */
.cfg-row:has(.cfg-ctl > textarea) > .cfg-label { align-items: flex-start; padding-top: 8px; }
.cfg-foot { display: flex; justify-content: flex-end; gap: 8px;
  padding-top: 12px; border-top: 1px solid var(--rule); margin-top: 4px; }
@media (max-width: 560px) { .cfg-row { grid-template-columns: 1fr; } }

/* Ô số + đơn vị đi liền. Đơn vị nằm NGOÀI input để không bị người dùng xoá mất. */
.num-wrap { display: inline-flex; align-items: center; gap: 7px; }
.num-in { max-width: 120px; text-align: right; font-variant-numeric: tabular-nums; }
.num-unit { font-size: var(--fs-small); color: var(--text-3); }

.wifi-form { border: 1px solid var(--border); border-radius: var(--r-card); padding: 12px; }
.wifi-form .field { margin-bottom: 12px; }
.wifi-form .check { display: flex; align-items: center; gap: 7px; margin-top: 5px; }
.wifi-form .check.off { opacity: .45; cursor: not-allowed; }
.wifi-form-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
.field-err { color: var(--bad-text); font-size: var(--fs-small); margin-top: 4px; }
.field-note { color: var(--text-3); font-size: var(--fs-small); margin: 4px 0 0 22px; }
.sub-head-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.sub-head-act { display: flex; gap: 6px; }

/* Tiêu đề phụ trong ngăn kéo: tách hai nhóm số liệu mà không phải dựng thêm thẻ. */
.sub-head {
  margin: 16px 0 7px; font-size: var(--fs-label); font-weight: 600;
  color: var(--text-2); letter-spacing: .02em;
}

.kv { display: grid; grid-template-columns: 128px minmax(0, 1fr); gap: 7px 12px; font-size: var(--fs-table); margin: 0; }
/* Bản gọn: nhãn ngắn hơn, dùng ở chỗ hẹp như ô đĩa. */
.kv-tight { grid-template-columns: auto minmax(0, 1fr); gap: 5px 10px; }
/* Trong thẻ máy chủ, cột nhãn hẹp lại: 128px + nội dung dài (chuỗi commit, tên
   file backup) làm bảng tràn khỏi ô lưới 273px. */
.host-cols > .kv { grid-template-columns: minmax(0, auto) minmax(0, 1fr); gap: 7px 10px; }
.kv dt { color: var(--text-2); }
.kv dd { margin: 0; }

.toast {
  position: fixed; right: 16px; bottom: 16px; z-index: 60;
  background: var(--accent); color: var(--on-accent);
  padding: 10px 14px; border-radius: var(--r-ctrl); font-size: var(--fs-table);
  animation: fade var(--dur) ease-out;
}
@keyframes fade { from { opacity: 0 } to { opacity: 1 } }

/* ── Ba mốc responsive, không hơn ─────────────────────────────────────── */
/* tablet: thu sidebar còn dải icon */
@media (max-width: 1279px) {
  .sidebar { flex-basis: 56px; }
  .sidebar .label, .sidebar-brand span:not(.tag):not(.env-badge), .nav-group { display: none; }
  .nav a { justify-content: center; padding: 9px 0; }
  .sidebar-brand { justify-content: center; padding: 14px 0; }
  .main-inner { padding: 16px 16px 40px; }
}
/* mobile: sidebar xuống dải đáy */
@media (max-width: 767px) {
  .shell { flex-direction: column; }
  .sidebar {
    position: fixed; bottom: 0; left: 0; right: 0; top: auto;
    height: auto; flex-basis: auto; flex-direction: row;
    border-right: none; border-top: 1px solid var(--border);
    padding-bottom: env(safe-area-inset-bottom); z-index: 30;
  }
  .sidebar-brand, .sidebar-foot { display: none; }
  .nav { display: flex; overflow-x: auto; padding: 5px 6px; }
  .nav a { flex: 1; min-width: 54px; }
  .main-inner { padding: 14px 12px 76px; }
  .tiles { grid-template-columns: repeat(auto-fit, minmax(136px, 1fr)); }
  .drawer { width: 100%; }
}

/* ── Đăng nhập ─────────────────────────────────────────────────────────────
   Một cột hẹp giữa trang. Không ảnh nền, không gradient: đây là công cụ làm
   việc, và mọi pixel trang trí là một pixel phải tải trên đường truyền của
   khách sạn. */
.login {
  min-height: 100vh; min-height: 100dvh;
  display: flex; align-items: center; justify-content: center;
  padding: 24px; background: var(--page);
}
.login-card {
  width: 100%; max-width: 352px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--r-card); padding: 22px;
}
.login-brand {
  display: flex; align-items: center; gap: 9px;
  font-weight: 600; letter-spacing: -0.01em; margin-bottom: 6px;
}

/* Ai đang đăng nhập + nút ra. Ở đây chứ không ở đầu trang: đầu trang là chỗ của
   nội dung, còn danh tính là thứ tra một lần rồi thôi. */
.sidebar-user { display: flex; align-items: center; gap: 8px; }
.sidebar-user .label { min-width: 0; flex: 1; }
.sidebar-user .nm {
  font-size: var(--fs-table); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidebar-user .rl { font-size: var(--fs-small); color: var(--text-3); }

/* Đang tải: một dòng chữ ĐỨNG YÊN, không skeleton nhấp nháy. */
.loading { padding: 40px 16px; text-align: center; color: var(--text-3); font-size: var(--fs-table); }
.page-err { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
