/**
 * Command header — thanh lệnh, hàng ĐẦU của khung.
 *
 *   logo │ danh tính trung tâm + địa danh │ đồng hồ + cài đặt
 *
 * Trước đây có hàng thứ hai chứa 8 tab. Hàng đó đã bỏ: 8 nút luôn hiện là quá
 * nhiều chữ cho một wall đọc từ 3–5m. Chỗ đó nay là dải phân hệ chỉ còn MỘT
 * chip tên màn (`screen-nav.css`), vẫn là một hàng riêng của khung.
 *
 * 20/09 hàng đó từng bị GỘP vào thanh này để lấy thêm 46px cho vùng dữ liệu;
 * 21/09 tách lại theo ảnh mẫu người dùng. Lý do: nhét cả ba tầng chữ (trung
 * tâm · địa danh · tên màn) vào 52px thì tầng nào cũng phải nhỏ đi, mà chữ nhỏ
 * trên tường đọc từ 3–5m là chữ không tồn tại. Thanh này giữ đúng hai tầng.
 *
 * Cụm phải cố tình mỏng. CRITICAL, REALTIME, GATEWAY, WALL, user từng nằm đây
 * và làm tiêu đề bị ĐÈ lên chúng ở cửa sổ thật; trạng thái đó Footer đã có,
 * nên đây bỏ hẳn chứ không thu nhỏ.
 *
 * Mọi cụm đều nowrap; chiều cao khoá cứng ở --h-masthead từ 1280 tới 4K. Khi
 * cửa sổ hẹp dần thì bỏ tải theo bậc ở cuối file, KHÔNG cho xuống dòng —
 * xuống dòng là vỡ chiều cao header và xô toàn bộ layout bên dưới.
 */

.ioc-header {
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* Thanh NỔI (mẫu S07 `.top`): bo góc, không viền, tách khỏi nền bằng
     hairline ảo + bắt sáng mép trên + bóng nhoè. Trước đây đây là một thanh
     HÀN vào mép màn (`border-bottom`); nay nó là một khối trôi trên nền
     giống mọi card khác, nên khung nhìn như một hệ chứ không như một trang. */
  border-radius: var(--r-panel);
  overflow: hidden;
  background: var(--grad-head);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-float);
  user-select: none;
  position: relative;
  z-index: var(--z-header);
}

/* Vệt sáng hắt lên GIỮA thanh để tôn tên trung tâm (mẫu S07 `.top::before`).
   Thay cho đường accent hàn ở mép dưới: thanh đã bo góc và trôi tự do nên một
   đường kẻ chạy hết bề ngang không còn chỗ bám, còn quầng sáng giữa thì kéo
   mắt đúng vào chỗ cần đọc trước. Vẫn ăn `--accent` nên đổi màn là đổi tông. */
.ioc-header::before {
  content: '';
  position: absolute;
  left: 50%;
  top: -70%;
  width: min(720px, 58%);
  height: 230%;
  transform: translateX(-50%);
  background: radial-gradient(
    ellipse 50% 50% at 50% 50%,
    color-mix(in srgb, var(--accent) 16%, transparent),
    color-mix(in srgb, var(--accent-vision) 7%, transparent) 45%,
    transparent 72%
  );
  pointer-events: none;
}

/* ── Masthead ─────────────────────────────────────────────────────────── */

.ioc-masthead {
  position: relative;
  z-index: 1;
  /* Grid, not flex: `1fr auto 1fr` optically centres the title on the *bar*
     rather than in the gap between clusters of unequal width.
     Cột giữa là minmax(0, auto), KHÔNG phải auto: track `auto` không co dưới
     min-content, mà tiêu đề nowrap có min-content = trọn cả dòng — nên khi
     hẹp nó tràn đè lên hai cụm bên. Cho co được thì cả hai tầng tự cắt "…"
     (xem `.ioc-nav__title` và `.ioc-title-block__app/__site`) thay vì chồng
     chữ lên nút. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-4);
  height: var(--h-masthead);
  flex: 0 0 var(--h-masthead);
  /* Hai mép lề bằng nhau (14/09): logo đã cắt lề trong suốt nên chữ cách
     mép đúng --sp-4 — trước đó lề "ảo" trong file ảnh làm chữ trôi xa mép. */
  padding: 0 var(--sp-4);
}

.ioc-masthead__side {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.ioc-masthead__side--right {
  justify-content: flex-end;
}

/* ── Brand ────────────────────────────────────────────────────────────── */

.ioc-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

.ioc-brand__logo {
  /* Khoá chiều cao theo token, không dùng % — % trong grid item không
     resolve, ảnh bung theo cỡ gốc tràn thanh: bug 17/08.
     15/09: thu từ cao-bằng-thanh xuống --h-brand-logo — chữ logo nhỏ hơn
     chữ tiêu đề (yêu cầu user). */
  height: var(--h-brand-logo);
  width: auto;
  /* 14/09: file logo đã CẮT hết lề trong suốt (trước đây chữ chỉ chiếm 19%
     chiều cao ảnh nên cao 52px mà chữ có 10px). Trần bề ngang để cụm trái
     không đè lên tiêu đề giữa khi cửa sổ hẹp. */
  max-width: 400px;
  object-fit: contain;
  object-position: left center;
  flex: 0 0 auto;
}
/* ── Centre title ─────────────────────────────────────────────────────── */

.ioc-masthead__title {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.ioc-title-rule {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
}

.ioc-title-rule__line {
  display: block;
  width: clamp(16px, 2.4vw, 56px);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent));
}

.ioc-title-rule--right .ioc-title-rule__line {
  background: linear-gradient(90deg, var(--accent), transparent);
}

.ioc-title-rule__gem {
  display: block;
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  transform: rotate(45deg);
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent);
}

/* Hai tầng chữ trần: danh tính trung tâm trên, địa danh dưới. */
.ioc-title-block {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* 2px → 1px: chỗ nhường cho bậc chữ mới của tầng trên (xem phép tính ở
     `.ioc-title-block__main`). */
  gap: 1px;
}

/* Danh tính trung tâm — tầng TRÊN, chữ to nhất masthead (bản mẫu `.hero__n`:
   chữ NÉN, trắng). <h1> duy nhất của tài liệu: thứ bậc thị giác và thứ bậc
   ngữ nghĩa trùng nhau. */
.ioc-title-block__main {
  margin: 0;
  min-width: 0;
  font-family: var(--font-cond);
  /* `--fs-sub` (17px) → `--fs-num` (22px) → `--fs-num-lg` (30px @1920, lệnh
     21/09). Dòng này là thứ người đi ngang phòng đọc đầu tiên, nó phải hơn hẳn
     mọi chữ còn lại trên thanh.
     Phép tính chỗ ở 1920 (thanh khoá cứng 52px, không đệm dọc): 30 × 1.1 = 33
     + 1px gap + tầng địa danh 10 × 1.2 = 12 → 46px, còn 6px thở. Đúng vì thế
     mà `line-height` xuống 1.1 và gap của `.ioc-title-block` xuống 1px cùng
     lúc — không có hai chỗ đó thì bậc chữ này không lọt. */
  font-size: var(--fs-num-lg);
  font-weight: var(--fw-title);
  letter-spacing: var(--track-label);
  line-height: 1.1;
  text-transform: uppercase;
  color: var(--text-hi);
  /* Van an toàn: hết chỗ thì cắt đuôi, không đè lên đồng hồ. KHÔNG xuống
     dòng — xuống dòng là vỡ --h-masthead và xô toàn bộ lưới bên dưới. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Địa danh — tầng DƯỚI (bản mẫu `.hero__s`: mono, hổ phách, nhỏ). Hổ phách
   thay vì trắng vì hai tầng chữ chồng nhau mà cùng màu thì mắt đọc thành một
   khối. */
.ioc-title-block__sub {
  margin: 0;
  min-width: 0;
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  letter-spacing: var(--track-wide);
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--accent-power);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ioc-title-block__kicker {
  margin-top: 3px;
  font-size: clamp(7px, 0.5vw, 10px);
  font-weight: 700;
  letter-spacing: clamp(1px, 0.18vw, 3.5px);
  color: var(--text-lo);
  white-space: nowrap;
}

/* ── Right cluster ────────────────────────────────────────────────────── */

/* Cụm giờ (chỉnh lần 3, 17/08): panel mỏng — thời tiết nằm ngang · thứ trong
   tile nhỏ cùng chất liệu ô lật · ngày mono dịu · kẻ dọc · đồng hồ lật co
   theo header. Nguyên tắc: MỘT thứ được to (đồng hồ), còn lại lùi về sau. */
.ioc-clock {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex: 0 0 auto;
  line-height: 1.1;
  /* Nổi thẳng trên nền header như logo — không khung, không nền (26/08).
     Header đã có nền riêng; lồng thêm một hộp nữa là hai lớp nền chồng nhau. */
  padding: 3px 0;
}

/* Đồng hồ lật co vừa header mỏng — chỉ đổi biến kích thước, cơ chế giữ nguyên. */
.ioc-clock .flip-clock {
  --flip-w: 17px;
  --flip-h: 26px;
}

/* Màu icon set inline theo hiện tượng (nắng vàng / đêm trắng) — không tint. */
.ioc-clock__weather {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
}

/* Nhiệt độ cùng cỡ + màu với thứ/ngày bên cạnh (30/09) — một dòng chữ liền
   mạch, không phải chú thích phụ nhỏ hơn. */
.ioc-clock__temp {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sub);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text-hi);
}

.ioc-clock__cal {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
}

/* "T2," viết trần cùng cỡ với ngày — bỏ tile bo (feedback 17/08). */
.ioc-clock__dow {
  font-family: var(--font-num);
  font-size: var(--fs-sub);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text-hi);
}

.ioc-clock__rule {
  width: 1px;
  height: 20px;
  background: var(--line-mid);
  flex: 0 0 auto;
}

.ioc-clock__time {
  font-family: var(--font-num);
  font-size: 22px;
  font-weight: 700;
  color: var(--text-hi);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}

/* Dòng thứ/ngày dùng --text-lo thì chìm hẳn vào nền tối khi nhìn từ 3–5m.
   Nâng lên --text-hi; cỡ chữ nhỏ hơn giờ nên vẫn không tranh chỗ với đồng hồ. */
.ioc-clock__date {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sub);
  font-weight: 700;
  color: var(--text-hi);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* Dấu gạch chéo dịu xuống — màu chỉ dành cho icon thời tiết. */
.ioc-clock__date i {
  font-style: normal;
  color: var(--text-lo);
  padding: 0 1px;
}

/* ── Narrow-window fallbacks ──────────────────────────────────────────── */

/* Một thanh phải gánh hai tầng chữ ở giữa lẫn hai cụm hai bên, nên bỏ tải sớm
   và theo bậc. Thứ tự bỏ = thứ tự từ ít quan trọng nhất: gạch trang trí → cụm
   giờ xếp hai tầng. Hai tầng chữ giữa không nằm trong danh sách hy sinh: hết
   chỗ thì chúng tự cắt `…` (xem `.ioc-title-block__main/__sub`). */

@media (max-width: 1440px) {
  .ioc-title-rule__line { width: 18px; }
}

/* ≤1520px: cụm giờ xếp 2 TẦNG — thời tiết + thứ/ngày nhảy LÊN TRÊN giờ phút
   giây (yêu cầu 18/08, cỡ ~1490 title đè lên cụm phải). Vẫn lọt --h-masthead
   52px nhờ thu ô lật; kẻ dọc thừa vai trò ngăn cách nên ẩn. */
@media (max-width: 1520px) {
  .ioc-clock {
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 1px;
    column-gap: var(--sp-2);
    padding: 2px 0;
  }
  .ioc-clock__rule { display: none; }
  .ioc-clock__weather svg { width: 14px; height: 14px; }
  .ioc-clock .flip-clock {
    flex-basis: 100%;
    justify-content: center;
    --flip-w: 15px;
    --flip-h: 22px;
  }
}

/**
 * Đồng hồ bảng lật (split-flap) ở header.
 *
 * Về luật chuyển động (rule 06 §4): đây KHÔNG phải animation lặp vô hạn — mỗi
 * ô chỉ chạy đúng một lượt 320ms khi chính nó đổi số, và chỉ bằng `transform`
 * nên trình duyệt xử lý trên GPU, không đụng layout. Ô giây lật mỗi giây, ô
 * phút mỗi phút, ô giờ hàng chục thì gần như đứng yên cả ngày.
 */

.flip-clock {
  --flip-duration: 320ms;
  --flip-w: 20px;
  --flip-h: 30px;

  display: inline-flex;
  align-items: center;
  gap: 3px;
  perspective: 220px;
}

.flip-clock__colon {
  font-family: var(--font-num);
  font-size: calc(var(--flip-h) * 0.62);
  font-weight: 700;
  color: var(--text-mid);
  padding-bottom: 2px;
}

/* ── Một ô số ─────────────────────────────────────────────────────────── */

.flip-digit {
  position: relative;
  display: inline-block;
  width: var(--flip-w);
  height: var(--flip-h);
  border-radius: 3px;
  background: var(--bg-raised);
  box-shadow: inset 0 0 0 1px var(--line-soft);
  font-family: var(--font-num);
  font-size: calc(var(--flip-h) * 0.72);
  font-weight: 700;
  color: var(--text-hi);
  font-variant-numeric: tabular-nums;
  transform-style: preserve-3d;
}

/* Khe giữa hai nửa — thứ làm nó ra dáng bảng lật cơ khí. */
.flip-digit::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  transform: translateY(-0.5px);
  background: var(--bg-void);
  z-index: 3;
}

/* Nửa tĩnh và cánh lật dùng chung cách "cắt đôi chữ số": chữ số luôn được vẽ
   ở kích thước đầy đủ trong `__glyph`, còn khung nửa cao 50% + overflow hidden
   quyết định nhìn thấy phần nào. */
.flip-digit__half,
.flip-digit__leaf {
  position: absolute;
  left: 0;
  width: 100%;
  height: 50%;
  overflow: hidden;
  backface-visibility: hidden;
}

.flip-digit__glyph {
  position: absolute;
  left: 0;
  width: 100%;
  height: var(--flip-h);
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* Nửa trên: chữ bắt đầu từ mép trên ⇒ lộ nửa trên. */
.flip-digit__half--top,
.flip-digit__leaf--fold {
  top: 0;
  border-radius: 3px 3px 0 0;
}

.flip-digit__half--top .flip-digit__glyph,
.flip-digit__leaf--fold .flip-digit__glyph {
  top: 0;
}

/* Nửa dưới: kéo chữ lên đúng nửa chiều cao ô ⇒ lộ nửa dưới. */
.flip-digit__half--bottom,
.flip-digit__leaf--unfold {
  bottom: 0;
  border-radius: 0 0 3px 3px;
}

.flip-digit__half--bottom .flip-digit__glyph,
.flip-digit__leaf--unfold .flip-digit__glyph {
  top: calc(var(--flip-h) * -0.5);
}

/* ── Hai cánh lật ─────────────────────────────────────────────────────── */

.flip-digit__leaf {
  z-index: 2;
  background: var(--bg-raised);
  transform-origin: bottom center;
  /* Đứng yên và vô hình cho tới khi ô này thật sự đổi số. */
  transform: rotateX(0deg);
  visibility: hidden;
}

.flip-digit__leaf--unfold {
  transform-origin: top center;
  transform: rotateX(90deg);
}

.flip-digit--turning .flip-digit__leaf--fold {
  visibility: visible;
  animation: flip-fold var(--flip-duration) ease-in forwards;
}

.flip-digit--turning .flip-digit__leaf--unfold {
  visibility: visible;
  animation: flip-unfold var(--flip-duration) ease-out forwards;
}

@keyframes flip-fold {
  from { transform: rotateX(0deg); }
  to   { transform: rotateX(-90deg); }
}

@keyframes flip-unfold {
  from { transform: rotateX(90deg); }
  to   { transform: rotateX(0deg); }
}

/* Người trực ca bật "giảm chuyển động" thì bỏ hẳn phần lật, số vẫn đổi đúng. */
@media (prefers-reduced-motion: reduce) {
  .flip-digit__leaf {
    display: none;
  }
}
/**
 * Dải phân hệ — hàng THỨ HAI của khung (mẫu S07 `.navbar`).
 *
 * 20/09 dải tab S01…S07 GỠ. Lý do là mô hình triển khai chứ không phải thẩm mỹ:
 * mỗi màn được chiếu lên MỘT màn hình riêng trong phòng điều hành
 * (`?screen=<id>&wall=1`), nên trên tường thật không có ai bấm tab — 7 nút chỉ
 * là chữ thừa. Hàng này nay làm đúng một việc: nói cho người đi ngang qua biết
 * họ đang nhìn hệ thống gì.
 *
 * Cùng ngày hàng riêng từng bị gộp luôn vào thanh lệnh; 21/09 tách lại theo ảnh
 * mẫu người dùng, nên `.ioc-nav` TRỞ LẠI là một thanh: có nền `--grad-nav`
 * (nhạt hơn thanh lệnh một nấc), bo góc và bóng nổi như mọi card khác.
 *
 * Lúc dev, chính chip chữ đó kiêm nút đổi màn (Radix Select — `ScreenNav.tsx`).
 * `?wall=1` thì nó là chữ TĨNH: không caret, không hover, không bấm được.
 *
 * Khung popup, hover, dấu check dùng lại `.rsel__*` của `overlay.css` — dưới
 * đây chỉ tô phần RIÊNG của hàng này.
 */

/* Chiều cao KHÔNG khai ở đây: hàng lưới `--h-nav` của `.shell` giữ, đúng một
   nguồn sự thật cho phép tính khung. `min-width: 0` để chữ bên trong còn chỗ
   mà cắt `…` khi cửa sổ hẹp lại. */
.ioc-nav {
  /* BA cột (22/09): khe trái · chip tên màn · khe phải. Hai cột biên cùng
     `1fr` nên chip tên màn ĐỨNG GIỮA THẬT kể cả khi khe trái có nội dung —
     đó là lý do phải là lưới chứ không phải flex + `justify-content: center`
     (flex thì nội dung khe trái đẩy tên màn lệch sang phải đúng bằng bề rộng
     của nó). Khe phải để trống, giữ chỗ cho cụm bên phải của bản mẫu. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  min-width: 0;
  max-width: 100%;
  padding: 0 var(--sp-2);
  border-radius: var(--r-row);
  overflow: hidden;
  background: var(--grad-nav);
  /* Không viền: tách khỏi nền bằng hairline ảo + bắt sáng mép trên + bóng nhoè
     (nguyên tắc VSS, `--panel-border-w: 0`). */
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-float);
}

/* ── Hai khe hai bên ──────────────────────────────────────────────────── */

/* Khe TRÁI — màn portal bộ điều khiển toàn màn vào đây (`ScreenNavSlot.tsx`).
   Rỗng ở 6 màn còn lại, và rỗng thì vẫn phải chiếm đúng `1fr` của nó, nếu
   không tên màn sẽ nhảy chỗ mỗi lần đổi màn. */
.ioc-nav__lead {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  justify-self: start;
}

.ioc-nav__trail {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  justify-self: end;
}

/* ── Chip PHẠM VI — bộ chọn tác động lên toàn màn ─────────────────────── */

/* MỘT khối viền gồm nhãn vai trò + ô chọn (bản mẫu toolbar của người dùng),
   chứ không phải hai thứ rời nhau: nhãn đứng ngoài khung thì lại thành một
   dòng chữ trang trí cạnh một ô chọn vô danh, đúng thứ đang cần sửa.

   Đây là Ô CHỌN THẬT, không phải tấm biển: nó đổi số của MỌI ô trên màn. Nên
   khác chip tên màn ở hai điểm cố ý — viền ăn `--accent` của phân hệ (đổi màn
   là đổi màu, mắt gắn ngay "bộ lọc này thuộc hệ thống đang xem") và GIỮ caret.

   ⚠ Chỉ được viết `var(--accent)`. Thanh này nằm ngoài `.scr` nhưng theme khai
   ở `:root[data-screen='…']` nên `--accent` ra đúng màu VSS; còn `--accent-power`
   ở tầng `:root` lại là #00E5FF của bản đồ — đúng cú pháp, sai màu, không báo
   lỗi ở đâu cả (xem đầu `screen-themes.css`). */
.ioc-nav__scope {
  display: inline-flex;
  align-items: center;
  min-width: 0;
  max-width: 100%;
  /* 28px: vừa `--h-nav` 36px với 4px thở mỗi bên, nên KHÔNG phải đụng
     `grid-template-rows` của `.shell`. */
  height: 28px;
  border-radius: var(--r-key);
  background: var(--bg-well);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
  overflow: hidden;
}

/* Nhãn vai trò — cái nói control này quản CẢ MÀN chứ không quản một card.
   Ăn `--accent` để cùng một câu với viền; vạch ngăn bên phải tách nó khỏi
   giá trị, khỏi bị đọc dính thành một cụm chữ. */
.ioc-nav__scope-role {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  align-self: stretch;
  padding: 0 var(--sp-2);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: inset -1px 0 0 color-mix(in srgb, var(--accent) 30%, transparent);
  color: var(--accent);
  font-family: var(--font-cond);
  font-size: var(--fs-label);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Trigger Radix nằm TRONG khung trên, nên tự nó trong suốt — viền là của
   khung, không phải của nút. */
.ioc-nav__scope-value {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  min-width: 0;
  flex: 1 1 auto;
  align-self: stretch;
  padding: 0 var(--sp-2);
  border: 0;
  background: transparent;
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  font-weight: var(--fw-value);
  letter-spacing: var(--track-chip);
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.16s;
}

/* Radix `Select.Value` là phần tử con giữ chữ, nên `…` phải đặt ở ĐÓ: nút là
   flex container, chỉ con mới có chỗ mà cắt (cùng lý do như `.ioc-nav__title`). */
.ioc-nav__scope-value > span:first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ioc-nav__scope-value:hover,
.ioc-nav__scope-value[data-state='open'] {
  background: var(--bg-hover);
}

/* Vành tiêu điểm đặt Ở NÚT (khung cha `overflow: hidden` nên vành vẽ gọn
   trong bo góc), không dùng `outline` vì nó vẽ tràn ra ngoài khung. */
.ioc-nav__scope-value:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--accent);
}

/* ── Bộ chọn phạm vi hai tab — màn Điện năng (30/09) ─────────────────────
   Nhà máy · CSVC chủ đầu tư, mỗi tab một danh sách tick nhiều. Khung ngoài là
   `.rpop` (Popover); ở đây chỉ lo bề ngang, tab và dòng tick. */
.escope {
  /* 30/09 chiều: chữ nâng từ `--fs-cap` lên `--fs-body-lg` (user: "hơi bé") nên
     khung nới theo để tên công ty dài vẫn gọn một dòng. */
  width: 440px;
  max-width: calc(100vw - 32px);
  padding: var(--sp-1);
}

.escope__tabs {
  display: flex;
  gap: 2px;
  padding: 2px;
  margin-bottom: var(--sp-1);
  border-radius: var(--r-sm);
  background: var(--bg-well);
}

.escope__tab {
  flex: 1 1 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px var(--sp-2);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text-mid);
  font-family: var(--font-ui);
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-label);
  cursor: pointer;
  transition: background 0.16s, color 0.16s;
}

.escope__tab:hover {
  color: var(--text-hi);
}

/* Tab đang hiệu lực ăn `--accent` của phân hệ — cùng một câu với viền ô phạm vi. */
.escope__tab[aria-selected='true'] {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent);
}

.escope__tab:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--accent);
}

.escope__count {
  font-size: var(--fs-body);
  color: var(--text-lo);
}

.escope__tab[aria-selected='true'] .escope__count {
  color: inherit;
}

.escope__list {
  max-height: 440px;
  overflow-y: auto;
}

.escope__row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 7px var(--sp-2);
  border-radius: var(--r-sm);
  font-size: var(--fs-body-lg);
  color: var(--text-hi);
  cursor: pointer;
  user-select: none;
}

.escope__row:hover {
  background: var(--bg-hover);
}

.escope__row[data-disabled] {
  color: var(--text-faint);
  cursor: default;
}

.escope__row--all {
  margin-bottom: 2px;
  border-bottom: 1px solid var(--line-soft);
  border-radius: 0;
  color: var(--text-mid);
  font-weight: var(--fw-label);
}

.escope__zone-group {
  margin-top: var(--sp-1);
}

.escope__zone {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--bg-well);
  color: var(--text-mid);
  font-size: var(--fs-body);
  font-weight: var(--fw-label);
  cursor: pointer;
  user-select: none;
}

.escope__zone[data-disabled] {
  color: var(--text-faint);
  cursor: default;
}

.escope__zone input {
  width: 1.05em;
  height: 1.05em;
  margin: 0;
  accent-color: var(--accent);
  cursor: inherit;
}

.escope__zone-label {
  flex: 1 1 auto;
}

.escope__zone-count {
  color: var(--text-lo);
  font-size: var(--fs-body);
}

.escope__zone--unassigned {
  cursor: default;
}

.escope__row input {
  flex: 0 0 auto;
  /* Ô tick lớn theo chữ, không đứng yên 14px cạnh dòng chữ đã to lên. */
  width: 1.05em;
  height: 1.05em;
  margin: 0;
  accent-color: var(--accent);
  cursor: inherit;
}

/* Chấm online to theo chữ (mặc định 6px của `.rsel__dot` dành cho chữ `--fs-cap`). */
.escope__row .rsel__dot {
  width: 8px;
  height: 8px;
}

/* Tên công ty dài thì XUỐNG DÒNG, không cắt "…" (lệnh 16/09). */
.escope__label {
  min-width: 0;
  overflow-wrap: anywhere;
}

.escope__empty {
  padding: var(--sp-3) var(--sp-2);
  font-size: var(--fs-body-lg);
  color: var(--text-lo);
  text-align: center;
}


/* ── Bộ chọn phạm vi Nước (GIỐNG HỆT Energy) ────────────────────────── */

/* Bề ngang CỐ ĐỊNH như `.escope` (440px). Thiếu dòng này thì popup co theo nội
   dung từng tab: tab "Nhà máy" 17 dòng tên dài ⇒ rộng, tab "CSVC Chủ Đầu Tư" 6
   dòng ⇒ hẹp — bấm qua lại thấy khung và tab phình/co (lỗi 02/10). */
.wscope {
  width: 440px;
  max-width: calc(100vw - 32px);
  padding: var(--sp-1);
}

.wscope__tabs {
  display: flex;
  gap: 2px;
  padding: 2px;
  margin-bottom: var(--sp-1);
  border-radius: var(--r-sm);
  background: var(--bg-well);
}

.wscope__tab {
  flex: 1 1 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px var(--sp-2);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text-mid);
  font-family: var(--font-ui);
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-label);
  cursor: pointer;
  transition: background 0.16s, color 0.16s;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.wscope__tab:hover {
  color: var(--text-hi);
}

.wscope__tab[aria-selected='true'] {
  background: color-mix(in srgb, var(--accent-water) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-water) 45%, transparent);
  color: var(--accent-water);
}

.wscope__tab:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--accent-water);
}

.wscope__count {
  font-size: var(--fs-body);
  color: var(--text-lo);
}

.wscope__tab[aria-selected='true'] .wscope__count {
  color: inherit;
}

.wscope__list {
  max-height: 440px;
  overflow-y: auto;
}

.wscope__row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 7px var(--sp-2);
  border-radius: var(--r-sm);
  font-size: var(--fs-body-lg);
  color: var(--text-hi);
  cursor: pointer;
  user-select: none;
}

.wscope__row:hover {
  background: var(--bg-hover);
}

.wscope__row[data-disabled] {
  color: var(--text-faint);
  cursor: default;
}

.wscope__row--all {
  margin-bottom: 2px;
  border-bottom: 1px solid var(--line-soft);
  border-radius: 0;
  color: var(--text-mid);
  font-weight: var(--fw-label);
}

.wscope__zone-group {
  margin-top: var(--sp-1);
}

.wscope__zone {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--bg-well);
  color: var(--text-mid);
  font-size: var(--fs-body);
  font-weight: var(--fw-label);
  cursor: pointer;
  user-select: none;
}

.wscope__zone[data-disabled] {
  color: var(--text-faint);
  cursor: default;
}

.wscope__zone input {
  width: 1.05em;
  height: 1.05em;
  margin: 0;
  accent-color: var(--accent-water);
  cursor: inherit;
}

.wscope__zone-label {
  flex: 1 1 auto;
}

.wscope__zone-count {
  color: var(--text-lo);
  font-size: var(--fs-body);
}

.wscope__zone--unassigned {
  cursor: default;
}

.wscope__row input {
  flex: 0 0 auto;
  width: 1.05em;
  height: 1.05em;
  margin: 0;
  accent-color: var(--accent-water);
  cursor: inherit;
}

.wscope__row .rsel__dot {
  width: 8px;
  height: 8px;
}

.wscope__label {
  min-width: 0;
  overflow-wrap: anywhere;
}

.wscope__empty {
  padding: var(--sp-3) var(--sp-2);
  font-size: var(--fs-body-lg);
  color: var(--text-lo);
  text-align: center;
}

/* ── Chip tên màn (cũng là trigger của Select) ────────────────────────── */

.ioc-nav__title {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  max-width: 100%;
  min-width: 0;
  padding: 4px var(--sp-3);
  border: 0;
  border-radius: var(--r-key);
  /* Chip nổi trên thanh: nền nhấc một nấc + viền ảo `--sh-hair` (không dùng
     `border` — nguyên tắc VSS `--panel-border-w: 0`). Viền là thứ nói "bấm
     được" từ xa, trước cả khi đọc kịp caret. */
  background: var(--bg-raised);
  box-shadow: var(--sh-hair);
  /* Chữ TRẮNG, không hổ phách (yêu cầu 21/09): hổ phách trong hệ này mang
     nghĩa "điện năng" (`--accent-power`), dùng cho tên màn thì một màu đang
     nói hai chuyện. Tên màn là chữ trung tính — phân biệt với địa danh ở thanh
     trên bằng VỊ TRÍ và chip có viền, không cần mượn thêm màu. */
  color: var(--text-hi);
  font-family: var(--font-num);
  /* 21/09: `--fs-cap` → `--fs-body-lg`. Đây là dòng chữ nói người xem đang
     nhìn hệ thống nào — cỡ caption là cỡ của chú thích, đọc không ra từ 3–5m.
     Vẫn vừa `--h-nav` 36px: 20px (trần) × 1.2 + 8px đệm = 32px. */
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-body);
  letter-spacing: var(--track-wide);
  line-height: 1.2;
  text-transform: uppercase;
  /* Ba media query cũ (1600/1440/1500) đã bỏ cùng dải tab: hàng này chỉ còn
     MỘT thứ nên không còn gì để hy sinh theo bậc nữa — hẹp tới đâu cắt chữ
     tới đó. KHÔNG xuống dòng: xuống dòng là vỡ `--h-nav` và xô cả lưới. */
  white-space: nowrap;
  overflow: hidden;
  cursor: pointer;
  transition: background 0.16s, color 0.16s;
}

/* Radix `Select.Value` là phần tử con giữ chữ, nên `…` phải đặt ở ĐÓ: nút là
   flex container, chỉ con mới có chỗ mà cắt. */
.ioc-nav__title > span:first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ioc-nav__title:hover {
  background: var(--bg-hover);
}

/* Vành tiêu điểm THAY viền ảo (cùng thuộc tính `box-shadow`, không cộng dồn
   được) — đủ dày để thấy rõ, nên không mất tín hiệu mép chip. */
.ioc-nav__title:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* Radix tự đặt `data-state` — không cần state React để tô lúc menu đang mở. */
.ioc-nav__title[data-state='open'] {
  background: var(--bg-hover);
}

/* Caret GỠ (yêu cầu 21/09): trên tường đây là tấm biển tên màn, không phải ô
   chọn — mũi tên chỉ mời người đi ngang bấm vào thứ chỉ dev mới dùng. Tín hiệu
   bấm được vẫn còn đủ: chip có viền ảo + nền đổi khi hover/mở.
   `display: none` chứ không xoá icon trong `Select.tsx` — caret là của
   component dùng chung, mọi select khác vẫn cần. */
.ioc-nav__title .rsel__chevron {
  display: none;
}

/* Bản TĨNH của chế độ tường — giữ nguyên dáng chữ, bỏ mọi tín hiệu bấm được. */
.ioc-nav__title--static {
  cursor: default;
  text-overflow: ellipsis;
}

.ioc-nav__title--static:hover {
  background: var(--bg-raised);
}

/* ── Một dòng trong dropdown ──────────────────────────────────────────── */

/* Danh sách màn phải hiện TRỌN, không cuộn: trần 320px chung của `.rsel__content` chỉ chứa
   được 7 dòng cỡ `--fs-sub`, màn thứ 8 (Cảnh báo & sự cố, 01/10) rơi khỏi khung mà không có
   dấu hiệu gì là còn nữa. Cao theo chỗ trống thật Radix đo được dưới nút. */
.rsel__content:has(.ioc-nav__item),
.rsel__content:has(.ioc-nav__item) .rsel__viewport {
  max-height: var(--radix-select-content-available-height);
}

/* Rộng theo TÊN DÀI NHẤT, không theo chip: chip đổi bề ngang theo tên màn đang chiếu (An ninh
   ngắn, Năng lượng điện dài) nên danh sách bám chip thì cứ ở màn tên ngắn là tên dài bị bẻ đôi
   (phản hồi 01/10). Trần là bề ngang cửa sổ Radix đo được. */
.rsel__content:has(.ioc-nav__item) {
  width: max-content;
  min-width: var(--radix-select-trigger-width);
  max-width: var(--radix-select-content-available-width);
}

/* Radix bọc nội dung item trong span riêng của `ItemText`. Span đó là flex
   child của `.rsel__item` nên mặc định KHÔNG co dưới bề rộng chữ — tên dài
   bị `.rsel__content` cắt cụt (thấy rõ ở bản EN của S05). Mở cho nó co thì
   tên dài mới xuống dòng được. */
.rsel__item > span:has(> .ioc-nav__item) {
  min-width: 0;
}

/* Một dòng = MỘT tên màn đầy đủ "Hệ thống …" (01/10).

   Chữ THƯỜNG có hoa đầu từ, font giao diện, không giãn chữ — tám dòng chữ HOA dãn rộng cùng mở
   đầu bằng "HỆ THỐNG" đọc thành một khối đặc, mắt không bám được dòng nào (phản hồi 01/10).
   Chip phía trên vẫn chữ hoa: đó là biển tên màn, còn đây là danh sách để đọc và chọn.

   Cỡ `--fs-sub` (to hơn chip) vì đây là danh sách bấm trúng từ xa. Cỡ chữ đặt ở DÒNG chứ không ở
   nhãn con: dấu check đo bằng `em` nên lớn theo dòng. `:has` khoanh đúng danh sách màn, select
   khác giữ nguyên. */
.rsel__content:has(.ioc-nav__item) .rsel__viewport {
  padding: var(--sp-2);
}

.rsel__item:has(.ioc-nav__item) {
  gap: var(--sp-4);
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-key);
  font-size: var(--fs-sub);
}

.rsel__item:has(.ioc-nav__item) + .rsel__item:has(.ioc-nav__item) {
  margin-top: 2px;
}

/* Màn đang chiếu: nền + chữ màu accent, không chỉ một dấu check nhỏ ở mép. */
.rsel__item:has(.ioc-nav__item)[data-state='checked'] {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

.rsel__item:has(.ioc-nav__item)[data-state='checked'] .ioc-nav__item {
  color: var(--accent);
  font-weight: var(--fw-title);
}

.rsel__item:has(.ioc-nav__item)[data-highlighted]:not([data-state='checked']) {
  background: var(--bg-hover);
}

.rsel__item:has(.ioc-nav__item) .rsel__check svg {
  width: 1em;
  height: 1em;
}

.ioc-nav__item {
  display: block;
  min-width: 0;
  font-family: var(--font-ui);
  font-weight: var(--fw-body);
  line-height: 1.3;
  color: var(--text-hi);
  /* Mỗi tên MỘT dòng — danh sách tự rộng ra cho vừa, xem `.rsel__content` ở trên.
     Không cắt "…" (lệnh 16/09). */
  white-space: nowrap;
}

/* Màn KCN đang xem chưa hỗ trợ (01/10): tên mờ đi — `.ioc-nav__item` ép
   `--text-hi` nên đè mất màu `[data-disabled]` chung của `.rsel__item`. */
.rsel__item[data-disabled] .ioc-nav__item {
  color: var(--text-faint);
}

/* Lý do màn bị khoá — dòng phụ dưới tên, chữ thường cỡ chú thích. */
.ioc-nav__note {
  display: block;
  min-width: 0;
  margin-top: 2px;
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  color: var(--text-lo);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/**
 * Sơ đồ mặt bằng KCN (SVG vẽ từ bản vẽ quy hoạch thật).
 *
 * Màu nằm ở đây chứ không trong component: SVG `fill`/`stroke` ăn được
 * `var(--…)` nên vẫn giữ đúng luật một nguồn token (rule 06). Bảng màu QGIS
 * của file gốc là màu ngẫu nhiên QGIS tự gán khi thêm layer, không phải quy
 * ước ngành — nên không bám theo, dùng token của hệ thống.
 */

.site-plan {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--bg-void);
  touch-action: none;
}

/* Lớp nền tile — nằm DƯỚI vector, không nhận chuột (khung ngoài lo pan/zoom). */
.site-plan__tiles {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.site-plan__tile-canvas,
.site-plan__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.site-plan__canvas {
  cursor: grab;
}

.site-plan__canvas--dragging {
  cursor: grabbing;
}

/* ── Địa vật ──────────────────────────────────────────────────────────── */

/* Bảng màu cố ý NHẠT.
   Đây là nền của một màn hình giám sát, không phải bản đồ quy hoạch in giấy:
   thứ được phép rực rỡ là cảnh báo và thiết bị đặt lên trên, còn địa vật chỉ
   cần đủ đọc. Bản trước tô cyan/vàng bão hoà nên nhìn như bản vẽ CAD dán đè
   lên ảnh, mọi thứ cùng hét lên một lúc. */

.site-plan__green {
  fill: color-mix(in srgb, var(--st-good) 9%, transparent);
  stroke: color-mix(in srgb, var(--st-good) 22%, transparent);
  stroke-width: 0.3;
  vector-effect: non-scaling-stroke;
}

.site-plan__water {
  fill: color-mix(in srgb, var(--accent-water) 14%, transparent);
  stroke: color-mix(in srgb, var(--accent-water) 35%, transparent);
  stroke-width: 0.4;
  vector-effect: non-scaling-stroke;
}

.site-plan__road {
  fill: color-mix(in srgb, var(--bg-raised) 32%, transparent);
  stroke: color-mix(in srgb, var(--line-mid) 45%, transparent);
  stroke-width: 0.4;
  vector-effect: non-scaling-stroke;
}

.site-plan__road--centre {
  fill: color-mix(in srgb, var(--text-faint) 12%, transparent);
  stroke: color-mix(in srgb, var(--text-lo) 30%, transparent);
}

/* Ranh lô là bộ xương của sơ đồ nên vẫn phải thấy, nhưng bằng nét xám mảnh
   chứ không phải một mạng lưới neon. */
.site-plan__lot {
  fill: none;
  stroke: color-mix(in srgb, var(--text-mid) 42%, transparent);
  stroke-width: 0.9;
  vector-effect: non-scaling-stroke;
}

/* Mã lô (CN02, CL07…) tại vị trí chữ của bản vẽ chia lô — chữ chìm trên nền,
   không nền không viền, như chữ in trên mặt bằng. */
.site-plan__lotcode {
  fill: color-mix(in srgb, var(--text-mid) 70%, transparent);
  font-family: var(--font-num);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-anchor: middle;
  pointer-events: none;
}

/* Vùng đất theo chức năng: khép kín nên bấm được → mở popup chi tiết.
   Ở trạng thái nghỉ chỉ là viền mờ; rê chuột mới sáng lên để biết bấm được. */
.site-plan__zone {
  fill: transparent;
  stroke: color-mix(in srgb, var(--st-warn) 38%, transparent);
  stroke-width: 1.1;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
  transition: fill 0.16s, stroke 0.16s;
}

/* Lô theo LỚP ĐẤT — chỉ KCN bật `caps.fills` (Thủy Nguyên: chưa có số liệu vận
   hành, nền là thông tin chính). Đặt TRƯỚC hover/selected để hai trạng thái đó
   vẫn thắng. Đồng Văn không mang các lớp này nên giữ nguyên mặt trong suốt. */
.site-plan__zone--lu-cx,
.site-plan__zone--lu-cl {
  fill: color-mix(in srgb, var(--st-good) 12%, transparent);
}

.site-plan__zone--lu-mn {
  fill: color-mix(in srgb, var(--accent-water) 18%, transparent);
}

.site-plan__zone--lu-cn,
.site-plan__zone--lu-dv,
.site-plan__zone--lu-ht {
  fill: color-mix(in srgb, var(--text-mid) 7%, transparent);
}

.site-plan__zone:hover {
  fill: color-mix(in srgb, var(--st-warn) 14%, transparent);
  stroke: var(--st-warn);
}

/* Khu đang CHỌN (popup mở): GIỮ viền vàng sáng tới khi bấm ra ngoài —
   CHỈ viền, không tô nền (chốt 17/08, đồng bộ 2D/3D). */
.site-plan__zone--selected,
.site-plan__zone--selected:hover {
  fill: transparent;
  stroke: var(--st-warn);
  stroke-width: 2;
}

/* KHU cơ sở vật chất (Nhà điều hành, Nhà máy XLNT — 20/10): viền đứt màu camera, bấm vào là ra
   camera của khu. Đứng SAU `--selected` trong file nhưng chọn rồi thì viền vàng phải thắng. */
.site-plan__zone--facility {
  fill: color-mix(in srgb, var(--accent-vision) 8%, transparent);
  stroke: var(--accent-vision);
  stroke-width: 1.6;
  stroke-dasharray: 6 4;
  vector-effect: non-scaling-stroke;
}

.site-plan__zone--facility.site-plan__zone--selected {
  fill: transparent;
  stroke: var(--st-warn);
  stroke-dasharray: none;
}

.site-plan__facility {
  fill: var(--accent-vision);
  font-family: var(--font-ui);
  font-weight: var(--fw-label);
  text-anchor: middle;
  dominant-baseline: middle;
  paint-order: stroke;
  stroke: var(--bg-void);
  stroke-width: 0.6px;
  pointer-events: none;
}

/* Lớp PHÂN LÔ (lô con CN01–CN09) — màu lớp set qua `color` inline trên <g>.
   Nghỉ: viền mảnh + mặt gần trong suốt (vẫn phải có fill để nhận chuột);
   rê chuột: tô nhạt + viền đậm, popup diện tích do SitePlanMap vẽ. */
.site-plan__sublot {
  fill: color-mix(in srgb, currentColor 4%, transparent);
  stroke: color-mix(in srgb, currentColor 55%, transparent);
  stroke-width: 0.8;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
  transition: fill 0.16s, stroke 0.16s;
}

.site-plan__sublot:hover {
  fill: color-mix(in srgb, currentColor 22%, transparent);
  stroke: currentColor;
  stroke-width: 1.6;
}

.site-plan__sublotcode {
  fill: currentColor;
  font-family: var(--font-num);
  font-weight: 700;
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}

/* Lớp TRẠM BTS — đoạn tuyến đang rê chuột "nổi" khỏi mặt bản đồ: nhấc lên 6px
   (khớp FLOAT_PX của TelecomOverlay2D) + đổ bóng. Nhấc MỘT lần trong 150ms khi
   vào đoạn, không lặp (rule 06 §4). Bóng tại chỗ cũ do component vẽ riêng. */
.telecom-float {
  transform: translateY(-6px);
  filter: drop-shadow(0 6px 3px var(--bg-void));
  animation: telecom-lift 150ms ease-out;
}

@keyframes telecom-lift {
  from {
    transform: translateY(0);
  }
}

/* ── KCN dựng bằng mapkcn/build-park-map.py (ParkLayer2D) ──────────────── */

/* Ranh dự án — nét đứt như chú giải "Ranh giới dự án giai đoạn 1" của bản vẽ. */
.site-plan__boundary {
  fill: none;
  stroke: var(--st-crit);
  stroke-width: 1.6;
  stroke-dasharray: 7 4;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

/* Khối nhà xưởng / nhà điều hành của bản vẽ kiến trúc cảnh quan. Không bắt
   chuột: bấm vào khối là chọn lô bên dưới. */
.site-plan__factory {
  fill: color-mix(in srgb, var(--text-mid) 24%, transparent);
  stroke: color-mix(in srgb, var(--text-mid) 60%, transparent);
  stroke-width: 0.6;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

/* Bối cảnh HIỆN TRẠNG ngoài ranh (bản đo 1:2000) — chìm hơn mọi lớp quy hoạch. */
.site-plan__ctx-house {
  fill: color-mix(in srgb, var(--text-lo) 30%, transparent);
  stroke: none;
  pointer-events: none;
}

.site-plan__ctx-road {
  fill: none;
  stroke: color-mix(in srgb, var(--text-lo) 45%, transparent);
  stroke-width: 0.6;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

.site-plan__ctx-canal {
  fill: none;
  stroke: color-mix(in srgb, var(--accent-water) 45%, transparent);
  stroke-width: 0.8;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

.site-plan__ctx-water {
  fill: color-mix(in srgb, var(--accent-water) 14%, transparent);
  stroke: none;
  pointer-events: none;
}

.site-plan__ctx-power {
  fill: none;
  stroke: color-mix(in srgb, var(--accent-power) 65%, transparent);
  stroke-width: 0.9;
  stroke-dasharray: 8 4;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

/* Tên tuyến đường ("TUYẾN 2") theo hướng chữ của bản vẽ. Bề dày quầng chữ đặt
   inline theo cỡ chữ (đơn vị viewBox đổi theo mức zoom). */
.site-plan__roadname {
  fill: color-mix(in srgb, var(--text-hi) 72%, transparent);
  font-family: var(--font-ui);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-anchor: middle;
  dominant-baseline: central;
  paint-order: stroke;
  stroke: var(--bg-void);
  stroke-linejoin: round;
  pointer-events: none;
}

/* ── Lớp bản vẽ CAD hạ tầng (màu theo nhóm, set qua `color` inline) ───── */

.site-plan__cad-line {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  opacity: 0.9;
}

.site-plan__cad-point {
  fill: color-mix(in srgb, currentColor 35%, transparent);
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* ── Điều khiển & chú giải ────────────────────────────────────────────── */

.site-plan__toolbar {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  display: flex;
  gap: 4px;
  z-index: 2;
}

.site-plan__legend {
  position: absolute;
  left: var(--sp-2);
  bottom: var(--sp-2);
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--sp-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  background: rgba(4, 8, 20, 0.82);
  font-size: var(--fs-micro);
  color: var(--text-mid);
}

.site-plan__legend-row {
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

.site-plan__swatch {
  width: 10px;
  height: 10px;
  flex: 0 0 auto;
  border-radius: 2px;
  border: 1px solid currentColor;
}

.site-plan__scale {
  position: absolute;
  right: var(--sp-2);
  bottom: var(--sp-2);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-family: var(--font-num);
  font-size: var(--fs-micro);
  color: var(--text-mid);
}

.site-plan__scale-bar {
  height: 4px;
  width: 100%;
  border: 1px solid var(--text-mid);
  border-top: none;
}

/* ── Lớp overlay neo theo bản đồ ──────────────────────────────────────── */
/* Bản thân lớp không bắt chuột (để còn kéo bản đồ xuyên qua); từng marker
   con tự bật pointer-events để bấm được. */
.site-plan__overlay {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 5;
}

.site-plan__overlay > * {
  pointer-events: auto;
}

/* ══════════════════════════════════════════════════════════════════════
   Cảnh báo màn Năng lượng — EnergyAlarmList.tsx (thiết kế lại 20/10)
   Nhóm theo trạng thái · biểu tượng phân hệ · không cắt chữ.
   ══════════════════════════════════════════════════════════════════════ */

.eal {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  gap: var(--sp-3);
  min-height: 0;
  overflow-y: auto;
}

/* Nhóm và dòng KHÔNG co: thân panel cao cố định, để co là các dòng đè lên nhau — cuộn thay vì ép. */
.eal__group {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  gap: 6px;
}

.eal__head,
.eal__row {
  flex: 0 0 auto;
}

/* Tiêu đề nhóm: chữ nhãn + viên số đếm, kẻ một đường mảnh tới mép phải. */
.eal__head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0 2px;
  color: var(--text-hi);
  font-family: var(--font-cond);
  font-size: var(--fs-cap);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.eal__head::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--line-soft);
}

.eal__count {
  min-width: 22px;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--bg-raised);
  color: var(--text-hi);
  font-family: var(--font-num);
  font-size: var(--fs-micro);
  line-height: 18px;
  text-align: center;
}

/* Dòng: biểu tượng | tên · vị trí | giá trị · giờ */
.eal__row {
  --eal-tone: var(--line-mid);
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) auto;
  grid-template-areas:
    'icon name  value'
    'icon place time';
  column-gap: var(--sp-2);
  row-gap: 2px;
  align-items: start;
  padding: 8px var(--sp-3) 8px var(--sp-2);
  border-radius: var(--r-key);
  background: var(--bg-panel-solid);
  box-shadow: inset 3px 0 0 var(--eal-tone);
}

.eal__row--crit {
  --eal-tone: var(--st-crit);
  background: color-mix(in srgb, var(--st-crit) 10%, var(--bg-panel-solid));
}

.eal__row--warn {
  --eal-tone: var(--st-warn);
  background: color-mix(in srgb, var(--st-warn) 9%, var(--bg-panel-solid));
}

.eal__row--info {
  --eal-tone: var(--st-info);
}

/* Đã xử lý: không vạch màu, nền trơn — mắt chỉ dừng ở dòng còn việc. */
.eal__row--done {
  box-shadow: none;
}

.eal__icon {
  grid-area: icon;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
}

.eal__icon--elec {
  color: var(--accent-power);
  background: color-mix(in srgb, var(--accent-power) 16%, var(--bg-void));
}

.eal__icon--water {
  color: var(--accent-water);
  background: color-mix(in srgb, var(--accent-water) 16%, var(--bg-void));
}

.eal__name {
  grid-area: name;
  margin: 0;
  color: var(--text-hi);
  font-size: var(--fs-body);
  font-weight: var(--fw-title);
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.eal__place {
  grid-area: place;
  margin: 0;
  color: var(--text-mid);
  font-size: var(--fs-cap);
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.eal__reading {
  grid-area: value;
  justify-self: end;
  color: var(--eal-tone);
  font-family: var(--font-num);
  font-size: var(--fs-body);
  font-weight: var(--fw-title);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.eal__row--done .eal__reading {
  color: var(--text-mid);
}

.eal__time {
  grid-area: time;
  justify-self: end;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  color: var(--text-mid);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.eal__date {
  color: var(--text-mid);
  font-size: var(--fs-micro);
}
/* ══════════════════════════════════════════════════════════════════════
   Xem lại từ NVR — CameraPlaybackDialog.tsx (11/09). Bọc trong `.rdlg`
   (overlay.css). Trình phát = media-chrome: màu/cỡ qua biến `--media-*`
   ánh xạ token. `left`/`width` của thanh tua là giá trị runtime (inline).
   ══════════════════════════════════════════════════════════════════════ */

.campb__row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.campb__field {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-cap);
  color: var(--text-mid);
}

/* Hộp thoại CỐ ĐỊNH 80% × 80% màn hình (lệnh 12/09) — không co theo nội dung.
   Thân hộp là cột flex: hàng chọn giờ · khung video + danh sách (lấp đầy) ·
   thanh tua · chân. */
.campb__dialog .rdlg__body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  overflow: hidden;
}

.campb__body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 280px;
  gap: var(--sp-3);
}

/* Cột trái chỉ chứa khung video; thanh tua duy nhất nằm dưới toàn bộ phần thân. */
.campb__main {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  gap: var(--sp-2);
}

/* Trong cột flex thì khung video lấp phần còn lại thay vì `height: 100%`. */
.campb__main > .campb__stage {
  flex: 1;
  height: auto;
}

/* Khung video lấp đầy ô lưới — trống hay đang phát đều CÙNG một cỡ; hình thật
   `contain` bên trong. */
.campb__stage {
  position: relative;
  height: 100%;
  min-height: 0;
  background: var(--bg-void);
  border-radius: var(--r-row);
  box-shadow: var(--sh-hair);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Chưa có luồng: chỉ đổi nền sáng hơn một bậc để phân biệt với video đen. */
.campb__stage--empty {
  background: var(--bg-well);
}

/* ── media-chrome: khung điều khiển ăn token ─────────────────────────── */
.campb__player {
  display: block;
  width: 100%;
  height: 100%;
  --media-control-background: color-mix(in srgb, var(--bg-void) 72%, transparent);
  --media-control-hover-background: color-mix(in srgb, var(--accent) 35%, transparent);
  --media-primary-color: var(--text-hi);
  --media-secondary-color: var(--bg-well);
  --media-icon-color: var(--text-hi);
  --media-text-color: var(--text-mid);
  --media-font-family: var(--font-num);
  --media-font-size: var(--fs-cap);
  --media-control-height: 32px;
  --media-focus-box-shadow: inset 0 0 0 2px var(--accent);
}

.campb__video-el {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  background: var(--bg-void);
}

/* PIPE WHEP: chỉ để giữ phiên WebRTC + đẻ MediaStream cho DVR, không hiển thị.
   `display:none` làm Chromium ngừng giải mã → không có track cho MediaRecorder;
   nên vẫn render nhưng giấu khỏi mắt (1×1, trong suốt, ngoài luồng). */
.campb__pipe {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* Nút ⏪ / ⏩ trong thanh điều khiển — đồng bộ chiều cao với media-chrome. */
.campb__skip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: var(--media-control-height);
  padding: 0 var(--sp-2);
  border: none;
  background: var(--media-control-background);
  color: var(--text-hi);
}

.campb__skip:hover:not(:disabled) {
  background: var(--media-control-hover-background);
}

.campb__skip:disabled {
  color: var(--text-faint);
  cursor: default;
}

.campb__skip-n {
  font-family: var(--font-num);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
}

/* "Đang tải" khi DVR chưa nạp được khung đầu — dùng đúng `.cam-loading` của
   ô camera (ba chấm CSS, rule 06 §4); ở đây chỉ định vị phủ lên khung. */
.campb__buffering {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Nút "VỀ TRỰC TIẾP" — sáng accent khi đang bám mép, mờ khi đang tua lui. */
.campb__live-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: var(--media-control-height);
  padding: 0 var(--sp-2);
  border: none;
  background: var(--media-control-background);
  color: var(--text-mid);
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
}

.campb__live-btn--on {
  color: var(--st-good);
}

.campb__live-btn:hover:not(:disabled) {
  background: var(--media-control-hover-background);
  color: var(--text-hi);
}

.campb__live-btn:disabled {
  color: var(--text-faint);
  cursor: default;
}

/* Đồng hồ giờ thật (NVR) trong thanh điều khiển — media-chrome chỉ biết
   `currentTime` của thẻ, vô nghĩa với luồng WebRTC. */
.campb__clock {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--sp-2);
  height: var(--media-control-height);
  background: var(--media-control-background);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  color: var(--text-hi);
}

.campb__spacer {
  flex: 1;
  height: var(--media-control-height);
  background: var(--media-control-background);
}

.campb__badges {
  position: absolute;
  top: var(--sp-2);
  left: var(--sp-2);
  display: flex;
  gap: var(--sp-1);
  pointer-events: none;
}

.campb__hint {
  max-width: 70%;
  padding: var(--sp-4);
  color: var(--text-mid);
  font-size: var(--fs-body-lg);
  text-align: center;
}

.campb__hint--crit {
  color: var(--st-crit);
  font-weight: 700;
}

.campb__side {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-height: 0;
  padding: var(--sp-2);
  border-radius: var(--r-row);
  box-shadow: var(--sh-hair);
}

.campb__list-head {
  font-size: var(--fs-cap);
  color: var(--text-lo);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.campb__list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  overflow-y: auto;
  min-height: 0;
}

/* Nhãn ngày chia nhóm trong danh sách đoạn — dính ở mép trên khi cuộn. */
.campb__day {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--sp-1) 0;
  background: var(--bg-panel-solid);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  color: var(--accent);
}

.campb__day:not(:first-child) {
  margin-top: var(--sp-1);
}

.campb__seg {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-key);
  background: var(--bg-well);
  color: var(--text-hi);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  text-align: left;
  cursor: pointer;
}

.campb__seg:hover:not(:disabled) {
  border-color: var(--line-mid);
}

.campb__seg--on {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.campb__seg-len,
.campb__note {
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

.campb__note--warn {
  color: var(--st-warn);
}

/* ── Thanh tua: trục Từ→Đến, đoạn đã ghi tô đậm, vạch đỏ = vị trí ─────── */
.campb__timeline {
  position: relative;
  height: 16px;
  margin: var(--sp-3) 0 var(--sp-4);
  border-radius: var(--r-pill);
  background: var(--bg-well);
  cursor: pointer;
  user-select: none;
  touch-action: none;
}

.campb__tl-seg {
  position: absolute;
  top: 0;
  height: 100%;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 55%, transparent);
  pointer-events: none;
}

.campb__tl-seg--on {
  background: var(--accent);
}

.campb__tl-pos {
  position: absolute;
  top: -4px;
  bottom: -4px;
  width: 2px;
  margin-left: -1px;
  background: var(--st-crit);
  box-shadow: 0 0 6px var(--st-crit);
  pointer-events: none;
}

/* Vạch mờ tại con trỏ khi rê — cho biết sẽ tua tới đâu trước khi bấm. */
.campb__tl-ghost {
  position: absolute;
  top: -2px;
  bottom: -2px;
  width: 1px;
  background: var(--text-mid);
  opacity: 0.7;
  pointer-events: none;
}

/* Nhãn giờ nổi khi rê / kéo. */
.campb__tl-scrub {
  position: absolute;
  bottom: calc(100% + var(--sp-1));
  transform: translateX(-50%);
  padding: 2px var(--sp-2);
  border-radius: var(--r-pill);
  background: var(--bg-panel-solid);
  box-shadow: var(--sh-hair);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  color: var(--text-hi);
  white-space: nowrap;
  pointer-events: none;
}

.campb__tl-label {
  position: absolute;
  top: 100%;
  margin-top: 2px;
  font-family: var(--font-num);
  font-size: var(--fs-micro);
  color: var(--text-lo);
  pointer-events: none;
}

.campb__tl-label--r {
  right: 0;
}

.campb__foot {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* Cao cố định: có/không có chữ (Dừng · vị trí · hết hạn) thì khung video
     phía trên vẫn đúng một cỡ, không nhích 22 px lúc bắt đầu phát. */
  min-height: 28px;
  font-size: var(--fs-cap);
  color: var(--text-mid);
}

.campb__pos {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  color: var(--text-hi);
}
/* Variables declaration */
.rdp-root {
  --rdp-accent-color: blue; /* The accent color used for selected days and UI elements. */
  --rdp-accent-background-color: #f0f0ff; /* The accent background color used for selected days and UI elements. */

  --rdp-day-height: 44px; /* The height of the day cells. */
  --rdp-day-width: 44px; /* The width of the day cells. */

  --rdp-day_button-border-radius: 100%; /* The border radius of the day cells. */
  --rdp-day_button-border: 2px solid transparent; /* The border of the day cells. */
  --rdp-day_button-height: 42px; /* The height of the day cells. */
  --rdp-day_button-width: 42px; /* The width of the day cells. */

  --rdp-selected-border: 2px solid var(--rdp-accent-color); /* The border of the selected days. */
  --rdp-disabled-opacity: 0.5; /* The opacity of the disabled days. */
  --rdp-outside-opacity: 0.75; /* The opacity of the days outside the current month. */
  --rdp-today-color: var(--rdp-accent-color); /* The color of the today's date. */

  --rdp-dropdown-gap: 0.5rem; /* The gap between the dropdowns used in the month captons. */

  --rdp-months-gap: 2rem; /* The gap between the months in the multi-month view. */

  --rdp-nav_button-disabled-opacity: 0.5; /* The opacity of the disabled navigation buttons. */
  --rdp-nav_button-height: 2.25rem; /* The height of the navigation buttons. */
  --rdp-nav_button-width: 2.25rem; /* The width of the navigation buttons. */
  --rdp-nav-height: 2.75rem; /* The height of the navigation bar. */

  --rdp-range_middle-background-color: var(--rdp-accent-background-color); /* The color of the background for days in the middle of a range. */
  --rdp-range_middle-color: inherit; /* The color of the range text. */

  --rdp-range_start-color: white; /* The color of the range text. */
  --rdp-range_start-background: linear-gradient(
    var(--rdp-gradient-direction),
    transparent 50%,
    var(--rdp-range_middle-background-color) 50%
  ); /* Used for the background of the start of the selected range. */
  --rdp-range_start-date-background-color: var(--rdp-accent-color); /* The background color of the date when at the start of the selected range. */

  --rdp-range_end-background: linear-gradient(
    var(--rdp-gradient-direction),
    var(--rdp-range_middle-background-color) 50%,
    transparent 50%
  ); /* Used for the background of the end of the selected range. */
  --rdp-range_end-color: white; /* The color of the range text. */
  --rdp-range_end-date-background-color: var(--rdp-accent-color); /* The background color of the date when at the end of the selected range. */

  --rdp-week_number-border-radius: 100%; /* The border radius of the week number. */
  --rdp-week_number-border: 2px solid transparent; /* The border of the week number. */

  --rdp-week_number-height: var(--rdp-day-height); /* The height of the week number cells. */
  --rdp-week_number-opacity: 0.75; /* The opacity of the week number. */
  --rdp-week_number-width: var(--rdp-day-width); /* The width of the week number cells. */
  --rdp-weeknumber-text-align: center; /* The text alignment of the weekday cells. */

  --rdp-weekday-opacity: 0.75; /* The opacity of the weekday. */
  --rdp-weekday-padding: 0.5rem 0rem; /* The padding of the weekday. */
  --rdp-weekday-text-align: center; /* The text alignment of the weekday cells. */

  --rdp-gradient-direction: 90deg;

  --rdp-animation_duration: 0.3s;
  --rdp-animation_timing: cubic-bezier(0.4, 0, 0.2, 1);
}

.rdp-root[dir="rtl"] {
  --rdp-gradient-direction: -90deg;
}

.rdp-root[data-broadcast-calendar="true"] {
  --rdp-outside-opacity: unset;
}

/* Root of the component. */
.rdp-root {
  position: relative; /* Required to position the navigation toolbar. */
  box-sizing: border-box;
}

.rdp-root * {
  box-sizing: border-box;
}

.rdp-day {
  width: var(--rdp-day-width);
  height: var(--rdp-day-height);
  text-align: center;
}

.rdp-day_button {
  background: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  font: inherit;
  color: inherit;
  justify-content: center;
  align-items: center;
  display: flex;

  width: var(--rdp-day_button-width);
  height: var(--rdp-day_button-height);
  border: var(--rdp-day_button-border);
  border-radius: var(--rdp-day_button-border-radius);
}

.rdp-day_button:disabled {
  cursor: revert;
}

.rdp-caption_label {
  z-index: 1;

  position: relative;
  display: inline-flex;
  align-items: center;

  white-space: nowrap;
  border: 0;
}

.rdp-dropdown:focus-visible ~ .rdp-caption_label {
  outline: 5px auto Highlight;
  /* biome-ignore lint/suspicious/noDuplicateProperties: backward compatibility */
  outline: 5px auto -webkit-focus-ring-color;
}

.rdp-button_next,
.rdp-button_previous {
  border: none;
  background: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  font: inherit;
  color: inherit;
  -moz-appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  appearance: none;

  width: var(--rdp-nav_button-width);
  height: var(--rdp-nav_button-height);
}

.rdp-button_next:disabled,
.rdp-button_next[aria-disabled="true"],
.rdp-button_previous:disabled,
.rdp-button_previous[aria-disabled="true"] {
  cursor: revert;

  opacity: var(--rdp-nav_button-disabled-opacity);
}

.rdp-chevron {
  display: inline-block;
  fill: var(--rdp-accent-color);
}

.rdp-root[dir="rtl"] .rdp-nav .rdp-chevron {
  transform: rotate(180deg);
  transform-origin: 50%;
}

.rdp-dropdowns {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--rdp-dropdown-gap);
}
.rdp-dropdown {
  z-index: 2;

  /* Reset */
  opacity: 0;
  appearance: none;
  position: absolute;
  inset-block-start: 0;
  inset-block-end: 0;
  inset-inline-start: 0;
  width: 100%;
  margin: 0;
  padding: 0;
  cursor: inherit;
  border: none;
  line-height: inherit;
}

.rdp-dropdown_root {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.rdp-dropdown_root[data-disabled="true"] .rdp-chevron {
  opacity: var(--rdp-disabled-opacity);
}

.rdp-month_caption {
  display: flex;
  align-content: center;
  height: var(--rdp-nav-height);
  font-weight: bold;
  font-size: large;
}

.rdp-root[data-nav-layout="around"] .rdp-month,
.rdp-root[data-nav-layout="after"] .rdp-month {
  position: relative;
}

.rdp-root[data-nav-layout="around"] .rdp-month_caption {
  justify-content: center;
  margin-inline-start: var(--rdp-nav_button-width);
  margin-inline-end: var(--rdp-nav_button-width);
  position: relative;
}

.rdp-root[data-nav-layout="around"] .rdp-button_previous {
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  height: var(--rdp-nav-height);
  display: inline-flex;
}

.rdp-root[data-nav-layout="around"] .rdp-button_next {
  position: absolute;
  inset-inline-end: 0;
  top: 0;
  height: var(--rdp-nav-height);
  display: inline-flex;
  justify-content: center;
}

.rdp-months {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: var(--rdp-months-gap);
  max-width: fit-content;
}

.rdp-month_grid {
  border-collapse: collapse;
}

.rdp-nav {
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;

  display: flex;
  align-items: center;

  height: var(--rdp-nav-height);
}

.rdp-weekday {
  opacity: var(--rdp-weekday-opacity);
  padding: var(--rdp-weekday-padding);
  font-weight: 500;
  font-size: smaller;
  text-align: var(--rdp-weekday-text-align);
  text-transform: var(--rdp-weekday-text-transform);
}

.rdp-week_number {
  opacity: var(--rdp-week_number-opacity);
  font-weight: 400;
  font-size: small;
  height: var(--rdp-week_number-height);
  width: var(--rdp-week_number-width);
  border: var(--rdp-week_number-border);
  border-radius: var(--rdp-week_number-border-radius);
  text-align: var(--rdp-weeknumber-text-align);
}

/* DAY MODIFIERS */
.rdp-today:not(.rdp-outside) {
  color: var(--rdp-today-color);
}

.rdp-selected {
  font-weight: bold;
  font-size: large;
}

.rdp-selected .rdp-day_button {
  border: var(--rdp-selected-border);
}

.rdp-outside {
  opacity: var(--rdp-outside-opacity);
}

.rdp-disabled:not(.rdp-selected) {
  opacity: var(--rdp-disabled-opacity);
}

.rdp-hidden {
  visibility: hidden;
  color: var(--rdp-range_start-color);
}

.rdp-range_start {
  background: var(--rdp-range_start-background);
}

.rdp-range_start .rdp-day_button {
  background-color: var(--rdp-range_start-date-background-color);
  color: var(--rdp-range_start-color);
}

.rdp-range_middle {
  background-color: var(--rdp-range_middle-background-color);
}

.rdp-range_middle .rdp-day_button {
  border: unset;
  border-radius: unset;
  color: var(--rdp-range_middle-color);
}

.rdp-range_end {
  background: var(--rdp-range_end-background);
  color: var(--rdp-range_end-color);
}

.rdp-range_end .rdp-day_button {
  color: var(--rdp-range_start-color);
  background-color: var(--rdp-range_end-date-background-color);
}

.rdp-range_start.rdp-range_end {
  background: revert;
}

.rdp-focusable {
  cursor: pointer;
}

@keyframes rdp-slide_in_left {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(0);
  }
}

@keyframes rdp-slide_in_right {
  0% {
    transform: translateX(100%);
  }
  100% {
    transform: translateX(0);
  }
}

@keyframes rdp-slide_out_left {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(-100%);
  }
}

@keyframes rdp-slide_out_right {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(100%);
  }
}

.rdp-weeks_before_enter {
  animation: rdp-slide_in_left var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-weeks_before_exit {
  animation: rdp-slide_out_left var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-weeks_after_enter {
  animation: rdp-slide_in_right var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-weeks_after_exit {
  animation: rdp-slide_out_right var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-root[dir="rtl"] .rdp-weeks_after_enter {
  animation: rdp-slide_in_left var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-root[dir="rtl"] .rdp-weeks_before_exit {
  animation: rdp-slide_out_right var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-root[dir="rtl"] .rdp-weeks_before_enter {
  animation: rdp-slide_in_right var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-root[dir="rtl"] .rdp-weeks_after_exit {
  animation: rdp-slide_out_left var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

@keyframes rdp-fade_in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes rdp-fade_out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

.rdp-caption_after_enter {
  animation: rdp-fade_in var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-caption_after_exit {
  animation: rdp-fade_out var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-caption_before_enter {
  animation: rdp-fade_in var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}

.rdp-caption_before_exit {
  animation: rdp-fade_out var(--rdp-animation_duration)
    var(--rdp-animation_timing) forwards;
}
/* ══════════════════════════════════════════════════════════════════════
   Chọn ngày giờ — DateTimePicker.tsx + Popover.tsx (12/09). Thay popup lịch
   của Chrome (xám, không tô được). Lịch = react-day-picker: tô qua biến
   `--rdp-*` chính thống của nó, ánh xạ token; KHÔNG fork CSS thư viện.
   ══════════════════════════════════════════════════════════════════════ */

/* ── Popover (vỏ chung) ─────────────────────────────────────────────── */
.rpop {
  /* +3: trên hộp thoại (`.rdlg` = +2) — picker sống bên trong hộp xem lại. */
  z-index: calc(var(--z-dialog) + 3);
  /* Dialog modal đặt `body { pointer-events: none }` và chỉ mở lại cho lớp
     nó quản. Popover portal ra `.shell` không được mở → bấm không ăn (12/09).
     Ép thẳng ở đây, không phụ thuộc hai package Radix có chung context hay không. */
  pointer-events: auto;
  background: var(--bg-panel-solid);
  border-radius: var(--r-row);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-sink);
  outline: none;
}

/* ── Nút mở (thay ô input) ──────────────────────────────────────────── */
.dtp__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: 28px;
  padding: 0 var(--sp-2);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-key);
  background: var(--bg-well);
  color: var(--text-hi);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.dtp__trigger svg {
  color: var(--text-lo);
}

.dtp__trigger:hover,
.dtp__trigger[data-state='open'] {
  border-color: var(--accent);
}

.dtp__trigger:focus-visible {
  border-color: var(--accent);
  outline: none;
}

.dtp__trigger--empty .dtp__value {
  color: var(--text-lo);
}

.dtp__trigger:disabled {
  color: var(--text-faint);
  cursor: default;
}

/* ── Thân: lịch · cột giờ · cột phút ────────────────────────────────── */
.dtp {
  --rdp-nav_button-height: 26px;
  --rdp-day-height: 30px;
  padding: var(--sp-2);
}

/* LỊCH quyết định chiều cao thân; hai cột giờ/phút chỉ được cao BẰNG lịch.
   Danh sách 24/60 dòng đặt `position: absolute` để không góp vào chiều cao
   (60 dòng phút mà tính vào là popup dài hết màn — lỗi 12/09). */
.dtp__grid {
  display: flex;
  align-items: stretch;
  gap: var(--sp-2);
}

.dtp__col {
  position: relative;
  width: 48px;
  border-left: 1px solid var(--line-soft);
  padding-left: var(--sp-1);
}

.dtp__col-head {
  height: var(--rdp-nav_button-height);
  line-height: var(--rdp-nav_button-height);
  text-align: center;
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-lo);
  user-select: none;
}

.dtp__list {
  position: absolute;
  top: var(--rdp-nav_button-height);
  bottom: 0;
  left: var(--sp-1);
  right: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding-right: 2px;
  scrollbar-width: none;
}

.dtp__list::-webkit-scrollbar {
  display: none;
}

.dtp__cell {
  flex: 0 0 auto;
  height: 26px;
  border: none;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text-mid);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
}

.dtp__cell:hover {
  background: var(--bg-hover);
  color: var(--text-hi);
}

.dtp__cell--on,
.dtp__cell--on:hover {
  background: var(--accent);
  color: var(--bg-void);
  font-weight: var(--fw-label);
}

.dtp__foot {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line-soft);
}

/* ── react-day-picker: biến chính thống ăn token ────────────────────── */
.dtp .rdp-root {
  --rdp-accent-color: var(--accent);
  --rdp-accent-background-color: var(--line-accent);
  --rdp-day-width: 34px;
  --rdp-day_button-height: 28px;
  --rdp-day_button-width: 32px;
  --rdp-day_button-border-radius: var(--r-sm);
  --rdp-selected-border: none;
  --rdp-today-color: var(--accent);
  --rdp-outside-opacity: 0.35;
  --rdp-disabled-opacity: 0.3;
  --rdp-nav_button-width: 26px;
  --rdp-weekday-opacity: 1;
  --rdp-months-gap: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  color: var(--text-hi);
}

.dtp .rdp-month_caption {
  height: var(--rdp-nav_button-height);
  margin: 0;
  font-size: var(--fs-cap);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-mid);
}

.dtp .rdp-weekday {
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-lo);
}

.dtp .rdp-day {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
}

.dtp .rdp-day_button:hover:not([disabled]) {
  background: var(--bg-hover);
}

.dtp .rdp-selected .rdp-day_button {
  background: var(--accent);
  color: var(--bg-void);
  font-weight: var(--fw-label);
}

.dtp .rdp-today:not(.rdp-selected) .rdp-day_button {
  box-shadow: inset 0 0 0 1px var(--accent);
}

.dtp .rdp-chevron {
  fill: var(--text-mid);
}

.dtp .rdp-button_next:hover .rdp-chevron,
.dtp .rdp-button_previous:hover .rdp-chevron {
  fill: var(--text-hi);
}
/* ══════════════════════════════════════════════════════════════════════
   Toàn màn hình một camera — CameraFullscreen.tsx (10/09)
   Dựng trên Radix Dialog (rule 06 §3) — scrim + content phủ trọn viewport,
   đè cả header/tab của shell. Màu/cỡ chỉ qua token.
   ══════════════════════════════════════════════════════════════════════ */

.camfs__scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-dialog);
  background: var(--bg-void);
}

.camfs {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-dialog) + 1);
  display: flex;
  flex-direction: column;
  background: var(--bg-void);
  outline: none;
}

.camfs__head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line-soft);
}

.camfs__title {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: var(--fs-body-lg);
  font-weight: 700;
  color: var(--text-hi);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.camfs__index {
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  color: var(--text-lo);
}

.camfs__stage {
  flex: 1;
  min-height: 0;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-void);
}

/* Khung chứa LƯỚI trực tiếp (11/09): chừa lề để nút ◄ ► không đè lên ô. */
.camfs__grid {
  position: absolute;
  inset: var(--sp-3) calc(var(--sp-8) * 1.5 + var(--sp-3) * 2);
  min-width: 0;
  min-height: 0;
}

/* contain, KHÔNG cover: thà có viền đen còn hơn cắt mất mép khung hình. */
.camfs__video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.camfs__empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  color: var(--text-lo);
  font-size: var(--fs-body);
  text-align: center;
}

.camfs__empty--crit {
  color: var(--st-crit);
  font-weight: 700;
}

/* Nút lật camera: to để bấm từ xa bằng chuột trên wall, nổi giữa hai mép.
   Selector ghép `.tbtn.camfs__nav` để thắng `height: 22px` của `.tbtn`
   (panel.css nạp sau file này). Nền mờ để nổi trên khung hình sáng. */
.tbtn.camfs__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: calc(var(--sp-8) * 1.5);
  height: calc(var(--sp-8) * 3);
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-key);
  background: color-mix(in srgb, var(--bg-void) 55%, transparent);
  color: var(--text-hi);
  z-index: var(--z-map-popup);
}

.tbtn.camfs__nav:hover {
  background: color-mix(in srgb, var(--bg-void) 75%, transparent);
  border-color: var(--accent);
}

.camfs__nav--prev {
  left: var(--sp-3);
}

.camfs__nav--next {
  right: var(--sp-3);
}

.camfs__clock {
  position: absolute;
  right: var(--sp-3);
  bottom: var(--sp-3);
  padding: 2px var(--sp-2);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--bg-void) 55%, transparent);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  color: var(--text-mid);
  pointer-events: none;
  z-index: var(--z-map-popup);
}
/* ══════════════════════════════════════════════════════════════════════
   Bộ chọn mẫu lưới camera — CameraGridPicker.tsx (20/10, kiểu liquid glass)
   Viên kính: nền trong + blur/saturate, mép trên sáng như mặt kính bắt sáng,
   ô đang chọn là "giọt kính" nhuộm màu phân hệ (`--accent` của màn).
   ══════════════════════════════════════════════════════════════════════ */

.cgp {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--text-hi) 10%, transparent), transparent 55%),
    color-mix(in srgb, var(--bg-panel-solid) 45%, transparent);
  border: 1px solid color-mix(in srgb, var(--text-hi) 14%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--text-hi) 22%, transparent),
    inset 0 -1px 0 color-mix(in srgb, var(--bg-void) 50%, transparent),
    0 4px 14px color-mix(in srgb, var(--bg-void) 45%, transparent);
  backdrop-filter: blur(14px) saturate(160%);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
}

.cgp__opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 24px;
  padding: 0 10px 0 8px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  color: var(--text-lo);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-weight: var(--fw-title);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition:
    color 160ms ease,
    background 160ms ease,
    border-color 160ms ease,
    box-shadow 160ms ease;
}

.cgp__opt:hover {
  color: var(--text-hi);
  background: color-mix(in srgb, var(--text-hi) 7%, transparent);
}

.cgp__opt:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

/* Giọt kính: nhuộm accent, bóng trong + viền sáng mép trên. */
.cgp__opt--on,
.cgp__opt--on:hover {
  color: var(--text-hi);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--text-hi) 26%, transparent), transparent 60%),
    color-mix(in srgb, var(--accent) 34%, transparent);
  border-color: color-mix(in srgb, var(--accent) 70%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--text-hi) 40%, transparent),
    0 0 12px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* Hình lưới thu nhỏ: khung 14×14, chia `--cgp-side` × `--cgp-side` ô. */
.cgp__glyph {
  display: grid;
  grid-template-columns: repeat(var(--cgp-side), 1fr);
  gap: 1px;
  width: 14px;
  height: 14px;
}

.cgp__glyph i {
  border-radius: 1px;
  background: currentColor;
  opacity: 0.55;
}

.cgp__opt--on .cgp__glyph i {
  opacity: 0.95;
}

.cgp__n {
  min-width: 1.4em;
  text-align: left;
}

@media (prefers-reduced-motion: reduce) {
  .cgp__opt {
    transition: none;
  }
}
/* ══════════════════════════════════════════════════════════════════════
   Ô camera — CameraTile.tsx
   ══════════════════════════════════════════════════════════════════════ */

/* "Đang tải" khi ô đang chờ mở phiên WHEP — ngoại lệ thứ BA được phép động
   lặp vô hạn (rule 06 §4). Một lưới nhiều chục ô đứng yên tuyệt đối trong
   lúc mở hàng loạt phiên (debounce 400ms + bắt tay SDP) trông như treo máy;
   ba chấm nhấp nháy là tín hiệu "đang sống" tối thiểu. Thuần CSS
   (animation-delay so le trên 3 span) — KHÔNG setInterval đổi text bằng JS,
   vì lưới có thể lên tới 100 ô cùng lúc (rule 07 §3 chuẩn hiệu năng). */
.cam-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  height: 100%;
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  letter-spacing: var(--track-label);
  color: var(--text-lo);
}

.cam-loading__dot {
  animation: cam-loading-dot 1.4s ease-in-out infinite;
}

.cam-loading__dot:nth-child(2) {
  animation-delay: 0.2s;
}

.cam-loading__dot:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes cam-loading-dot {
  0%,
  80%,
  100% {
    opacity: 0.2;
  }
  40% {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cam-loading__dot {
    animation: none;
    opacity: 1;
  }
}


/* ── Box phát hiện AI trên ô video (kênh `ai.detected`, sống 5 s) ─────────────────────────────────
   Lớp phủ trùng ĐÚNG vùng ảnh của `<video object-fit: contain>`: khung con giữ tỉ lệ luồng
   (`--cam-aspect`) và co theo ô bằng đơn vị container query. Toạ độ box là phần trăm của khung đó. */
.cam-ai-layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  container-type: size;
  pointer-events: none;
}

.cam-ai-layer__frame {
  position: relative;
  width: min(100cqw, 100cqh * var(--cam-aspect));
  height: min(100cqh, 100cqw / var(--cam-aspect));
}

.cam-ai-box {
  position: absolute;
  border: 1.5px solid var(--cam-ai-color);
  border-radius: 1px;
}

.cam-ai-box__label {
  position: absolute;
  left: -1.5px;
  bottom: 100%;
  padding: 0 var(--sp-1);
  white-space: nowrap;
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
  color: var(--bg-void);
  background: var(--cam-ai-color);
}

.cam-ai-box--top .cam-ai-box__label {
  top: 0;
  bottom: auto;
}

/* Phát hiện không có toạ độ: viền ô nháy BA lần rồi đứng (không lặp vô hạn — rule 06 §4), ≤ 3 lần/giây. */
.cam-ai-flash {
  animation: cam-ai-flash 0.6s ease-in-out 3;
}

@keyframes cam-ai-flash {
  50% {
    box-shadow: inset 0 0 0 2px var(--st-warn);
  }
}

@media (prefers-reduced-motion: reduce) {
  .cam-ai-flash {
    animation: none;
    box-shadow: inset 0 0 0 2px var(--st-warn) !important;
  }
}
/* ══════════════════════════════════════════════════════════════════════
   Hộp thoại nhóm camera — CameraGroupDialog.tsx (làm lại 20/10)
   Hộp TO, NỀN ĐẶC ĐẬM (user: "không kính mờ nữa"), ba cột:
   ① danh mục · ② đã chọn theo thứ tự · ③ lưới mô phỏng (to nhất).
   ══════════════════════════════════════════════════════════════════════ */

.rdlg.cgdlg {
  background: var(--bg-app);
  border: 1px solid var(--line-mid);
}

.cgdlg .rdlg__body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.cgdlg .cgp {
  background: var(--bg-panel-solid);
  border-color: var(--line-mid);
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* ── ĐỘ TƯƠNG PHẢN (20/10 — user: "nền tối chữ còn xám, khó nhìn"): mọi chữ nội dung trong hộp
   là `--text-hi`; chữ phụ (gợi ý, số đếm, ô trống) tối thiểu `--text-mid` — KHÔNG dùng
   `--text-lo`/`--text-faint` ở đây. Nhãn `.lbl` toàn app mặc định `--text-lo` nên ghi đè. ── */
.cgdlg .lbl,
.cgdlg .rdlg__title {
  color: var(--text-hi);
}

.cgdlg .cgp__opt {
  color: var(--text-hi);
}

.cgdlg .cgp__glyph i {
  opacity: 0.8;
}

.cgdlg .cam-loading {
  color: var(--text-hi);
}

/* ── Hàng trên: tên nhóm · cỡ lưới ─────────────────────────────────── */
.cgdlg__top {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-4);
  margin-bottom: var(--sp-3);
}

.cgdlg__field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-1);
  min-width: 0;
}

.cgdlg__field--name {
  flex: 0 1 460px;
}

.cgdlg__input {
  width: 100%;
  min-width: 0;
  height: 36px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-key);
  background: var(--bg-panel-solid);
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  outline: none;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

.cgdlg__input::placeholder {
  color: var(--text-mid);
}

.cgdlg__input:focus-visible {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* ── Ba cột: hàng tiêu đề cột + hàng thân ──────────────────────────── */
.cgdlg__cols {
  display: grid;
  grid-template-columns: minmax(260px, 1fr) minmax(240px, 0.9fr) minmax(0, 2.6fr);
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--sp-2) var(--sp-3);
  flex: 1 1 auto;
  min-height: 0;
}

.cgdlg__colhead {
  color: var(--text-hi);
}

.cgdlg__col {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-width: 0;
  min-height: 0;
  padding: var(--sp-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  background: var(--bg-void);
}

.cgdlg__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* Ô tìm */
.cgdlg__search {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  height: 32px;
  padding: 0 var(--sp-2);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-key);
  background: var(--bg-panel-solid);
  color: var(--text-mid);
}

.cgdlg__search:focus-within {
  border-color: var(--accent);
}

.cgdlg__search input {
  flex: 1 1 auto;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
}

/* ── Cột ①: khu + dòng camera ──────────────────────────────────────── */
.cgdlg__zone-head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2);
  margin-bottom: 4px;
  border-bottom: 1px solid var(--line-soft);
  background: var(--bg-void);
  color: var(--text-hi);
}

.cgdlg__zone + .cgdlg__zone .cgdlg__zone-head {
  margin-top: var(--sp-2);
}

.cgdlg__zone-count {
  margin-right: auto;
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  color: var(--text-mid);
  font-variant-numeric: tabular-nums;
}

.cgdlg__zone-btn {
  height: 22px;
  padding: 0 var(--sp-3);
  border: 1px solid color-mix(in srgb, var(--accent) 55%, var(--bg-void));
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 14%, var(--bg-void));
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  font-weight: var(--fw-title);
  white-space: nowrap;
  cursor: pointer;
  transition: background 160ms ease;
}

.cgdlg__zone-btn:hover {
  background: color-mix(in srgb, var(--accent) 28%, var(--bg-void));
}

.cgdlg__item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  min-height: 34px;
  margin-bottom: 4px;
  padding: 5px var(--sp-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-key);
  background: var(--bg-panel-solid);
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  text-align: left;
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
}

.cgdlg__item:hover {
  color: var(--text-hi);
  background: var(--bg-hover);
}

.cgdlg__item:focus-visible,
.cgdlg__cell:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

.cgdlg__item--on,
.cgdlg__item--on:hover {
  color: var(--text-hi);
  border-color: color-mix(in srgb, var(--accent) 65%, var(--bg-panel-solid));
  background: color-mix(in srgb, var(--accent) 16%, var(--bg-panel-solid));
}

.cgdlg__tick {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  border: 1px solid var(--line-mid);
  border-radius: 5px;
  background: var(--bg-void);
  color: var(--bg-void);
}

.cgdlg__item--on .cgdlg__tick {
  border-color: var(--accent);
  background: var(--accent);
}

/* Tên camera BE trả có thể dài — xuống dòng, không cắt "…". */
.cgdlg__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  line-height: 1.25;
}

/* ── Cột ②: đã chọn theo thứ tự ────────────────────────────────────── */
.cgdlg__picked {
  margin: 0;
  padding: 0;
  list-style: none;
}

.cgdlg__prow {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 34px;
  margin-bottom: 4px;
  padding: 4px 4px 4px var(--sp-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-key);
  background: var(--bg-panel-solid);
  color: var(--text-hi);
  font-size: var(--fs-body);
  cursor: grab;
}

.cgdlg__pos {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  min-width: 22px;
  height: 22px;
  padding: 0 4px;
  border-radius: 6px;
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-void));
  color: var(--text-hi);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-weight: var(--fw-title);
}

.cgdlg__ppage {
  flex: 0 0 auto;
  padding: 0 6px;
  border: 1px solid var(--line-mid);
  border-radius: 999px;
  color: var(--text-mid);
  font-family: var(--font-num);
  font-size: var(--fs-micro);
}

.cgdlg__pbtns {
  display: inline-flex;
  flex: 0 0 auto;
  gap: 2px;
}

.cgdlg__ibtn {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--text-mid);
  cursor: pointer;
}

.cgdlg__ibtn:hover:not(:disabled) {
  color: var(--text-hi);
  background: var(--bg-hover);
}

.cgdlg__ibtn:disabled {
  opacity: 0.3;
  cursor: default;
}

.cgdlg__ibtn--x:hover:not(:disabled) {
  color: var(--st-crit);
}

/* ── Cột ③: lưới mô phỏng ──────────────────────────────────────────── */
.cgdlg__col--grid {
  padding: var(--sp-3);
}

/* Sân khấu chiếm hết chỗ; bảng giữ đúng 16:9 và co theo cạnh hẹp hơn. */
.cgdlg__stage {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  place-items: center;
  container-type: size;
}

.cgdlg__board {
  display: grid;
  grid-template-columns: repeat(var(--cg-side), minmax(0, 1fr));
  grid-template-rows: repeat(var(--cg-side), minmax(0, 1fr));
  gap: 6px;
  width: min(100cqw, calc(100cqh * 16 / 9));
  aspect-ratio: 16 / 9;
}

.cgdlg__cell {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  min-height: 0;
  padding: 4px;
  border: 1px dashed var(--line-mid);
  border-radius: var(--r-key);
  background: var(--bg-panel-solid);
  color: var(--text-mid);
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  text-align: center;
  overflow: hidden;
  cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease;
}

.cgdlg__cell--full {
  border-style: solid;
  border-color: var(--line-mid);
  background: color-mix(in srgb, var(--accent) 10%, var(--bg-panel-solid));
  color: var(--text-hi);
  cursor: grab;
}

.cgdlg__cell:hover {
  border-color: color-mix(in srgb, var(--accent) 60%, var(--line-mid));
}

/* Hình xem trước phủ kín ô, KHÔNG bắt chuột — kéo thả / bấm luôn rơi vào ô. */
.cgdlg__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--bg-void);
  pointer-events: none;
}

.cgdlg__cell--full {
  background: var(--bg-void);
}

.cgdlg__loading {
  position: absolute;
  inset: auto 6px auto auto;
  top: 4px;
  z-index: 1;
  width: auto;
  height: auto;
  background: none;
  font-size: var(--fs-micro);
  pointer-events: none;
}

.cgdlg__cellno {
  z-index: 1;
  padding: 0 5px;
  border-radius: 4px;
  background: var(--bg-void);
}

/* Có hình rồi thì tên xuống góc dưới-trái thành viên nhãn ÔM VỪA CHỮ (20/10 — dải kéo hết bề
   ngang thành vạch đen che hình). Tên dài thì xuống dòng trong giới hạn bề ngang ô. */
.cgdlg__cell--live .cgdlg__cellname {
  position: absolute;
  left: 6px;
  bottom: 6px;
  z-index: 1;
  max-width: calc(100% - 12px);
  padding: 2px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--bg-void) 78%, transparent);
  text-align: left;
}

/* Lưới dày (4×4 trở lên): ô nhỏ ⇒ chữ về cỡ chú thích để tên còn đọc trọn. */
.cgdlg__board--dense .cgdlg__cellname {
  font-size: var(--fs-cap);
  font-weight: var(--fw-label);
}

/* Ô đang được bấm chọn để đổi chỗ / ô đang có camera kéo ngang qua. */
.cgdlg__cell--armed,
.cgdlg__cell--over {
  border-style: solid;
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 24%, var(--bg-panel-solid));
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
}

.cgdlg__cellno {
  position: absolute;
  top: 4px;
  left: 6px;
  color: var(--text-mid);
  font-family: var(--font-num);
  font-size: var(--fs-micro);
}

.cgdlg__cellname {
  position: relative;
  z-index: 1;
  max-width: 100%;
  font-size: var(--fs-body);
  font-weight: var(--fw-title);
  overflow-wrap: anywhere;
  line-height: 1.2;
}

.cgdlg__pages {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
}

.cgdlg__pageno {
  min-width: 7em;
  text-align: center;
  color: var(--text-mid);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
}

/* ── Chân hộp thoại ────────────────────────────────────────────────── */
.cgdlg__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-3);
}

.cgdlg__count {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-cap);
  color: var(--text-mid);
}

.cgdlg__count--on {
  color: var(--accent);
  font-weight: var(--fw-strong);
}

.cgdlg__fits {
  padding: 1px 8px;
  border: 1px solid var(--line-mid);
  border-radius: 999px;
  background: var(--bg-panel-solid);
  color: var(--text-mid);
  font-family: var(--font-num);
  font-weight: var(--fw-title);
  font-variant-numeric: tabular-nums;
}

/* Chọn nhiều hơn số ô lưới: phần dư sang trang sau — nhắc bằng màu cảnh báo. */
.cgdlg__search input::placeholder {
  color: var(--text-mid);
}
/* ══════════════════════════════════════════════════════════════════════
   Bàn điều khiển PTZ — CameraScreen.tsx (thiết kế lại 11/09)
   D-pad 3×3 + hàng zoom. Nút có chiều sâu (gradient + bóng đáy), nhấn thì
   LÚN xuống, phát sáng trong và icon trượt theo đúng hướng; giữ nút (chạy
   liên tục) giữ nguyên trạng thái nhấn nhờ `:active`. Thuần CSS, transition
   ≤ 200 ms, không animation lặp (rule 06 §4). Màu/cỡ chỉ qua token.
   ══════════════════════════════════════════════════════════════════════ */

/* Màu của bàn PTZ = accent của màn (từ 11/09 cả màn An ninh & AI chuyển sang
   tím, xem `--accent-vision` trong tokens.css + screens.config.ts). Muốn bàn
   PTZ khác màu màn thì đổi biến này. */
.ptz-pad,
.ptz-zoom {
  --ptz-accent: var(--accent);
}

/* ── Khung đo (23/09) ──────────────────────────────────────────────────
   Bàn phím TRƯỚC ĐÂY chỉ đo bề NGANG (`width: min(100%, 320px)`) rồi để nút
   `aspect-ratio: 1` tự đẩy chiều cao. Card PTZ là card kéo-thả: kéo thấp
   xuống thì flex ép hộp lưới lại (đo thật: hộp còn 101px trong khi ba hàng
   nút cần ~250px) — hàng nút TRÀN ra ngoài hộp, đè lên hàng zoom và tụt khỏi
   card. Nay bàn phím đo CẢ chiều cao còn lại: `container-type: size` để
   `100cqh` = chiều cao thật của vùng điều khiển.
   ────────────────────────────────────────────────────────────────────── */
.ptz-ctl {
  container-type: size;
  /* Vùng cuộn là ĐÂY, không phải thân panel: `container-type: size` khiến hộp
     này không nở theo nội dung nên thân panel không bao giờ biết mình tràn.
     Chỉ chạm tới khi card bị kéo thấp hơn cả cỡ sàn của bàn phím — vẫn hơn
     cắt cụt dòng chữ như trước (screen-grids §4: tràn hấp thụ ở LÁ). */
  overflow-y: auto;
  overflow-x: hidden;
  /* Cao của một nút zoom — cũng là phần đất phải chừa lại cho hàng zoom. */
  --ptz-zoom-h: clamp(28px, 20cqh, calc(var(--sp-8) + var(--sp-2)));
  /* Lề giữa bàn phím · hàng zoom · dòng lý do. */
  --ptz-stack-gap: var(--sp-3);
}

/* ⚠ Biến đo đặt ở BÀN PHÍM, không ở `.ptz-ctl`: phần tử mang `container-type`
   KHÔNG BAO GIỜ khớp `@container` của chính nó, nên khai ở `.ptz-ctl` thì chế
   độ dồn lề bên dưới không bao giờ ăn (đã dính một lần 23/09). */
.ptz-pad {
  /* Sàn: dưới mức này bấm không còn trúng nút. */
  --ptz-pad-min: 108px;
}

.ptz-pad {
  /* ── Bàn phím LẤP ĐẦY ô, không còn là hình vuông canh giữa (23/09, vòng 2) ──
     Bản trước ép bàn phím VUÔNG nên cạnh phải lấy ràng buộc chặt nhất; card
     rộng mà thấp (card thật ~370×340) thì cạnh lấy theo CHIỀU CAO, để thừa
     ~100px lề hai bên trong khi hàng zoom ngay dưới chạy hết bề ngang — nhìn
     là thấy lệch. Nay:

       · CHIỀU CAO do FLEX chia (`flex: 1 1 0`) — lấy đúng phần còn lại sau
         hàng zoom và dòng lý do. Tự trừ bằng `100cqh - <tự cộng lấy>` thì chỉ
         lệch vài px là sinh thanh cuộn, đã dính đúng bẫy đó.
       · CHIỀU RỘNG bám bề ngang card (`max-width: 100%`), `aspect-ratio` chỉ
         còn là TRẦN ĐỘ DẸT của ô nút — card quá thấp thì thà để lề còn hơn
         nút méo thành thanh ngang.
     ────────────────────────────────────────────────────────────────────── */
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Hàng `1fr` chứ không `auto`: ô nhận cỡ từ LƯỚI, nên lưới bị thu nhỏ thì
     nút nhỏ theo thay vì tràn ra ngoài. */
  grid-template-rows: repeat(3, minmax(0, 1fr));
  /* Khe hở TỈ LỆ với bàn phím, không cố định: bàn phím co lại mà khe vẫn 12px
     thì khe ăn mất nút (nút 31px / khe 12px), bàn phím to ra thì các nút lại
     dính vào nhau. 4,5% cạnh ≈ 1/8 nút ở mọi cỡ — đúng độ tách của bản mẫu. */
  gap: clamp(5px, 4.5%, var(--sp-3));

  flex: 1 1 0;
  /* Sàn nằm ở rule `.ptz-ctl .ptz-pad` bên dưới, KHÔNG ở đây — xem lý do ở đó. */
  /* Không cao hơn bề ngang card: quá mức đó ô nút thành cột dựng đứng, mà bàn
     phím hướng thì phải đọc ra hình vuông. */
  max-height: 100cqw;
  /* `align-self` phải là `center`, KHÔNG `stretch` (mặc định), nếu không bề
     ngang bị kéo full và `aspect-ratio` mất tác dụng. */
  align-self: center;
  /* 1,7 là trần ĐỘ DẸT của ô nút, chọn theo card thật (~370×340): ở tỉ lệ này
     bàn phím chạm đúng hai mép như hàng zoom, mà ô nút vẫn còn ra hình phím.
     Hạ xuống 1,5 thì card đó hụt 17px mỗi bên — nhìn vẫn lệch. */
  aspect-ratio: 1.7;
  max-width: 100%;
  margin: 0 auto var(--ptz-stack-gap);
}

.ptz-pad__btn,
.ptz-zoom__btn {
  --nudge-x: 0px;
  --nudge-y: 0px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  border-radius: var(--r-key);
  border: 1px solid color-mix(in srgb, var(--ptz-accent) 38%, transparent);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--ptz-accent) 16%, var(--bg-well)),
    color-mix(in srgb, var(--ptz-accent) 6%, var(--bg-well))
  );
  color: var(--ptz-accent);
  cursor: pointer;
  user-select: none;
  touch-action: none;
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--text-hi) 10%, transparent),
    0 3px 0 color-mix(in srgb, var(--bg-void) 80%, transparent),
    0 4px 10px color-mix(in srgb, var(--bg-void) 55%, transparent);
  transition:
    transform 0.12s ease,
    box-shadow 0.12s ease,
    background 0.12s ease,
    border-color 0.12s ease,
    color 0.12s ease;
}

/* KHÔNG `aspect-ratio` ở nút nữa: lưới đã vuông và hàng là `1fr` nên ô tự
   vuông. Để lại thì nút tự quyết chiều cao và lại tràn khỏi lưới khi card thấp. */
.ptz-pad__btn {
  min-width: 0;
  min-height: 0;
  padding: 0;
}

/* Icon co theo NÚT (trước đây cứng 22px do `size` của lucide): nút 30px mà
   mũi tên 22px thì kín mít, không còn ra hình phím bấm. Trần giữ đúng cỡ
   thiết kế 11/09. */
.ptz-pad__btn svg {
  width: min(22px, 46%);
  height: min(22px, 46%);
}

.ptz-pad__btn--home svg {
  width: min(18px, 34%);
  height: min(18px, 34%);
}

.ptz-pad__btn svg,
.ptz-zoom__btn svg {
  flex: 0 0 auto;
  transition: transform 0.12s ease;
}

.ptz-pad__btn:hover,
.ptz-zoom__btn:hover {
  border-color: var(--ptz-accent);
  color: var(--text-hi);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--ptz-accent) 24%, var(--bg-well)),
    color-mix(in srgb, var(--ptz-accent) 10%, var(--bg-well))
  );
}

/* NHẤN: lún xuống hết bóng đáy, phát sáng trong, icon trượt theo hướng. */
.ptz-pad__btn:active,
.ptz-zoom__btn:active {
  transform: translateY(3px) scale(0.96);
  border-color: var(--ptz-accent);
  color: var(--text-hi);
  background: color-mix(in srgb, var(--ptz-accent) 34%, var(--bg-well));
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--ptz-accent) 70%, transparent),
    inset 0 0 16px color-mix(in srgb, var(--ptz-accent) 45%, transparent),
    0 0 0 transparent,
    0 0 18px color-mix(in srgb, var(--ptz-accent) 35%, transparent);
}

.ptz-pad__btn:active svg,
.ptz-zoom__btn:active svg {
  transform: translate(var(--nudge-x), var(--nudge-y)) scale(1.12);
}

/* Hướng trượt của icon khi nhấn — đúng hướng camera sắp xoay. */
.ptz-pad__btn--n  { --nudge-y: -3px; }
.ptz-pad__btn--s  { --nudge-y: 3px; }
.ptz-pad__btn--w  { --nudge-x: -3px; }
.ptz-pad__btn--e  { --nudge-x: 3px; }
.ptz-pad__btn--nw { --nudge-x: -2px; --nudge-y: -2px; }
.ptz-pad__btn--ne { --nudge-x: 2px;  --nudge-y: -2px; }
.ptz-pad__btn--sw { --nudge-x: -2px; --nudge-y: 2px; }
.ptz-pad__btn--se { --nudge-x: 2px;  --nudge-y: 2px; }

/* ⚠ Sàn chiều cao phải khai với MỘT BẬC LỚP nữa. Lưới an toàn của màn có
   `.scr * { min-height: 0 }` (screen-grids.css) — cùng độ đặc hiệu với
   `.ptz-pad` mà nạp SAU trong `index.css`, nên khai ở rule kia thì sàn bị xoá
   sạch và bàn phím co tới 60px (đo 23/09). */
.ptz-ctl .ptz-pad {
  min-height: var(--ptz-pad-min);
}

/* HOME: tròn, tông info, chữ nhỏ dưới icon. */
.ptz-pad__btn--home {
  flex-direction: column;
  gap: 2px;
  /* Ô nút có thể là chữ nhật NẰM NGANG (bàn phím lấp đầy ô) — HOME lấy cạnh
     NGẮN là chiều cao ô rồi canh giữa, để `border-radius: 50%` ra hình TRÒN
     chứ không phải bầu dục. Ô không bao giờ dựng đứng (xem `max-height` của
     bàn phím) nên lấy chiều cao là an toàn. */
  place-self: center;
  height: 100%;
  width: auto;
  aspect-ratio: 1;
  border-radius: 50%;
  border-color: color-mix(in srgb, var(--st-info) 55%, transparent);
  background: radial-gradient(
    circle at 50% 30%,
    color-mix(in srgb, var(--st-info) 38%, var(--bg-well)),
    color-mix(in srgb, var(--st-info) 14%, var(--bg-well))
  );
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: var(--track-label);
}

.ptz-pad__btn--home:hover {
  border-color: var(--st-info);
  background: radial-gradient(
    circle at 50% 30%,
    color-mix(in srgb, var(--st-info) 48%, var(--bg-well)),
    color-mix(in srgb, var(--st-info) 20%, var(--bg-well))
  );
}

.ptz-pad__btn--home:active {
  background: color-mix(in srgb, var(--st-info) 55%, var(--bg-well));
  box-shadow:
    inset 0 0 0 1px var(--st-info),
    inset 0 0 16px color-mix(in srgb, var(--st-info) 50%, transparent),
    0 0 18px color-mix(in srgb, var(--st-info) 40%, transparent);
}

/* Tới biên: mờ, không nhấn được, không hiệu ứng. */
.ptz-pad__btn:disabled {
  opacity: 0.28;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

/* Hàng zoom: hai nút ngang + ô giá trị. */
.ptz-zoom {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: var(--sp-2);
  align-items: stretch;
  /* Hàng này giữ nguyên cỡ, phần đất còn lại mới là của bàn phím — cấm flex
     bóp (đo 23/09: hàng zoom còn 16px, nút 48px tràn đè lên nhau). */
  flex: 0 0 auto;
}

.ptz-zoom__btn {
  height: var(--ptz-zoom-h, calc(var(--sp-8) + var(--sp-2)));
  min-width: 0;
  padding: 0 var(--sp-2);
  overflow: hidden;
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
}

.ptz-zoom__label {
  overflow: hidden;
  text-overflow: ellipsis;
}

.ptz-zoom__btn--in svg  { --nudge-y: -1px; }
.ptz-zoom__btn--out svg { --nudge-y: 1px; }

.ptz-zoom__value {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: calc(var(--sp-8) * 1.6);
  padding: 0 var(--sp-2);
  border-radius: var(--r-key);
  border: 1px solid var(--line-soft);
  background: var(--bg-well);
  font-family: var(--font-num);
  font-size: var(--fs-body);
  font-variant-numeric: tabular-nums;
  color: var(--text-hi);
}

.ptz-zoom__unit {
  margin-left: 2px;
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

/* ── Card hẹp: bỏ chữ, giữ nghĩa ───────────────────────────────────────
   Nút zoom rộng = (bề ngang − ô giá trị − khe) / 2. Dưới ~240px thì phần đó
   còn ~80px, không đủ cho "PHÓNG TO" (~96px) — chữ bị cắt ngang trông như
   lỗi. Giữ icon (đã có `aria-label`/`title` nên trình đọc màn hình vẫn đọc).
   ────────────────────────────────────────────────────────────────────── */
@container (max-width: 240px) {
  .ptz-zoom__label {
    display: none;
  }

  .ptz-zoom__value {
    min-width: calc(var(--sp-8) * 1.2);
  }
}

/* ── Card bị kéo rất thấp: dồn lề, KHÔNG đẩy hàng zoom xuống dưới tầm mắt ──
   Cuộn trong một card điều khiển là bẫy: người trực không biết dưới nếp gấp
   còn hai nút zoom. Thà lề mỏng đi và nút nhỏ lại một chút để cả cụm cùng
   nằm trong ô (đo 23/09: card 281×188 — cụm 173px trong 146px đất).
   ────────────────────────────────────────────────────────────────────── */
@container (max-height: 200px) {
  .ptz-pad,
  .ptz-off__why {
    --ptz-stack-gap: var(--sp-1);
  }

  .ptz-pad {
    --ptz-pad-min: 96px;
  }
}

/* Bàn phím quá nhỏ thì chữ HOME không còn đọc được — để lại mỗi icon nhà. */
@container (max-height: 260px) {
  .ptz-pad__btn--home span {
    display: none;
  }

  .ptz-pad__btn--home svg {
    width: min(18px, 46%);
    height: min(18px, 46%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ptz-pad__btn,
  .ptz-zoom__btn,
  .ptz-pad__btn svg,
  .ptz-zoom__btn svg {
    transition: none;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   CAMERA CỐ ĐỊNH — bàn phím vẫn đứng đó, chỉ là không dùng được (21/09).
   Trước đây card đổi hẳn thành một dòng chữ; với 24/25 camera trong sổ là
   camera cố định thì card "Điều khiển camera" gần như luôn trống, người
   trực ca không biết nó vốn để làm gì. Giữ bàn phím = giữ câu trả lời.
   Không bịa khả năng: nút đã `disabled` thật ở JSX, đây chỉ là lớp nhìn.
   ══════════════════════════════════════════════════════════════════════ */

/* Hạ tương phản CẢ CỤM (bàn phím + hàng zoom) chứ không riêng từng nút: mờ
   lẻ từng nút thì ô giá trị zoom và khe hở vẫn sáng, cụm trông như đang
   hỏng dở chứ không phải "không áp dụng". */
.ptz-off .ptz-pad,
.ptz-off .ptz-zoom {
  --ptz-accent: var(--st-idle);
}

.ptz-off .ptz-zoom__value {
  color: var(--text-faint);
}

/* Lý do đặt DƯỚI bàn phím, không thay thế nó. */
.ptz-off__why {
  /* `0 0 auto`: flex column từng bóp dòng này về CAO 0 (đo 23/09) — chữ vẫn
     vẽ ra nhưng tràn khỏi hộp, chồng lên mép card. Chữ phải giữ đủ chỗ của nó. */
  flex: 0 0 auto;
  margin: var(--ptz-stack-gap) 0 0;
  text-align: center;
  font-size: var(--fs-cap);
  line-height: var(--lh-label);
  color: var(--text-faint);
}

/* Camera đang thu sự kiện AI: xoay/zoom làm lệch vùng nhận diện. */
.ptz-ai-warn {
  flex: 0 0 auto;
  margin: var(--ptz-stack-gap) 0 0;
  text-align: center;
  font-size: var(--fs-cap);
  line-height: var(--lh-label);
  color: var(--st-warn);
}
/* inter-cyrillic-ext-400-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./inter-cyrillic-ext-400-normal-BQZuk6qB.woff2) format('woff2'), url(./inter-cyrillic-ext-400-normal-DQukG94-.woff) format('woff');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* inter-cyrillic-400-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./inter-cyrillic-400-normal-obahsSVq.woff2) format('woff2'), url(./inter-cyrillic-400-normal-HOLc17fK.woff) format('woff');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* inter-greek-ext-400-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./inter-greek-ext-400-normal-DGGRlc-M.woff2) format('woff2'), url(./inter-greek-ext-400-normal-KugGGMne.woff) format('woff');
  unicode-range: U+1F00-1FFF;
}

/* inter-greek-400-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./inter-greek-400-normal-B4URO6DV.woff2) format('woff2'), url(./inter-greek-400-normal-q2sYcFCs.woff) format('woff');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* inter-vietnamese-400-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./inter-vietnamese-400-normal-DMkecbls.woff2) format('woff2'), url(./inter-vietnamese-400-normal-Bbgyi5SW.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* inter-latin-ext-400-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./inter-latin-ext-400-normal-C1nco2VV.woff2) format('woff2'), url(./inter-latin-ext-400-normal-77YHD8bZ.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* inter-latin-400-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./inter-latin-400-normal-C38fXH4l.woff2) format('woff2'), url(./inter-latin-400-normal-CyCys3Eg.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* inter-cyrillic-ext-500-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./inter-cyrillic-ext-500-normal-B0yAr1jD.woff2) format('woff2'), url(./inter-cyrillic-ext-500-normal-BmqWE9Dz.woff) format('woff');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* inter-cyrillic-500-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./inter-cyrillic-500-normal-BasfLYem.woff2) format('woff2'), url(./inter-cyrillic-500-normal-CxZf_p3X.woff) format('woff');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* inter-greek-ext-500-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./inter-greek-ext-500-normal-C4iEst2y.woff2) format('woff2'), url(./inter-greek-ext-500-normal-2j5mBUwD.woff) format('woff');
  unicode-range: U+1F00-1FFF;
}

/* inter-greek-500-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./inter-greek-500-normal-BIZE56-Y.woff2) format('woff2'), url(./inter-greek-500-normal-Xzm54t5V.woff) format('woff');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* inter-vietnamese-500-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./inter-vietnamese-500-normal-DOriooB6.woff2) format('woff2'), url(./inter-vietnamese-500-normal-mJboJaSs.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* inter-latin-ext-500-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./inter-latin-ext-500-normal-CV4jyFjo.woff2) format('woff2'), url(./inter-latin-ext-500-normal-BxGbmqWO.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* inter-latin-500-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./inter-latin-500-normal-Cerq10X2.woff2) format('woff2'), url(./inter-latin-500-normal-BL9OpVg8.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* inter-cyrillic-ext-600-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./inter-cyrillic-ext-600-normal-Dfes3d0z.woff2) format('woff2'), url(./inter-cyrillic-ext-600-normal-Bcila6Z-.woff) format('woff');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* inter-cyrillic-600-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./inter-cyrillic-600-normal-CWCymEST.woff2) format('woff2'), url(./inter-cyrillic-600-normal-4D_pXhcN.woff) format('woff');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* inter-greek-ext-600-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./inter-greek-ext-600-normal-DRtmH8MT.woff2) format('woff2'), url(./inter-greek-ext-600-normal-B8X0CLgF.woff) format('woff');
  unicode-range: U+1F00-1FFF;
}

/* inter-greek-600-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./inter-greek-600-normal-plRanbMR.woff2) format('woff2'), url(./inter-greek-600-normal-BZpKdvQh.woff) format('woff');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* inter-vietnamese-600-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./inter-vietnamese-600-normal-Cc8MFFhd.woff2) format('woff2'), url(./inter-vietnamese-600-normal-BuLX-rYi.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* inter-latin-ext-600-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./inter-latin-ext-600-normal-D2bJ5OIk.woff2) format('woff2'), url(./inter-latin-ext-600-normal-CIVaiw4L.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* inter-latin-600-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./inter-latin-600-normal-LgqL8muc.woff2) format('woff2'), url(./inter-latin-600-normal-CiBQ2DWP.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* inter-cyrillic-ext-700-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./inter-cyrillic-ext-700-normal-BjwYoWNd.woff2) format('woff2'), url(./inter-cyrillic-ext-700-normal-LO58E6JB.woff) format('woff');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* inter-cyrillic-700-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./inter-cyrillic-700-normal-CjBOestx.woff2) format('woff2'), url(./inter-cyrillic-700-normal-DrXBdSj3.woff) format('woff');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* inter-greek-ext-700-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./inter-greek-ext-700-normal-qfdV9bQt.woff2) format('woff2'), url(./inter-greek-ext-700-normal-BoQ6DsYi.woff) format('woff');
  unicode-range: U+1F00-1FFF;
}

/* inter-greek-700-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./inter-greek-700-normal-C3JjAnD8.woff2) format('woff2'), url(./inter-greek-700-normal-BUv2fZ6O.woff) format('woff');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* inter-vietnamese-700-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./inter-vietnamese-700-normal-DlLaEgI2.woff2) format('woff2'), url(./inter-vietnamese-700-normal-BZaoP0fm.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* inter-latin-ext-700-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./inter-latin-ext-700-normal-Ca8adRJv.woff2) format('woff2'), url(./inter-latin-ext-700-normal-TidjK2hL.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* inter-latin-700-normal */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./inter-latin-700-normal-Yt3aPRUw.woff2) format('woff2'), url(./inter-latin-700-normal-BLAVimhd.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* barlow-condensed-vietnamese-600-normal */
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2) format('woff2'), url(./barlow-condensed-vietnamese-600-normal-CNlPk46_.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* barlow-condensed-latin-ext-600-normal */
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./barlow-condensed-latin-ext-600-normal-18ESti3H.woff2) format('woff2'), url(./barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* barlow-condensed-latin-600-normal */
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(./barlow-condensed-latin-600-normal-DepVgxBB.woff2) format('woff2'), url(./barlow-condensed-latin-600-normal-BFJEwTuo.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* barlow-condensed-vietnamese-700-normal */
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./barlow-condensed-vietnamese-700-normal-DYeBwlKR.woff2) format('woff2'), url(./barlow-condensed-vietnamese-700-normal-DhIzd8Tb.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* barlow-condensed-latin-ext-700-normal */
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./barlow-condensed-latin-ext-700-normal-CwuXbfVR.woff2) format('woff2'), url(./barlow-condensed-latin-ext-700-normal-BIHFfxf0.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* barlow-condensed-latin-700-normal */
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./barlow-condensed-latin-700-normal-v1xN8_Wq.woff2) format('woff2'), url(./barlow-condensed-latin-700-normal-Dmwat-ge.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* jetbrains-mono-cyrillic-ext-400-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(data:font/woff2;base64,d09GMgABAAAAAASIABAAAAAACQQAAAQuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoBmA/U1RBVF4AdBEICoRkhAALIAABNgIkAzoEIAWFAAeBFAwHG2oHKJ6DceNW4lJEFHn9tEOE37LNsvkjHr7f739rn3u+OSrSxLQytE91Dw2PYtNVq3Qaw/vJ7fUBMdOj/u2ImB98cT5WUx9F13ZKt06mU7tG1sAYcM26yCRX4f/0904bmwq8hwkUUMTRX61pa9C3xlpAGYw1vOu8C2SBZZFlld7DAW82RJWmwYMI1AJCKBNCIASqdZGFmtPSNQRhMpR0EKjWTwm6z6sJ+4jqhxjovTinVdRpZaQjaQzAYjI8NgAKDJCokgunCn9oUQE8VZd/F//+v6P4F1U9gLr58yNI/dJX9BAshEoJbTDogz7ocKoUsPvES8UK/aQIUQgpGgECBVLkOEV6iASTYgNAPKdNXayhDPL7IJuAZnEYSZ0eOLc9i5Rv5/+lEi3sW/kfnCf/+fTe0sxS7CKLx5erRJkKOixEqQaoqrvx5HN4iz4VhX0gS6DjFIWUzn/fIXJRVFY1NFRVNVeFt+SYmrK6vCU6eJsnKnJj5HTESMTQZOTI3Y/tzr3rUcOsiRy/ciP50s3Mycgxmhz9GMXhjzVH30ut796NHP0UUa31/Zoa6vCH6iPvdIM0IRANaT60FablmZlZd+UKNFy9NrvnFGxb5NC2CRxR0/rIyF/WNn+35sv9r+tHVhkseuWGKkCPPh96+GXt8Lc/Sh24ujfjf+tNw1lZWreiPuHb/PSpyjdv2rf/++ZE5TSKjFdZxYsBvr1sEHl5STuHZqYR7jGYGvGBBdjcaB5bODY1GAxMjsd0wDMIzXAXODRd74DxNMtO4YuYHclgLQRzp9KlUmxptCwv9bYgvWGD2xBW7r9413fdJu+mxzvzAUYeeONQvZP4kgfqZle4jhH/MWh+UbxgtJdrLdXLymNL53PRU0D3Q6FcXTfGqGcul6vrqhgLGAVSv+IVIfZOHpu+mlLqSlnj903j8mazXDsba/wbxhS/4Oubh1c5/uXDgt4j5KtnGEy5BIJHo0ur+jD+r2qCkuP1aRVM8EpUoRYSJyqNRA38uwDvHBNo0AejO2Z+ZYLLal1QiFyFh1EDQOgSCSCpjwwKU8yDbIjroEwne0G5Qca49cYp4AlqtQNZfWTQoFb1tRpFYAmRD+HXTkV4uQhG2bg4OKmyoypAsUGtWgQXyFxYrkpODeUxvGqIZA61erXo0sFG1UBGuPgpuon8RNUG2DhovAjZCBuZwtXrU3mQGki9+pm0eVZAXoWxLWTQoYHBPrJR4WunNxIFbCHfkTyPK+sqxjxDmGArZvf79JGJ3GwEqjwc7a7NIrl/7bJ7Nzu4+n1Ow6shEPnUrtVo7cnd5Wi5qCUbBIQbNKCP95FMCSZylEV5VCiiMqqiOmrKZ/I/C0gI1fyuulVcM2E1r4MhtcV/fbCD+HwEvVzH1dGNAzFE0FTbKzyQz3gootrNbN2a4PuG0j0JOgE=) format('woff2'), url(data:font/woff;base64,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) format('woff');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* jetbrains-mono-cyrillic-400-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2) format('woff2'), url(./jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff) format('woff');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* jetbrains-mono-greek-400-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./jetbrains-mono-greek-400-normal-C190GLew.woff2) format('woff2'), url(./jetbrains-mono-greek-400-normal-B9oWc5Lo.woff) format('woff');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* jetbrains-mono-vietnamese-400-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(data:font/woff2;base64,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) format('woff2'), url(./jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* jetbrains-mono-latin-ext-400-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2) format('woff2'), url(./jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* jetbrains-mono-latin-400-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(./jetbrains-mono-latin-400-normal-V6pRDFza.woff2) format('woff2'), url(./jetbrains-mono-latin-400-normal-6-qcROiO.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* jetbrains-mono-cyrillic-ext-500-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(data:font/woff2;base64,d09GMgABAAAAAASUABAAAAAACRAAAAQ3AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoBmA/U1RBVEwAdBEICoRkg3oLIAABNgIkAzoEIAWFHgeBFAwHG3AHKB4HzukrRjyF5Gb4kQ/B873d17mv8TWkoqUUGaHodJCtoqntw7m8tiPWyGkfWDU/IMpzVk4zAs9Czv3c7Os5NvKFG1Ht9vBVXySRZpJc4tTrbM3sTRFcLa3zVboKWRIoAksKSKgqUQWEwlSoqjp44V5J9/kjd0+Fi5xaFjwJArgBAIJgIAgCCAI4CdNMqGuYXYUKdADLAkAAJwl6iX0+bZqEm1AH0Hu9rvrJHwxQG1Q/gFYwWYbgTRWgwEESVzbmtjQAt3DXO7Tu/+9Q/KNKWhTCgZ8fQSWoL0CUUgTBjgL8iEAcce7YHBeA8ZvogieCnxI0BCVeACCAAhBD/oDcAdUPABSgAX4kBPBTwjCAjwYSCneiqF65LUo/qXdYFrRwTujtMrKm07plTbSqLQFAiHBN7E4DG5wkiGXhCYBNETh3K/AciHN69iFfQjFOr9EXQQAAQXRNszs8PR0OH4fDV/dwGe76CHdj1MQa11DXYGcfZ6+B1X0OfwjY+i7KpQ1w9d+1z3/HydCB1f3U8A0fRFv+0bXxPWrZu3d9Nn4UcbLs/QCXLH/j3PCKblKOiZxLOR78W9brPo7eGTLz/F27ZPmePQdCB1YP4nJU91o4gU9cy64L3c3SPu8GfDn6ZXD2cGmllVqSxtD5vCXHm5xaeHlnpffx7DcPvXog5NnjA//cPz2btsz3yf+0MI8ri33tuv7upNbYmW5STbosW8bYItV/SPzxvXC0KnpWnFdC91oojit5Lyo6WcJQ1YzwBYbi52ohJVpcxfcHBqsv0nFz8U74rzS6M7r0RVlXZ3jJC4G92O//7Lm78VOtnUx+aQxAzsqPnrRr1t9zDrezKePvZ027x+fGte4s4NXu7/jEp3G9cM1bICYgACbuETUUD5wmenGXA/IBpAKVrl0AEJ5cqr6Q89tGq+SpqkDfX2DtG+pZ9teuae0AX98c301CX179InyErnSG8WCiABB4HFbVrw/lpTdBIOk82nXbpHKoQGyBpUovAAD4XQA8ZTUCeBJHIpI0v0FRrgcnckV3ANckrwYJKXEihQfdSKPER6XeExmEeEMmSe6QBwWmkZdmSOSthhibgIkBlUYreBKd/0NeVAE9WAyEaQpH6KikDWlUVg43Jy2FOmaYlsZhDEGZXO3mWEyWHI6zuaJkMlcSQ9k4F+KidIiFyZItGDc7r1pUj6wP6zrWB0/fbeeFeHl4YOvETMNiM8Ny+7OO6Qh2wur20YVxFts5dlORdl5Kwd5utR7SRcxqX8RA5Ehacha76hg7c6yzNm/Xf6vOLNrv544lIEkWg7xWo/ItBAeIJEmt0XgBwgM8gTifoiiLJroYYooNTeziEKe4zOoR/wvIKp89hSr+6jFAqw/LaonjmZHSscMNg4iPlWpFrO1RN3zkMU+/xUP+JQ99TbvTl1Vg0gtirT6lbwYAAAA=) format('woff2'), url(data:font/woff;base64,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) format('woff');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* jetbrains-mono-cyrillic-500-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2) format('woff2'), url(./jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff) format('woff');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* jetbrains-mono-greek-500-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./jetbrains-mono-greek-500-normal-JpySY46c.woff2) format('woff2'), url(./jetbrains-mono-greek-500-normal-D7SFKleX.woff) format('woff');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* jetbrains-mono-vietnamese-500-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(data:font/woff2;base64,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) format('woff2'), url(./jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* jetbrains-mono-latin-ext-500-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2) format('woff2'), url(./jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* jetbrains-mono-latin-500-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(./jetbrains-mono-latin-500-normal-BWZEU5yA.woff2) format('woff2'), url(./jetbrains-mono-latin-500-normal-CJOVTJB7.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* jetbrains-mono-cyrillic-ext-700-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(data:font/woff2;base64,d09GMgABAAAAAASAABAAAAAACNgAAAQlAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoBmA/U1RBVEwAdBEICoRkg3gLIAABNgIkAzoEIAWEaAeBFAwHGzgHSJ4DbjN9OmtkIy0becnT/RjJ4uGfw99974/GXrhoTCqLok66NY3jTloEUdrmwCevn/Ls30xSpFlZ6UJMrW+fTNwAEdSuvSzCRANvEgnjUmw8swT+87+w7I/7qb8XlIB7NpGBjXiUjRX0b7ZpJ8+orRLzvGhWYCMdAU3BbRdPuqOeKAXuRKAWEEJJCIEQqNbLD9osq2kBviL8A4Fq4yLgAa+6DNaiAwcYOrqYHfgQcMgL3AFgG4fHHqREEUBSxUtmBfzNWxXADfviy8Kff7dA4QfFBQG6fnoX5Dh+gEEcIVRKtMIYhc/kCgsCDiarQY4QWpEKQoYTAAIJUcaHAHIXIN9BooBWhEAr0j7QzAkh1RnEWOY4wuRW8uX/f5o5W8gXcjriOUb9z/wjACRuWaWrpKIyLMQ/DUDN/TT+ErgHRpPBIRQrKCcZCvxBBCBEkSD4lKMjRblQlCvpQHPsSZE9R6K2o/vpXkGboKWbbltxx2feLT+a6KJb5293n7vTt53uwOzUO4gYc5eedhvS+a1bbdNuIiSA89cuGo25IZh6DXrJJIIUUibFUG0zt+ijO+cvnj8fjVm4cKlvN90DwzrdMmy8H9GjDyv90Xz2W10PVj3oTY7LLfwfmxMHoef+8n33R0duOxF3LPCYFXtymWNdcLBD3VF4S91+GWf8c9k7sZC6fPpnbENnCmayerBNki0ZbFUXiHV/wkxWklGmydCkaR5o0oZnlj3oSVdnZKjTITaXStOyWGYakzUgTSt1gvSs/otJL1FkzE/3e+aXvj7n+TOv9HkZ6evS4e3OXafCx8iqJfNGpQLM7rkt0ppRJ8N3gd1IjHLaROM0cLkhdf2NQjZ4bmjo2mBWKJ4CPA4IZXX9hFlPBnhxWhXzgBGQ88QeRD3j1/AD5a/JxMnL5gK5deaC2H7HvK98gngBbzfWLKD0g2ufeA7nMsQepBwDgpdhy90BzC+8DSCers8O6KKloYn0RCtKo9EM9LsAd7AgNBhFGEAsvl4K1LpCiCIF3IZHFUKvX5AqfUPBgkO+ovHpUNJtOJSN6hDWWxAigkCt9hbEXdaOHLvI6lM5gQA79LRYOpUsWBpKYu2klOQULGlGCwOzHIkSmYmZKBlQrlmCQ0mDv5SeiVyiemVqVJGyEDJhKemY1dLT0YsnpKch0UbKxExJTydIigQpkiUvYA3SVFDqFH2uNggh343VmPs2ycX0DIYwPYTigMak5AjS0ohI8zmoo4EJwypS4iVcDOuDuvRMgytHPa5LTjnoc1YiCcT0tBI3qjXxqqFomJf2PylAOHMDRp1HGBaDQCTiIC7iQSH4iEICRHMZkUlqk/KsOmVSGlOK7NIseRNZFp3NkbNaLateTmKScHGTAyu2WqQf6lD8rMNg1+lKTcpwkfvQ/7eRMwAA) format('woff2'), url(data:font/woff;base64,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) format('woff');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* jetbrains-mono-cyrillic-700-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2) format('woff2'), url(./jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff) format('woff');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* jetbrains-mono-greek-700-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./jetbrains-mono-greek-700-normal-C6CZE3T8.woff2) format('woff2'), url(./jetbrains-mono-greek-700-normal-DEigVDxa.woff) format('woff');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* jetbrains-mono-vietnamese-700-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(data:font/woff2;base64,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) format('woff2'), url(./jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* jetbrains-mono-latin-ext-700-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2) format('woff2'), url(./jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* jetbrains-mono-latin-700-normal */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 700;
  src: url(./jetbrains-mono-latin-700-normal-BYuf6tUa.woff2) format('woff2'), url(./jetbrains-mono-latin-700-normal-D3wTyLJW.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}.react-grid-layout {
  position: relative;
  transition: height 200ms ease;
}
.react-grid-item {
  transition: all 200ms ease;
  transition-property: left, top, width, height;
}
.react-grid-item img {
  pointer-events: none;
  user-select: none;
}
.react-grid-item.cssTransforms {
  transition-property: transform, width, height;
}
.react-grid-item.resizing {
  transition: none;
  z-index: 1;
  will-change: width, height;
}

.react-grid-item.react-draggable-dragging {
  transition: none;
  z-index: 3;
  will-change: transform;
}

.react-grid-item.dropping {
  visibility: hidden;
}

.react-grid-item.react-grid-placeholder {
  background: red;
  opacity: 0.2;
  transition-duration: 100ms;
  z-index: 2;
  user-select: none;
}

.react-grid-item.react-grid-placeholder.placeholder-resizing {
  transition: none;
}

.react-grid-item > .react-resizable-handle {
  position: absolute;
  width: 20px;
  height: 20px;
  opacity: 0;
}

.react-grid-item:hover > .react-resizable-handle {
  opacity: 1;
}

.react-grid-item > .react-resizable-handle::after {
  content: "";
  position: absolute;
  right: 3px;
  bottom: 3px;
  width: 5px;
  height: 5px;
  border-right: 2px solid rgba(0, 0, 0, 0.4);
  border-bottom: 2px solid rgba(0, 0, 0, 0.4);
}

.react-resizable-hide > .react-resizable-handle {
  display: none;
}

.react-grid-item > .react-resizable-handle.react-resizable-handle-sw {
  bottom: 0;
  left: 0;
  cursor: sw-resize;
  transform: rotate(90deg);
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-se {
  bottom: 0;
  right: 0;
  cursor: se-resize;
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-nw {
  top: 0;
  left: 0;
  cursor: nw-resize;
  transform: rotate(180deg);
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-ne {
  top: 0;
  right: 0;
  cursor: ne-resize;
  transform: rotate(270deg);
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-w,
.react-grid-item > .react-resizable-handle.react-resizable-handle-e {
  top: 50%;
  margin-top: -10px;
  cursor: ew-resize;
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-w {
  left: 0;
  transform: rotate(135deg);
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-e {
  right: 0;
  transform: rotate(315deg);
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-n,
.react-grid-item > .react-resizable-handle.react-resizable-handle-s {
  left: 50%;
  margin-left: -10px;
  cursor: ns-resize;
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-n {
  top: 0;
  transform: rotate(225deg);
}
.react-grid-item > .react-resizable-handle.react-resizable-handle-s {
  bottom: 0;
  transform: rotate(45deg);
}
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties {
  @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
    *, :before, :after, ::backdrop {
      --tw-rotate-x: initial;
      --tw-rotate-y: initial;
      --tw-rotate-z: initial;
      --tw-skew-x: initial;
      --tw-skew-y: initial;
      --tw-border-style: solid;
      --tw-leading: initial;
      --tw-font-weight: initial;
      --tw-tracking: initial;
      --tw-ordinal: initial;
      --tw-slashed-zero: initial;
      --tw-numeric-figure: initial;
      --tw-numeric-spacing: initial;
      --tw-numeric-fraction: initial;
      --tw-shadow: 0 0 #0000;
      --tw-shadow-color: initial;
      --tw-shadow-alpha: 100%;
      --tw-inset-shadow: 0 0 #0000;
      --tw-inset-shadow-color: initial;
      --tw-inset-shadow-alpha: 100%;
      --tw-ring-color: initial;
      --tw-ring-shadow: 0 0 #0000;
      --tw-inset-ring-color: initial;
      --tw-inset-ring-shadow: 0 0 #0000;
      --tw-ring-inset: initial;
      --tw-ring-offset-width: 0px;
      --tw-ring-offset-color: #fff;
      --tw-ring-offset-shadow: 0 0 #0000;
      --tw-outline-style: solid;
      --tw-blur: initial;
      --tw-brightness: initial;
      --tw-contrast: initial;
      --tw-grayscale: initial;
      --tw-hue-rotate: initial;
      --tw-invert: initial;
      --tw-opacity: initial;
      --tw-saturate: initial;
      --tw-sepia: initial;
      --tw-drop-shadow: initial;
      --tw-drop-shadow-color: initial;
      --tw-drop-shadow-alpha: 100%;
      --tw-drop-shadow-size: initial;
      --tw-backdrop-blur: initial;
      --tw-backdrop-brightness: initial;
      --tw-backdrop-contrast: initial;
      --tw-backdrop-grayscale: initial;
      --tw-backdrop-hue-rotate: initial;
      --tw-backdrop-invert: initial;
      --tw-backdrop-opacity: initial;
      --tw-backdrop-saturate: initial;
      --tw-backdrop-sepia: initial;
      --tw-duration: initial;
      --tw-ease: initial;
    }
  }
}

@layer theme {
  :root, :host {
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
      "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
      "Segoe UI Symbol", "Noto Color Emoji";
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
      "Courier New", monospace;
    --spacing: .25rem;
    --container-xl: 36rem;
    --container-2xl: 42rem;
    --text-xs: .75rem;
    --text-xs--line-height: calc(1 / .75);
    --text-sm: .875rem;
    --text-sm--line-height: calc(1.25 / .875);
    --text-base: 1rem;
    --text-base--line-height: calc(1.5 / 1);
    --text-2xl: 1.5rem;
    --text-2xl--line-height: calc(2 / 1.5);
    --text-3xl: 1.875rem;
    --text-3xl--line-height: calc(2.25 / 1.875);
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --tracking-wide: .025em;
    --leading-relaxed: 1.625;
    --ease-in-out: cubic-bezier(.4, 0, .2, 1);
    --animate-spin: spin 1s linear infinite;
    --default-transition-duration: .15s;
    --default-transition-timing-function: cubic-bezier(.4, 0, .2, 1);
    --default-font-family: var(--font-sans);
    --default-mono-font-family: var(--font-mono);
    --font-ui: var(--font-ui);
    --font-cond: var(--font-cond);
    --font-num: var(--font-num);
  }
}

@layer base {
  *, :after, :before, ::backdrop {
    box-sizing: border-box;
    border: 0 solid;
    margin: 0;
    padding: 0;
  }

  ::file-selector-button {
    box-sizing: border-box;
    border: 0 solid;
    margin: 0;
    padding: 0;
  }

  html, :host {
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
    line-height: 1.5;
    font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
    font-feature-settings: var(--default-font-feature-settings, normal);
    font-variation-settings: var(--default-font-variation-settings, normal);
    -webkit-tap-highlight-color: transparent;
  }

  hr {
    height: 0;
    color: inherit;
    border-top-width: 1px;
  }

  abbr:where([title]) {
    -webkit-text-decoration: underline dotted;
    text-decoration: underline dotted;
  }

  h1, h2, h3, h4, h5, h6 {
    font-size: inherit;
    font-weight: inherit;
  }

  a {
    color: inherit;
    -webkit-text-decoration: inherit;
    -webkit-text-decoration: inherit;
    -webkit-text-decoration: inherit;
    text-decoration: inherit;
  }

  b, strong {
    font-weight: bolder;
  }

  code, kbd, samp, pre {
    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
    font-feature-settings: var(--default-mono-font-feature-settings, normal);
    font-variation-settings: var(--default-mono-font-variation-settings, normal);
    font-size: 1em;
  }

  small {
    font-size: 80%;
  }

  sub, sup {
    vertical-align: baseline;
    font-size: 75%;
    line-height: 0;
    position: relative;
  }

  sub {
    bottom: -.25em;
  }

  sup {
    top: -.5em;
  }

  table {
    text-indent: 0;
    border-color: inherit;
    border-collapse: collapse;
  }

  :-moz-focusring:where(:not(iframe)) {
    outline: auto;
  }

  progress {
    vertical-align: baseline;
  }

  summary {
    display: list-item;
  }

  ol, ul, menu {
    list-style: none;
  }

  img, svg, video, canvas, audio, iframe, embed, object {
    vertical-align: middle;
    display: block;
  }

  img, video {
    max-width: 100%;
    height: auto;
  }

  button, input, select, optgroup, textarea {
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    opacity: 1;
    background-color: #0000;
    border-radius: 0;
  }

  ::file-selector-button {
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    opacity: 1;
    background-color: #0000;
    border-radius: 0;
  }

  :where(select:is([multiple], [size])) optgroup {
    font-weight: bolder;
  }

  :where(select:is([multiple], [size])) optgroup option {
    padding-inline-start: 20px;
  }

  ::file-selector-button {
    margin-inline-end: 4px;
  }

  ::placeholder {
    opacity: 1;
  }

  @supports (not ((-webkit-appearance: -apple-pay-button))) or (contain-intrinsic-size: 1px) {
    ::placeholder {
      color: currentColor;
    }

    @supports (color: color-mix(in lab, red, red)) {
      ::placeholder {
        color: color-mix(in oklab, currentcolor 50%, transparent);
      }
    }
  }

  textarea {
    resize: vertical;
  }

  ::-webkit-search-decoration {
    -webkit-appearance: none;
  }

  ::-webkit-date-and-time-value {
    min-height: 1lh;
    text-align: inherit;
  }

  ::-webkit-datetime-edit {
    display: inline-flex;
  }

  ::-webkit-datetime-edit-fields-wrapper {
    padding: 0;
  }

  ::-webkit-datetime-edit {
    padding-block: 0;
  }

  ::-webkit-datetime-edit-year-field {
    padding-block: 0;
  }

  ::-webkit-datetime-edit-month-field {
    padding-block: 0;
  }

  ::-webkit-datetime-edit-day-field {
    padding-block: 0;
  }

  ::-webkit-datetime-edit-hour-field {
    padding-block: 0;
  }

  ::-webkit-datetime-edit-minute-field {
    padding-block: 0;
  }

  ::-webkit-datetime-edit-second-field {
    padding-block: 0;
  }

  ::-webkit-datetime-edit-millisecond-field {
    padding-block: 0;
  }

  ::-webkit-datetime-edit-meridiem-field {
    padding-block: 0;
  }

  ::-webkit-calendar-picker-indicator {
    line-height: 1;
  }

  :-moz-ui-invalid {
    box-shadow: none;
  }

  button, input:where([type="button"], [type="reset"], [type="submit"]) {
    appearance: button;
  }

  ::file-selector-button {
    appearance: button;
  }

  ::-webkit-inner-spin-button {
    height: auto;
  }

  ::-webkit-outer-spin-button {
    height: auto;
  }

  [hidden]:where(:not([hidden="until-found"])) {
    display: none !important;
  }
}

@layer components;

@layer utilities {
  .\@container {
    container-type: inline-size;
  }

  .collapse {
    visibility: collapse;
  }

  .visible {
    visibility: visible;
  }

  .sr-only {
    clip-path: inset(50%);
    white-space: nowrap;
    border-width: 0;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    position: absolute;
    overflow: hidden;
  }

  .absolute {
    position: absolute;
  }

  .fixed {
    position: fixed;
  }

  .relative {
    position: relative;
  }

  .static {
    position: static;
  }

  .isolate {
    isolation: isolate;
  }

  .container {
    width: 100%;
  }

  @media (min-width: 40rem) {
    .container {
      max-width: 40rem;
    }
  }

  @media (min-width: 48rem) {
    .container {
      max-width: 48rem;
    }
  }

  @media (min-width: 64rem) {
    .container {
      max-width: 64rem;
    }
  }

  @media (min-width: 80rem) {
    .container {
      max-width: 80rem;
    }
  }

  @media (min-width: 96rem) {
    .container {
      max-width: 96rem;
    }
  }

  .mt-4 {
    margin-top: var(--sp-4);
  }

  .mt-px {
    margin-top: 1px;
  }

  .mb-1 {
    margin-bottom: var(--sp-1);
  }

  .mb-2 {
    margin-bottom: var(--sp-2);
  }

  .mb-3 {
    margin-bottom: var(--sp-3);
  }

  .mb-5 {
    margin-bottom: var(--sp-5);
  }

  .mb-6 {
    margin-bottom: var(--sp-6);
  }

  .mb-8 {
    margin-bottom: var(--sp-8);
  }

  .block {
    display: block;
  }

  .contents {
    display: contents;
  }

  .flex {
    display: flex;
  }

  .grid {
    display: grid;
  }

  .hidden {
    display: none;
  }

  .inline {
    display: inline;
  }

  .inline-block {
    display: inline-block;
  }

  .inline-flex {
    display: inline-flex;
  }

  .table {
    display: table;
  }

  .size-16 {
    width: calc(var(--spacing) * 16);
    height: calc(var(--spacing) * 16);
  }

  .size-44 {
    width: calc(var(--spacing) * 44);
    height: calc(var(--spacing) * 44);
  }

  .h-2 {
    height: var(--sp-2);
  }

  .h-full {
    height: 100%;
  }

  .w-full {
    width: 100%;
  }

  .max-w-2xl {
    max-width: var(--container-2xl);
  }

  .max-w-xl {
    max-width: var(--container-xl);
  }

  .min-w-0 {
    min-width: 0;
  }

  .shrink-0 {
    flex-shrink: 0;
  }

  .grow {
    flex-grow: 1;
  }

  .transform {
    transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
  }

  .animate-spin {
    animation: var(--animate-spin);
  }

  .resize {
    resize: both;
  }

  .flex-col {
    flex-direction: column;
  }

  .items-center {
    align-items: center;
  }

  .items-start {
    align-items: flex-start;
  }

  .justify-center {
    justify-content: center;
  }

  .gap-2 {
    gap: var(--sp-2);
  }

  .gap-3 {
    gap: var(--sp-3);
  }

  .gap-4 {
    gap: var(--sp-4);
  }

  .gap-6 {
    gap: var(--sp-6);
  }

  .truncate {
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
  }

  .overflow-hidden {
    overflow: hidden;
  }

  .rounded {
    border-radius: .25rem;
  }

  .rounded-lg {
    border-radius: var(--r-lg);
  }

  .rounded-md {
    border-radius: var(--r-md);
  }

  .border {
    border-style: var(--tw-border-style);
    border-width: 1px;
  }

  .border-crit\/40 {
    border-color: var(--st-crit);
  }

  @supports (color: color-mix(in lab, red, red)) {
    .border-crit\/40 {
      border-color: color-mix(in oklab, var(--st-crit) 40%, transparent);
    }
  }

  .border-line-mid {
    border-color: var(--line-mid);
  }

  .bg-accent, .bg-accent\/15 {
    background-color: var(--accent);
  }

  @supports (color: color-mix(in lab, red, red)) {
    .bg-accent\/15 {
      background-color: color-mix(in oklab, var(--accent) 15%, transparent);
    }
  }

  .bg-app {
    background-color: var(--bg-app);
  }

  .bg-crit\/10 {
    background-color: var(--st-crit);
  }

  @supports (color: color-mix(in lab, red, red)) {
    .bg-crit\/10 {
      background-color: color-mix(in oklab, var(--st-crit) 10%, transparent);
    }
  }

  .bg-hi {
    background-color: var(--text-hi);
  }

  .bg-panel {
    background-color: var(--bg-panel-solid);
  }

  .bg-void {
    background-color: var(--bg-void);
  }

  .bg-well {
    background-color: var(--bg-well);
  }

  .p-2 {
    padding: var(--sp-2);
  }

  .p-8 {
    padding: var(--sp-8);
  }

  .p-12 {
    padding: calc(var(--spacing) * 12);
  }

  .px-3 {
    padding-inline: var(--sp-3);
  }

  .px-4 {
    padding-inline: var(--sp-4);
  }

  .py-1\.5 {
    padding-block: calc(var(--spacing) * 1.5);
  }

  .py-2 {
    padding-block: var(--sp-2);
  }

  .py-2\.5 {
    padding-block: calc(var(--spacing) * 2.5);
  }

  .py-3 {
    padding-block: var(--sp-3);
  }

  .py-3\.5 {
    padding-block: calc(var(--spacing) * 3.5);
  }

  .py-4 {
    padding-block: var(--sp-4);
  }

  .text-center {
    text-align: center;
  }

  .font-num {
    font-family: var(--font-num);
  }

  .text-2xl {
    font-size: var(--text-2xl);
    line-height: var(--tw-leading, var(--text-2xl--line-height));
  }

  .text-3xl {
    font-size: var(--text-3xl);
    line-height: var(--tw-leading, var(--text-3xl--line-height));
  }

  .text-base {
    font-size: var(--text-base);
    line-height: var(--tw-leading, var(--text-base--line-height));
  }

  .text-sm {
    font-size: var(--text-sm);
    line-height: var(--tw-leading, var(--text-sm--line-height));
  }

  .text-xs {
    font-size: var(--text-xs);
    line-height: var(--tw-leading, var(--text-xs--line-height));
  }

  .text-\[11px\] {
    font-size: 11px;
  }

  .leading-relaxed {
    --tw-leading: var(--leading-relaxed);
    line-height: var(--leading-relaxed);
  }

  .font-medium {
    --tw-font-weight: var(--font-weight-medium);
    font-weight: var(--font-weight-medium);
  }

  .font-semibold {
    --tw-font-weight: var(--font-weight-semibold);
    font-weight: var(--font-weight-semibold);
  }

  .tracking-\[0\.4em\] {
    --tw-tracking: .4em;
    letter-spacing: .4em;
  }

  .tracking-wide {
    --tw-tracking: var(--tracking-wide);
    letter-spacing: var(--tracking-wide);
  }

  .break-all {
    word-break: break-all;
  }

  .text-accent {
    color: var(--accent);
  }

  .text-crit {
    color: var(--st-crit);
  }

  .text-hi {
    color: var(--text-hi);
  }

  .text-lo {
    color: var(--text-lo);
  }

  .text-mid {
    color: var(--text-mid);
  }

  .text-void {
    color: var(--bg-void);
  }

  .capitalize {
    text-transform: capitalize;
  }

  .uppercase {
    text-transform: uppercase;
  }

  .tabular-nums {
    --tw-numeric-spacing: tabular-nums;
    font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
  }

  .shadow {
    --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, #0000001a), 0 1px 2px -1px var(--tw-shadow-color, #0000001a);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }

  .shadow-2xl {
    --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, #00000040);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }

  .ring {
    --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }

  .outline {
    outline-style: var(--tw-outline-style);
    outline-width: 1px;
  }

  .blur {
    --tw-blur: blur(8px);
    filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
  }

  .drop-shadow {
    --tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, #0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color, #0000000f));
    --tw-drop-shadow: drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);
    filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
  }

  .filter {
    filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
  }

  .backdrop-filter {
    -webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
    backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
  }

  .transition {
    transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }

  .transition-colors {
    transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }

  .transition-opacity {
    transition-property: opacity;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }

  .duration-150 {
    --tw-duration: .15s;
    transition-duration: .15s;
  }

  .ease-in-out {
    --tw-ease: var(--ease-in-out);
    transition-timing-function: var(--ease-in-out);
  }

  .outline-none {
    --tw-outline-style: none;
    outline-style: none;
  }

  .select-all {
    -webkit-user-select: all;
    user-select: all;
  }

  .\[image-rendering\:pixelated\] {
    image-rendering: pixelated;
  }

  @media (hover: hover) {
    .hover\:text-hi:hover {
      color: var(--text-hi);
    }

    .hover\:text-mid:hover {
      color: var(--text-mid);
    }

    .hover\:opacity-90:hover {
      opacity: .9;
    }
  }

  .focus\:border-accent:focus {
    border-color: var(--accent);
  }

  .disabled\:opacity-50:disabled {
    opacity: .5;
  }
}

@property --tw-rotate-x {
  syntax: "*";
  inherits: false
}

@property --tw-rotate-y {
  syntax: "*";
  inherits: false
}

@property --tw-rotate-z {
  syntax: "*";
  inherits: false
}

@property --tw-skew-x {
  syntax: "*";
  inherits: false
}

@property --tw-skew-y {
  syntax: "*";
  inherits: false
}

@property --tw-border-style {
  syntax: "*";
  inherits: false;
  initial-value: solid;
}

@property --tw-leading {
  syntax: "*";
  inherits: false
}

@property --tw-font-weight {
  syntax: "*";
  inherits: false
}

@property --tw-tracking {
  syntax: "*";
  inherits: false
}

@property --tw-ordinal {
  syntax: "*";
  inherits: false
}

@property --tw-slashed-zero {
  syntax: "*";
  inherits: false
}

@property --tw-numeric-figure {
  syntax: "*";
  inherits: false
}

@property --tw-numeric-spacing {
  syntax: "*";
  inherits: false
}

@property --tw-numeric-fraction {
  syntax: "*";
  inherits: false
}

@property --tw-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}

@property --tw-shadow-color {
  syntax: "*";
  inherits: false
}

@property --tw-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}

@property --tw-inset-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}

@property --tw-inset-shadow-color {
  syntax: "*";
  inherits: false
}

@property --tw-inset-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}

@property --tw-ring-color {
  syntax: "*";
  inherits: false
}

@property --tw-ring-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}

@property --tw-inset-ring-color {
  syntax: "*";
  inherits: false
}

@property --tw-inset-ring-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}

@property --tw-ring-inset {
  syntax: "*";
  inherits: false
}

@property --tw-ring-offset-width {
  syntax: "<length>";
  inherits: false;
  initial-value: 0;
}

@property --tw-ring-offset-color {
  syntax: "*";
  inherits: false;
  initial-value: #fff;
}

@property --tw-ring-offset-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}

@property --tw-outline-style {
  syntax: "*";
  inherits: false;
  initial-value: solid;
}

@property --tw-blur {
  syntax: "*";
  inherits: false
}

@property --tw-brightness {
  syntax: "*";
  inherits: false
}

@property --tw-contrast {
  syntax: "*";
  inherits: false
}

@property --tw-grayscale {
  syntax: "*";
  inherits: false
}

@property --tw-hue-rotate {
  syntax: "*";
  inherits: false
}

@property --tw-invert {
  syntax: "*";
  inherits: false
}

@property --tw-opacity {
  syntax: "*";
  inherits: false
}

@property --tw-saturate {
  syntax: "*";
  inherits: false
}

@property --tw-sepia {
  syntax: "*";
  inherits: false
}

@property --tw-drop-shadow {
  syntax: "*";
  inherits: false
}

@property --tw-drop-shadow-color {
  syntax: "*";
  inherits: false
}

@property --tw-drop-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}

@property --tw-drop-shadow-size {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-blur {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-brightness {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-contrast {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-grayscale {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-hue-rotate {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-invert {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-opacity {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-saturate {
  syntax: "*";
  inherits: false
}

@property --tw-backdrop-sepia {
  syntax: "*";
  inherits: false
}

@property --tw-duration {
  syntax: "*";
  inherits: false
}

@property --tw-ease {
  syntax: "*";
  inherits: false
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
/**
 * Design Tokens — IOC KCN Đồng Văn III
 * Theme: VSS DARK — không viền, chỉ tầng màu + bóng mềm
 *
 * VIEWING CONTEXT (drives every number below): each tab is projected onto its
 * own physical display in the operations room and read from 3–5 metres away.
 * That is why the type scale starts where a normal web app's ends, and why
 * nothing here scrolls — a wall display has no operator holding a mouse.
 *
 * LAYOUT LAW: the shell is a fixed-height flex column (header / main / footer).
 * Every screen is a CSS grid that fills `main` exactly. Grid/flex children
 * declare min-width:0 / min-height:0 so a long Vietnamese label truncates
 * instead of inflating its track.
 *
 * ⚠ ĐẠI TU UI 22/08 — ĐỌC TRƯỚC KHI SỬA FILE NÀY
 * ─────────────────────────────────────────────────────────────────────
 * File có BA tầng, cố ý:
 *
 *   :root       BẢNG MÀU CŨ cho những token mà BẢN ĐỒ tiêu thụ.
 *               Cảnh 3D (`Map3DView.tsx:115` tokenColor) gọi
 *               getComputedStyle(document.documentElement) ĐÚNG MỘT LẦN lúc
 *               mount, nên nó luôn đọc tầng này. Giữ nguyên = bản đồ 3D
 *               không đổi một pixel.
 *
 *   .shell      BẢNG MÀU MỚI (VSS). Mọi thứ nhìn thấy của app nằm trong
 *               `.shell` nên đổi theo tầng này.
 *
 *   .site-plan  GHIM LẠI BẢNG CŨ. SVG bản đồ 2D nằm trong `.shell` nên phải
 *               ghim đè, nếu không nó sẽ đổi màu theo bảng mới.
 *
 * Lệnh của người dùng 22/08: **bản đồ giữ nguyên, không động vào**. Ba tầng
 * trên là cách thực hiện lệnh đó mà KHÔNG sửa một dòng nào trong
 * src/components/map/** hay src/screens/map/**.
 * Chi tiết: plans/260822-1930-dai-tu-ui-vss/00-chot-truoc-khi-code.md §D1.
 */

/* CHẾ ĐỘ TƯỜNG (`?wall=1`, 14/09): mọi số đo tỉ lệ với 1600 thay vì 1920 —
   tức to hơn 20% ở cùng độ phân giải. Lý do: TV 55" FHD đọc từ 3–5m cần chữ
   hoa cao ≈ 15mm ≈ 24px, còn thang chuẩn ngắm người ngồi bàn. Chỉ đổi MỘT
   biến gốc — thang chữ, khoảng cách, bo góc đều đi theo; chart và lưới đo
   theo px thật nên không vỡ, bảng dài hơn thì cuộn. Đặt trước `:root` nhưng
   thắng nhờ specificity cao hơn. */
:root[data-wall='1'] {
  --ui-px: calc(100vw / 1600);
}

:root {
  /* ══════════════════════════════════════════════════════════════════════
     TẦNG 1 — BẢNG MÀU CŨ (vùng cách ly bản đồ)
     Khai một lần dưới tên --map-*, rồi gán cho token thật. Sửa ở đây là
     bản đồ đổi màu — đừng sửa trừ khi có lệnh mới.
     ══════════════════════════════════════════════════════════════════════ */
  --map-void: #04070F;
  --map-app: #070C1A;
  --map-panel: rgba(15, 24, 46, 0.82);
  --map-panel-solid: #0E1730;
  --map-well: rgba(3, 7, 18, 0.72);
  --map-raised: rgba(255, 255, 255, 0.045);
  --map-hover: rgba(255, 255, 255, 0.07);
  --map-good: #00E676;
  --map-warn: #FFB020;
  --map-crit: #FF3B67;
  --map-info: #38BDF8;
  --map-idle: #64748B;
  --map-text-hi: #F2F6FF;
  --map-text-mid: #A6B4D0;
  --map-text-lo: #6B7A99;
  --map-text-faint: #465066;
  --map-line-soft: rgba(255, 255, 255, 0.07);
  --map-line-mid: rgba(255, 255, 255, 0.12);
  --map-ac-power: #00E5FF;
  --map-ac-water: #22D3A5;
  --map-ac-vision: #A78BFA;
  --map-ac-alarm: #FF3B67;
  --map-ac-iot: #38BDF8;
  --map-ac-effluent: #4FC3F7;
  --map-ac-env: #34D399;
  --map-ac-light: #FFC53D;
  --map-ac-command: #00E5FF;
  /* Lớp "Trạm BTS" (bản vẽ thông tin liên lạc) — ĐÚNG màu bản vẽ TT-MBTT-001
     (yêu cầu 29/09): ACI 1 đỏ = tuyến ống D110 + hố ga, ACI 6 hồng = cáp quang
     8FO + mã trạm BTS. Dùng thẳng, không qua --accent-* nên rail, map 2D và
     cảnh 3D ra cùng một màu. */
  --map-ac-telecom: #FF0000;
  --map-ac-telecom-fiber: #FF00FF;
  --map-font-num: 'JetBrains Mono', 'SF Mono', 'Fira Code', ui-monospace, monospace;

  /* Token thật, mặc định = bảng CŨ (đây là thứ cảnh 3D đọc lúc mount). */
  --bg-void: var(--map-void);
  --bg-app: var(--map-app);
  --bg-panel: var(--map-panel);
  --bg-panel-solid: var(--map-panel-solid);
  --bg-well: var(--map-well);
  --bg-raised: var(--map-raised);
  --bg-hover: var(--map-hover);

  --accent-power: var(--map-ac-power);
  --accent-water: var(--map-ac-water);
  --accent-vision: var(--map-ac-vision);
  --accent-alarm: var(--map-ac-alarm);
  --accent-iot: var(--map-ac-iot);
  --accent-effluent: var(--map-ac-effluent);
  --accent-env: var(--map-ac-env);
  --accent-light: var(--map-ac-light);
  --accent-command: var(--map-ac-command);

  --st-good: var(--map-good);
  --st-warn: var(--map-warn);
  --st-crit: var(--map-crit);
  --st-info: var(--map-info);
  --st-idle: var(--map-idle);

  /* ── Màu MỨC của thang sao (06/10, spec plans/261006-1249-thang-sao-canh-bao §6) ─────────────────────
     Cùng bảng màu CÓ TÊN với app CMS (`--level-<tên>` nền tối): Admin chọn màu của một mức ở CMS (mặc định
     Cảnh báo 1★ vàng · Nghiêm trọng 2★ cam · Khẩn cấp 3★ đỏ), tường tô nhãn mức theo đúng tên đó — đổi màu
     ở CMS là tường đổi theo. KHÔNG phải màu trạng thái: chỉ nhãn mức cảnh báo/phiếu dùng (`LevelBadge`). */
  --lv-red: #f0654f;
  --lv-orange: #f5874d;
  --lv-amber: #f0a53a;
  --lv-yellow: #e2c53a;
  --lv-lime: #9ccc3a;
  --lv-green: #3ec27a;
  --lv-teal: #33c2ad;
  --lv-blue: #6a90ff;
  --lv-violet: #a585f0;
  --lv-slate: #8a9ab2;

  --text-hi: var(--map-text-hi);
  --text-mid: var(--map-text-mid);
  --text-lo: var(--map-text-lo);
  --text-faint: var(--map-text-faint);

  --line-soft: var(--map-line-soft);
  --line-mid: var(--map-line-mid);

  /* ══════════════════════════════════════════════════════════════════════
     TẦNG 2 — TOKEN KHÔNG PHẢI MÀU: dùng chung, giá trị MỚI ngay tại :root
     ══════════════════════════════════════════════════════════════════════ */

  /* ── Nền dòng danh sách (mẫu "viên rời" của VSS) ─────────────────────── */
  --bg-row: rgba(255, 255, 255, 0.022);
  --bg-row-hover: rgba(255, 255, 255, 0.055);
  --bg-row-sel: color-mix(in srgb, var(--accent) 13%, transparent);
  --bg-scrim: rgba(4, 7, 11, 0.6);

  /* ── Nền trạng thái (giữ tên cũ, dùng cho badge/chip) ────────────────── */
  --st-good-bg: color-mix(in srgb, var(--st-good) 13%, transparent);
  --st-warn-bg: color-mix(in srgb, var(--st-warn) 13%, transparent);
  --st-crit-bg: color-mix(in srgb, var(--st-crit) 15%, transparent);
  --st-info-bg: color-mix(in srgb, var(--st-info) 12%, transparent);

  /* ── Đường kẻ ăn theo accent (trước đây khoá cứng cyan — "token giả") ── */
  --line-accent: color-mix(in srgb, var(--accent) 22%, transparent);

  /* ── Đổ bóng: chữ ký thị giác của VSS. Không dùng `border` cho khung. ── */
  --sh-hair: inset 0 0 0 1px rgba(255, 255, 255, 0.05);   /* viền ảo */
  --sh-lite: inset 0 1px 0 rgba(255, 255, 255, 0.075);    /* bắt sáng mép trên */
  --sh-sink: 0 16px 34px -14px rgba(0, 0, 0, 0.72), 0 4px 12px -6px rgba(0, 0, 0, 0.6);
  --sh-halo: 0 0 32px -20px color-mix(in srgb, var(--accent) 70%, transparent);
  --sh-head: 0 10px 16px -14px rgba(0, 0, 0, 0.95), inset 0 -1px 0 rgba(255, 255, 255, 0.02);
  /* Thanh chrome NỔI (header · dải phân hệ · băng đáy): bóng rất nhoè và rất
     lệch âm, chỉ đủ tách thanh khỏi nền chứ không vẽ ra một cái hộp. */
  --sh-float: 0 16px 34px -28px rgba(0, 0, 0, 0.95);

  --glow-accent: 0 0 18px -6px color-mix(in srgb, var(--accent) 85%, transparent);
  --glow-crit: 0 0 14px -5px color-mix(in srgb, var(--st-crit) 95%, transparent);
  --glow-warn: 0 0 14px -6px color-mix(in srgb, var(--st-warn) 90%, transparent);
  --glow-good: 0 0 14px -6px color-mix(in srgb, var(--st-good) 90%, transparent);
  /* Hợp nhất metric.css:113 (24px/45%) và metric.css:301 (20px/40%) — cùng một ý. */
  --glow-value: 0 0 24px color-mix(in srgb, var(--mt-accent, var(--accent)) 45%, transparent);

  /* Giữ tên cũ cho tương thích (bản đồ và vài component đang dùng). */
  --elev-panel: var(--sh-sink);
  --elev-float: 0 18px 48px rgba(0, 0, 0, 0.7);

  /* ── Typography ──────────────────────────────────────────────────────────
     Ba họ chữ, ba vai trò (bất biến §4): câu chữ · nhãn HOA · con số.

     LỊCH SỬ — đọc trước khi đổi:
     · 23/08 gỡ Barlow Condensed vì "dựng dấu tiếng Việt xấu" (dấu nặng/ngã bám
       sát thân chữ), `--font-cond` khi đó trỏ tạm về Inter.
     · Đại tu VSS: nạp lại Barlow Condensed ĐÚNG như bản mẫu — mẫu dùng
       `--cond: "Barlow Condensed", "Be Vietnam Pro"` với Google Fonts phục vụ
       cả subset latin lẫn vietnamese, nên nhãn HOA tiếng Việt của mẫu thật sự
       là Barlow Condensed. Font nạp ở `main.tsx` (xem chú thích ở đó: phải
       import file KHÔNG tiền tố subset).

     ⛔ ĐÂY LÀ CÔNG TẮC HOÀN NGUYÊN. Nếu đối chiếu ở cự ly tường thấy dấu vẫn
     xấu (mốc thử: "CÔNG SUẤT PHẢN KHÁNG TỔNG" màn Điện năng), đổi đúng dòng
     `--font-cond` về `var(--font-ui)` — ~30 chỗ trong design-system gọi tên
     này nên một dòng là đủ, KHÔNG sửa component. */
  --font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-cond: 'Barlow Condensed', var(--font-ui);
  --font-num: var(--map-font-num);

  /* Thang hiệu chỉnh cho wall 55" đọc từ 3m (1px ≈ 0,634mm; chiều cao chữ
     ≥ khoảng cách/200). Nhãn dùng --font-cond nên tăng px mà KHÔNG tốn thêm
     bề ngang — đây là chỗ Q4 lấy được mật độ mà không hy sinh khả năng đọc. */
  /* ⭐ HỆ SỐ CO GIÃN THEO MÀN (26/08) — vì sao phải có:
     Toàn bộ thang chữ trước đây là PX CỐ ĐỊNH. Trên màn 1080p thì vừa, nhưng
     cùng bộ giao diện đó chiếu lên 4K (3840×2160) thì mỗi pixel CSS chỉ còn
     nửa kích thước vật lý — chữ 12px teo bằng 6px trên 1080p, đứng cách 3m
     không đọc nổi. Người dùng yêu cầu "4K và 1080p phải chuẩn giống nhau"
     (26/08), nghĩa là giao diện phải chiếm CÙNG MỘT GÓC NHÌN ở mọi độ phân
     giải — tức mọi số đo phải tỉ lệ với bề ngang màn, không phải hằng số px.

     `--ui-px` là ĐỘ DÀI, không phải số: `100vw / 1920` = 1px ở Full HD và 2px
     ở 4K. Mọi số đo bên dưới viết là `calc(var(--ui-px) * N)` — tức "N pixel
     của màn 1920", rồi `clamp` chặn hai đầu: không co dưới cỡ Full HD (chữ đã
     ở sàn đọc được) và không phóng quá gấp đôi.

     ⚠ KHÔNG viết `clamp(1, calc(100vw / 1920), 2)` rồi đem nhân: `1` và `2` là
     SỐ còn `100vw / 1920` là ĐỘ DÀI — clamp trộn hai kiểu là khai không hợp lệ,
     trình duyệt bỏ cả dòng và mọi cỡ chữ rơi về 16px mặc định (đo 26/08). */
  --ui-px: calc(100vw / 1920);

  /* Thang cỡ chữ — SỐ GỐC là cỡ ở Full HD. Đợt 26/08 nhích mỗi bậc lên một
     nấc theo yêu cầu "cho to lên cho hợp lý": bản mẫu ngắm màn ngồi gần, còn
     đây là tường điều hành đọc từ 3–5m. */
  --fs-micro: clamp(9px, calc(var(--ui-px) * 9), 14px);   /* tick trục chart — CẤM dùng cho con số */
  --fs-cap: clamp(10px, calc(var(--ui-px) * 10), 15px);     /* caption, đơn vị sau số, meta bảng */
  --fs-label: clamp(11px, calc(var(--ui-px) * 11), 16px);   /* nhãn HOA chuẩn */
  --fs-body: clamp(12px, calc(var(--ui-px) * 12), 18px);    /* chữ chạy văn */
  --fs-body-lg: clamp(13px, calc(var(--ui-px) * 13), 20px); /* tên thiết bị, tiêu đề panel */
  /* Tên màn cạnh logo (15/09): chữ hoa PHẢI thấp hơn chữ logo ở mọi bề rộng.
     Đo bằng vw thô như --h-brand-logo và tiêu đề giữa, KHÔNG qua --ui-px —
     chế độ tường đổi --ui-px sang /1600 nên chữ sẽ vượt logo. Sàn 11px (chữ
     hoa ~8px) để vẫn dưới logo khi logo chạm sàn 9px. 1920: 15px. */
  --fs-heading: clamp(11px, 0.78vw, 20px);
  --fs-sub: clamp(17px, calc(var(--ui-px) * 17), 26px);     /* số phụ */
  --fs-num: clamp(22px, calc(var(--ui-px) * 22), 33px);     /* số trong bảng / trong hàng */
  --fs-num-lg: clamp(30px, calc(var(--ui-px) * 30), 45px);  /* số cấp mục */
  --fs-kpi: clamp(34px, calc(var(--ui-px) * 34), 51px);     /* số của dải KPI */
  --fs-hero: clamp(44px, calc(var(--ui-px) * 44), 66px);    /* số định danh màn */
  --fs-hero-xl: clamp(60px, calc(var(--ui-px) * 60), 90px); /* số to nhất một màn */

  --track-label: 0.09em;
  --track-wide: 0.16em;
  --track-tab: 0.08em;
  --track-chip: 0.05em;
  --track-num: -0.02em;

  --lh-tight: 1.05;
  --lh-num: 1.15;
  --lh-label: 1.2;   /* bắt buộc cho nhãn HOA — dấu tiếng Việt cần chỗ */
  --lh-body: 1.35;
  --lh-loose: 1.5;

  --fw-body: 400;
  --fw-strong: 600;
  --fw-label: 600;
  --fw-title: 700;
  --fw-value: 500;
  --fw-hero: 700;

  /* ── Spacing (4px base) ─────────────────────────────────────────────── */
  /* Khoảng cách cũng co giãn theo `--ui-px`: chữ to gấp đôi mà đệm giữ nguyên thì
     chữ dính sát mép, khối trông chật hơn hẳn bản 1080p. */
  --sp-1: clamp(4px, calc(var(--ui-px) * 4), 6px);
  --sp-2: clamp(8px, calc(var(--ui-px) * 8), 12px);
  --sp-3: clamp(12px, calc(var(--ui-px) * 12), 18px);
  --sp-4: clamp(16px, calc(var(--ui-px) * 16), 24px);
  --sp-5: clamp(20px, calc(var(--ui-px) * 20), 30px);
  --sp-6: clamp(24px, calc(var(--ui-px) * 24), 36px);
  --sp-8: clamp(32px, calc(var(--ui-px) * 32), 48px);

  /* Khoảng đệm của hệ VSS — dùng cho component mới, không đụng --sp-*. */
  --pad-panel: clamp(8px, calc(var(--ui-px) * 8), 12px) clamp(10px, calc(var(--ui-px) * 10), 15px);
  --pad-row: clamp(7px, calc(var(--ui-px) * 7), 10px) clamp(9px, calc(var(--ui-px) * 9), 14px);
  --pad-chip: clamp(2px, calc(var(--ui-px) * 2), 3px) clamp(7px, calc(var(--ui-px) * 7), 10px);
  --gap-row: 5px;
  --inset-row: 7px;
  --dot-size: 6px;

  /* ── Radii ──────────────────────────────────────────────────────────── */
  /* --r-sm/md/lg GIỮ NGUYÊN: `.tbtn` dùng --r-md và `.tbtn` là nút toolbar
     của bản đồ ở 18 điểm. Hệ VSS dùng ba biến MỚI bên dưới. */
  --r-sm: 3px;
  --r-md: 5px;
  --r-lg: 8px;
  --r-pill: 999px;
  --r-panel: 15px;  /* khung panel/card/KPI */
  --r-row: 10px;    /* dòng danh sách, chip vuông */
  --r-key: 8px;     /* nút vuông, ô kv */

  /* ── Vạch accent & viền ─────────────────────────────────────────────── */
  --rail-h: 2px;
  /* Độ mờ kính của lớp nổi (card nổi trên bản đồ, ngăn phải) — template ghi đè được. */
  --blur-glass: 10px;
  --mt-bar-w: 2px;
  /* Bề rộng ô hình trong thẻ KPI dẹt. 76 → 52px (23/08): ở thẻ rộng 238px
     thì 76px nuốt mất chỗ của đơn vị, chữ tràn xuống dưới hình. Gauge và
     TankLevelIcon vẫn giữ nguyên vai trò — chỉ nhỏ lại. */
  --kpi-aside-w: 52px;
  --panel-border-w: 0px; /* nguyên tắc VSS: không viền, dùng --sh-hair */

  /* ── Shell geometry (single source of truth for layout maths) ───────── */
  --h-masthead: 52px;
  /* Chiều cao logo công ty (15/09): chữ logo phải NHỎ HƠN chữ tiêu đề giữa.
     Tiêu đề clamp(12px, 1vw, 22px) → chữ hoa cao ~0.73 cỡ (14px ở 1920);
     chữ logo chiếm ~95% ảnh. Co giãn cùng nhịp vw để ở mọi bề rộng logo
     vẫn thấp hơn chữ tiêu đề (1920: ảnh 13px → chữ ~12.4px). */
  --h-brand-logo: clamp(9px, 0.68vw, 16px);
  --h-header: var(--h-masthead);
  /* Dải phân hệ — hàng thứ hai của khung, chứa đúng MỘT thứ: chip tên màn
     đang chiếu (mẫu S07 `.navbar` dùng 30px cho dải tab chữ hoa 11px; ở đây
     chip có viền ảo nên cần thêm chỗ thở). Hàng này từng bị gộp vào thanh lệnh
     hôm 20/09 rồi tách lại 21/09 theo ảnh mẫu người dùng: tên TRUNG TÂM và tên
     MÀN là hai câu khác loại, chồng ba tầng chữ trong 52px thì không tầng nào
     đọc được từ 3–5m. Giá phải trả là 36px + một rãnh của vùng dữ liệu. */
  --h-nav: 36px;
  --h-footer: 26px;
  --h-panel-head: 26px; /* nén từ 30px — phần đất Q4 */
  --h-btn: 24px;
  --h-btn-sm: 20px;
  --h-tab: 26px;
  --gutter: 10px;       /* gap between screen-level panels */
  --grid-gap: var(--gutter);
  /* ⚠ ĐỒNG BỘ TAY với DraggableScreen.tsx:13 `const GAP = 10` — hai nơi,
     không có ràng buộc tự động. Sửa một chỗ mà quên chỗ kia là lệch lưới. */
  --grid-cols: 12;
  --grid-rows: 24;

  /* ── Thang z-index (bản đồ chiếm tới 30 — lớp mới phải ≥ 31) ────────── */
  --z-map-popup: 30;
  --z-grid-drag: 40;
  /* Ngăn phải của màn lưới: trên card, dưới cụm nút Sắp xếp và Thư viện. */
  --z-grid-drawer: 50;
  --z-grid-fab: 60;
  --z-grid-lib: 70;
  --z-tick: 80;
  --z-tabs: 95;
  --z-header: 100;
  --z-overlay: 120;
  --z-dialog: 200;
  --z-toast: 300;

  /* ── Nền màn ────────────────────────────────────────────────────────── */
  --bg-grid-size: 64px;
  --bg-grid-opacity: 0.014;
  --blur-scrim: 4px;

  /* Active screen accent — each screen sets this on its root so panels,
     rails and charts inherit one colour instead of hard-coding it. */
  --accent: var(--accent-command);
  --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
}


/* ══════════════════════════════════════════════════════════════════════
   TẦNG 2 — BẢNG MÀU MỚI (VSS), áp cho toàn bộ phần nhìn thấy của app.
   Đặt ở `.shell` chứ không phải `:root` để cảnh 3D của bản đồ (đọc `:root`
   một lần lúc mount) không bị đổi màu. Xem chú thích đầu file.
   ══════════════════════════════════════════════════════════════════════ */

.shell {
  /* Nền: 7 tầng tonal, không dùng viền để phân tách */
  --bg-void: #06090E;
  /* 16/09 — bằng ĐÚNG `--bg-void`, theo mẫu S07: ở đó `.app` trong suốt, nền
     nhìn thấy là `body{background:var(--void)}`. Tầng #080C13 cũ sáng hơn mẫu
     một nấc và làm mọi thanh chrome trong suốt bên trên nó nhạt theo. */
  --bg-app: #06090E;
  --bg-panel: rgba(14, 20, 29, 0.86);
  --bg-panel-solid: #0E141D;
  --bg-panel-hi: #141B26;      /* đỉnh gradient panel */
  --bg-elevated: #1A222E;      /* khối nổi / hover đục */
  --bg-well: rgba(8, 12, 17, 0.72);
  --bg-raised: rgba(255, 255, 255, 0.045);
  --bg-hover: rgba(255, 255, 255, 0.07);

  /* Accent theo phân hệ — một màu một hệ thống, bất biến toàn app.
     Hex nằm ở `--vss-*` trong `screen-themes.css` (nguồn duy nhất); ở đây
     chỉ ánh xạ sang tên token mà component đang gọi. Khối theme theo màn
     cũng trỏ về đúng bộ `--vss-*` đó, nên không còn chỗ nào đồng bộ tay. */
  --accent-power: var(--vss-power);
  --accent-water: var(--vss-water);
  --accent-vision: var(--vss-vision);
  --accent-alarm: var(--vss-alarm);
  --accent-iot: var(--vss-iot);
  --accent-effluent: var(--vss-effluent);
  --accent-env: var(--vss-env);
  --accent-light: var(--vss-light);
  --accent-command: var(--vss-command);

  /* Trạng thái — chỉ 5 nghĩa, dùng xuyên 10 màn */
  --st-good: #22C48E;
  /* 20/09 — TÁCH khỏi `--vss-power` (#FFB020). Trước đây hai token cùng MỘT hex,
     nên trên màn Điện năng màu cảnh báo chính là màu phân hệ: cảnh báo mất khả
     năng báo động, và cả màn vàng như nhau nên mắt không có điểm neo. Đã phải vá
     tại chỗ hai lần vì đúng lý do này — lát donut "điện mặt trời" và đường dự báo
     từng BIẾN MẤT vào nền amber (ElectricityScreen.tsx:44 và :466).
     Cam #FF8A3D đủ xa #FFB020 (điện), #EDE05A (chiếu sáng) và #FF3B4E (crit) khi
     đọc từ 3–5m.
     ⛔ KHÔNG đổi `--st-warn` ở tầng `:root` (dòng 102) và `.site-plan` (dòng 424):
     hai tầng đó nuôi màu bản đồ 2D SVG + cảnh 3D, là vùng cấm chạm. */
  --st-warn: #FF8A3D;
  --st-crit: #FF3B4E;
  --st-info: #35B0F2;
  --st-idle: #4E6072;

  /* Hai biến dẫn xuất từ `--st-warn` khai ở `:root` (dòng 127 và 146). `var()`
     lồng trong custom property được thay tại computed-value time CỦA `:root`,
     nên chúng vẫn giữ hex cũ dù `.shell` đã đổi — phải khai lại ở đây. Thiếu
     bước này thì badge WARNING có chữ cam mới trên nền cam cũ. */
  --st-warn-bg: color-mix(in srgb, var(--st-warn) 13%, transparent);
  --glow-warn: 0 0 14px -6px color-mix(in srgb, var(--st-warn) 90%, transparent);

  /* "Bạn đang ở đây" trên dải phân hệ — hổ phách CỐ ĐỊNH, không đổi theo màn
     (mẫu S07 dùng đúng một màu cho `[aria-current]` ở cả S01→S09). Khai riêng
     chứ không mượn `--st-warn`/`--accent-power`: đây là NGHĨA khác (vị trí,
     không phải cảnh báo hay điện năng), mượn tên là lần sau sửa nhầm. */
  --nav-current: #FFB020;

  /* MÀU CỦA CHẤT trong dụng cụ thí nghiệm — dải KPI màn Nước thải (lệnh 21/09:
     "màu của chất lỏng là màu của chính chất hoá học đó"). Chất vốn không màu
     thì lấy sắc gần nhất đã có trong bảng màu app, không sáng tác màu mới.
     Khai riêng chứ không mượn `--st-*`: đây là nghĩa CHẤT, không phải trạng thái.
     ⛔ KHÔNG chất nào được đỏ — đỏ dành riêng cho VƯỢT NGƯỠNG (lúc đó bình đầy
     và chuyển hẳn sang `--st-crit`); để một chất sẵn màu đỏ là mất luôn tín hiệu đó. */
  --chem-ph: var(--accent-env); /* quỳ tím */
  --chem-cod: #E3B341; /* hổ phách — dung dịch K₂Cr₂O₇ khi chuẩn độ COD */
  --chem-tss: #B08968; /* nâu phù sa — nước đục vì chất rắn lơ lửng */
  --chem-nh4: var(--accent-effluent); /* amoni không màu — lấy sắc nước thải sau xử lý */
  --chem-do: var(--accent-water); /* oxy hoà tan trong nước */
  --chem-temp: #C3CEDA; /* bạc — cột thuỷ ngân của nhiệt kế */

  /* Chữ — 4 cấp, tất cả ≥ 4,5:1 trên nền panel (kiểm 22/08) */
  --text-hi: #DCE7F2;
  --text-mid: #9DB0C4;
  --text-lo: #8194A8;
  --text-faint: #6E8299;

  --line-soft: rgba(255, 255, 255, 0.05);
  --line-mid: rgba(255, 255, 255, 0.16);

  /* Gradient của hệ VSS (khai ở đây vì phụ thuộc bảng màu mới) */
  --grad-panel: linear-gradient(180deg, var(--bg-panel-hi) 0%, var(--bg-panel-solid) 48%, #0C1119 100%);
  --grad-head: linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.012));
  /* Dải phân hệ NHẠT hơn thanh lệnh một nấc (mẫu S07 `.navbar` .043/.008 so
     với `.top` .055/.012): hai thanh nằm sát nhau, cùng độ sáng thì đọc ra
     thành một khối dày. */
  --grad-nav: linear-gradient(180deg, rgba(255, 255, 255, 0.043), rgba(255, 255, 255, 0.008));
  --grad-header: var(--grad-head);
  --grad-kpi: linear-gradient(180deg, var(--bg-panel-hi), #0A0E15);
  --bg-thead: linear-gradient(180deg, #151C27, #111823);
}


/* ══════════════════════════════════════════════════════════════════════
   TẦNG 3 — GHIM LẠI BẢNG CŨ CHO SVG BẢN ĐỒ 2D
   `.site-plan` nằm trong `.shell` nên nếu không ghim, nó sẽ ăn bảng mới.
   Đây là toàn bộ token mà site-plan.css + SitePlanMap.tsx tiêu thụ
   (khảo sát: plans/…/khao-sat/app-06-map-gis.md §3.1).
   ⛔ KHÔNG XOÁ khối này — xoá là bản đồ đổi màu, trái lệnh 22/08.
   ══════════════════════════════════════════════════════════════════════ */

.site-plan {
  --bg-void: var(--map-void);
  --bg-app: var(--map-app);
  --bg-panel: var(--map-panel);
  --bg-panel-solid: var(--map-panel-solid);
  --bg-well: var(--map-well);
  --bg-raised: var(--map-raised);
  --bg-hover: var(--map-hover);

  --accent-power: var(--map-ac-power);
  --accent-water: var(--map-ac-water);
  --accent-vision: var(--map-ac-vision);
  --accent-alarm: var(--map-ac-alarm);
  --accent-iot: var(--map-ac-iot);
  --accent-effluent: var(--map-ac-effluent);
  --accent-env: var(--map-ac-env);
  --accent-light: var(--map-ac-light);
  --accent-command: var(--map-ac-command);

  --st-good: var(--map-good);
  --st-warn: var(--map-warn);
  --st-crit: var(--map-crit);
  --st-info: var(--map-info);
  --st-idle: var(--map-idle);

  --text-hi: var(--map-text-hi);
  --text-mid: var(--map-text-mid);
  --text-lo: var(--map-text-lo);
  --text-faint: var(--map-text-faint);

  --line-soft: var(--map-line-soft);
  --line-mid: var(--map-line-mid);

  --font-num: var(--map-font-num);
  --fs-micro: 9px;
  --fs-cap: 10px;
  --fs-body: 12px;
  --fs-sub: 15px;
}


/* ══════════════════════════════════════════════════════════════════════
   Layout helpers
   ══════════════════════════════════════════════════════════════════════ */

/* Flex/grid child allowed to shrink below its content size. Without this a
   single long label silently widens its whole track. */
.fit {
  min-width: 0;
  min-height: 0;
}

.truncate {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The only sanctioned scroll region: inside a panel, never at screen level. */
.scroll-y {
  overflow-y: auto;
  overflow-x: hidden;
  min-height: 0;
}

/* ══════════════════════════════════════════════════════════════════════
   Typographic primitives
   ══════════════════════════════════════════════════════════════════════ */

/* Nhãn HOA chuẩn: Barlow Condensed + giãn chữ. Condensed nên tăng cỡ chữ
   mà bề ngang không đổi — đúng cách VSS lấy mật độ. */
.lbl {
  font-family: var(--font-cond);
  font-size: var(--fs-label);
  font-weight: var(--fw-label);
  line-height: var(--lh-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-lo);
  white-space: nowrap;
}

.lbl--mid {
  color: var(--text-mid);
}

/* Ký hiệu khoa học GIỮ NGUYÊN hoa/thường: pH, Fe, NH₄⁺, mg/L, cosφ — viết hoa
   toàn bộ là SAI chuẩn (IUPAC/SI) và người nước ngoài đọc ra nghĩa khác.
   Dùng cho nhãn nào là công thức/đơn vị, không dùng cho chữ thường. */
.lbl--symbol {
  text-transform: none;
  font-family: var(--font-ui);
}

/* Any number that changes at runtime is tabular — digits must not dance. */
.num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  letter-spacing: var(--track-num);
}

/* ══════════════════════════════════════════════════════════════════════
   Scrollbar — mảnh 5px trong rãnh 9px (mẹo border + background-clip)
   ══════════════════════════════════════════════════════════════════════ */

* {
  scrollbar-width: thin;
  scrollbar-color: rgba(120, 160, 195, 0.34) transparent;
}

::-webkit-scrollbar {
  width: 9px;
  height: 9px;
}

::-webkit-scrollbar-track {
  background: rgba(0, 0, 0, 0.22);
  border-radius: 9px;
}

::-webkit-scrollbar-thumb {
  background-color: rgba(126, 163, 196, 0.3);
  background-clip: content-box;
  border: 2px solid transparent;
  border-radius: 9px;
}

::-webkit-scrollbar-thumb:hover {
  background-color: color-mix(in srgb, var(--accent) 62%, transparent);
}

::-webkit-scrollbar-corner {
  background: transparent;
}

/* ══════════════════════════════════════════════════════════════════════
   THEME THEO MÀN — accent, accent phụ và dải màu chart của từng phân hệ
   ══════════════════════════════════════════════════════════════════════

   VÌ SAO LÀ CSS CHỨ KHÔNG PHẢI JS:

   `use-wall-mode.ts` vốn đã ghi `root.dataset.screen = screenId`, nên
   `:root[data-screen='…']` là selector CÓ SẴN. Khai theme ở đây thì KHÔNG
   còn dòng JS nào ghi biến theme lúc chạy — "một người ghi duy nhất" trở
   thành đúng theo cấu trúc chứ không phải theo giao ước. Trước đây
   `use-wall-mode` ghi `--accent` bằng `style.setProperty`, và hex màu phải
   ĐỒNG BỘ TAY giữa `screens.config.ts` với `.shell` trong `tokens.css`.

   ⚠ BA TẦNG CÁCH LY BẢN ĐỒ — đọc kỹ trước khi sửa (bất biến §1):

   `:root` giữ bảng màu CŨ, nên ở tầng `:root` thì `--accent-power` là
   #00E5FF (lam bản đồ) chứ KHÔNG phải #FFB020 (hổ phách VSS). Khối theme
   dưới đây nằm ở `:root[data-screen]`, tức cũng ở tầng `:root`. Viết
   `--accent: var(--accent-power)` ở đây sẽ ra MÀU BẢN ĐỒ — đúng cú pháp,
   sai màu, và không báo lỗi ở đâu cả.

   Nên bảng màu VSS được khai một lần dưới TÊN RIÊNG `--vss-*` ngay bên
   dưới. Tên này không trùng bất kỳ token nào cảnh 3D đọc lúc mount, nên
   thêm vào `:root` không phá tầng cách ly. Cả `.shell` (tokens.css) lẫn
   khối theme đều trỏ về đây ⇒ mỗi màu CHỈ CÒN MỘT chỗ viết hex.

   ⚠ Theme phải ở tầng `:root`, KHÔNG phải `.shell`: Radix `Dialog` và
   `Tooltip` portal ra thẳng `document.body`, nằm NGOÀI `.shell`. Hạ khối
   này xuống `.shell` là mọi hộp thoại rơi về accent mặc định.

   ⛔ KHÔNG khai `--accent-soft` ở đây. `tokens.css` đã tính nó bằng
   color-mix từ `--accent`; ghi đè tay chính là lỗi "token giả luôn ra lam".
   ══════════════════════════════════════════════════════════════════════ */

:root {
  /* Bảng màu VSS — NGUỒN DUY NHẤT của hex phân hệ.
     `.shell` trong tokens.css trỏ về đây; khối theme bên dưới cũng vậy. */
  --vss-power: #FFB020;      /* Điện năng — hổ phách */
  --vss-water: #35B0F2;      /* Nước cấp — lam; cũng là màu tương tác chung */
  /* Camera & AI — TÍM, cùng màu lớp camera trên Sơ đồ KCN (`--map-ac-vision`).
     20/10 (lệnh user): bỏ hồng #FF5C7A của bản mẫu VSS — nó gần như trùng
     `--st-crit`, cột camera đứng yên mà nhìn như đang có sự cố. Màu phân hệ
     KHÔNG được trùng họ màu trạng thái (rule 06 §2). */
  --vss-vision: #A78BFA;
  --vss-alarm: #FF3B4E;      /* Cảnh báo */
  --vss-iot: #8FA3B8;        /* Hạ tầng IoT — xám xanh; cũng là đường phụ trong chart */
  /* Nước thải — teal. Trước 20/10 là #22C48E, trùng từng chữ số `--st-good`:
     trạm vượt QCVN mà khung, tab vẫn xanh "ổn". */
  --vss-effluent: #2DD4BF;
  --vss-env: #A98BF5;        /* Quan trắc môi trường — tím nhạt */
  --vss-light: #EDE05A;      /* Chiếu sáng — vàng chanh */
  --vss-command: #35B0F2;    /* Tổng quan */

  /* Accent phụ + dải chart mặc định, để màn chưa khai theme vẫn có đủ biến
     (thiếu khối theme thì rơi về đây, không vỡ giao diện). */
  --accent-2: var(--vss-iot);
  --series-1: var(--accent);
  --series-2: var(--vss-iot);
  --series-3: var(--accent-2);
  --series-4: var(--vss-alarm);
}

/* ── Từng màn ──────────────────────────────────────────────────────────
   Quy ước dải chart: 1 = accent của màn · 2 = xám xanh (đường phụ/tham
   chiếu) · 3 = accent phụ · 4 = đỏ cảnh báo. Chart nhận màu qua prop
   `color`, nên nơi gọi truyền `var(--series-N)` là ăn theo màn ngay. */

:root[data-screen='overview'] {
  --accent: var(--vss-command);
  --accent-2: var(--vss-power);
  --series-3: var(--vss-power);
}

/* ⛔ BẢN ĐỒ — accent ĐÓNG BĂNG ở #00E5FF: cảnh 3D đọc token lúc mount, đổi
   màu ở đây là đổi màu cảnh. Đây chính là `--map-ac-command`. */
:root[data-screen='map'] {
  --accent: #00E5FF;
  --accent-2: var(--vss-water);
  --series-3: var(--vss-water);
}

:root[data-screen='electricity'] {
  --accent: var(--vss-power);
  --accent-2: var(--vss-water);
  --series-3: var(--vss-water);
}

/* Năng lượng: khung theo màu Điện (phân hệ chính), accent phụ lam của Nước. */
:root[data-screen='energy'] {
  --accent: var(--vss-power);
  --accent-2: var(--vss-water);
  --series-3: var(--vss-water);
}

:root[data-screen='water'] {
  --accent: var(--vss-water);
  --accent-2: var(--vss-effluent);
  --series-3: var(--vss-effluent);
}

:root[data-screen='wastewater'] {
  --accent: var(--vss-effluent);
  --accent-2: var(--vss-water);
  --series-3: var(--vss-water);
}

/* Quan trắc môi trường (01/10): tím nhạt `--vss-env` — màu phân hệ có sẵn trong bảng VSS.
   Cột VN_AQI tô theo THANG MÀU CHUẨN của QĐ 1459, không theo accent: màu thang là thứ người
   đọc đã quen, accent chỉ lo khung, tab, đường phụ. */
:root[data-screen='wwtp'] {
  --accent: var(--vss-effluent);
  --accent-2: var(--vss-water);
  --series-3: var(--vss-water);
}

:root[data-screen='environment'] {
  --accent: var(--vss-env);
  --accent-2: var(--vss-water);
  --series-3: var(--vss-water);
}

:root[data-screen='lighting'] {
  --accent: var(--vss-light);
  --accent-2: var(--vss-power);
  --series-3: var(--vss-power);
}

:root[data-screen='camera'] {
  --accent: var(--vss-vision);
  --accent-2: var(--vss-water);
  --series-3: var(--vss-water);
}

/* ⛔ CẢNH BÁO & SỰ CỐ — accent CỐ Ý KHÔNG phải đỏ `--vss-alarm`.
   `--vss-alarm` (#FF3B4E) trùng đúng hex với `--st-crit`. Đem nó làm accent thì khung panel,
   nút đang chọn và nền dòng đang chọn đều đỏ — tức đỏ như chính dòng NGHIÊM TRỌNG — và trên
   màn có nhiều dòng nghiêm trọng nhất app, dòng nghiêm trọng lại là thứ không còn nổi lên.
   Đây đúng là bài học 20/09 ở màn Điện năng (`--st-warn` từng trùng `--vss-power`): màu cảnh
   báo trùng màu phân hệ thì cảnh báo mất khả năng báo động.
   Lam `--vss-command` là màu tương tác chung, cùng họ với Tổng quan — hai màn "toàn khu".
   Màn yên thì trông yên; chỗ nào đỏ là chỗ đó có chuyện. */
:root[data-screen='alarms'] {
  --accent: var(--vss-command);
  --accent-2: var(--vss-iot);
  --series-3: var(--vss-iot);
}
/**
 * Tactical panel — the single container primitive every screen is built from.
 *
 *   .tp              the panel box
 *   .tp__rail        vạch 2px mép trên — TRUNG TÍNH, không mang màu phân hệ
 *   .tp__head        title row: label left, tools right
 *   .tp__body        content; the only thing allowed to scroll
 *   .tp--bracket     adds HUD corner brackets — reserved for a screen's hero panel
 *
 * A panel is a *frame*, not a card: no drop shadow halo, no rounded-pill feel.
 * It reads as an instrument cutout in a console, which is why the border is a
 * hairline and the depth comes from the inset body rather than an outer glow.
 */

.tp {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  box-sizing: border-box;
  /* Hệ VSS phân tách bằng TẦNG MÀU và BÓNG, không bằng đường kẻ. `--sh-hair`
     là "viền ảo" vẽ bằng inset shadow: cho cảm giác mép mà không tốn 1px layout
     và không cộng dồn thành lưới xám khi nhiều panel nằm cạnh nhau. */
  /* ⚠ 17/09 — ĐẢO LẠI QUYẾT ĐỊNH 16/09.
     Trước đó nền panel và viền tóc được phủ màu phân hệ của tab, với lý do
     "mỗi tab có sắc riêng". Hệ quả trên màn thật: 8–10 panel cùng một màu làm
     CẢ MÀN bị nhuộm đơn sắc — Nước thải xanh lá toàn bộ, Chiếu sáng vàng toàn
     bộ. Đối chiếu bản mẫu (`tham-chieu-vybev/ioc.css`): `.panel` của họ TRUNG
     TÍNH hoàn toàn, màu chỉ đến từ dải KPI và nội dung bên trong.
     Màu của panel rút về đúng `.tp__key` (ô 3×12px trước tiêu đề) — tương
     đương dấu mono màu trong `.ph` của mẫu.
     ⚠ 20/09 — ô khoá đó nay CŨNG bỏ (xem chú thích ở chỗ `.tp__title`). Khung
     panel trung tính hoàn toàn; màu phân hệ chỉ còn đến từ dải KPI và nội
     dung bên trong, đúng như bản mẫu. */
  background: var(--grad-panel);
  border-radius: var(--r-panel);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-sink);
  overflow: hidden;
  /* Panels sit on a blurred backdrop so the ambient grid reads *through* them
     without the text losing contrast. */
  backdrop-filter: blur(var(--blur-glass, 10px));
  -webkit-backdrop-filter: blur(var(--blur-glass, 10px));
}

/* Vạch đỉnh khung — TRUNG TÍNH từ 17/09 (xem chú thích ở `.tp`). Giữ lại để
   mép trên panel còn một nét sáng bắt đèn, nhưng không còn mang màu phân hệ
   và không còn quầng `--glow-accent`. */
.tp__rail {
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--rail-h);
  background: linear-gradient(90deg, var(--line-mid) 0%, transparent 70%);
  pointer-events: none;
  z-index: 2;
}

/* ── Head ─────────────────────────────────────────────────────────────── */

.tp__head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-width: 0;
  height: var(--h-panel-head);
  padding: 0 var(--sp-3);
  /* Đầu khung sáng hơn thân một nấc: đây là chỗ mắt bắt đầu đọc mỗi khung.
     17/09: bỏ lớp phủ màu phân hệ 22%→5% — mảng lớn nhất gây đơn sắc. */
  background: var(--grad-head);
  /* Bóng nhoè thay `border-bottom`: mép đầu panel vẫn tách khỏi thân mà không
     có đường kẻ xám cắt ngang — đúng nguyên tắc "không viền" của hệ VSS. */
  box-shadow: var(--sh-head);
}

.tp__title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  font-size: var(--fs-label);
  /* 800 là weight KHÔNG có file (Barlow Condensed nạp 600/700) — trình duyệt
     bôi đậm giả, chữ nhoè trên wall. */
  font-weight: var(--fw-title);
  font-family: var(--font-cond);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-hi);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ⚠ 20/09 — Ô KHOÁ MÀU TRƯỚC TIÊU ĐỀ (`.tp__key`, 3×12px) ĐÃ BỎ TOÀN APP theo
   lệnh người dùng. Trước đó nó bị ẩn lẻ tẻ bằng `.tp--nokey` cho vài panel màn
   Điện; nay không panel nào còn vẽ nên cả glyph lẫn modifier đều gỡ khỏi DOM
   (`Panel.tsx`) chứ không ẩn bằng CSS — một `<span>` chết nhân với mười mấy
   panel mỗi màn là DOM thừa vô nghĩa trên máy chạy 24/7.
   Prop `accent` VẪN có tác dụng: `--rail` còn nuôi khung góc HUD (`.tp--bracket`)
   ở cuối file. */

/* Thanh công cụ ở đầu panel.
 *
 * ⚠ LỖI 17/09 — CHỮ CHỒNG LÊN NHAU. Trước đây khối này `flex: 0 0 auto` (không
 * co) nhưng CON của nó lại không khoá `flex`, nên khi tổng bề ngang vượt đầu
 * panel thì từng nút/ô chọn bị ép NHỎ HƠN nội dung và chữ tràn ra ngoài khung
 * của chính nó — nhìn thành các nhãn đè lên nhau (màn An ninh: dải "1 4 9 16 25"
 * đè lên chip nhóm camera, "TỰ ĐỘNG" đè lên ô chọn chu kỳ). Tên nhóm camera lấy
 * từ DB nên độ dài không đoán trước được, kiểu gì cũng có lúc tràn.
 *
 * Cách chữa: con KHÔNG co (`.tp__tools > *` bên dưới), còn khối thì được phép
 * co và CUỘN NGANG — cùng lối `.tabs` của bản mẫu. Thà phải
 * cuộn còn hơn đọc không ra chữ nào. */
.tp__tools {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: 0 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.tp__tools::-webkit-scrollbar {
  display: none;
}

/* Mỗi control giữ ĐÚNG bề ngang nội dung của nó — đây là phần chữa lỗi chồng
   chữ. Không dùng `flex: none` cho `.tsel` mặc định: ô chọn của cây camera cố ý
   chiếm trọn bề ngang (xem `.tsel`). */
.tp__tools > .tseg,
.tp__tools > .tbtn {
  flex: none;
}

.tp__body {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  padding: var(--pad-panel);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.tp__body--flush {
  padding: 0;
  gap: 0;
}

.tp__body--tight {
  padding: var(--sp-2);
  gap: var(--sp-2);
}

/* ── HUD corner brackets ──────────────────────────────────────────────── */
/* Two elements, four corners: each draws an L on two opposite corners via
   border sides, so the effect costs no extra DOM per corner. */

.tp--bracket::before,
.tp--bracket::after {
  content: '';
  position: absolute;
  width: 14px;
  height: 14px;
  border-color: var(--rail, var(--accent));
  opacity: 0.55;
  pointer-events: none;
  z-index: 3;
}

.tp--bracket::before {
  top: 5px;
  left: 5px;
  border-top: 1px solid;
  border-left: 1px solid;
}

.tp--bracket::after {
  bottom: 5px;
  right: 5px;
  border-bottom: 1px solid;
  border-right: 1px solid;
}

/* `.tp-well` đã xoá 23/08 — không component nào còn gắn class này. */

/* ── Buttons ──────────────────────────────────────────────────────────── */
/* ⛔ `.tbtn` GIỮ NGUYÊN DÁNG CŨ — KHÔNG đổi sang chất liệu VSS.
   Vùng bản đồ dùng class này ở 13 chỗ (nút 2D/3D, thước, tâm, khoá lớp) và
   người dùng ra lệnh bản đồ giữ nguyên không đụng vào (lệnh D1 22/08). Sửa
   `.tbtn` là sửa giao diện bản đồ dù không mở file bản đồ nào — đúng lỗi đã
   mắc ngày 23/08. Nút mang dáng mới thì đặt class khác. */

/* Ô CHỌN trong thanh công cụ panel — bản mẫu S07 dùng một `<select>` chạy trọn
   bề ngang dưới tiêu đề cây camera (lọc khu vực). Cùng chiều cao, cùng bảng màu
   với `.tbtn` để đứng cạnh nhau không lệch. */
.tsel {
  width: 100%;
  min-width: 0;
  height: 22px;
  padding: 0 var(--sp-2);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-sm);
  background: var(--bg-well);
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  font-weight: var(--fw-value);
  outline: none;
  cursor: pointer;
}

/* Ô chọn CO THEO NỘI DUNG — dùng cho ô chọn nằm lẫn giữa các nút trên thanh
   công cụ (chu kỳ slide show). Mặc định `.tsel` chiếm trọn bề ngang, hợp với ô
   chọn đứng một mình dưới tiêu đề (cây camera) nhưng sai khi xếp cùng nút. */
.tsel--auto {
  width: auto;
  flex: none;
}

.tsel:focus-visible {
  border-color: var(--accent);
}

.tbtn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 22px;
  padding: 0 var(--sp-2);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text-mid);
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  font-weight: var(--fw-title);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  /* Nhãn nút KHÔNG xuống dòng: đầu panel cao cố định nên chữ xuống dòng là
     tràn ra ngoài khung nút. */
  white-space: nowrap;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background-color 0.15s;
}

.tbtn:hover {
  color: var(--text-hi);
  border-color: var(--accent);
  background: var(--accent-soft);
}

.tbtn--on {
  color: var(--bg-void);
  background: var(--accent);
  border-color: var(--accent);
}

.tbtn--on:hover {
  color: var(--bg-void);
  background: var(--accent);
}

.tbtn--danger {
  color: var(--st-crit);
  border-color: color-mix(in srgb, var(--st-crit) 40%, transparent);
}

.tbtn--danger:hover {
  color: var(--text-hi);
  background: color-mix(in srgb, var(--st-crit) 22%, transparent);
  border-color: var(--st-crit);
}

/* Segmented control (time-range switches). */
.tseg {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  box-shadow: var(--sh-hair);
  border-radius: var(--r-key);
  background: var(--bg-well);
}

.tseg > .tbtn {
  height: 18px;
  border: none;
  border-radius: 2px;
}

/* ══════════════════════════════════════════════════════════════════════
   Status pill
   ══════════════════════════════════════════════════════════════════════ */

/* ⭐ Sức nặng của badge (chỉnh 25/08 theo bản mẫu `public/mau`).
   Trước đây MỌI badge đều có viền đặc `currentColor` + chấm phát sáng + chữ
   700, nên một bảng 6 dòng "ĐẠT" thành 6 khối sáng ngang nhau và cảnh báo đỏ
   chìm nghỉm. Bản mẫu tô màu y hệt nhưng để rất mờ (`.tag` chỉ có nền 13%,
   không viền, không chấm) — đỏ nổi lên được là nhờ mọi thứ quanh nó nhạt.
   Nay: badge thường mờ đi; CRITICAL giữ nguyên sức nặng cũ. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  padding: var(--pad-chip);
  border-radius: var(--r-pill);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, currentColor 26%, transparent),
    0 0 14px -6px currentColor;
  font-size: var(--fs-cap);
  font-weight: var(--fw-value);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  line-height: 1.5;
  white-space: nowrap;
}

.pill__dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* Nguy hại là thứ DUY NHẤT được giữ viền đặc + quầng sáng + chấm phát sáng. */
.pill--crit {
  box-shadow: inset 0 0 0 1px currentColor, var(--glow-crit);
  font-weight: var(--fw-label);
}

.pill--crit .pill__dot {
  width: 5px;
  height: 5px;
  box-shadow: 0 0 6px currentColor;
}

/* Viền nay vẽ bằng `inset shadow` theo `currentColor` ở `.pill`, nên mỗi biến
   thể chỉ cần khai MÀU CHỮ và NỀN — bỏ được 5 chuỗi rgba viết tay. */
.pill--good { color: var(--st-good); background: var(--st-good-bg); }
.pill--warn { color: var(--st-warn); background: var(--st-warn-bg); }
.pill--crit { color: var(--st-crit); background: var(--st-crit-bg); }
.pill--info { color: var(--st-info); background: var(--st-info-bg); }
.pill--idle { color: var(--st-idle); background: color-mix(in srgb, var(--st-idle) 13%, transparent); }

/* Nhãn MỨC của thang sao (06/10): màu theo mức do Admin chọn (`--lv-<tên>`, gán qua biến `--pill-lv` ở inline style —
   giá trị runtime), sao 1–3 đứng trước chữ. Phiếu 3★ còn chờ được viền đặc + quầng như `.pill--crit`. */
.pill--lv { color: var(--pill-lv); background: color-mix(in srgb, var(--pill-lv) 15%, transparent); }
.pill--lv.pill--hot { box-shadow: inset 0 0 0 1px currentColor, 0 0 14px -5px color-mix(in srgb, var(--pill-lv) 95%, transparent); font-weight: var(--fw-label); }
.pill__stars { display: inline-flex; gap: 1px; }
.pill__stars svg { width: 0.9em; height: 0.9em; fill: currentColor; }

/* A critical pill on a wall must attract the eye without strobing it. */
.pill--crit.pill--live .pill__dot {
  animation: t-blink 1.4s steps(1, end) infinite;
}

@keyframes t-blink {
  0%, 55% { opacity: 1; }
  56%, 100% { opacity: 0.15; }
}

@keyframes t-breathe {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

@keyframes t-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(300%); }
}

@media (prefers-reduced-motion: reduce) {
  .pill--crit.pill--live .pill__dot { animation: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   .glass-panel — khung kính cho các khối nổi trên nền bản đồ

   Class này được 5 component dùng (KpiCard, AlarmTicker, RealtimeChart,
   MasterOverview, CameraScreen) nhưng TRƯỚC ĐÂY KHÔNG CÓ ĐỊNH NGHĨA ở bất kỳ
   file CSS nào đang nạp — nên chúng render thành chữ trần, không nền không
   viền. Định nghĩa ở đây để trả lại đúng hình dạng thiết kế.

   Khác `.tp`: mỏng hơn, nền trong hơn, dùng cho khối chồng lên bản đồ hoặc
   nằm trong khe hẹp — không phải khung instrument đầy đủ.
   ══════════════════════════════════════════════════════════════════════ */

/*
 * Biến thể PHẲNG: cùng khung, bỏ làm mờ nền.
 *
 * `backdrop-filter` bắt trình duyệt raster riêng phần nền sau mỗi phần tử.
 * Với ô video thì hiệu ứng đó vô nghĩa — thẻ `<video>` phủ kín ô, chẳng có gì
 * để nhìn xuyên qua — nhưng vẫn tốn đúng bằng lúc có tác dụng. Lưới 25 ô là
 * 25 lớp như vậy, đủ để Chromium kêu `tile memory limits exceeded` và BỎ VẼ
 * bớt nội dung (đo thật 26/08: log tràn cảnh báo, nhiều ô đen).
 *
 * Dùng cho mọi ô có nội dung phủ kín: khung video, ảnh chụp, canvas.
 */
.glass-panel--flat {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.glass-panel {
  position: relative;
  box-sizing: border-box;
  background: var(--bg-panel);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  box-shadow: var(--elev-panel);
  /* Nền mờ để lưới nền đọc xuyên qua mà chữ không mất tương phản. */
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}


/* Cột thiết bị tự lo phần cuộn (xem `.bcol` trong list.css) — thân panel
   thôi cuộn để không có hai thanh cuộn lồng nhau.
   `container-type: size` để `.bcol` đo được phần đất thật bằng đơn vị `cqh`:
   `100%` KHÔNG quy chiếu được ở đây (thân panel là flex item cao tự động),
   nên phép cắt theo bội số hộp im lặng không chạy và hộp bị cắt nửa vời. */
.tp__body:has(> .bcol) {
  overflow: hidden;
  container-type: size;
}

/* Ô có component ném lỗi — GIỮ CHỖ trong lưới, nói rõ là không có gì để hiện.
   Không thu nhỏ, không ẩn: bố cục người trực ca đã căn không được xê dịch. */
.dgi__failed {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

.dgi__failed-txt {
  font-size: var(--fs-cap);
  letter-spacing: var(--track-label);
  color: var(--text-faint);
}

/* ── Chữ sửa được trong chế độ Sửa nhãn ──────────────────────
   Chuyển từ `draggable-grid.css` sang đây khi engine kéo-thả bị gỡ: `EditableText`
   nằm trong tiêu đề `Panel` và nhãn `MetricTile`, không dính gì tới lưới. */

.ed-hint {
  cursor: text;
  border-bottom: 1px dashed color-mix(in srgb, var(--accent) 65%, transparent);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ed-hint:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.ed-input {
  /* Ô nhập ôm đúng độ dài chữ và nở ra khi gõ (Chromium 123+ — Electron 30
     dùng Chromium 124 nên luôn có; trình duyệt cũ rơi về bề rộng mặc định). */
  field-sizing: content;
  width: auto;
  min-width: 60px;
  max-width: 100%;
  background: var(--bg-well);
  border: 1px solid var(--accent);
  border-radius: var(--r-sm);
  color: var(--text-hi);
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  padding: 0 4px;
  outline: none;
  box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* ══ HEADER CARD KIỂU MỚI — `CardHeader` (tiền tố `chd`) ═══════════════════════════════════
   "Tab nổi trên đường kẻ": header KHÔNG còn là một thanh/khối nền dính mép card.
     · Cả hàng chỉ là MỘT đường kẻ mảnh chạy hết bề ngang.
     · TIÊU ĐỀ là một tab hình thang (hai cạnh bên vát chéo) đứng trên đường kẻ, nền tối sáng hơn
       thân card một nấc, chữ to sáng, quầng chữ nhẹ.
     · CÔNG CỤ (chip/tab/nút) nằm rời bên phải, ngồi trên đường kẻ, không có nền hộp.
   Không viền hộp, không vạch nhấn màu. Dùng khi `<Panel header="card">`. Tông `--chd-tone`
   lấy từ `accent` của Panel, rồi fallback về accent của màn hiện tại. */
.chd {
  --chd-tone: var(--rail, var(--accent));
  --chd-cut: clamp(10px, calc(var(--ui-px) * 14), 20px);
  flex: 0 0 auto;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-3);
  min-width: 0;
  min-height: calc(var(--h-panel-head) + var(--sp-3));
  margin: var(--sp-2) var(--sp-3) 0;
  padding: 0;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.05)) no-repeat left bottom / 100% 1px;
}

.chd__title {
  display: flex;
  align-items: center;
  align-self: stretch;
  gap: var(--sp-3);
  min-width: 0;
  overflow: hidden;
  padding: 0 calc(var(--chd-cut) + var(--sp-3));
  /* ⚠ Màu ĐẶC: `--bg-raised` là trắng 4,5% trong suốt nên pha ra trùng màu card, tab biến mất. */
  background: linear-gradient(180deg, color-mix(in srgb, var(--chd-tone) 24%, var(--bg-panel-hi)), var(--bg-panel-hi));
  clip-path: polygon(var(--chd-cut) 0, calc(100% - var(--chd-cut)) 0, 100% 100%, 0 100%);
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: calc(var(--fs-body-lg) * 1.3);
  font-weight: var(--fw-strong);
  letter-spacing: normal;
  text-overflow: ellipsis;
  text-shadow: 0 0 14px color-mix(in srgb, var(--chd-tone) 40%, transparent);
  text-transform: none;
  white-space: nowrap;
}

/* Công cụ: rời, ngồi trên đường kẻ. Được co + cuộn ngang thay vì đè chữ. */
.chd__tools {
  display: flex;
  align-items: center;
  align-self: center;
  gap: var(--sp-2);
  flex: 0 1 auto;
  min-width: 0;
  overflow-x: auto;
  padding-top: var(--sp-1);
  scrollbar-width: none;
}

.chd__tools::-webkit-scrollbar {
  display: none;
}

/* Mỗi control giữ đúng bề ngang nội dung (chữa lỗi chồng chữ 17/09 ở `.tp__tools`). */
.chd__tools > .tseg,
.chd__tools > .tbtn {
  flex: none;
}

/* Chữ phụ trong công cụ: được co và rút gọn bằng "…" thay vì đè lên chip bên cạnh. */
.chd__tools > .lbl {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Khung ngoài của card dùng header mới: bỏ "viền tóc" (`--sh-hair`, 1px trắng 5%) và vạch sáng mép
   trên (`.tp__rail`) — hai thứ này vẽ thành một hộp viền quanh cả card, chọi với tab tiêu đề.
   Giữ bóng chìm nhẹ để card vẫn tách khỏi nền; viền chỉ còn đến từ chính đường kẻ của header. */
.tp--card {
  box-shadow: var(--sh-sink);
}

.tp--card > .tp__rail {
  display: none;
}

/* Thân card danh sách dùng chung. Màu nền tự đi theo `accent` của Panel/màn. */
.tp__body--card-list {
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3);
  background:
    radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--rail, var(--accent)) 10%, transparent), transparent 42%),
    var(--bg-void);
}

/* Tab có bộ đếm: khe nhãn–pill và đệm phải thu một nấc. Đệm phải vốn chỉ là chỗ trống trước
   mép vát; trước 20/10 chữ tràn lấn vào đó nên "trông vừa", nay nhãn co đúng ranh nội dung thì
   phải trả lại phần chỗ ấy — nếu không cột 308px ở màn 1536px đã rút "Cấp n…". */
.chd__title:has(> .card-title__count) {
  gap: var(--sp-2);
  padding-right: calc(var(--chd-cut) + var(--sp-1));
}

/* Hết chỗ thì nhãn co + rút "…", bộ đếm KHÔNG co (xem `CardTitle.tsx`). */
.card-title__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-title__count {
  flex: none;
  padding: 2px var(--sp-2);
  border: 1px solid color-mix(in srgb, currentColor 40%, transparent);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, currentColor 14%, transparent);
  color: var(--st-good);
  font-family: var(--font-num);
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-value);
  letter-spacing: normal;
  text-transform: none;
  white-space: nowrap;
}

.card-title__count small {
  margin-left: var(--sp-1);
  color: var(--text-lo);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
}

.card-title__count--warn {
  color: var(--st-warn);
}

.card-title__count--crit {
  color: var(--st-crit);
}

.card-title__count--neutral {
  color: var(--text-hi);
}

/**
 * Metric primitives — how a number is presented on the wall.
 *
 * RULE: no naked numbers. Every metric carries at least one of
 *   · a threshold meter (where the value sits inside its safe band)
 *   · a delta vs the previous period
 *   · a sparkline
 * A number without context cannot be acted on from across the room.
 *
 *   .mt            metric tile (label / value / context, stacked)
 *   .mt--hero      the one loudest metric on a screen
 *   .mt__val       mono tabular value
 *   .mt__unit      unit suffix, optically aligned to the value baseline
 *   .mt-meter      horizontal threshold meter
 *   .kv            dense key/value row (used inside detail panels)
 */

.mt {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Centred, not top-aligned: a tile stretched by its grid row would otherwise
     hug the top edge and leave the dead space this redesign exists to remove. */
  justify-content: center;
  gap: 4px;
  min-width: 0;
  padding: var(--sp-3);
  /* Mặt thẻ TRUNG TÍNH (mẫu S07 `.kpi`): nền chuyển tông xám xanh + viền tóc
     TRẮNG. Mặt thẻ KHÔNG mang màu phân hệ: màu chỉ ở quầng góc (`::after`),
     vạch mép trái (`::before`) và hình/số bên trong.
     ⚠ 16/09 từng làm ngược lại: phủ màng accent 16% lên cả mặt thẻ và tô viền
     tóc theo accent. Đảo lại theo yêu cầu "màu giống file HTML" — mẫu để mặt
     thẻ trung tính nên con số là thứ sáng nhất trong ô, chứ không phải nền. */
  background: var(--grad-kpi);
  border: none;
  border-radius: var(--r-panel);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-sink);
  overflow: hidden;
}

/* Left edge carries the metric's status colour — readable even when the
   number itself is out of focus at distance. */
.mt::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--mt-accent, var(--accent));
  opacity: 0.85;
}

.mt--plain::before {
  display: none;
}

.mt__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  min-width: 0;
}

.mt__label {
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-lo);
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Nhãn là KÝ HIỆU khoa học (pH, Fe, cosφ) — giữ nguyên hoa/thường, cùng luật với
   `.lbl--symbol` trong tokens.css: viết hoa toàn bộ là sai chuẩn IUPAC/SI. */
.mt--sym .mt__label {
  text-transform: none;
}

/* Value row: number and unit share a baseline, unit never wraps below. */
.mt__row {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
}

.mt__val {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-num-lg);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--text-hi);
  min-width: 0;
}

/* KHÔNG uppercase ô đơn vị: "kWh" ép hoa thành "KWH", "m³/h" thành "M³/H" —
   sai chuẩn SI. Đơn vị hiển thị đúng như chuỗi truyền vào. */
.mt__unit {
  font-size: var(--fs-cap);
  font-weight: 700;
  letter-spacing: var(--track-label);
  color: var(--text-lo);
  flex: 0 0 auto;
}

.mt__foot {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  font-size: var(--fs-cap);
  color: var(--text-lo);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Hero variant ─────────────────────────────────────────────────────── */

.mt--hero .mt__val {
  font-size: var(--fs-hero);
  font-weight: var(--fw-title);
  color: var(--text-hi);
  text-shadow: 0 0 24px color-mix(in srgb, var(--mt-accent, var(--accent)) 45%, transparent);
}

.mt--hero .mt__unit {
  font-size: var(--fs-body);
}

.mt--xl .mt__val {
  font-size: var(--fs-hero-xl);
}

/* Tone the value by status without touching the layout. */
.mt--good .mt__val { color: var(--st-good); }
.mt--warn .mt__val { color: var(--st-warn); }
.mt--crit .mt__val { color: var(--st-crit); }

/* Thẻ KPI mức NGUY HẠI: design tô đỏ CẢ Ô (bản mẫu gọi là `.kpi--red`) chứ
   không chỉ đổi màu con số — một ô đỏ giữa dải xám là thứ đập vào mắt trước
   khi người trực kịp đọc chữ. */
.mt--strip.mt--crit {
  background: linear-gradient(180deg, color-mix(in srgb, var(--st-crit) 13%, transparent), var(--bg-panel-solid) 78%);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--st-crit) 20%, transparent),
    var(--sh-lite), var(--sh-sink), 0 0 30px -16px var(--st-crit);
}

/* Dải KPI Tổng quan (20/10): cùng ô đỏ nhưng KHÔNG quầng sáng ra ngoài (taste §9.A).
   Sáu ô kia nay nền trung tính nên chỉ riêng lớp đỏ đã đủ nổi. */
.mt--tally.mt--crit {
  background: linear-gradient(180deg, color-mix(in srgb, var(--st-crit) 16%, transparent), var(--bg-panel-solid) 78%);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--st-crit) 30%, transparent), var(--sh-lite), var(--sh-sink);
}

/* ── Delta ────────────────────────────────────────────────────────────── */

.delta {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex: 0 0 auto;
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-weight: 700;
}

/* Tô theo NGHĨA (tốt/xấu) chứ không theo hướng mũi tên — prop `deltaGood`
   của MetricTile quyết định chiều nào là tin tốt cho từng chỉ số. */
.delta--good { color: var(--st-good); }
.delta--bad { color: var(--st-crit); }
.delta--flat { color: var(--text-lo); }

/* "so với hôm qua": chữ thường, xám — chỉ con số % mang màu tốt/xấu. Không bọc dòng giữa
   chừng: hết chỗ thì cả cụm xuống dòng dưới (dòng chân thẻ tally là flex-wrap). */
.delta__vs {
  font-family: var(--font-ui);
  white-space: nowrap;
  color: var(--text-lo);
}

/* ── Threshold meter ──────────────────────────────────────────────────── */
/* A track with an optional safe band and a marker at the current value —
   answers "is this normal?" faster than reading the digits. */

.mt-meter {
  position: relative;
  height: 4px;
  border-radius: 2px;
  background: var(--bg-hover);
  overflow: hidden;
  flex: 0 0 auto;
}

.mt-meter__band {
  position: absolute;
  top: 0;
  bottom: 0;
  background: color-mix(in srgb, var(--st-good) 20%, transparent);
}

.mt-meter__fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  border-radius: 2px;
  background: var(--mt-accent, var(--accent));
  box-shadow: 0 0 8px var(--mt-accent, var(--accent));
  transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Vạch mốc — khai SAU `__fill` để nằm TRÊN nó. `__band` bị phần lấp che ngay khi
   số đo vượt mốc, tức đúng lúc câu hỏi "đang trên hay dưới ngưỡng" cần trả lời;
   vạch này là thứ còn nhìn thấy. Màu chữ sáng để nổi trên CẢ phần lấp lẫn phần
   rãnh tối. Cùng ý với `.wwb__tick` của thanh bullet màn Nước thải. */
.mt-meter__mark {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--text-hi);
}

/* ĐÃ XOÁ 22/08: .mt-meter--zones/__seg/__pin — chỉ ZoneMeter của khuôn KPI cũ dùng, khuôn đó đã bỏ. */

/* ĐÃ XOÁ 22/08: .kpi2 / .kpi-cell* / .kpi-ring / .kpi-leg* — khuôn KPI riêng
   của Dashboard. Dashboard giờ dùng chung MetricTile size="strip" như mọi màn. */

/* ══════════════════════════════════════════════════════════════════════
   Key/value rows
   ══════════════════════════════════════════════════════════════════════ */

.kv {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-width: 0;
  padding: 5px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: var(--fs-body);
}

.kv:last-child {
  border-bottom: none;
}

.kv__k {
  color: var(--text-lo);
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.kv__v {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--text-hi);
  flex: 0 0 auto;
}

/* ══════════════════════════════════════════════════════════════════════
   Grids used by screens to place metric tiles
   ══════════════════════════════════════════════════════════════════════ */

/* minmax(0,1fr) everywhere: `1fr` alone lets a wide child blow out the row. */
.g2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gutter); min-height: 0; }
.g3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gutter); min-height: 0; }
.g4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--gutter); min-height: 0; }

/* DẢI KPI — mọi ô phải NGANG HÀNG (18/08).
   Trước đây mỗi ô tự căn: ô thì `center`, ô thì `space-between`, nên nhãn và
   số mỗi ô nằm một độ cao khác nhau — nhìn ngang thấy so le. Nay ép chung:
   nhãn dính mép trên → số ngay dưới nhãn (cùng một đường) → chân đế rơi đáy.
   CHỈ áp trong dải KPI (`.g4`), `.mt` dùng chỗ khác giữ căn giữa.
   `.g5`/`.g6` xoá 23/08 — không màn nào còn dải 5 hoặc 6 ô. */
.g4 > .mt {
  justify-content: flex-start;
}

.g4 > .mt > .mt__foot {
  margin-top: auto;
}

.stack { display: flex; flex-direction: column; gap: var(--gutter); min-height: 0; min-width: 0; }
.row { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }

/* ĐÃ XOÁ 22/08: toàn bộ .kpibar* — dải KPI tổng hợp đã gỡ khỏi Dashboard. */

/* ── Biến thể GỌN (mt--strip) — dải KPI kiểu "chữ trái · hình phải" ─────
   Chốt 20/08 theo bố cục bản tham khảo: thẻ DẸT, nội dung nằm NGANG thay vì
   xếp chồng dọc. Dải KPI cũ cao 4,5–6 hàng lưới (200–270px) mà phần lớn là
   khoảng trống; bản này cao 3 hàng (~123px) — trả lại 1,5–3 hàng cho nội
   dung thật, và nhìn gọn hơn hẳn.

   Giữ NGUYÊN chất liệu của app (đồng hồ kiểu siêu xe, thùng nước, vòng
   gauge) — chỉ đổi cách sắp, không đổi thứ được vẽ. */

.mt--strip {
  display: grid;
  /* ⭐ Cột hình GIÃN theo thẻ, không cố định 52px (sửa 26/08 cho khớp design).
     Bản mẫu để `<span class="kpi__viz chart">`, mà `.chart{flex:1}` khai sau
     `.kpi__viz{width:50px}` nên ô hình nuốt hết phần còn lại của thẻ — đo trên
     ảnh design: hình chiếm ~42% bề ngang ô KPI. Ép cứng 52px thì trên thẻ rộng
     ~260px hình chỉ còn 20% và trông teo hẳn so với design. */
  /* 08/09: cột hình rộng ĐÚNG BẰNG hình (`auto`), không chiếm 42% thẻ nữa —
     hình (bể, sparkline 74px) đứng gọn bên phải và ăn trọn chiều cao thân thẻ,
     phần chữ được ~70% bề ngang nên dòng chân không còn bị cắt "…". */
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto auto;
  align-items: center;
  column-gap: var(--sp-2);
  row-gap: 1px;
  padding: var(--sp-2) var(--sp-3);
  /* ⭐ 30/09 — HỆ SỐ và BỀ NGANG cột hình khai ở CẤP THẺ (trước đó khai trên
     `.mt__aside`): con số cần biết cột hình rộng bao nhiêu để tự co vừa phần còn
     lại (`--mt-val-fit` ngay dưới). Khai trên ô hình thì ô số — anh em chứ không
     phải con — không đọc được. `100cqh`/`cqw` để nguyên dạng token, quy ra px ở
     chỗ dùng (ô hình, ô số) — cả hai đều đo theo chính thẻ này. */
  --kpi-aside-k: 0.72;
  --mt-aside-w: 0px;
  /* Cỡ số VỪA KHÍT phần chữ: (bề ngang thẻ − cột hình − khe − đơn vị) chia cho
     bề ngang ký tự JetBrains Mono (0,62em — cùng hệ số với DonutChart /
     RadialGauge, luật hiển thị số 19/08); đơn vị tính 0,85em/ký tự vì chữ HOA
     rộng ("MWh") + đậm + giãn chữ. `--mt-chars`/`--mt-unit-chars` do
     `MetricTile` gắn theo chuỗi đang hiện. Lỗi 30/09: cửa sổ cao hơn 16:9 thì số
     to theo chiều cao mà cột chữ không rộng thêm — "3,11" chồng lên "MWh",
     "985,6 kW" bị cắt. */
  --mt-val-fit: calc(
    (100cqw - var(--mt-aside-w) - var(--sp-2) - 6px - var(--mt-unit-chars, 0) * 0.85 * var(--fs-cap)) /
      (0.62 * max(var(--mt-chars, 4), 2))
  );
}

/* Có ô hình ⇒ cột hình rộng theo CHIỀU CAO thẻ (hệ số × cao — xem `.mt__aside`
   bên dưới) nhưng KHÔNG quá 42% bề ngang thẻ: đúng tỉ lệ đo trên ảnh design
   (ghi chú 26/08 ở trên). Không chặn trần thì thẻ càng cao hình càng phình,
   ăn mất chỗ của con số. */
.mt--strip:has(> .mt__aside) {
  --mt-aside-w: clamp(var(--kpi-aside-w), calc(100cqh * var(--kpi-aside-k)), 42cqw);
}

/* Thanh đo trong thẻ dẹt. ⚠ `.mt--strip` khai `grid-column` tường minh cho
   `.mt__top`/`.mt__row`/`.mt__foot`/`.mt__aside` nhưng trước 20/09 KHÔNG có rule
   nào cho `.mt-meter` — prop `meter` chưa từng được dùng ở đâu trong app, nên
   không ai phát hiện. Thiếu dòng này thanh đo tự rơi sang cột hình và vỡ thẻ. */
.mt--strip .mt-meter {
  grid-column: 1;
}

/* Thẻ đang được chọn trong một dải thẻ bấm được (dải KPI lái card khác). Viền
   ảo bằng bóng để không cộng thêm 1px vào hộp và làm lệch cả dải. */
.mt--sel {
  box-shadow: var(--sh-hair), inset 0 0 0 1px var(--mt-accent, var(--accent));
}

/* NHÃN chiếm trọn bề ngang (kể cả phần trên hình): thẻ chỉ ~300px, để hình
   ăn chỗ ngay dòng nhãn thì tên bị cắt còn "LƯU LƯỢNG …" (đo 20/08). Hình
   đứng cạnh SỐ và dòng chân là đủ, dòng nhãn không cần né. */
.mt--strip .mt__top {
  grid-column: 1 / -1;
  min-width: 0;
}

.mt--strip .mt__row {
  grid-column: 1;
  min-width: 0;
  overflow: hidden;
}

.mt--strip .mt__foot {
  grid-column: 1;
  min-width: 0;
  /* Ô lưới KHÔNG tự cắt phần thừa: thiếu dòng này thì đơn vị dài ("thiết bị",
     "camera") chạy tiếp sang cột 2 và nằm ĐÈ dưới hình tròn — lỗi đã thấy
     trên wall 1920 ngày 23/08. */
  overflow: hidden;
}

/* Đơn vị đứng sau số: cho phép co và cắt, số thì không bao giờ cắt. */
.mt--strip .mt__row > .mt__unit {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mt--strip .mt__aside {
  grid-column: 2;
  /* Hình ăn trọn thân thẻ (dòng số + dòng chân) để to, cân đối; chữ không
     bị cắt vì cột hình đã là `auto` (xem `.mt--strip`). */
  grid-row: 2 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}


/* Số nhỏ hơn hero một bậc: thẻ dẹt nên chữ quá lớn sẽ nuốt hết chiều cao,
   nhưng vẫn phải đọc được từ 3–5m nên KHÔNG xuống dưới --fs-num-lg. */
.mt--strip .mt__val {
  font-size: var(--fs-num-lg);
  /* 700 chứ không phải 800: bộ JetBrains Mono đóng gói chỉ có 400/500/700.
     Xin 800 thì trình duyệt tự bôi đậm giả — nét nhoè, đọc từ 3m thì rõ là
     bẩn. Dùng đúng weight có file. */
  font-weight: var(--fw-title);
  color: var(--text-hi);
  line-height: 1.05;
  text-shadow: 0 0 20px color-mix(in srgb, var(--mt-accent, var(--accent)) 40%, transparent);
}

.mt--strip .mt__unit { font-size: var(--fs-cap); }

/* Nhãn và dòng chân ép về một dòng — thẻ dẹt không có chỗ cho chữ tràn. */
.mt--strip .mt__label,
.mt--strip .mt__foot {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mt--strip .mt__foot { margin-top: 0; }

/* `.kpi-strip` xoá 23/08 — dải KPI nay là card rời trên lưới, không còn khối
   grid cứng 6 ô. */

/* Thẻ dẹt: dòng chân chứa badge + chữ — badge giữ nguyên cỡ, chữ co lại. */
.mt--strip .mt__foot {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
}

.mt--strip .mt__foot > .pill { flex: 0 0 auto; }
.mt--strip .mt__foot > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* Hình trong thẻ dẹt phải CO THEO Ô, không giữ px cứng: cùng một thẻ cao
   69px trên khung 720 và ~123px trên wall 1080 — icon 64px cứng thì tràn mép
   (đo 20/08).
   SVG neo TUYỆT ĐỐI trong khung 76px: nhờ vậy nó không tự đẩy chiều cao hàng
   (đặt height:100% cho svg là vòng lặp — hàng cao theo icon, icon cao theo
   hàng), mà chỉ co vào phần chỗ còn lại. */
.mt--strip .mt__aside {
  position: relative;
  /* Bề ngang do CỘT LƯỚI quyết (xem `.mt--strip`), không ép ở đây nữa —
     `--kpi-aside-w` chỉ còn là sàn tối thiểu để hình không biến mất khi thẻ
     bị bóp rất hẹp. */
  /* 08/09 — "cân đối theo chiều cao card": bề ngang ô hình = hệ số × CHIỀU CAO
     THẺ (`cqh` = chiều cao HỘP NỘI DUNG của thẻ, tức đã trừ padding; thẻ là
     container — xem `:has` bên dưới). Thẻ 114px → nội dung 98px → ô hình
     ~70px = đúng chiều cao thân thẻ (đo 08/09); thẻ cao lên là hình to lên.
     Không dùng
     `aspect-ratio`/`width:auto`: track `auto` của lưới chốt bề ngang TRƯỚC khi
     chiều cao 100% có giá trị nên luôn ra sàn 52px (đo 08/09).
     Bể/vòng: 0.72 (vuông); sparkline: 1.44 (2:1). Hệ số + trần 42% khai ở
     cấp thẻ từ 30/09 (`.mt--strip`, `--mt-aside-w`). */
  width: var(--mt-aside-w);
  min-height: 0;
  /* Lưới đặt align-items:center nên ô con KHÔNG tự giãn — phải nói rõ, không
     thì khung neo cao 0 và icon (đang position:absolute) biến mất. */
  align-self: stretch;
}

.mt--strip:has(> .mt__aside--spark) {
  --kpi-aside-k: 1.44;
}

/* Đồng hồ NỬA VÒNG có tỉ lệ 1,85:1 y như sparkline, không vuông như bể chứa /
   vòng tròn kín — dùng chung hệ số để 6 ô của một dải KPI cùng một cỡ hình.
   Bản mẫu S07 để cả đường lẫn đồng hồ trong CÙNG một hộp (`.kpi__viz.chart`),
   nên hệ số 0,72 (dành cho hình vuông) làm đồng hồ bé hơn đường hẳn một bậc
   và bị letterbox trên/dưới — sửa 18/09. */
.mt--strip:has(> .mt__aside > .ringgauge--semi) {
  --kpi-aside-k: 1.44;
}

/* Lưới chấm "n trên tổng m" — cùng hệ số 1,44 với đường và đồng hồ để sáu ô của
   một dải KPI có ô hình BẰNG NHAU (lý do ở khối trên). Lưới vuông nên
   `preserveAspectRatio` tự căn giữa trong hộp 1,44:1; để hệ số 0,72 thì hộp co
   còn một nửa và chấm teo xuống ~9px, từ 3m không đếm được nữa. */
.mt--strip:has(> .mt__aside > .countdots) {
  --kpi-aside-k: 1.44;
}

/* Thẻ dẹt và thẻ hero là CONTAINER đo chiều cao (`cqh`) — 14/09 mở rộng từ
   "chỉ thẻ có hình": số cũng phải to theo thẻ, không riêng gì hình. Cả hai
   loại thẻ luôn được ô lưới / thân panel cấp chiều cao (`height:100%`,
   `flex:1`), nên `container-type: size` không làm thẻ sập về 0. */
.mt--strip,
.mt--hero {
  container-type: size;
}

/* Số CO THEO THẺ (14/09): sàn là cỡ cũ (đọc được từ 3–5m), trần là bậc hero
   kế tiếp — card kéo cao gấp đôi thì số to lên chứ không để trống nửa thẻ
   (đo 14/09: thẻ KPI không hình chỉ đầy 40%). Tỉ lệ theo cqh = chiều cao HỘP
   NỘI DUNG thẻ: thẻ 3 hàng lưới (~98px nội dung) → 34cqh ≈ 33px ≈ sàn. */
.mt--strip .mt__val {
  /* 30/09: thêm trần "vừa khít bề ngang" (`--mt-val-fit`). Sàn hạ về `--fs-sub`
     CHỈ để lưới an toàn co được khi số quá dài — bình thường số vẫn ≥
     `--fs-num-lg` nhờ leo thang đơn vị (kWh→MWh…, luật 19/08). */
  font-size: clamp(var(--fs-sub), min(34cqh, var(--mt-val-fit)), var(--fs-hero));
}

.mt--hero .mt__val {
  font-size: clamp(var(--fs-hero), 42cqh, var(--fs-hero-xl));
}

/* ── Thẻ mà GIÁ TRỊ LÀ CHỮ, không phải số (`mt--word`) ─────────────────
   "THỦ CÔNG", "SẴN SÀNG", "ĐANG KÍCH HOẠT" (dải KPI màn Chiếu sáng) đi qua
   đúng ô `mt__val` dựng cho con số: mono tabular, cỡ số cấp mục, dòng 1,05,
   tracking âm. Cả bốn thứ đúng với chữ số và sai với chữ Việt:
   · hộp dòng 1,05 hẹp hơn vùng chữ của font, nên `overflow: hidden` của
     `.mt__row` XÉN MẤT dấu trên chữ HOA (Ủ · Ô · Ẵ · À — thấy 21/09);
   · mono kéo một từ tám chữ cái rộng gấp rưỡi mức cần, đẩy chữ chạm mép thẻ.
   Chữ thì dùng font UI, dòng cao bằng nhãn HOA (`--lh-label` sinh ra đúng vì
   lý do dấu tiếng Việt) và nhỏ hơn số một bậc. */
.mt--word .mt__val {
  font-family: var(--font-ui);
  letter-spacing: 0;
  line-height: var(--lh-label);
}

.mt--strip.mt--word .mt__val {
  /* Trần `--fs-num` chứ không `--fs-num-lg`: nhãn dài nhất của dải ("ĐANG KÍCH
     HOẠT") phải nằm trọn cột chữ ~200px thay vì rơi vào "…". Sàn `--fs-body-lg`
     vẫn trên ngưỡng đọc 3–5m vì đây là chữ HOA, không phải chữ chạy văn. */
  font-size: clamp(var(--fs-body-lg), 26cqh, var(--fs-num));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mt--strip .mt__aside svg,
.mt--tally .mt__aside svg {
  position: absolute;
  inset: 0;
  /* width+height 100% chứ KHÔNG max-*: đặt hai max riêng lẻ làm hộp svg thành
     64×34 → hình méo. Icon đều có viewBox nên preserveAspectRatio mặc định
     (meet) tự căn giữa và giữ tỉ lệ trong hộp; sparkline cố ý dùng none để
     kéo giãn nên không ảnh hưởng. */
  width: 100%;
  height: 100%;
}

/* Ô ĐỒNG HỒ trong dải KPI dẹt: giữ nguyên đồng hồ kiểu "siêu xe" (user chốt
   giữ), chỉ cho nó CO THEO Ô như các icon khác — svg có viewBox 100×100 nên
   thu nhỏ không méo, không mất chi tiết vành màu. */
.gauge-cell {
  position: relative;
  overflow: hidden;
  /* ⭐ 25/08: ô đồng hồ MƯỢN ĐÚNG VỎ của `.mt--strip`. Trước đây nó tự mang
     nền `--bg-well` + viền `--line-soft` nên trong dải KPI có 4 thẻ nền sáng
     không viền đứng cạnh 2 ô nền tối có viền — dải gãy làm đôi. Bản mẫu giữ
     6 ô KPI cùng một khung xương, không ngoại lệ nào. */
  background: var(--grad-kpi, var(--bg-well));
  border: none;
  border-radius: var(--r-panel);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-sink);
}

.gauge-cell > * {
  /* RadialGauge size="auto" tự đo ô này rồi chọn đường kính — không ép bằng
     CSS nữa vì nhãn đồng hồ nằm NGOÀI svg, ép max-height là nhãn tràn ra. */
  width: 100%;
  height: 100%;
}

/* ══════════════════════════════════════════════════════════════════════
   Chất liệu thẻ KPI theo mẫu VSS (23/08)
   Ba thứ làm nên "cái nhìn" của bản tham khảo, app đang thiếu cả ba:
   nền dốc, vạch neon bo tròn ở mép trái, và quầng sáng mờ góc trên-trái.
   ══════════════════════════════════════════════════════════════════════ */

.mt--strip {
  /* Mặt thẻ TRUNG TÍNH (mẫu S07 `.kpi`): nền chuyển tông xám xanh + viền tóc
     TRẮNG. Mặt thẻ KHÔNG mang màu phân hệ: màu chỉ ở quầng góc (`::after`),
     vạch mép trái (`::before`) và hình/số bên trong.
     ⚠ 16/09 từng làm ngược lại: phủ màng accent 16% lên cả mặt thẻ và tô viền
     tóc theo accent. Đảo lại theo yêu cầu "màu giống file HTML" — mẫu để mặt
     thẻ trung tính nên con số là thứ sáng nhất trong ô, chứ không phải nền. */
  background: var(--grad-kpi);
  border: none;
  border-radius: var(--r-panel);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-sink);
  transition: background 0.18s, transform 0.18s, box-shadow 0.18s;
}

/* Nhãn thẻ KPI TRUNG TÍNH, như mẫu S07 (`.kpi__t span{color:var(--ink2)}`;
   ở đó chỉ ICON mới ăn màu phân hệ).
   16/09 tô nhãn theo accent để "thay vai vạch mép đã bỏ" — nay vạch mép đã
   dựng lại nên nhãn không còn phải gánh vai đó, và bỏ được một nguồn màu nữa
   khỏi thẻ. */
.mt--strip .mt__label {
  color: var(--text-mid);
}

/* Quầng sáng mờ góc trên-trái, ăn theo màu phân hệ của chính thẻ.
   `.mt--tally` (dải KPI Tổng quan) KHÔNG còn quầng từ 20/10 — xem khối TALLY. */
.mt--strip::after {
  content: '';
  position: absolute;
  left: -34px;
  top: -38px;
  width: 130px;
  height: 96px;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    color-mix(in srgb, var(--mt-accent, var(--accent)) 26%, transparent),
    transparent 70%
  );
  filter: blur(9px);
  opacity: 0.5;
  pointer-events: none;
  z-index: 0;
}

/* ⭐ Quầng của thẻ DẸT trả về ĐÚNG công thức mẫu S07 `.kpi::before` (18/09):
   một khối màu ĐẶC nhoè rất rộng, không phải radial-gradient nhoè hẹp.
   Bản cũ (gradient 26% · blur 9px · opacity .5) cho ra một ĐỐM màu cứng ở góc
   — nhìn thấy rõ ranh giới; mẫu cho một vệt sáng loang hết 1/3 thẻ. Cùng độ
   sáng trung bình, khác hẳn về chất.
   `.mt--tally` (dải KPI Tổng quan) KHÔNG đổi: quầng ở đó đang gánh vai của
   vạch mép đã bỏ, đã cân riêng 16/09. */
.mt--strip::after {
  background: var(--mt-accent, var(--accent));
  filter: blur(30px);
  opacity: 0.17;
}

/* Vạch mép trái của thẻ DẸT: BỎ (20/09, yêu cầu người dùng). Dựng lại 16/09
   rồi gỡ hẳn — lý do là số lượng: sáu thẻ nằm sát nhau, mỗi thẻ một vạch dọc ở
   mép, mắt đọc cả dải ra thành một hàng rào sọc chứ không ra sáu dấu hiệu. Một
   vạch giữa nền trống thì chỉ chỗ được; sáu vạch cách đều thì thành hoa văn.
   Màu phân hệ nay nói bằng ĐÚNG MỘT thứ trên mặt thẻ — quầng sáng góc trên-trái
   `::after` ngay trên đây — thay vì hai thứ chia nhau một vai.
   `.mt--tally` (dải KPI Tổng quan) vốn đã tắt vạch từ 16/09 vì cùng lý do, nên
   hai màn nay gộp chung một luật.
   ⚠ Base `.mt::before` GIỮ NGUYÊN: tile trong panel đứng lẻ hoặc thành cột dọc,
   ở đó vạch vẫn là dấu hiệu chứ không thành sọc. */
.mt--strip::before,
.mt--tally::before {
  display: none;
}

.mt--strip:hover {
  transform: translateY(-1px);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-sink), var(--sh-halo);
}

/* Nội dung phải nổi lên trên quầng sáng. */
.mt--strip .mt__top,
.mt--strip .mt__row,
.mt--strip .mt__foot,
.mt--strip .mt__aside,
.mt--tally .mt__top,
.mt--tally .mt__row,
.mt--tally .mt__foot,
.mt--tally .mt__aside {
  position: relative;
  z-index: 1;
}

/* Vòng gauge: vẽ dần lúc hiện ra rồi trượt mềm mỗi khi số liệu đổi.
   Không quầng sáng (20/10): vòng phát sáng thành thứ to nhất dải KPI dù chỉ là hình phụ. */
.ringgauge__arc {
  transition: stroke-dashoffset 0.9s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Người khai báo hạn chế chuyển động thì tắt sạch — kể cả sóng nước SMIL. */
@media (prefers-reduced-motion: reduce) {
  .ringgauge__arc {
    transition: none;
  }

  .mt--strip {
    transition: none;
  }
}

/* ── Thẻ "NHÃN + HÌNH" (`figure` — 08/09, thẻ cosφ màn Điện) ──────────
   Ở thẻ dẹt: nhãn đứng cột trái, HÌNH đứng cột phải và ăn TRỌN chiều cao thẻ
   (cả hàng nhãn) — đặt hình dưới nhãn thì chỉ còn ~70px, bằng đúng icon cũ,
   không to lên được. Hình `size: auto` tự co theo ô này. */
.mt__figure {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.mt--strip.mt--figure {
  /* 14/09 (lệnh user): ĐỒNG HỒ Ở GIỮA, NHÃN Ở DƯỚI — một cột. Bố cục hai cột
     cũ (nhãn góc trên trái, đồng hồ bên phải) để nửa trái thẻ trống. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  row-gap: 2px;
}

.mt--strip.mt--figure .mt__top {
  grid-column: 1;
  grid-row: 2;
  align-self: end;
  justify-content: center;
  width: 100%;
}

.mt--strip.mt--figure .mt__top .mt__label {
  text-align: center;
}

.mt--strip.mt--figure .mt__figure {
  grid-column: 1;
  grid-row: 1;
  align-self: stretch;
  width: 100%;
}

/* ══ TALLY — dải KPI Tổng quan ═════════════════════════════════════════
   14/09: số trên cùng · nhãn dưới số · dòng ngữ cảnh (DOM vẫn nhãn-trước-số
   nên đặt hàng tường minh).
   16/09 theo bản S01 vybev (user duyệt ảnh thử): số TRẮNG, nhỏ hơn một bậc;
   mỗi thẻ một HÌNH RIÊNG (`aside`) neo phải thay cho đường xu hướng giống nhau.
   20/10 (lệnh user, đại tu UI) — màu phân hệ trên mặt thẻ chỉ còn MỘT dấu: chữ
   nhãn (+ hình bên phải). Bỏ màng màu ở đỉnh, viền tóc cùng màu, quầng góc:
   bảy thẻ mỗi thẻ ba lớp màu thành một dải cầu vồng, ô đỏ nguy hại chìm
   giữa đám màu. Nền trung tính thì màu trên dải lại mang nghĩa trạng thái. */
.mt--tally {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto auto;
  align-content: center;
  column-gap: var(--sp-2);
  row-gap: 4px;
  padding: var(--sp-2) var(--sp-3);
  background: var(--grad-kpi, var(--bg-well));
  border: none;
  border-radius: var(--r-panel);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-sink);
  container-type: size;
}


/* Thẻ không có hình (Cảnh báo; thẻ chưa có số để vẽ): chữ ăn trọn bề ngang. */
.mt--tally.mt--noviz {
  grid-template-columns: minmax(0, 1fr);
}

.mt--tally .mt__row { grid-column: 1; grid-row: 1; align-items: baseline; gap: 8px; min-width: 0; overflow: hidden; }
.mt--tally .mt__top { grid-column: 1; grid-row: 2; min-width: 0; }
.mt--tally .mt__foot { grid-column: 1; grid-row: 3; }

/* Hình riêng: cao trọn ba hàng chữ, bề ngang = nửa chiều cao nội dung thẻ
   (thẻ 4 hàng lưới ~141px → ~70px). Hẹp hơn thì số "2.348,4 m³" bị cắt. */
.mt--tally .mt__aside {
  grid-column: 2;
  grid-row: 1 / -1;
  align-self: stretch;
  width: calc(100cqh * 0.5);
  min-width: var(--kpi-aside-w);
}

/* Số: trắng, weight 500, KHÔNG quầng — màu phân hệ đã ở vạch mép và hình.
   Trần --fs-num-lg thay vì --fs-hero như 14/09 (user 16/09: "số nhỏ hơn 1 chút"). */
.mt--tally .mt__val {
  font-size: clamp(var(--fs-num), 22cqh, var(--fs-num-lg));
  font-weight: var(--fw-value);
  color: var(--text-hi);
  letter-spacing: -0.02em;
  line-height: 1;
  text-shadow: none;
}

.mt--tally .mt__unit {
  font-size: clamp(var(--fs-body-lg), 11cqh, var(--fs-sub));
  font-weight: var(--fw-body);
  color: var(--text-lo);
  letter-spacing: 0;
}

/* Đơn vị ("camera", "cảnh báo") giữ một cụm, không cắt; số thì không bao giờ cắt. */
.mt--tally .mt__row > .mt__unit {
  white-space: nowrap;
}

/* Nhãn: dưới số, đậm, ĂN MÀU PHÂN HỆ (16/09 — thay vai của vạch mép đã bỏ;
   bản mẫu vybev cũng để màu phân hệ ở hàng nhãn, qua icon). Dài thì XUỐNG DÒNG,
   không cắt "…" — phải nói rõ vì `.mt__label` gốc đang nowrap + ellipsis. */
.mt--tally .mt__label {
  font-size: var(--fs-label);
  font-weight: 700;
  color: var(--mt-accent, var(--accent));
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: break-word;
}

/* Dòng ngữ cảnh: mono, cỡ thân; badge (nếu có) giữ nguyên cỡ. */
.mt--tally .mt__foot {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-family: var(--font-num);
  font-size: var(--fs-body);
  color: var(--text-lo);
  /* Xuống dòng, không cắt "…" (lệnh 16/09). */
  flex-wrap: wrap;
  row-gap: 2px;
  white-space: normal;
  line-height: 1.3;
}

.mt--tally .mt__foot > .pill { flex: 0 0 auto; }
.mt--tally .mt__foot > span { min-width: 0; }
.mt--tally .delta { font-size: var(--fs-body); }

/* Thẻ dẹt KHÔNG có hình bên phải: một cột, số + đơn vị + dòng chân ăn trọn
   bề ngang (thẻ Chiếu sáng · An ninh & AI · Cảnh báo sau lệnh 08/09). */
.mt--strip.mt--noviz {
  grid-template-columns: minmax(0, 1fr);
}

/* Chip phân hệ — dải KPI trộn nhiều phân hệ (màn Năng lượng, 20/10). Không vạch màu mép trái. */
/* Hàng chip + nhãn GHIM sát mép trên, cùng một đường ngang cho cả dải — lưới thẻ dẹt tự giãn hàng
   nên để `center` thì thẻ ít dòng có nhãn tụt xuống (20/10). */
.mt--dom.mt--strip .mt__top {
  align-self: start;
}

/* Số + dòng chân căn GIỮA cột chữ (20/10 — lệnh user), nhãn vẫn ghim góc trên trái. */
.mt--dom.mt--strip:not(.mt--figure) .mt__row,
.mt--dom.mt--strip:not(.mt--figure) .mt__foot {
  justify-self: stretch;
  /* `safe`: dòng dài hơn cột thì về căn trái, không tràn ra hai mép. */
  justify-content: safe center;
}

/* Sáu thẻ một dải ⇒ số CÙNG CỠ: cỡ tự co tính theo chuỗi dài nhất có thể ("192,6" + "m³/h"),
   không theo chuỗi đang hiện — nếu không "9" to gấp rưỡi "83,8" và số mỗi thẻ một độ cao (20/10).
   `!important` vì `MetricTile` gắn hai biến này inline theo chuỗi đang hiện. */
.mt--dom.mt--strip {
  --mt-chars: 5 !important;
  --mt-unit-chars: 4 !important;
}

/* Hàng chân CAO CỐ ĐỊNH (đủ 2 dòng) cho cả dải: thẻ có chân 2 dòng không đẩy số lên cao hơn
   thẻ khác — số sáu thẻ cùng một độ cao (20/10). Thẻ hình (cosφ) giữ lưới riêng. */
.mt--dom.mt--strip:not(.mt--figure) {
  grid-template-rows: auto minmax(0, 1fr) 40px;
}

.mt--dom.mt--strip:not(.mt--figure) .mt__foot {
  align-self: start;
}

/* Dòng chân chật (chip trạng thái + chữ) thì XUỐNG DÒNG, không cắt "…" (20/10). */
.mt--dom.mt--strip .mt__foot {
  flex-wrap: wrap;
  row-gap: 2px;
}

.mt--dom.mt--strip .mt__foot .truncate {
  overflow: visible;
  white-space: nowrap;
  text-overflow: clip;
}

.mt--dom-power {
  --mt-dom: var(--accent-power);
}

.mt--dom-water {
  --mt-dom: var(--accent-water);
}

.mt__kicker {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 4px;
  padding: 1px 7px;
  border: 1px solid color-mix(in srgb, var(--mt-dom) 60%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--mt-dom) 16%, var(--bg-void));
  color: var(--mt-dom);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  font-weight: var(--fw-title);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.mt__kicker + .mt__label {
  margin-right: auto;
}

/* Thẻ hình (cosφ): nhãn căn GIỮA dưới đồng hồ như bản đã duyệt 14/09; chip phân hệ neo GÓC TRÊN
   TRÁI cùng chỗ với các thẻ khác trong dải (20/10). */
.mt--strip.mt--figure .mt__corner {
  grid-column: 1;
  grid-row: 1;
  justify-self: start;
  align-self: start;
  z-index: 1;
}
/**
 * Dense data table for wall reading.
 *
 * Sticky header + zebra-free rows (a hairline separator reads cleaner at
 * distance than alternating fills, which shimmer on a projector). Numeric
 * columns are mono/tabular and right-aligned so magnitudes line up vertically
 * — that column edge is what an operator actually scans.
 */

.tt-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
}

.tt {
  width: 100%;
  border-collapse: separate;
  /* Khe dọc 4px giữa các hàng — đúng `.tbl` của bản mẫu (`border-spacing:0 4px`).
     Bo góc "viên rời" ở dưới chỉ có nghĩa khi các hàng thật sự RỜI nhau; để
     spacing 0 thì chúng dính thành một khối và bảng đọc như bảng tính. */
  border-spacing: 0 4px;
  font-size: var(--fs-body);
}

.tt thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  /* 16/09 — hàng tiêu đề ngả theo màu phân hệ của tab (nền 10%, chữ pha 45%):
     bảng không còn là mảng xám giữa các khung đã ăn màu. */
  background:
    linear-gradient(0deg, color-mix(in srgb, var(--accent) 10%, transparent), color-mix(in srgb, var(--accent) 10%, transparent)),
    var(--bg-thead, var(--bg-panel-solid));
  padding: var(--pad-row);
  text-align: left;
  font-size: var(--fs-cap);
  font-weight: var(--fw-title);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--accent) 45%, var(--text-mid));
  white-space: nowrap;
  /* Cột co theo card (bề rộng cột lưu theo TỈ LỆ — DraggableScreen): tiêu đề
     dài hơn cột thì "…" trong ô của nó, không lấn sang ô bên / tràn khỏi card. */
  overflow: hidden;
  text-overflow: ellipsis;
  /* Bóng nhoè thay đường kẻ: hàng tiêu đề dính vẫn tách khỏi thân bảng khi
     cuộn, mà không kẻ một vạch xám ngang màn. */
  box-shadow: var(--sh-head);
}

.tt tbody td {
  padding: var(--pad-row);
  color: var(--text-mid);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 0; /* with table-layout auto this forces ellipsis to engage */
}

.tt tbody tr:hover td {
  background: var(--bg-row-hover);
}

.tt tbody tr {
  background: var(--bg-row);
}

/* Dòng đang chọn — dùng chung một nghĩa với danh sách (`.lrow--sel`). */
.tt tbody tr.is-sel td {
  background: var(--bg-row-sel);
}

/* Bo góc "viên rời" cho cả dòng: chỉ ô đầu và ô cuối bo. */
.tt tbody td:first-child {
  border-top-left-radius: var(--r-row);
  border-bottom-left-radius: var(--r-row);
}

.tt tbody td:last-child {
  border-top-right-radius: var(--r-row);
  border-bottom-right-radius: var(--r-row);
}

/* DÒNG CON trong cây (lộ dưới tủ — 08/09): thụt CẢ VIÊN, không chỉ chữ. Ô đầu
   chừa một dải trong suốt bên trái (border trong suốt + background-clip) nên
   nền viên, bo góc và cả vạch đỏ `sev-*` (inset shadow vẽ trong padding-box)
   đều bắt đầu từ chỗ thụt. Nền của <tr> phải tắt, nếu không nó lộ ra dưới dải
   trong suốt. Độ sâu chỉnh bằng `--tt-leaf-indent` ở nơi dùng. */
.tt .tt__leaf {
  background: transparent;
}

.tt .tt__leaf td {
  background: var(--bg-row);
}

.tt .tt__leaf td:first-child {
  border-left: var(--tt-leaf-indent, 36px) solid transparent;
  background-clip: padding-box;
  /* Bo góc tính từ mép NGOÀI: cộng thêm phần thụt để góc nhìn thấy vẫn tròn đúng --r-row. */
  border-top-left-radius: calc(var(--r-row) + var(--tt-leaf-indent, 36px));
  border-bottom-left-radius: calc(var(--r-row) + var(--tt-leaf-indent, 36px));
}

/* Cháu trong cây (pha A/B/C dưới lộ — 13/09): thụt thêm một bậc. */
.tt .tt__leaf--deep {
  --tt-leaf-indent: 72px;
}

/* Numeric column modifier — apply to both th and td. */
.tt .n {
  text-align: right;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  color: var(--text-hi);
  font-weight: 600;
}

/* Primary column: the row's identity. */
.tt .id {
  color: var(--text-hi);
  font-weight: 700;
}

/* ── Tỉ lệ cột ────────────────────────────────────────────────────────
   Mặc định `max-width: 0` chia đều bề ngang → cột tên bị cắt cụt trong khi
   cột số/badge phình to. Sửa: cột số, cột mã và cột badge CO THEO NỘI DUNG
   (width 1% + bỏ trần max-width), chỗ trống còn lại dồn cho cột chữ (ô td
   thường vẫn giữ max-width:0 để ellipsis). Cột cần nuốt toàn bộ chỗ trống
   trong bảng toàn-cột-số → thêm class `grow`. */

.tt .n,
.tt .id,
.tt td:has(> .pill),
.tt td:has(> input[type='checkbox']) {
  width: 1%;
  max-width: none;
}

/* Cột "nuốt chỗ trống" của BẢNG. ⚠ Tên cũ là `.grow`, trùng với `.grow` của
   `screen-grids.css` (dải ngang của lưới màn, `display:grid`). Hai file cùng nạp
   toàn cục, mà `display` chỉ khai ở file kia nên không rule bảng nào ghi đè được:
   ô `<td>` biến thành grid container, lại KẾ THỪA `--cols` của dải màn bao ngoài
   nên chữ co về gần 0 và mất luôn ellipsis — đúng lỗi cột "Tuyến" chỉ còn "C"/"N"
   ở màn Cấp nước (20/09). Đặt tiền tố `tt__` như `.tt__leaf` để hết đụng. */
.tt .tt__grow {
  width: 100%;
  max-width: 0;
}

/* An unacknowledged critical row gets a tinted fill so it separates from the
   list even when the operator is scanning the number columns. */
.tt tr.sev-crit td { background: color-mix(in srgb, var(--st-crit) 6%, transparent); }
.tt tr.sev-crit:hover td { background: color-mix(in srgb, var(--st-crit) 10%, transparent); }

/* Bảng khai bề ngang cột TƯỜNG MINH bằng `<colgroup>`.
   Không có `table-layout: fixed` thì trình duyệt vẫn tự chia theo nội dung và
   coi width trong colgroup chỉ là gợi ý — đo 23/08: cột "Tên thông báo" nuốt
   697px còn "Hệ thống" bị bóp 78px, cắt mất chữ "An ninh & AI". */
.tt--fixed {
  table-layout: fixed;
}
/**
 * LƯỚI MÀN — khung bố cục cố định, thay cho lưới kéo-thả đọc từ MongoDB.
 *
 *   .scr        gốc một màn (do `ScreenFrame` dựng) — chiếm trọn canvas
 *   .scr--edit  đang ở chế độ Sửa nhãn (chỉ hiện khi bật Develop)
 *   .scr__fab   cụm nút góc dưới-phải
 *
 * Phần dưới là bộ khung chung (`.kpis`, `.pbody`, `.rowA`…) lấy từ bản mẫu
 * VSS; mỗi màn thêm một khối riêng khai số cột/hàng của nó.
 *
 * ══ LUẬT TRÀN — chép từ bản mẫu, không tự nghĩ ═════════════════════════
 *
 *   1. TRANG KHÔNG BAO GIỜ CUỘN. `html, body, #root, .shell` đều
 *      `overflow:hidden` (index.css) — giữ nguyên.
 *   2. MỌI container lưới/flex phải có `min-height:0` đi kèm track
 *      `minmax(0,1fr)`. Thiếu một chỗ là chart trong đó tụt về 0px hoặc
 *      card tràn đè lên hàng dưới. Bản mẫu đặt lưới an toàn `*{min-width:0;
 *      min-height:0}` — ta cũng đặt (ngay dưới đây).
 *   3. Ô panel LUÔN cắt (`.tp{overflow:hidden}` trong panel.css). Muốn cuộn
 *      thì mở tường minh bằng `<Panel scroll>` (`.scroll-y`).
 *   4. Tràn được hấp thụ ở LÁ, đúng một trong: `.tt-wrap` (bảng),
 *      `.list-wrap` (danh sách dòng), `.scroll-y` (thân panel), `.bcol`
 *      (cột thiết bị). KHÔNG lồng hai vùng cuộn vào nhau.
 *   5. Chữ cắt bằng `text-overflow:ellipsis`, KHÔNG xuống dòng.
 *
 * ⚠ ĐÍCH LÀ 1920×1080 (TV 55" FHD của phòng điều hành). Bản mẫu có
 * breakpoint 1680/1500/1280/760 và một chế độ xếp chồng cho màn hẹp; ta CỐ Ý
 * không chép sang. Cửa sổ nhỏ hơn thì bố cục bị cắt — dev trên laptop hãy
 * thu phóng trình duyệt, đừng thêm breakpoint rồi tự hỏi vì sao tường lệch.
 */

/* Lưới an toàn của bản mẫu (ioc.css:38). Không có nó thì mỗi lần quên
   `min-height:0` ở một tầng là một chart tụt 0px — lỗi khó thấy vì chỉ hiện
   khi dữ liệu đủ dài. */
.scr *,
.scr *::before,
.scr *::after {
  min-width: 0;
  min-height: 0;
}

.scr {
  position: relative;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  display: grid;
  /* Mặc định MỘT hàng chiếm hết. Màn có dải trên cùng (KPI, băng trạng thái)
     tự khai lại bằng modifier riêng — xem từng khối ở cuối file. */
  grid-template-rows: minmax(0, 1fr);
  gap: var(--gutter);
}

.scr__fab {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: var(--z-grid-fab);
  display: flex;
  align-items: center;
  gap: 6px;
  opacity: 0.55;
  transition: opacity 0.2s;
  padding: 4px;
  background: color-mix(in srgb, var(--bg-panel) 20%, transparent);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  box-shadow: none;
}

/* ══ KHUNG CHUNG ═══════════════════════════════════════════════════════ */

/* Dải thẻ KPI trên cùng. Số cột khai theo màn bằng `--kpi-cols`. */
.kpis {
  display: grid;
  grid-template-columns: repeat(var(--kpi-cols, 6), minmax(0, 1fr));
  gap: var(--gutter);
  min-height: 0;
}

/* Thân màn dưới dải KPI. */
.pbody {
  display: grid;
  gap: var(--gutter);
  min-height: 0;
}

/* Hai dải ngang: dải chính co giãn + dải dưới cao cố định theo chiều cao màn. */
.pbody--2r {
  grid-template-rows: minmax(0, 1fr) clamp(180px, 24vh, 250px);
}

.pbody--1r {
  grid-template-rows: minmax(0, 1fr);
}

/* Cột dọc trong một dải — các ô chia đều nếu không khai khác. */
.gcol {
  display: grid;
  gap: var(--gutter);
  min-height: 0;
  grid-auto-rows: minmax(0, 1fr);
}

/* Dải ngang chung: số cột khai bằng biến để mỗi màn không phải đẻ class mới. */
.grow {
  display: grid;
  grid-template-columns: var(--cols, minmax(0, 1fr));
  gap: var(--gutter);
  min-height: 0;
}

/* ══ S05 · NƯỚC THẢI ═══════════════════════════════════════════════════
     dải KPI         6 chỉ tiêu có nguồn realtime
     thân (27/10)    nửa trái: CAMERA cao trọn thân · nửa phải: hai dải
       dải chính     → xu hướng | lưu lượng   (5 phần)
       dải giữa      → 30 ngày | thiết bị     (4 phần)
       dải dưới      → cảnh báo (cả bề ngang) (3 phần)
     (băng phán quyết: NGĂN PHẢI từ 30/09)

   20/09 — băng phán quyết từng chiếm nguyên hàng đầu (màn DUY NHẤT không có
   dải KPI dưới thanh điều hướng) rồi bị đẩy xuống làm cột trái của dải chính.
   20/09 (chiều) — theo mẫu vybev S04 và lệnh người dùng: băng TRỞ LẠI đầu màn
   nhưng ở dạng DẢI NGANG (`@container` trong `wastewater.css` tự chuyển dạng
   khi ô thấp ≤150px), còn dải KPI xuống hàng 2 — giữ được cả nhịp đọc chung
   lẫn câu trả lời "đạt hay không" ở chỗ mắt chạm đầu tiên.
   Phần 3fr mà băng nhả ra chia đôi cho Xu hướng và Lưu lượng: hai chart trước
   xếp CHỒNG trong một cột (`.ww-colM`, đã xoá) nên mỗi cái chỉ cao ~260/190px
   — mẫu S04 xếp chúng CẠNH NHAU, nay mỗi cái cao trọn dải chính.
   Bảng quan trắc vẫn ở ngăn phải (nó lặp đúng bộ chỉ tiêu dải KPI đang hiện);
   camera giữ 5/12 ≈ 42% — hero của màn theo nguyên tắc "mỗi màn một hero
   ≥ 40%".
   30/09 — băng phán quyết RỜI TƯỜNG vào ngăn phải (lệnh người dùng): badge
   ĐẠT/VƯỢT của từng thẻ KPI và card Cảnh báo đã trả lời "đạt hay không".
   ~86px + một gutter băng nhả ra chảy vào `.ww-body`, chia cho dải chính và
   dải dưới theo đúng tỉ lệ 12 : 10 sẵn có — dải KPI KHÔNG cao thêm.       */

.scr--ww {
  /* Hàng 1 = dải KPI, bằng đúng `.scr--elec` và `.scr--water` — dải KPI cùng
     một chiều cao trên mọi màn, người trực đổi màn không phải chỉnh lại mắt. */
  grid-template-rows: clamp(96px, 12vh, 140px) minmax(0, 1fr);
}

/* 27/10 (lệnh user "camera to ra, chiếm nửa trái"): thân màn chia ĐÔI theo chiều ngang — nửa trái là tường
   camera cao trọn thân màn (lưới tự chọn số cột cho ô 16:9 to nhất, xem `EffluentCameraCard`), nửa phải là
   ba dải `.ww-side`. */
.ww-body {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
}

.ww-side {
  grid-template-rows: minmax(0, 5fr) minmax(0, 4fr) minmax(0, 3fr);
}

/* Ô hình của dải KPI S05 là DỤNG CỤ THÍ NGHIỆM (ống nghiệm, bình tam giác,
   nhiệt kế — lệnh 21/09), khung 32×48 ĐỨNG chứ không vuông như bể chứa hay
   vòng gauge. Cùng cách `.scr--light` chỉnh hệ số cho dải KPI của nó ngay
   dưới đây. */
.scr--ww .kpis .mt--strip {
  /* Bề ngang ô hình theo ĐÚNG tỉ lệ 32:48 (+ biên thở). SVG co theo cạnh NHỎ
     nên để rộng hơn cũng không làm dụng cụ to lên, chỉ thừa chỗ hai bên.
     Khai ở cấp THẺ (30/09 — xem `.mt--strip` trong metric.css). */
  --kpi-aside-k: 0.68;
}

.scr--ww .kpis .mt__aside {
  /* Cao TRỌN thẻ thay vì chỉ hai hàng số + chân (mặc định `2 / -1` ở
     metric.css): dụng cụ cao thêm ~40%, đọc được mức dung dịch từ 5m — lệnh
     21/09 "tăng kích thước bình lên một chút". Nhãn lui về cột 1 ở rule ngay
     dưới nên chữ không bao giờ chui xuống dưới hình. Cùng cách `.mt--tally`
     cho hình cao trọn thẻ. */
  grid-row: 1 / -1;
}

.scr--ww .kpis .mt__top {
  /* Trả nhãn về CỘT 1. Mặc định nhãn ăn cả hai cột (metric.css) vì ô hình
     thường chỉ cao hai hàng dưới; ở đây hình cao trọn thẻ nên nhãn phải nhường
     cột 2. An toàn vì nhãn 6 chỉ tiêu đều ngắn (pH · COD · TSS · Amoni NH₄⁺ ·
     DO · Nhiệt độ) — không cái nào chạm mép cột hình. */
  grid-column: 1;
}

/* Xu hướng 24h · lưu lượng xả — cạnh nhau, chia đều nửa phải (camera đã sang nửa trái, 27/10). */
.ww-rowA {
  --cols: minmax(0, 1fr) minmax(0, 1fr);
}

/* 30 ngày · thiết bị — chia đôi nửa phải. Cảnh báo xuống dải riêng `.ww-rowC` (27/10): ba card trong nửa
   màn thì tiêu đề "Thiết bị trạm xử lý nước thải" / "Cảnh báo trạm · n chưa xử lý" bị cắt. */
.ww-rowB {
  --cols: minmax(0, 1fr) minmax(0, 1fr);
}

/* ══ S06 · QUAN TRẮC MÔI TRƯỜNG (01/10) ═════════════════════════════════
     dải KPI    6 thẻ — cùng chiều cao với mọi màn phân hệ
     dải chính  VN_AQI 24h | chỉ tiêu không khí | khí tượng & gió     (≈ 11 phần)
     dải dưới   tiếng ồn ranh | nước mặt tiếp nhận | CẢNH BÁO          (≈ 9 phần)
   Cảnh báo ở góc dưới phải như Điện năng · Nước cấp · Nước thải (01/10). */

.scr--env {
  grid-template-rows: clamp(96px, 12vh, 140px) minmax(0, 1fr);
}

.env-body {
  grid-template-rows: minmax(0, 11fr) minmax(0, 9fr);
}

.env-rowA {
  --cols: minmax(0, 4.4fr) minmax(0, 4.6fr) minmax(0, 3fr);
}

.env-rowB {
  --cols: minmax(0, 4fr) minmax(0, 4fr) minmax(0, 4fr);
}

/* ══ S04 · NƯỚC CẤP ════════════════════════════════════════════════════
     dải KPI   y=0  h=3   (6 thẻ)
     dải chính y=3  h=12  → 7 | 5 cột
     dải dưới  y=15 h=9   → 4 | 4 | 4 cột

   20/09 — dải dưới chia lại từ `5 | 4 | 3`. Cột phải (CẢNH BÁO + LỊCH SỬ) chứa
   thứ người trực đọc nhiều nhất mà lại đang HẸP NHẤT: giá trị/ngưỡng bị bóp và
   danh sách chỉ đủ ~5 dòng, dòng ở mép còn bị cắt ngang. Card cột theo giờ sau
   khi cắt đuôi ở giờ hiện tại thì cần ít bề ngang hơn nên nhường được.
   (Mã màn là S04 — chú thích cũ ghi S03 từ trước khi chèn màn Bản đồ.)      */

.scr--water {
  grid-template-rows: clamp(96px, 12vh, 140px) minmax(0, 1fr);
}

.water-body {
  grid-template-rows: minmax(0, 12fr) minmax(0, 9fr);
}

.water-rowA {
  --cols: minmax(0, 7fr) minmax(0, 5fr);
}

.water-rowB {
  --cols: minmax(0, 4fr) minmax(0, 4fr) minmax(0, 4fr);
}

/* Cột phải dải dưới: LỊCH SỬ CẤP NƯỚC trên, CẢNH BÁO dưới — cảnh báo ở góc dưới phải như
   mọi màn phân hệ (01/10). Cảnh báo vẫn ăn 1,6 phần (20/09): nó là thứ sinh ra hành động,
   còn lịch sử chỉ để tra lại. */
.water-colR {
  grid-auto-rows: unset;
  grid-template-rows: minmax(0, 1fr) minmax(0, 1.6fr);
}

/* ══ S03 · ĐIỆN NĂNG ═══════════════════════════════════════════════════
     dải KPI    (6 thẻ)
     dải trên   → xu hướng+phụ tải 7 | khung giờ/nguồn 2,4 | xếp hạng 2,6
     dải dưới   → 30 ngày 4,3 | chất lượng điện 2 | cảnh báo 3,4

   20/09 — chia lại theo GIÁ TRỊ THÔNG TIN, không theo thói quen: sơ đồ điện
   từng ăn ≈30% diện tích màn để hiện đúng 2 marker trên nền phần lớn trống.

   20/09 (chiều) — dựng lại theo bản mẫu S02 của bên thiết kế: thân màn còn
   SÁU panel thay vì chín. Hai thay đổi đáng ghi lại vì sau này dễ bị "sửa
   ngược":
     · Xu hướng và Phụ tải gộp làm MỘT panel hai chart chồng (`.elec-charts`).
       Hai chart này luôn được đọc cùng nhau — tách ra thì mắt phải nhảy chéo
       màn để so cùng một mốc giờ.
     · Sơ đồ điện và danh sách tủ điện rời lưới, xuống NGĂN PHẢI (không xoá).
       Chỗ trống chảy vào Cảnh báo — nay đủ rộng cho bảng 5 cột thật, thứ
       người trực ca phải đọc từng dòng.
   Tỉ lệ cột suy từ chính ảnh mẫu (mẫu khoá cứng 300px/320px; ở đây phải là
   `fr` để còn co đúng ở chế độ wall).
   ⛔ Chỉ số hàng/cột đổi — KHÔNG chạm một dòng nào trong `src/components/map/**`
   hay `src/screens/map/**`.                                              */

.scr--elec {
  grid-template-rows: clamp(96px, 12vh, 140px) minmax(0, 1fr);
}

/* Dải trên nới hơn 12/9 cũ: panel đầu nay chứa HAI chart chồng nên mỗi chart
   chỉ còn nửa chiều cao — thiếu vài chục px là trục X mất nhãn. */
.elec-body {
  grid-template-rows: minmax(0, 12fr) minmax(0, 8.5fr);
}

.elec-rowA {
  --cols: minmax(0, 7fr) minmax(0, 2.4fr) minmax(0, 2.6fr);
}

/* Cột giữa: hai donut cơ cấu tiêu thụ, chia đôi đều. */
.elec-colM {
  grid-auto-rows: unset;
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}

/* Thân panel "Xu hướng tiêu thụ & phụ tải" — hai chart chồng, cao bằng nhau
   để so hai đường theo cùng một mốc giờ mà không phải quy đổi bằng mắt. */
.elec-charts {
  display: grid;
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--gutter);
  min-height: 0;
  flex: 1 1 auto;
}

/* 17/09 thêm CHẤT LƯỢNG ĐIỆN vào dải (mẫu S02 đặt nó giữa "30 ngày" và
   "Cảnh báo điện"). Xếp hạng đã lên dải trên nên dải này còn BA panel —
   phần Xếp hạng nhường lại dồn hết cho Cảnh báo. */
.elec-rowB {
  --cols: minmax(0, 3.8fr) minmax(0, 2.4fr) minmax(0, 3.8fr);
}

/* ══ S11 · NĂNG LƯỢNG (điện + nước, 15/10) ═════════════════════════════
     dải KPI    (6 thẻ, cùng chiều cao các màn phân hệ)
     dải trên   → xu hướng điện 4,4 | lưu lượng nước 4,4 | khung giờ / nguồn chồng 3,2
     dải dưới   → điện 30 ngày 3,2 | nước theo ngày 2,6 | xếp hạng 2,8 | cảnh báo 3,4
   Góp ý 15/10 (lần 3, xem trên TV 55" FHD — 1 px ≈ 0,63 mm): hai donut đứng cạnh nhau thành vòng
   ~220 px (~14 cm) chỉ mang 3–4 số, chú giải dồn xuống đáy chữ nhỏ. Nay chồng trong một cột: vòng
   nhỏ, chú giải bên phải đọc ngang; bề ngang dư trả cho hai đường 24h và bảng xếp hạng (tên điểm
   đo không bị cắt "…"). */

.scr--enr {
  grid-template-rows: clamp(96px, 12vh, 140px) minmax(0, 1fr);
}

.enr-body {
  grid-template-columns: minmax(0, 3.4fr) minmax(0, 2.6fr) minmax(0, 3.4fr) minmax(0, 2.6fr);
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas:
    'trend  donuts flow  alarms'
    'days30 rank   wdays alarms';
}

.enr-body > div {
  display: grid;
  min-width: 0;
  min-height: 0;
}

.enr-a-trend { grid-area: trend; }
.enr-a-donuts { grid-area: donuts; grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); grid-auto-rows: unset; }
.enr-a-flow { grid-area: flow; }
.enr-a-alarms { grid-area: alarms; }
.enr-a-days30 { grid-area: days30; }
.enr-a-rank { grid-area: rank; }
.enr-a-wdays { grid-area: wdays; }

/* ══ S06 · CHIẾU SÁNG ══════════════════════════════════════════════════
     dải KPI   y=0  h=3   (6 thẻ)
     dải chính y=3  h=15  → điều khiển lộ 1 | sơ đồ tuyến 1,2
     dải dưới  y=18 h=6   → lịch 1 | 30 ngày 1 | nhật ký & cảnh báo 1,1

   20/09 — dựng lại theo bản mẫu S06 của bên thiết kế. Trước đó S06 là màn DUY
   NHẤT còn bố cục HAI CỘT DỌC toàn chiều cao, dải KPI nhét trong cột phải: năm
   màn kia đều là "dải KPI ngang suốt màn → thân hai dải", nên người trực đổi
   sang màn này phải chỉnh lại mắt mỗi lần. Nay cùng một nhịp với chúng.
   Ba thay đổi kéo theo, ghi lại vì sau này dễ bị "sửa ngược":
     · Chế độ vận hành và Dừng khẩn cấp lên thành thẻ KPI (#2 và #6) — mẫu để
       chúng ở đó, và dải KPI vốn là chỗ mắt chạm đầu tiên.
     · Nhật ký lệnh và Cảnh báo gộp làm MỘT panel có tab (mẫu làm vậy): bảng
       cảnh báo 4 cột nay đủ rộng thay vì bị bóp trong 2,75/12 bề ngang.
     · Card "Vận hành" tách đôi: cặp nút chế độ về `tools` của panel Lịch, còn
       danh sách lịch thành bảng `.tt` đúng mẫu.
   Tỉ lệ cột lấy thẳng từ mẫu (`.rowA` 1 | 1,2 · `.rowB` 1 | 1 | 1,1).
   Thân màn dùng `.pbody--2r` chung — mẫu khai đúng cùng một giá trị.
   ⛔ Chỉ số hàng/cột đổi — KHÔNG chạm một dòng nào trong `src/components/map/**`
   hay `src/screens/map/**`.                                              */

.scr--light {
  /* Dải KPI CÙNG chiều cao với `.scr--elec`/`.scr--water`/`.scr--ww` — đổi màn
     thì dải không nhảy. Đây là lý do chính S06 bỏ bố cục hai cột. */
  grid-template-rows: clamp(96px, 12vh, 140px) minmax(0, 1fr);
}

.light-rowA {
  /* 21/09: 1 | 1,2 → 0,85 | 1,35. Ma trận điều khiển chỉ có ba cột (tên · tuyến ·
     công tắc) nên ~39% là dư; phần nhả ra về SƠ ĐỒ TUYẾN, hero thật của màn.
     Đổi được là nhờ cùng lượt gộp ba pha vào một dòng (LightingScreen), bảng từ
     ~39 dòng còn 12 nên không cần bề ngang để bù chiều cao nữa. */
  --cols: minmax(0, 0.85fr) minmax(0, 1.35fr);
}

.light-rowB {
  --cols: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.1fr);
}

/* Dải KPI S06 là dải DUY NHẤT có thẻ mà GIÁ TRỊ là chữ dài ("THỦ CÔNG",
   "SẴN SÀNG"), không phải số ngắn như năm màn kia. Hai luật dưới CHỈ áp cho
   S06 — không đụng tỉ lệ hình mà các màn kia đã nghiệm thu. */
.scr--light .kpis .mt__val {
  /* Giá trị chữ cắt bằng "…" chứ không xuống dòng — thẻ dẹt chỉ có một hàng
     cho số, xuống dòng là tràn ra khỏi hàng lưới. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.scr--light .kpis .mt--strip {
  /* Ô hình của thẻ dẹt rộng theo CHIỀU CAO thẻ (`--kpi-aside-k`, metric.css)
     chứ không theo bề ngang: ở 6 thẻ trên 1920 hệ số 1,44 cho ra ~47% bề ngang
     thẻ, gấp ba bản mẫu S06 và bóp nghẹt phần chữ. 1,0 đưa về ~33%. Khai ở cấp
     THẺ (30/09 — xem `.mt--strip` trong metric.css). */
  --kpi-aside-k: 1;
}

/* ══ S07 · AN NINH & AI ════════════════════════════════════════════════
     dải KPI y=0 h=3  (3 thẻ)
     thân    y=3 h=21 → cây camera 2 | tường video 7,75 | cột phải 2,25
       cột phải        → sự kiện AI h=12,25 trên PTZ h=8,75              */

.scr--cam {
  grid-template-rows: clamp(96px, 12vh, 140px) minmax(0, 1fr);
}

.cam-rowA {
  --cols: minmax(0, 2fr) minmax(0, 7.75fr) minmax(0, 2.25fr);
}

.cam-colR {
  grid-auto-rows: unset;
  grid-template-rows: minmax(0, 12.25fr) minmax(0, 8.75fr);
}

/* ══ S08 · CẢNH BÁO & XỬ LÝ SỰ CỐ — bản tường "chỉ nhìn" (02/10) ═══════════
     dải KPI  (5 thẻ — cùng chiều cao với mọi màn)
     thân     → bảng phiếu ba làn 3,4 | bảy phân hệ 1
   Không có ô chi tiết, không nút: thao tác xử lý sự cố ở app CMS. Mỗi làn rộng
   ~460px ở 1920 — đủ cho tên sự cố xuống tối đa hai dòng, chữ to đọc từ xa. */

.scr--alm {
  /* Dải KPI CÙNG chiều cao với `.scr--elec`/`.scr--water`/`.scr--ww`/`.scr--light`
     — đổi màn thì dải không nhảy. */
  grid-template-rows: clamp(96px, 12vh, 140px) minmax(0, 1fr);
  /* Năm ô số trải hết bề ngang — không chừa ô trống thứ sáu. */
  --kpi-cols: 5;
}

.alm-body {
  /* Bản tường "chỉ nhìn" (02/10): bảng phiếu ba làn bên trái. 05/10: ô "Theo phân hệ" bỏ, cột phải là bảng
     cảnh báo (bảy cột — cần rộng hơn cột phân hệ cũ nhiều). */
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1.3fr);
  grid-template-rows: minmax(0, 1fr);
}


/* ══ S01 · TỔNG QUAN ═══════════════════════════════════════════════════
   Theo mẫu S01: dải KPI · dải chính · dải `board` 6 cột thiết bị.
     dải KPI   h=3,25  (7 thẻ)
     dải chính        → camera 1,75 | bản đồ (co giãn) | dòng sự kiện 2,5
     board     cao cố định theo mẫu (`clamp(170px,22vh,218px)`) · 6 cột đều */

.scr--ov {
  grid-template-rows: clamp(104px, 13vh, 150px) minmax(0, 1fr);
}

.ov-body {
  grid-template-rows: minmax(0, 1fr) clamp(170px, 22vh, 218px);
}

.ov-rowA {
  --cols: minmax(0, 1.75fr) minmax(0, 7.75fr) minmax(0, 2.5fr);
}

.ov-board {
  --cols: repeat(6, minmax(0, 1fr));
}

/* ══ NỀN PHỦ KÍN MÀN + CARD NỔI ════════════════════════════════════════
   Màn truyền `backdrop` cho `ScreenFrame` (Tổng quan: bản đồ KCN). Card nền
   vẽ hết khung phía sau lưới, các card khác nổi trên nó như HUD.

   Luật port nguyên từ engine cũ (`draggable-grid.css` §NỀN TOÀN MÀN 16/09) —
   đổi tên `.dgs*` → `.scr*`, bỏ phần dính chế độ kéo-thả. */

.scr__backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
}

.scr__backdrop > * {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

/* Card nền KHÔNG phải một ô: lột hết khung panel để nó là nền thật sự. */
.scr__backdrop .tp {
  border-radius: 0;
  box-shadow: none;
  background: var(--bg-void);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.scr__backdrop .tp > .tp__head,
.scr__backdrop .tp > .chd,
.scr__backdrop .tp > .tp__rail,
.scr__backdrop .tp--bracket::before,
.scr__backdrop .tp--bracket::after {
  display: none;
}

/* Tối dần về mép màn — card nổi và chữ ở mép mới đọc rõ trên nền bản đồ sáng. */
.scr__backdrop::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    ellipse 78% 72% at 50% 52%,
    transparent 50%,
    color-mix(in srgb, var(--bg-void) 72%, transparent) 100%
  );
}

/* ⭐ CHUỘT XUYÊN QUA CHỖ TRỐNG. Không có khối này thì lưới (dù trong suốt) vẫn
   nuốt sạch thao tác và bản đồ phía sau thành một tấm ảnh chết — không kéo,
   không zoom, không bấm được marker.
   `pointer-events` DI TRUYỀN, nên đặt `none` ở khung lưới rồi bật lại `auto`
   ở từng card là đủ; không phải liệt kê mọi tầng ở giữa. */
/* `.sdr` (ngăn phải) LOẠI khỏi đây: nó phải giữ `absolute; inset:0` của chính nó để thanh tab
   dính giữa mép phải như mọi màn. Bị ép `relative` thì ngăn rơi vào luồng lưới và tab trôi xuống
   góc dưới, đè lên card (15/10). Ngăn tự lo chuột: lớp ngoài `none`, tab + ngăn trượt `auto`. */
.scr--floating > *:not(.scr__backdrop):not(.scr__fab):not(.sdr) {
  position: relative;
  z-index: 1;
  pointer-events: none;
}

.scr--floating .tp,
.scr--floating .mt {
  pointer-events: auto;
}

/* Card nổi: kính mờ + bóng sâu để tách khỏi nền bản đồ. */
.scr--floating .tp,
.scr--floating .mt {
  background: color-mix(in srgb, var(--bg-panel-solid) 82%, transparent);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--elev-float);
  backdrop-filter: blur(var(--blur-glass));
  -webkit-backdrop-filter: blur(var(--blur-glass));
}

/* ══ S01 · TỔNG QUAN — BIẾN THỂ NỀN BẢN ĐỒ (17/09, sửa bố cục 23/09) ═══
   BẢN ĐỒ LÀ NỀN, phủ kín màn, nằm SAU tất cả (khe `backdrop` của ScreenFrame).
   Mọi thứ khác là CARD NỔI. Lưới dưới đây chỉ ĐỊNH CHỖ cho card — nó KHÔNG
   phải khung chứa bản đồ, và không bao giờ bóp bản đồ lại.

   23/09 — ba thay đổi theo lệnh người dùng:
     · thẻ "Camera điểm nóng" bám mép trái BỎ (camera nay xem theo VÙNG);
     · card Sự kiện rời mép phải, xuống DẢI ĐÁY trải hết bề ngang;
     · bấm một vùng trên bản đồ thì mép trái hiện camera của vùng, mép phải
       hiện sự kiện của vùng.
   Sau đó (23/09): dải sự kiện đáy BỎ (trùng card sự kiện theo vùng), thay
   bằng 6 cột thiết bị theo phân hệ `.ov-devices`. */

.scr--ov-map {
  /* Cao dải đáy — một biến cho cả track lưới lẫn chỗ nâng chip bản đồ, để hai
     nơi không lệch nhau. Đủ 2–3 dòng thiết bị 64px mỗi cột, phần còn lại cuộn. */
  --ov-dock-h: clamp(190px, 24vh, 260px);
  /* Cao dải KPI và rộng hai cột bên của băng giữa — biến vì `--map-safe-inset`
     dưới đây phải khớp đúng lưới. */
  --ov-kpi-h: clamp(104px, 13vh, 150px);
  /* 20vw (20/10, trước 18vw): ở màn 1536px card Sự kiện 276px không đủ cho tiêu đề + bộ đếm.
     Trần 340px giữ nguyên nên tường 1920px không đổi. */
  --ov-side-w: clamp(260px, 20vw, 340px);
  /* KPI · băng giữa (nền bản đồ lộ ra) · bảng thiết bị đáy */
  grid-template-rows: var(--ov-kpi-h) minmax(0, 1fr) var(--ov-dock-h);
  /* Chip 3D/2D/VN của bản đồ nằm TRONG backdrop nên card đáy luôn vẽ đè nó.
     Bản đồ tự nâng chip lên khỏi dải đáy bằng biến này; màn Bản đồ độc lập
     không khai thì chip rơi về `12px` như cũ. */
  --map-hud-bottom: calc(var(--ov-dock-h) + var(--gutter) + 12px);
  /* Phần nền KHÔNG bị card che — view VN co bản đồ + panel mạng lưới KCN vào
     đây (30/09: trước đó Hải Phòng/Ninh Bình và tia truyền nằm dưới dải đáy,
     panel nằm dưới card Sự kiện). Trái = 0: bấm VN là bỏ chọn vùng nên cột
     camera vùng rỗng. */
  --map-safe-inset: calc(var(--ov-kpi-h) + var(--gutter)) calc(var(--ov-side-w) + var(--gutter))
    calc(var(--ov-dock-h) + var(--gutter)) 0;
}

/* Băng giữa. Track GIỮA là LỖ NHÌN XUYÊN xuống nền bản đồ — không có card nào
   trong đó, và `.scr--floating` làm nó không bắt chuột nên bản đồ phía sau vẫn
   kéo/zoom được. Hai track bên LUÔN được khai kể cả lúc chưa chọn vùng: ô rỗng
   thì vô hình, nhưng nhờ vậy card xuất hiện không làm nhảy bố cục. */
.ov-float {
  display: grid;
  grid-template-columns: var(--ov-side-w) minmax(0, 1fr) var(--ov-side-w);
  gap: var(--gutter);
  min-height: 0;
}

/* ⚠ Dải đáy PHẢI có lớp bọc này, không được đặt card thẳng làm con của `.scr`.
   Lý do là độ ưu tiên CSS, không phải thẩm mỹ: `.scr--floating > *:not(...)`
   (0,3,0) tắt chuột cho mọi con trực tiếp, còn `.scr--floating .tp` (0,2,0) bật
   lại — con trực tiếp NÀO cũng là `.tp` thì luật tắt thắng và card thành không
   cuộn, không bấm được. Bọc một lớp là card lùi xuống hàng cháu, chỉ còn luật
   bật áp lên nó. Đây cũng là lý do `.ov-float` tồn tại.
   Nội dung: 6 cột thiết bị theo phân hệ (bản mẫu S01 `.board`). */
.ov-devices {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--gutter);
  min-height: 0;
}

/* ── THU GỌN (09/10, lệnh user) ──────────────────────────────────────
   Dải đáy thu: track về 0 ⇒ bản đồ giãn xuống, chip bản đồ hạ theo
   (`--map-hud-bottom`, `--map-safe-inset` đều tính từ biến này). */
.scr--ov-dock-off {
  --ov-dock-h: 0px;
}

/* Tay nắm thu/mở dải đáy: giữa mép TRÊN dải (đứng trong khe gutter). Dải thu
   về 0 thì nó tự rơi xuống sát đáy màn — cùng một luật `bottom: 100%`.
   `.ov-devices` đã `position: relative` nhờ luật `.scr--floating > *`. */
.ov-dock-toggle {
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  height: var(--h-btn-sm);
  padding: 0 var(--sp-3);
  border: 0;
  border-radius: var(--r-md) var(--r-md) 0 0;
  background: color-mix(in srgb, var(--bg-panel-solid) 88%, transparent);
  box-shadow: var(--sh-hair), var(--sh-sink);
  color: var(--text-mid);
  font: inherit;
  cursor: pointer;
  pointer-events: auto;
  backdrop-filter: blur(var(--blur-glass));
  -webkit-backdrop-filter: blur(var(--blur-glass));
  transition: color 0.15s;
}

.ov-dock-toggle:hover,
.ov-dock-toggle:hover .lbl {
  color: var(--text-hi);
}

/* Tab ngăn phải màn này: mép phải màn đã là cột card Sự kiện (cao gần hết băng giữa), tab
   giữa mép phải như các màn khác sẽ đè lên card. Đặt ở góc DƯỚI của khoảng nhìn bản đồ, sát
   mép trái cột Sự kiện — chỗ duy nhất không đè card nào. Ngăn mở ra thì trả về luật chung. */
.scr--ov-map .sdr:not(.sdr--open) .sdr__rail {
  top: auto;
  bottom: calc(var(--ov-dock-h) + var(--gutter) * 2);
  right: calc(var(--ov-side-w) + var(--gutter) * 2);
  transform: none;
}

/* Card Sự kiện đã thu: tab dọc (`.sdr__tab`) dính mép phải của cột phải. */
.ov-edge-tab {
  justify-self: end;
  pointer-events: auto;
}

/* Dòng "Khu: …" dưới tiêu đề card Camera / Sự kiện khi chọn vùng (29/10). Tên khu đọc TRỌN, dài thì
   xuống dòng — trước đây ghép vào tiêu đề nên bị cắt "…" và đẩy nút thu ra khỏi mép card. */
.ov-feed-filter {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm);
  background: var(--bg-raised);
}
.ov-feed-filter__name {
  min-width: 0;
  color: var(--text-hi);
  font-size: var(--fs-body);
  font-weight: var(--fw-value);
  overflow-wrap: anywhere;
}

/* Card nổi neo ĐỈNH và bị chặn cao — không card nào được kéo hết băng giữa
   (lỗi 23/09: card Sự kiện cao bằng màn, thành khối to nhất trên tường).
   `max-height` chứ không `height`: ít nội dung thì card co lại, nền bản đồ lộ
   thêm. */
.ov-float > * {
  align-self: start;
  max-height: clamp(300px, 45vh, 480px);
}

/* Card vừa hiện ra khi bấm vùng. CHỈ `opacity` + `transform` — animate track
   lưới là layout thrash mỗi frame trên GPU yếu của máy tường. */
@media (prefers-reduced-motion: no-preference) {
  .ov-float > * {
    animation: ov-card-in 160ms cubic-bezier(0.2, 0.75, 0.25, 1);
  }
}

@keyframes ov-card-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
}

/* Cụm nút góc dưới-phải (báo lỗi kho layout, Sửa nhãn) chỉ là phụ: để mờ cho
   không tranh mắt với card phía sau, rõ lại khi rê chuột hoặc đưa tiêu điểm vào. */
.scr__fab:hover,
.scr__fab:focus-within {
  opacity: 1;
}
/* TẠM THỜI — engine kéo-thả còn sống cho các màn CHƯA chuyển sang
   `ScreenFrame`. Xoá cùng `DraggableScreen.tsx` khi màn cuối xong. */
/**
 * Lưới kéo-thả (DraggableScreen + react-grid-layout v2).
 *
 *   .dgs         gốc màn — chiếm trọn canvas, chứa lưới + nút Sắp xếp
 *   .dgs--edit   đang ở chế độ sắp xếp
 *   .dgi         một ô card — con trực tiếp của GridLayout
 *   .dgs__fab    cụm nút Sắp xếp / Xong / Mặc định (góc dưới-phải)
 *
 * Placeholder + handle của RGL tô lại bằng token (rule 07 §3) — không giữ
 * màu đỏ mặc định của thư viện.
 */

.dgs {
  position: relative;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

/* Đang sắp xếp: cho phép cuộn dọc — kéo card xuống dưới cùng vẫn thấy nó. */
.dgs--edit {
  overflow-y: auto;
  overflow-x: hidden;
}

/* Lưới chạy margin 0 (lưới mịn 48 cột — margin RGL 10px × 95 hàng là vượt
   chiều cao màn); rãnh giữa card tạo bằng padding GAP/2 trong từng ô. Lưới
   nới ra GAP/2 mỗi mép (margin âm + width cộng GAP ở component) để card mép
   vẫn chạm đúng cạnh màn như hệ cũ. */
.dgs__grid {
  position: relative;
  min-height: 100%;
  margin: calc(var(--gutter, 10px) / -2);
}

/* Ô card: con của RGL được đặt vị trí bằng transform; bên trong là flex để
   panel lấp đầy đúng ô trừ rãnh padding. */
.dgi {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  padding: calc(var(--gutter, 10px) / 2);
}

.dgi > * {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

/* Chế độ sắp xếp: viền đứt + tay cầm rõ ràng. */
.dgs--edit .dgi {
  cursor: grab;
}

.dgs--edit .dgi:active {
  cursor: grabbing;
}

.dgs--edit .dgi > :first-child {
  outline: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent);
  outline-offset: -1px;
}

.dgi.react-draggable-dragging {
  z-index: var(--z-grid-drag);
  opacity: 0.92;
}

.dgi.react-draggable-dragging > * {
  outline-color: var(--accent);
  box-shadow: var(--elev-float), 0 0 24px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* Bóng chỗ sắp thả — tô lại bằng token thay cho màu đỏ mặc định của RGL. */
.dgs .react-grid-placeholder {
  background: var(--accent-soft) !important;
  border: 1px dashed var(--accent);
  border-radius: var(--r-lg);
  opacity: 0.7;
  transition: transform 0.12s ease;
}

/* Handle resize góc phải-dưới — chỉ hiện khi đang sắp xếp (RGL tự ẩn/hiện
   theo resizeConfig, đây là phần tô màu). */
.dgs .react-resizable-handle {
  opacity: 0.65;
  /* Tay nắm PHẢI nằm trên mọi thứ trong card. Thẻ nào gần như toàn bộ là một
     vùng cuộn (Cảnh báo & sự kiện) thì vùng cuộn ăn kín mép, và chỉ cần một
     phần tử con tạo stacking context là tay nắm chui xuống dưới — người dùng
     rê đúng mép mà không kéo được (báo 26/08). */
  z-index: 3;
}

.dgs .react-resizable-handle::after {
  border-color: var(--accent) !important;
}

/* Cụm nút Sắp xếp — nổi trên góc dưới-phải của màn. */
.dgs__fab {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: var(--z-grid-fab);
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px;
  background: var(--bg-panel);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  box-shadow: var(--elev-panel);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* `.sdr*` -> screen-drawer.css ; `.ed-*` -> panel.css (chuyen 17/09). */

/* ── Thư viện card (sidebar trượt phải, chỉ hiện trong chế độ Sắp xếp) ── */

.dgs__lib {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-grid-lib);
  width: 320px;
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border-left: 1px solid var(--line-mid);
  box-shadow: var(--elev-float);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.dgs__lib-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line-soft);
}

.dgs__lib-hint {
  flex: 0 0 auto;
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-cap);
  color: var(--text-lo);
  line-height: 1.5;
  border-bottom: 1px solid var(--line-soft);
}

.dgs__lib-body {
  flex: 1 1 auto;
  padding: var(--sp-2) var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.dgs__lib-group {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.dgs__lib-item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  padding: 6px var(--sp-2);
  background: var(--bg-well);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  font-size: var(--fs-body);
  color: var(--text-hi);
  cursor: grab;
  transition: border-color 0.15s, background 0.15s;
}

.dgs__lib-item:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.dgs__lib-item:active {
  cursor: grabbing;
}

.dgs__lib-item--active {
  opacity: 0.45;
  cursor: default;
}

.dgs__lib-item--active:hover {
  border-color: var(--line-soft);
  background: var(--bg-well);
}

.dgs__lib-grip {
  flex: 0 0 auto;
  color: var(--text-lo);
  font-size: var(--fs-body);
}

/* Nút gỡ card khỏi lưới — chỉ hiện trong chế độ Sắp xếp của màn có thư viện. */
.dgi__remove {
  position: absolute;
  top: 4px;
  right: 4px;
  z-index: var(--z-grid-drag);
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--st-crit) 50%, transparent);
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--st-crit) 22%, var(--bg-void));
  color: var(--st-crit);
  font-size: 10px;
  font-weight: var(--fw-title);
  cursor: pointer;
  transition: background 0.15s, transform 0.15s;
}

.dgi__remove:hover {
  background: var(--st-crit);
  color: var(--text-hi);
  transform: scale(1.1);
}

/* Bóng mờ card đang kéo từ thư viện — bám theo con trỏ. */
.dgs__lib-ghost {
  position: fixed;
  z-index: var(--z-overlay);
  padding: 6px 10px;
  background: var(--accent-soft);
  border: 1px dashed var(--accent);
  border-radius: var(--r-md);
  color: var(--text-hi);
  font-size: var(--fs-body);
  font-weight: 700;
  pointer-events: none;
  box-shadow: var(--sh-sink);
}


/* ── Animation trượt sidebar + preview card ───────────────────────────── */

.dgs__lib--open {
  animation: dgs-lib-in 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}

.dgs__lib--closing {
  animation: dgs-lib-out 0.24s ease-in forwards;
}

@keyframes dgs-lib-in {
  from { transform: translateX(105%); opacity: 0.5; }
  to { transform: translateX(0); opacity: 1; }
}

@keyframes dgs-lib-out {
  to { transform: translateX(105%); opacity: 0.4; }
}

/* Item thư viện giờ là THẺ có preview: ảnh sống trên, tên + nút dưới. */
.dgs__lib-item {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  padding: 0;
  overflow: hidden;
}

.dgs__lib-prev {
  position: relative;
  height: 130px;
  overflow: hidden;
  background: var(--bg-app);
  border-bottom: 1px solid var(--line-soft);
}

.dgs__lib-prev-inner {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: top left;
  display: flex;
  flex-direction: column;
}

.dgs__lib-prev-inner > * {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

/* Tấm chắn hứng chuột — preview chỉ để nhìn, kéo/bấm là việc của item. */
.dgs__lib-prev-cover {
  position: absolute;
  inset: 0;
  z-index: 5;
}

.dgs__lib-prev-badge {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-scrim);
  color: var(--st-good);
  font-size: var(--fs-cap);
  font-weight: var(--fw-title);
  letter-spacing: var(--track-label);
}

.dgs__lib-item-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  padding: 6px var(--sp-2);
}

/* Card vừa thêm từ thư viện — nháy sáng để không bị bỏ lỡ. */
.dgi--new > * {
  animation: dgi-new 2s ease;
}

@keyframes dgi-new {
  0% { outline: 2px solid var(--accent); box-shadow: 0 0 0 6px var(--accent-soft), 0 0 34px var(--accent); }
  70% { outline: 2px solid var(--accent); box-shadow: 0 0 0 3px var(--accent-soft), 0 0 18px var(--accent); }
  100% { outline: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent); box-shadow: none; }
}

/* ── Tab «/» mở-đóng thư viện ở mép phải màn ─────────────────────────── */

.dgs__lib-tab {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  z-index: 69;
  width: 26px;
  height: 72px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--accent);
  border-right: none;
  border-radius: var(--r-md) 0 0 var(--r-md);
  background: var(--bg-panel);
  color: var(--accent);
  font-size: 16px;
  font-weight: var(--fw-title);
  cursor: pointer;
  box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 30%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: width 0.15s, box-shadow 0.15s;
}

.dgs__lib-tab:hover {
  width: 32px;
  box-shadow: 0 0 26px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* Khi sidebar mở: tab dính vào mép trái sidebar, trượt cùng sidebar. */
.dgs__lib-tab--attached {
  right: 100%;
  border-right: none;
}

/* Tay nắm resize cạnh — vùng bắt rộng, phát sáng khi rê vào.
   ⚠ PHẢI tắt background-image: icon mũi tên mặc định của RGL bị kéo giãn
   theo tay nắm dài → thành vệt chéo vắt ngang màn (bug đã bị chửi). */
.dgs--edit .react-resizable-handle-e,
.dgs--edit .react-resizable-handle-w {
  width: 12px;
  top: 0;
  bottom: 0;
  height: auto;
  transform: none;
  margin-top: 0;
  cursor: ew-resize;
  background-image: none;
}

.dgs--edit .react-resizable-handle-s {
  height: 12px;
  left: 0;
  right: 0;
  width: auto;
  transform: none;
  margin-left: 0;
  cursor: ns-resize;
  background-image: none;
}

/* Góc SW cũng bỏ icon (giữ chức năng kéo chéo). */
.dgs--edit .react-resizable-handle-sw {
  background-image: none;
}

.dgs--edit .react-resizable-handle-e:hover,
.dgs--edit .react-resizable-handle-w:hover {
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--accent) 30%, transparent));
}

.dgs--edit .react-resizable-handle-s:hover {
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--accent) 30%, transparent));
}

/* ── Tay nắm resize = vùng bắt chuột VÔ HÌNH tuyệt đối ────────────────
   Icon mũi tên + transform xoay 45° mặc định của RGL biến tay nắm dài
   thành "thanh chéo chữ nhật" vắt ngang màn ở chế độ Sắp xếp (đã bị chửi
   2 lần). Chế độ Sắp xếp phải SẠCH như chế độ xem: tay nắm chỉ lộ qua
   con trỏ ↔/↕ và ánh accent khi rê vào. */
.dgs .react-resizable-handle,
.dgs .react-resizable-handle::before,
.dgs .react-resizable-handle::after {
  background: none !important;
  background-image: none !important;
  border: none !important;
  outline: none !important;
  transform: none !important;
  box-shadow: none !important;
}

/* ══ NỀN TOÀN MÀN + CARD NỔI (16/09) ════════════════════════════════════
   Màn có `__view:backdrop` trong screen_texts: card đó phủ KÍN màn phía sau
   lưới (bản đồ), các card khác nổi trên nó. Card nền bỏ khung panel; chỗ
   trống của lưới không bắt chuột để bản đồ phía sau vẫn kéo/zoom được. */
.dgs__backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
}

.dgs__backdrop > * {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

.dgs__backdrop .tp {
  border-radius: 0;
  box-shadow: none;
  background: var(--bg-void);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.dgs__backdrop .tp > .tp__head,
.dgs__backdrop .tp > .chd,
.dgs__backdrop .tp > .tp__rail,
.dgs__backdrop .tp--bracket::before,
.dgs__backdrop .tp--bracket::after {
  display: none;
}

/* Tối dần về mép màn — card nổi và chữ ở mép đọc rõ trên nền bản đồ sáng. */
.dgs__backdrop::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    ellipse 78% 72% at 50% 52%,
    transparent 50%,
    color-mix(in srgb, var(--bg-void) 72%, transparent) 100%
  );
}

.dgs--floating .dgs__grid {
  z-index: 1;
}

.dgs--floating:not(.dgs--edit) .dgs__grid {
  pointer-events: none;
}

.dgs--floating .dgi {
  pointer-events: auto;
}

/* Card nổi: kính mờ nhẹ + bóng sâu để tách khỏi nền bản đồ. */
.dgs--floating .dgi > .tp,
.dgs--floating .dgi > .mt {
  background: color-mix(in srgb, var(--bg-panel-solid) 82%, transparent);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--elev-float);
  backdrop-filter: blur(var(--blur-glass));
  -webkit-backdrop-filter: blur(var(--blur-glass));
}

/* Nút thao tác card trong chế độ Sắp xếp: làm nền · đưa vào ngăn · gỡ. */
.dgi__actions {
  position: absolute;
  top: 4px;
  right: 4px;
  z-index: var(--z-grid-drag);
  display: flex;
  gap: 4px;
}

.dgi__actions .dgi__remove {
  position: static;
}

.dgi__action {
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-void));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 50%, transparent);
  color: var(--accent);
  font-size: 11px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.dgi__action:hover {
  background: var(--accent);
  color: var(--bg-void);
}

/**
 * NGĂN PHẢI của màn — `ScreenDrawer`.
 *
 * Tách khỏi `draggable-grid.css` khi engine kéo-thả bị gỡ (bố cục chuyển vào
 * lưới CSS trong code). Ngăn phải KHÔNG thuộc về engine đó: nó vẫn là chỗ
 * chứa những card ít xem, nay do `ScreenFrame` dựng với nhóm khai bằng code.
 */

/* ══ NGĂN PHẢI (16/09) — `ScreenDrawer` ═════════════════════════════════
   Thanh tab dọc dính mép phải; bấm tab là ngăn trượt ra. Lớp ngoài không bắt
   chuột (chỉ tab và ngăn bắt) để nền phía sau vẫn tương tác được. */
.sdr {
  --sdr-w: clamp(420px, 32vw, 640px);
  position: absolute;
  inset: 0;
  z-index: var(--z-grid-drawer);
  pointer-events: none;
}

/* Màn có ngăn phải chừa mép phải đúng bề rộng thanh tab dọc (`.sdr__tab`: một dòng chữ
   `--fs-label` + đệm `--sp-1` hai bên, cộng một khe) — trước 15/10 tab đè lên cột số của card
   sát mép. `.sdr` và nền `.scr__backdrop` là `inset:0` theo khung đệm nên tab vẫn dính mép màn
   và bản đồ nền vẫn phủ kín; chỉ lưới card co lại. */
.scr--has-drawer {
  padding-right: calc(var(--fs-label) * 1.3 + var(--sp-1) * 3);
}

.sdr__rail {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  pointer-events: auto;
  transition: right 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

.sdr--open .sdr__rail {
  right: calc(var(--sdr-w) + var(--sp-2));
}

.sdr__tab {
  writing-mode: vertical-rl;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-1);
  border: 0;
  border-radius: var(--r-md) 0 0 var(--r-md);
  background: color-mix(in srgb, var(--bg-panel-solid) 88%, transparent);
  box-shadow: var(--sh-hair), var(--sh-sink);
  color: var(--text-mid);
  font: inherit;
  font-size: var(--fs-label);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
  backdrop-filter: blur(var(--blur-glass));
  -webkit-backdrop-filter: blur(var(--blur-glass));
  transition: color 0.15s, background 0.15s, box-shadow 0.15s;
}

.sdr__tab:hover {
  color: var(--text-hi);
}

.sdr__tab--on {
  color: var(--text-hi);
  background: color-mix(in srgb, var(--accent) 16%, var(--bg-panel-solid));
  box-shadow: inset 2px 0 0 var(--accent), var(--sh-sink);
}

.sdr__tab-count {
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

.sdr__panel {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  bottom: var(--sp-2);
  width: var(--sdr-w);
  display: flex;
  flex-direction: column;
  border-radius: var(--r-panel);
  background: color-mix(in srgb, var(--bg-panel-solid) 94%, transparent);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--elev-float);
  backdrop-filter: blur(var(--blur-glass));
  -webkit-backdrop-filter: blur(var(--blur-glass));
  transform: translateX(calc(100% + var(--sp-4)));
  transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: auto;
}

.sdr--open .sdr__panel {
  transform: none;
}

.sdr__head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  box-shadow: var(--sh-head);
}

.sdr__title {
  color: var(--text-hi);
}

.sdr__body {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(var(--sdr-cols, 1), minmax(0, 1fr));
  grid-auto-rows: minmax(0, 1fr);
  gap: var(--gutter);
  padding: var(--sp-2);
}

.sdr__cell {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.sdr__cell > :first-child {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

/* ── Đổi cỡ trong ngăn phải (16/09) ───────────────────────────────────────
   Hai tay kéo, chỉ hiện khi đang Sắp xếp: mép TRÁI ngăn đổi bề ngang, mép DƯỚI
   mỗi thẻ đổi chiều cao. Vùng bắt tay rộng hơn vạch nhìn thấy — kéo bằng chuột
   trên tường 55" không phải nhắm 2px. */

.sdr__grip {
  position: absolute;
  left: -5px;
  top: 0;
  bottom: 0;
  width: 12px;
  cursor: col-resize;
  touch-action: none;
  z-index: 3;
}

.sdr__grip::after {
  content: '';
  position: absolute;
  left: 5px;
  top: 50%;
  transform: translateY(-50%);
  width: 2px;
  height: 48px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 55%, transparent);
  transition: background 0.15s, height 0.15s;
}

.sdr__grip:hover::after {
  height: 72px;
  background: var(--accent);
}

.sdr__cell-grip {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -4px;
  height: 10px;
  cursor: row-resize;
  touch-action: none;
  z-index: 3;
}

.sdr__cell-grip::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 3px;
  transform: translateX(-50%);
  width: 44px;
  height: 2px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 45%, transparent);
  transition: background 0.15s, width 0.15s;
}

.sdr__cell-grip:hover::after {
  width: 72px;
  background: var(--accent);
}

/* Đã chốt chiều cao từng thẻ: hàng không chia đều nữa, tổng vượt ngăn thì CUỘN. */
.sdr__body--sized {
  grid-auto-rows: auto;
  overflow-y: auto;
}

@media (prefers-reduced-motion: reduce) {
  .sdr__rail,
  .sdr__panel {
    transition: none;
  }
}

/**
 * Dòng danh sách dùng chung — mẫu "viên rời" của hệ VSS.
 *
 * Trước đây mỗi màn tự viết nền/đệm/bo góc cho dòng của mình, nên cùng một thứ
 * (một dòng thiết bị) lại dày mỏng khác nhau giữa các màn. File này là bản duy
 * nhất; màn nào cần dòng danh sách thì gắn `.lrow`, không tự chế bản riêng.
 *
 * Phân tách bằng NỀN, không bằng đường kẻ — nhiều đường kẻ cạnh nhau trên wall
 * đọc thành lưới xám và nuốt mất nội dung.
 */

.lrow {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--pad-row);
  background: var(--bg-row);
  border-radius: var(--r-row);
  min-width: 0;
  transition: background 0.15s;
}

.lrow:hover {
  background: var(--bg-row-hover);
}

.lrow--sel {
  background: var(--bg-row-sel);
}

/* Dòng bấm được: dùng <button> thật để có bàn phím và trình đọc màn hình,
   nên phải gỡ style mặc định của nút. */
.lrow--btn {
  font: inherit;
  text-align: left;
  border: none;
  cursor: pointer;
}

.lrow__head {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  min-width: 0;
}

.lrow__name {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-title);
  line-height: var(--lh-label);
  color: var(--text-hi);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.lrow__meta {
  min-width: 0;
  font-size: var(--fs-cap);
  color: var(--text-lo);
  letter-spacing: var(--track-chip);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Số trong dòng: luôn tabular, nếu không thì số nhảy realtime làm giật cả cột. */
.lrow__num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-num);
}

/* Dải tiêu đề NHÓM trong danh sách — bản mẫu S07 gom camera theo khu vực và
   ghi số đang trực tuyến trên tổng ("Khu vực 1  3/3"). Không phải một dòng dữ
   liệu, nên nhạt hơn hẳn dòng thường. */
.lgroup {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2) var(--sp-1);
  font-size: var(--fs-cap);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-lo);
}

.lgroup::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--line-soft), transparent);
}

.lgroup > b {
  flex: 0 0 auto;
  font-family: var(--font-num);
  font-weight: var(--fw-value);
  letter-spacing: 0;
  color: var(--text-mid);
}

/* ══════════════════════════════════════════════════════════════════════
   Chấm trạng thái — StatusDot.tsx
   ══════════════════════════════════════════════════════════════════════ */

/* Chấm CHỈ hiện khi trạng thái khác bình thường. Dòng nào cũng có chấm thì
   chấm hết nghĩa và mắt người trực không còn chỗ bám. */
.sdot {
  width: var(--dot-size);
  height: var(--dot-size);
  border-radius: var(--r-pill);
  flex: 0 0 auto;
  background: currentColor;
  box-shadow: 0 0 8px -2px currentColor;
}

.sdot--good { color: var(--st-good); }
.sdot--warn { color: var(--st-warn); }
.sdot--crit { color: var(--st-crit); }
.sdot--info { color: var(--st-info); }
.sdot--idle { color: var(--st-idle); }

/* Nhấp nháy CHỈ dành cho mức nguy cấp — một trong hai ngoại lệ được phép
   chuyển động lặp vô hạn (rule 06 §4). `steps()` để nó chớp dứt khoát chứ
   không thở mờ ảo, nhìn từ 5m mới bắt được. */
.sdot--pulse {
  animation: sdot-blink 1.4s steps(1, end) infinite;
}

@keyframes sdot-blink {
  0%, 55% { opacity: 1; }
  56%, 100% { opacity: 0.18; }
}

/* ══════════════════════════════════════════════════════════════════════
   Dòng thiết bị — DeviceRow.tsx
   ══════════════════════════════════════════════════════════════════════ */

/* 20/10: lưới hai cột — tên + mã bên trái, SỐ ĐO bên phải ăn trọn hai hàng chữ, đường xu
   hướng là hàng thứ ba chạy hết bề ngang. Số là thứ đọc từ xa nên nó to và đứng riêng. */
.drow {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto auto;
  align-content: start;
  column-gap: var(--sp-2);
  row-gap: 1px;
}

/* Dòng không có xu hướng: hai hàng chữ đứng giữa dòng, không dồn lên đỉnh chừa đáy trống. */
.drow--flat {
  align-content: center;
}

.drow > .lrow__head { grid-column: 1; grid-row: 1; }
.drow > .lrow__meta { grid-column: 1; grid-row: 2; }

.drow__val {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  display: flex;
  align-items: baseline;
  gap: var(--sp-1);
  white-space: nowrap;
}

.drow__num {
  font-family: var(--font-num);
  font-size: var(--fs-sub);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-value);
  line-height: 1;
  color: var(--text-hi);
}

.drow__unit {
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

/* Thiết bị không định lượng (camera): chữ trạng thái thay số. Bình thường thì xám — chỉ khi
   có chuyện mới mang màu mức. */
.drow__val--word {
  font-size: var(--fs-cap);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  color: var(--text-mid);
}

.drow__val--crit { color: var(--st-crit); }
.drow__val--warn { color: var(--st-warn); }
.drow__val--idle { color: var(--st-idle); }

/* Đường xu hướng nằm dưới cùng, chạm mép trái-phải của dòng — là NGỮ CẢNH của số, nên mờ
   hơn chữ. `pointer-events` tắt để nó không nuốt cú bấm vào dòng. */
.drow__spark {
  grid-column: 1 / -1;
  grid-row: 3;
  margin-top: 2px;
  height: 18px;
  min-height: 0;
  pointer-events: none;
  opacity: 0.6;
}

/* ══════════════════════════════════════════════════════════════════════
   Dòng sự kiện AI — EventRow.tsx
   ══════════════════════════════════════════════════════════════════════ */

.erow {
  /* Mức độ mặc định: không tô. Dòng KHÔNG truyền `severity` (màn Tổng quan) giữ nguyên
     hình dạng cũ — vạch trong suốt, không chiếm thêm chỗ. */
  --erow-rail: transparent;
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--pad-row);
  background: var(--bg-row);
  border-radius: var(--r-row);
  min-width: 0;
  font: inherit;
  text-align: left;
  border: none;
  cursor: pointer;
  /* Vạch mức độ vẽ bằng `inset box-shadow` chứ không phải `border-left`: không đụng vào hộp
     nên không đẩy chữ sang, và tự bo theo `--r-row`. */
  box-shadow: inset 3px 0 0 var(--erow-rail);
  transition:
    background 0.15s,
    transform 0.12s ease;
}

.erow:hover {
  background: var(--bg-row-hover);
}

/* Bấm thì LÚN nhẹ — cùng ngôn ngữ xúc giác với nút bàn PTZ (`ptz-pad.css`). Dòng sự kiện là
   thứ bấm được (nhảy tới camera), mà trước đây bấm xuống không có phản hồi nào. */
.erow:active {
  transform: translateY(1px);
}

/* Mức độ → màu vạch. Một nghĩa màu toàn app (`utils/severity.ts` → token `--st-*`).
   ⚠ Màu chỉ để QUÉT nhanh ở 3–5 m; nghĩa thật nằm ở badge có CHỮ bên trong dòng. */
.erow--crit { --erow-rail: var(--st-crit); }
.erow--warn { --erow-rail: var(--st-warn); }
.erow--info { --erow-rail: var(--st-info); }
.erow--good { --erow-rail: var(--st-good); }
.erow--idle { --erow-rail: var(--st-idle); }

.erow__body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

/* XUỐNG DÒNG, không cắt cụt (21/09) — cùng lý do `AlarmRow` tồn tại: cột phải của màn An
   ninh chỉ rộng ~280px, mà "Phát hiện khói / lửa" cắt thành "Phát hiện khói / …" là mất đúng
   chữ quan trọng nhất. Chặn 2 dòng để danh sách không thành một khối chữ. */
.erow__type {
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-title);
  line-height: var(--lh-label);
  color: var(--text-hi);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.erow__type--crit {
  color: var(--st-crit);
}

.erow__meta {
  font-size: var(--fs-cap);
  color: var(--text-lo);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Độ tin cậy của mô hình: là SỐ nên dùng font số, và neo phải để mắt dò dọc
   một cột thay vì nhảy theo độ dài tên sự kiện. */
.erow__conf {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
}

.erow__conf-num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-body);
  font-weight: var(--fw-value);
  color: var(--accent-vision);
}

/* Nhãn của con số. Một con số trần trên tường (`0,94`) là con số không ai biết là gì —
   nhất là khi nó KHÔNG phải phần trăm mà là điểm của mô hình. */
.erow__conf-label {
  font-size: var(--fs-micro);
  line-height: var(--lh-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
}

/* ── Khung hình thật của một sự kiện — AiThumb.tsx chế độ ảnh ────────────
   Khung nhận dạng chỉ vẽ khi có `bbox` thật — không có thì không khoanh. */
.aithumb-frame {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--r-key);
  background: var(--bg-void);
  box-shadow: var(--sh-hair);
}

.aithumb-frame__img {
  width: 100%;
  height: 100%;
  /* Ô 52×36 không cùng tỉ lệ ảnh: `fill` ép ảnh đầy ô (méo nhẹ) để box % trùng đúng chỗ đã chấm. */
  object-fit: fill;
  display: block;
  /* Khung hình camera đêm vốn tối; hạ thêm một chút để chữ trắng cạnh nó vẫn là thứ nổi nhất. */
  opacity: 0.86;
}

.aithumb-frame--note {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--sp-1);
  text-align: center;
  font-size: var(--fs-micro);
  line-height: var(--lh-label);
  color: var(--text-faint);
}

/* Ảnh lớn trong hộp thoại sự kiện AI: ảnh đủ sáng, khung nhận dạng dày hơn để thấy rõ trên khung 960px. */
.ai-snap .aithumb-frame__img {
  opacity: 1;
}

.ai-snap .aithumb-frame__box {
  border-width: 2px;
}

.ai-snap .aithumb-frame--note {
  font-size: var(--fs-body);
}

.aithumb-frame__box {
  position: absolute;
  border: 1px solid currentcolor;
  border-radius: 1px;
  /* Nét đứt = "máy khoanh", phân biệt với khung cứng của giao diện. */
  border-style: dashed;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .erow,
  .erow:active {
    transition: background 0.15s;
    transform: none;
  }

  .sdot--pulse {
    animation: none;
  }
}

/* Vùng cuộn của một DANH SÁCH dòng (đối xứng với `.tt-wrap` của bảng).
   Bản mẫu gọi là `.scroll`; ở đây đặt tên theo vai trò để khỏi đụng tên chung. */
.list-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--sp-1) 0 var(--sp-2);
}

/* Danh sách rỗng: nói thẳng, không để mảng đen (bản mẫu dùng `.empty`). */
.list-empty {
  padding: var(--sp-5) var(--sp-3);
  text-align: center;
  color: var(--text-lo);
  font-size: var(--fs-body);
}

/* Cảnh HỎNG có đường tự chữa — màn tường không có ai bấm F5 (rule 03 §6). */
.list-empty__retry {
  display: block;
  margin: var(--sp-3) auto 0;
}

/* Cảnh ĐANG TẢI: khung xám ĐÚNG HÌNH dòng thật, không phải vòng xoay. Bảng không nhảy
   chiều cao lúc dữ liệu về, và người xem biết chỗ này sắp có gì. */
.list-skel {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-1) 0 var(--sp-2);
}

.list-skel__row {
  /* Cao đúng bằng `.erow`: ảnh 36px + hai lần `--pad-row`. */
  height: calc(36px + 2 * clamp(7px, calc(var(--ui-px) * 7), 10px));
  border-radius: var(--r-row);
  background: var(--bg-row);
  box-shadow: var(--sh-hair);
}

.erow__thumb {
  display: block;
  width: 52px;
  height: 36px;
}


/* ══════════════════════════════════════════════════════════════════════
   Dòng cảnh báo — AlarmRow.tsx
   Bốn cột: vạch mức độ · giờ · nội dung (co giãn) · số đo neo phải.
   Nội dung ĐƯỢC xuống dòng — đây là khác biệt so với bảng, và là lý do
   dòng này tồn tại (bảng cắt cụt câu trong thẻ hẹp).
   ══════════════════════════════════════════════════════════════════════ */

.arow {
  display: grid;
  /* vạch mức độ · giờ · [ảnh nếu có] · nội dung · số đo.
     `auto` cho cột ảnh: dòng không có ảnh thì cột rộng 0, không tạo lệch. */
  grid-template-columns: 2px auto auto minmax(0, 1fr) auto;
  gap: 7px;
  align-items: start;
  width: 100%;
  padding: var(--pad-row);
  background: var(--bg-row);
  border-radius: var(--r-row);
  border: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s;
}

.arow:hover {
  background: var(--bg-row-hover);
}

.arow__bar {
  width: 2px;
  height: 100%;
  min-height: 24px;
  border-radius: 2px;
  background: var(--ac, var(--st-info));
  box-shadow: 0 0 9px -1px var(--ac, var(--st-info));
}

.arow__time {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  /* Giờ và nguồn giữ cỡ caption; cả thang nay đã co giãn theo màn (`--ui-px`)
     nên không cần nhích tay từng chỗ nữa. */
  font-size: var(--fs-cap);
  color: var(--text-lo);
  padding-top: 1px;
}

/* Ngày của cảnh báo cũ — dòng thứ hai trong CÙNG cột giờ, nhỏ và nhạt hơn để
   giờ vẫn là thứ đọc trước. Cột nội dung bên cạnh vốn đã hai dòng nên thêm dòng
   này thường không làm cao thêm hàng. */
.arow__date {
  display: block;
  font-style: normal;
  font-size: var(--fs-micro);
  color: var(--text-faint);
  line-height: 1.2;
}

.arow__body {
  min-width: 0;
}

/* Không `white-space: nowrap`: câu dài xuống dòng chứ không cắt cụt. */
.arow__msg {
  display: block;
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-strong);
  line-height: 1.3;
  color: var(--text-hi);
}

.arow__src {
  display: block;
  margin-top: 2px;
  font-style: normal;
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  color: var(--text-lo);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.arow__side {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-cap);
  color: var(--ac, var(--text-mid));
  text-align: right;
  white-space: nowrap;
  /* Số đo và nút thao tác xếp DỌC, canh phải. Để mặc định (inline, nowrap) thì
     hai thứ nằm cùng một dòng và nút đè lên số — "0 / 0,85" bị nút "Xác nhận
     xử lý" trùm lên (26/08). */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

.arow__st {
  display: block;
  font-style: normal;
  font-size: var(--fs-micro);
  color: var(--text-lo);
}


/* ══════════════════════════════════════════════════════════════════════
   Danh sách CẮT THEO DÒNG NGUYÊN — cột thiết bị màn Tổng quan
   Yêu cầu 23/08: ô cao vừa 1 dòng thì hiện đúng 1 dòng, vừa 3 thì hiện 3,
   phần còn lại cuộn xuống. KHÔNG để dòng bị cắt ngang thân.

   Làm bằng CSS thuần, không ResizeObserver: app chạy 24/7, thêm một
   observer cho mỗi card là thêm một chỗ có thể rò. `round(down, …)` cắt
   chiều cao khung xuống bội số gần nhất của một bước dòng.
   ══════════════════════════════════════════════════════════════════════ */

.bcol {
  /* Bước dòng = thân dòng + rãnh. Sửa một chỗ này là cả phép cắt đi theo.
     64px = 7 đệm trên + 16 tên + 1 rãnh + 13 dòng phụ + 2 rãnh + 22 đường
     xu hướng + 7 đệm dưới. Ghim thấp hơn con số này là BÓP nội dung: dòng
     phụ bị nén còn 4,6px và mất chữ — lỗi mắc ngày 23/08. */
  /* 14/09 (lệnh user: hàng thiết bị chiếm quá nhiều chỗ, map bé): dòng
     GỌN 44px — tên + dòng phụ xếp chồng, đường xu hướng thành NỀN MỜ đè dưới
     chữ thay vì một hàng riêng 22px. 44 = 4 đệm + 16 tên + 1 + 13 phụ + 4 đệm
     + 6 dư cho dấu tiếng Việt. */
  /* 23/09 (lệnh user: biểu đồ như bản mẫu S01): quay lại dòng 64px với đường
     xu hướng là HÀNG RIÊNG 22px, tô dốc + chấm mốc cuối — công thức 64 ở trên.
     Bù chỗ: dải đáy Tổng quan cao thêm (`--ov-dock-h` trong screen-grids.css). */
  --drow-h: 64px;
  --drow-step: calc(var(--drow-h) + var(--gap-row));

  /* Khung tự cuộn (thay vì để .tp__body cuộn) — nhờ vậy `100%` ở dưới đo
     đúng phần đất trong thân panel, không dính chiều cao đầu panel. */
  overflow-y: auto;
  /* n dòng chiếm n×dòng + (n−1)×rãnh, KHÔNG phải n×(dòng+rãnh) — dòng cuối
     không có rãnh theo sau. Cộng một rãnh vào trước khi chia rồi trừ lại,
     nếu không khung dư đúng một rãnh và nhìn như "hở một tí" ở đáy. */
  max-height: calc(round(down, 100cqh + var(--gap-row), var(--drow-step)) - var(--gap-row));

  /* Cuộn dừng đúng mép dòng, không dừng lửng lơ giữa hai dòng. */
  scroll-snap-type: y proximity;
}

/* Chiều cao dòng phải CỐ ĐỊNH thì phép chia mới đúng. Tên dài cắt đuôi
   thay vì xuống hàng — đo 23/08: một dòng tên hai hàng cao 62px trong khi
   các dòng khác 45px, đủ để lệch cả cột. */
.bcol .drow {
  height: var(--drow-h);
  flex: 0 0 var(--drow-h);
  scroll-snap-align: start;
}

/* Ba phần bên trong KHÔNG được co. Mặc định của flex là cho phép co, nên khi
   khung bị ghim chiều cao thì chữ bị nén dẹt thay vì giữ nguyên và tràn —
   đúng kiểu hỏng "mất chữ dòng phụ". */
.bcol .drow .lrow__head,
.bcol .drow .lrow__meta {
  flex: 0 0 auto;
}

.bcol .drow .lrow__meta {
  line-height: 1.3;
}

.bcol .drow {
  padding-top: 7px;
  padding-bottom: 7px;
  justify-content: flex-start;
}

/* Xu hướng là HÀNG RIÊNG dưới chữ (bản mẫu S01 `.bitem svg`): rộng hết dòng, 18px (20/10:
   22px → 18px, chỗ dôi ra để chữ thở), mờ hơn chữ — xem `.drow__spark`. */
.bcol .drow .drow__spark {
  position: static;
  height: 18px;
  margin: 4px 0 0;
}

.bcol .drow .lrow__head,
.bcol .drow .lrow__meta {
  position: relative;
  z-index: 1;
}

.bcol .drow .lrow__name {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Ảnh nhỏ đầu dòng — chỉ sự kiện AI có. Lưới của `.arow` tự nhận thêm cột
   nhờ `grid-auto-columns`, nên dòng không có ảnh vẫn thẳng hàng với dòng có. */
.arow__thumb {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}

/* Dòng sự kiện có NÚT PHỤ (⏪ xem lại, 11/09): thêm một cột auto cho nút. */
.erow--act {
  grid-template-columns: 52px minmax(0, 1fr) auto auto;
}

/* ── Nội dung card dùng chung ───────────────────────────────────────────────────────────
   Hai khuôn dưới đây không chứa dữ liệu/nghiệp vụ theo màn. Mỗi màn chỉ khai báo cột và
   nội dung dòng; màu mặc định lấy từ `accent` hiện tại. */

/* Vùng cuộn của khuôn thẻ cảnh báo. ⛔ Con của `.card-feed` BẮT BUỘC
   `flex: 0 0 auto`: màn có luật `.scr * { min-height: 0 }` nên flex item mặc định bị
   CO để vừa khung — dòng lịch sử bị nén đè chữ, thẻ cảnh báo co về 0 (lỗi 03/10). */
.card-feed {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  gap: var(--sp-1);
  min-height: 0;
  overflow-y: auto;
}

.card-feed > * {
  flex: 0 0 auto;
}

/* ══ KHUÔN 2 — "THẺ CẢNH BÁO" (`card-feed`) ═══════════════════════════════════════
   Vùng cuộn chứa các thẻ hai dòng. "VIỀN MÀU" thay vạch trái (20/10): thẻ trung tính có
   viền mảnh; chỉ Nghiêm trọng (viền đỏ + nền ửng) và Cảnh báo (viền cam) mang màu — thẻ đã
   xử lý lùi về nền nên thẻ đang mở nổi lên. Nghĩa thật vẫn ở nhãn chữ trạng thái. */
.card-feed__card {
  --card-feed-tone: var(--st-info);
  --card-feed-ring: var(--line-soft);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border-radius: var(--r-md);
  background: var(--bg-raised);
  box-shadow: inset 0 0 0 1px var(--card-feed-ring), var(--sh-lite);
  overflow: hidden;
}

.card-feed__card--crit {
  --card-feed-tone: var(--st-crit);
  --card-feed-ring: color-mix(in srgb, var(--st-crit) 55%, transparent);
  background: color-mix(in srgb, var(--st-crit) 8%, var(--bg-raised));
}

.card-feed__card--warn {
  --card-feed-tone: var(--st-warn);
  --card-feed-ring: color-mix(in srgb, var(--st-warn) 40%, transparent);
}

.card-feed__card--good {
  --card-feed-tone: var(--st-good);
}

.card-feed__card-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--sp-3);
  row-gap: 2px;
  padding: var(--sp-1) var(--sp-3);
}

.card-feed__card-msg {
  margin: 0;
  overflow: hidden;
  color: var(--text-hi);
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-value);
  line-height: 1.25;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

.card-feed__card-reading {
  justify-self: end;
  color: var(--card-feed-tone);
  font-family: var(--font-num);
  font-size: var(--fs-body);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-value);
  white-space: nowrap;
}

.card-feed__card-meta {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  min-width: 0;
  white-space: nowrap;
}

.card-feed__card-time {
  /* Không co: dòng meta chật thì nguồn (cột giữa) nhường chỗ, giờ + ngày không bị cắt đè chữ (20/10). */
  flex: 0 0 auto;
  color: var(--text-lo);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  font-variant-numeric: tabular-nums;
}

.card-feed__card-date {
  margin-left: var(--sp-1);
  color: var(--text-faint);
  font-size: var(--fs-micro);
}

.card-feed__card-state {
  padding: 1px var(--sp-2);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--card-feed-tone) 14%, transparent);
  color: var(--card-feed-tone);
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* Nguồn/điểm đo nằm giữa thời gian và trạng thái; co trước để hai đầu luôn đọc được. */
.card-feed__card-source {
  min-width: 0;
  overflow: hidden;
  color: var(--text-faint);
  font-size: var(--fs-cap);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ══ KHUÔN 1 — "BẢNG DÒNG" (`card-table`) ════════════════════════════════════════
   Khuôn thay thế bảng HTML khi mỗi bản ghi cần hiển thị như một thẻ. Cấu tạo:
     .card-table            lưới cha khai cột (modifier theo nội dung chỉ đổi số cột)
     .card-table__caption   dòng tiêu đề cột (tuỳ chọn, dính đỉnh khi cuộn)
     .card-table__row       một dòng = subgrid ăn đúng cột của cha ⇒ số/đơn vị thẳng hàng
     .card-table__key / __name / __num / __bar / __label   các loại ô
   Mọi dòng chung một hệ cột (`max-content` theo dòng dài nhất) + số căn phải nên `m³/h`,
   `m³` luôn thẳng hàng dù số lớn hay nhỏ. Chỉ cuộn DỌC. ⛔ Con của `.card-table` BẮT BUỘC
   không co (màn có luật `.scr * { min-height: 0 }`). */
.card-table {
  --card-table-cols: max-content minmax(0, 1fr) max-content max-content;
  display: grid;
  flex: 1 1 0;
  grid-template-columns: var(--card-table-cols);
  grid-auto-rows: max-content;
  align-content: start;
  gap: var(--sp-1) var(--sp-3);
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  container-type: inline-size;
}

.card-table--meters {
  --card-table-cols: max-content minmax(0, 1fr) max-content max-content max-content;
}

/* Lịch sử điện: mốc giờ + ba đại lượng trên hàng đầu, ba đại lượng còn lại tự xuống
   hàng hai trong cùng một thẻ. Khuôn này thay bảng HTML bảy cột bị bóp trong ngăn. */
.card-table--energy-history {
  --card-table-cols: max-content repeat(3, minmax(max-content, 1fr));
}

.card-table--energy-history .card-table__row {
  row-gap: var(--sp-2);
}

/* Tủ/công tơ có một hàng thông tin chính và lưới số ba pha bên dưới. Vẫn dùng mặt
   thẻ, khoảng cách và vùng cuộn của `card-table`, chỉ đổi bố cục nội dung đặc thù. */
.card-table--energy-meters {
  --card-table-cols: minmax(0, 1fr);
}

.card-table--energy-meters .card-table__row {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.card-table--energy-meters .card-table__meter-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

.card-table__meter-time {
  color: var(--text-faint);
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  white-space: nowrap;
}

.card-table__meter-phases {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-1) var(--sp-3);
  color: var(--text-hi);
  font-family: var(--font-num);
  font-size: var(--fs-body);
}

.card-table__meter-phases span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-table > .list-empty {
  grid-column: 1 / -1;
}

.card-table__row {
  /* Cùng ngôn ngữ "viền màu" với khuôn 2: dòng thường trung tính, chỉ dòng hỏng mang màu. */
  --card-row-tone: var(--rail, var(--accent));
  --card-row-ring: var(--line-soft);
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: center;
  row-gap: 2px;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-md);
  background: var(--bg-raised);
  box-shadow: inset 0 0 0 1px var(--card-row-ring), var(--sh-lite);
}

.card-table__row--warn {
  --card-row-tone: var(--st-warn);
  --card-row-ring: color-mix(in srgb, var(--st-warn) 40%, transparent);
}

.card-table__row--crit {
  --card-row-tone: var(--st-crit);
  --card-row-ring: color-mix(in srgb, var(--st-crit) 55%, transparent);
  background: color-mix(in srgb, var(--st-crit) 8%, var(--bg-raised));
}

.card-table__caption {
  position: sticky;
  top: 0;
  z-index: 1;
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: baseline;
  padding: 0 var(--sp-3) 2px;
  background: var(--bg-void);
  color: var(--text-lo);
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.card-table__caption > :nth-child(n + 3) {
  text-align: right;
}

.card-table__key {
  color: var(--text-hi);
  font-family: var(--font-num);
  font-size: var(--fs-body-lg);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-value);
  white-space: nowrap;
}

.card-table__name {
  overflow: hidden;
  color: var(--text-mid);
  font-size: var(--fs-body);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Dòng hai tầng: tên đồng hồ chiếm phần còn lại của tầng trên. */
.card-table__name--wide {
  grid-column: 2 / -1;
}

.card-table__num {
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

.card-table__num strong {
  color: var(--text-hi);
  font-family: var(--font-num);
  font-size: var(--fs-body);
  font-weight: var(--fw-value);
}

/* Số chính của dòng: to hơn, mang màu trạng thái. */
.card-table__num--lead strong {
  color: var(--card-row-tone);
  font-size: var(--fs-sub);
}

.card-table__num em {
  margin-left: var(--sp-1);
  color: var(--text-lo);
  font-size: var(--fs-cap);
  font-style: normal;
}

.card-table__label {
  margin-right: var(--sp-1);
  color: var(--text-faint);
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.card-table__bar {
  height: clamp(4px, calc(var(--ui-px) * 5), 7px);
  overflow: hidden;
  border-radius: var(--r-pill);
  background: var(--bg-well);
}

.card-table__bar i {
  display: block;
  height: 100%;
  min-width: 2px;
  border-radius: var(--r-pill);
  background: var(--card-row-tone);
  box-shadow: 0 0 10px -2px var(--card-row-tone);
  transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}

.card-table__row > .pill {
  justify-self: end;
  font-size: var(--fs-cap);
}

/* Card hẹp: bỏ nhãn chữ "LŨY KẾ"/"NGÀY" để không tràn ngang (số vẫn đủ). */
@container (max-width: 520px) {
  .card-table__label {
    display: none;
  }
}

/* Xếp hạng (RankBars): thứ hạng · tên + thanh tỷ lệ · giá trị. */
.card-table--rank { --card-table-cols: max-content minmax(0, 1fr) max-content; }
.card-table--rank .card-table__key { color: var(--card-row-tone); }
.card-table--rank .card-table__bar { grid-column: 2 / -1; }
.card-table__code { margin-left: var(--sp-2); color: var(--text-faint); font-family: var(--font-num); font-size: var(--fs-micro); font-weight: var(--fw-label); }


/* Tủ điện và công tơ: tên, mã, công suất và số ba pha. */
.card-table__meter-name { flex: 1 1 auto; min-width: 0; overflow: hidden; color: var(--text-hi); font-size: var(--fs-body-lg); font-weight: var(--fw-value); text-overflow: ellipsis; white-space: nowrap; }
.card-table__meter-code { color: var(--text-lo); font-size: var(--fs-cap); white-space: nowrap; }
.card-table__meter-power { display: flex; align-items: baseline; gap: var(--sp-1); }
.card-table__meter-power-value { color: var(--accent); font-size: var(--fs-sub); font-weight: var(--fw-value); }
.card-table__meter-unit { color: var(--text-lo); }
.card-table__meter-phases .num { font-weight: var(--fw-value); }

/* Nước thải: dữ liệu quan trắc QCVN — chỉ tiêu · giá trị · ngưỡng · trạng thái. */
.card-table--ww-params { --card-table-cols: minmax(0, 1fr) max-content max-content max-content; }
.card-table--ww-params .card-table__name { color: var(--text-hi); font-weight: var(--fw-title); }
.card-table--ww-params .card-table__caption :nth-child(2) { text-align: right; }

/* Thông tin tủ (CabinetInfo): nhãn · thanh mức · số. */
.card-table--cabinet { --card-table-cols: max-content minmax(0, 1fr) max-content; }
.card-table--cabinet .card-table__name { color: var(--text-hi); }

/* Tiêu đề cột dài (tên trạm) xuống dòng theo từ: góp bề rộng cố định vào cột, không kéo cột
   giãn theo cả câu. */
.card-table__caption-wrap { width: 7em; justify-self: end; white-space: normal; line-height: 1.25; }
/* Tiêu đề phủ hai cột (thanh + %). */
.card-table__caption > .card-table__caption-span { grid-column: span 2; text-align: left; }
/* Chữ phụ neo phải của dòng ("Tháng trước …"). */
.card-table__prev { color: var(--text-lo); font-size: var(--fs-cap); text-align: right; white-space: nowrap; }

/* Môi trường — chỉ tiêu không khí: chỉ tiêu · TB · N trạm (runtime) · ngưỡng · thanh % · %. */
.card-table--env-air {
  --card-table-cols: minmax(0, 1fr) max-content repeat(var(--env-air-stations, 1), max-content) max-content minmax(48px, 0.7fr) max-content;
}
/* Bảy chỉ tiêu giãn đều kín card như bảng cũ (không để khoảng chết dưới đáy); thiếu chỗ thì dòng
   giữ cao tối thiểu và cuộn. Hàng đầu là dòng tiêu đề cột — không giãn. */
.card-table--env-air {
  grid-template-rows: max-content;
  grid-auto-rows: minmax(max-content, 1fr);
  /* Thanh "% ngưỡng" của dòng đạt: xanh, không tím của màn (tím trùng mức "Rất xấu" của VN_AQI).
     Dòng warn/crit tự đè `--card-row-tone` nên vẫn cam/đỏ. */
  --rail: var(--st-good);
}
.card-table--env-air .card-table__caption { align-items: end; }
/* Tiêu đề cột hai bảng chỉ tiêu: `--fs-micro` không đọc nổi từ 3 m (20/10). */
.card-table--env-air .card-table__caption,
.card-table--env-surface .card-table__caption { font-size: var(--fs-cap); }
/* Tên trạm dài ("Trạm không khí ranh phía Nam"): đủ rộng để gãy 2 dòng, không thành cột chữ dọc. */
.card-table--env-air .card-table__caption-wrap { width: 11em; }
.card-table--env-air .card-table__name,
.card-table--env-surface .card-table__name { color: var(--text-hi); font-weight: var(--fw-strong); }

/* Môi trường — nước mặt: chỉ tiêu · thượng lưu · hạ lưu · ngưỡng. Tám chỉ tiêu phải hiện TRỌN
   không cuộn (màn tường không ai cuộn) nên dòng gọn hơn khuôn chuẩn. */
.card-table--env-surface { --card-table-cols: minmax(0, 1fr) max-content max-content max-content; row-gap: 2px; }
.card-table--env-surface .card-table__row { padding-block: 2px; }
.card-table--env-surface .card-table__caption :nth-child(2) { text-align: right; }

/* Môi trường — khí thải ống khói: nhà máy/ống · 4 chất · O₂ · trạng thái. */
.card-table--env-stacks { --card-table-cols: minmax(0, 1fr) repeat(5, max-content) max-content; }
.card-table--env-stacks .card-table__caption { align-items: end; }
.card-table--env-stacks .card-table__caption :nth-child(2) { text-align: right; }

/* Môi trường — chất thải: loại · tháng này · tháng trước. */
.card-table--env-waste { --card-table-cols: minmax(0, 1fr) max-content max-content; }
.card-table--env-waste .card-table__name { color: var(--text-hi); font-weight: var(--fw-strong); }

/* Môi trường — tuân thủ: mốc + lần gần nhất · hạn + số ngày · trạng thái. */
.card-table--env-compliance { --card-table-cols: minmax(0, 1fr) max-content max-content; }

/* Nước cấp — chỉ số công tơ: giờ · Qt · lũy kế (giữa co giãn, hai số neo phải). */
.card-table--water-index { --card-table-cols: max-content minmax(0, 1fr) max-content; }
.card-table--water-index .card-table__caption :nth-child(2) { text-align: right; }

/* Chiếu sáng — lịch vận hành: giờ · lệnh (co theo chữ) · đối tượng (nuốt chỗ trống) · đếm ngược. */
.card-table--light-schedule { --card-table-cols: max-content max-content minmax(0, 1fr) max-content; }
.card-table--light-schedule .card-table__caption :nth-child(3) { text-align: left; }
.card-table--light-schedule .card-table__row > :nth-child(2) { color: var(--text-hi); font-weight: var(--fw-strong); }

/* Chiếu sáng — nhật ký lệnh: giờ · thao tác · kết quả. */
.card-table--light-log { --card-table-cols: max-content minmax(0, 1fr) max-content; }
/* Hàng 1 là chữ inline để `card-table__name` cắt bằng dấu "…" khi chật. */
.light-log__cab { color: var(--text-hi); font-weight: var(--fw-strong); }
.light-log__blk { margin-left: var(--sp-2); }
.light-log__blk--on { color: var(--st-good); }
.light-log__blk--off { color: var(--text-lo); }
.light-log__blk--mode { color: var(--text-mid); }
.light-log__verb { font-weight: var(--fw-strong); text-transform: uppercase; }

/* ── Dòng BẤM ĐƯỢC của khuôn 1 (`<button class="card-table__row card-table__row--btn">`) ── */
.card-table__row--btn {
  width: 100%;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s;
}
.card-table__row--btn:hover {
  background: color-mix(in srgb, var(--card-row-tone) 12%, var(--bg-raised));
}
.card-table__row--btn:focus-visible { outline: 2px solid var(--card-row-tone); outline-offset: -2px; }
/* Dòng đang chọn: viền sáng cả khung + nền đậm hơn — đọc được từ 3–5 m. */
.card-table__row--sel,
.card-table__row--sel:hover {
  background: color-mix(in srgb, var(--card-row-tone) 18%, var(--bg-raised));
  box-shadow: inset 0 0 0 1px var(--card-row-tone);
}

/* Ô xếp chồng: tên + dòng phụ trong cùng một cột. */
.card-table__stack { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.card-table__sub { overflow: hidden; color: var(--text-lo); font-size: var(--fs-cap); text-overflow: ellipsis; white-space: nowrap; }

/* Dải tiêu đề NHÓM nằm trong lưới khuôn 1 — trải hết các cột. */
.card-table > .lgroup { grid-column: 1 / -1; padding-inline: var(--sp-1); }

/* An ninh — danh sách camera: chấm trạng thái · tên + dòng phụ · nhãn AI. */
.card-table--cam-list { --card-table-cols: max-content minmax(0, 1fr) max-content; }
.card-table--cam-list .card-table__name { color: var(--text-hi); font-weight: var(--fw-title); }
/* Ô chọn khu vực đứng trên danh sách: không được co (luật `.scr * { min-height: 0 }`). */
.tp__body--card-list > .tsel { flex: none; height: 28px; }

/* ── KHUÔN 2 có ẢNH và NÚT (`card-feed__card--media`) — sự kiện AI ──────────────────────
   ảnh 52×36 · thân (loại · tin cậy / giờ · nguồn / badge) · nút ⏪. */
.card-feed__card--media {
  grid-template-columns: max-content minmax(0, 1fr) max-content;
  align-items: center;
}
/* Thẻ BẤM ĐƯỢC (`role="button"`) mới có con trỏ tay và hover — thẻ chỉ để nhìn thì không giả vờ bấm được. */
.card-feed__card[role='button'] { cursor: pointer; transition: background 0.15s; }
.card-feed__card[role='button']:hover { background: color-mix(in srgb, var(--card-feed-tone) 12%, var(--bg-raised)); }
.card-feed__card[role='button']:focus-visible { outline: 2px solid var(--card-feed-tone); outline-offset: -2px; }
.card-feed__card-thumb { display: block; width: 52px; height: 36px; margin-left: var(--sp-2); }
.card-feed__card-sub {
  justify-self: end;
  color: var(--text-faint);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
}
.card-feed__card-badge { grid-column: 1 / -1; justify-self: start; }
.card-feed__card-action { margin-right: var(--sp-2); }
/**
 * Lớp phủ: hộp thoại · chú giải · công tắc — vỏ cho primitive của Radix.
 *
 * Radix lo hành vi (bẫy tiêu điểm, ARIA, Esc, khoá cuộn nền); file này chỉ lo
 * diện mạo. Không chỗ nào khác được tự dựng overlay nữa.
 */

.rdlg__scrim {
  position: fixed;
  inset: 0;
  /* +2: phải nằm TRÊN lớp phủ toàn màn hình camera (`.camfs` = +1) — hộp
     thoại mở từ trong lớp phủ (⏪ xem lại) từng bị chìm dưới nó (12/09). */
  z-index: calc(var(--z-dialog) + 2);
  background: var(--bg-scrim);
  backdrop-filter: blur(var(--blur-scrim));
  -webkit-backdrop-filter: blur(var(--blur-scrim));
}

.rdlg {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: calc(var(--z-dialog) + 2);
  max-width: calc(100vw - var(--sp-8));
  max-height: calc(100vh - var(--sp-8));
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-4);
  background: var(--grad-panel);
  border-radius: var(--r-panel);
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-sink);
}

/* Khung hộp thoại TỰ DỰNG trên Radix (không dùng `.rdlg`, ví dụ hộp Cài đặt)
   phải ở CÙNG tầng với nền mờ `.rdlg__scrim`. Tự đặt số tầng riêng thì nền mờ
   phủ lên hộp: chữ mờ, bấm/gõ đều rơi vào nền (lỗi 14/09, sau khi nền mờ lên +2
   hôm 12/09). Đổi tầng nền mờ thì đổi cả ở đây. */
.rdlg__layer {
  z-index: calc(var(--z-dialog) + 2);
}

.rdlg__title {
  margin: 0;
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-title);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-hi);
}

.rdlg__desc {
  margin: 0;
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

.rdlg__body {
  min-height: 0;
  overflow-y: auto;
}

.rtip {
  z-index: var(--z-overlay);
  padding: var(--pad-chip);
  border-radius: var(--r-sm);
  background: var(--bg-elevated, var(--bg-panel-solid));
  box-shadow: var(--sh-hair), var(--sh-sink);
  font-size: var(--fs-cap);
  color: var(--text-hi);
  max-width: 280px;
}

.rtip__arrow {
  fill: var(--bg-elevated, var(--bg-panel-solid));
}

/* Select tự vẽ popup (Radix) — dùng khi `<select>` gốc không tô được nhãn
   nhóm/icon/chấm trạng thái bên trong popup. */
.rsel__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 3px 12px;
  cursor: pointer;
}

.rsel__trigger[data-placeholder] {
  color: var(--text-lo);
}

.rsel__chevron {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--text-lo);
}

.rsel__content {
  z-index: var(--z-overlay);
  width: var(--radix-select-trigger-width);
  min-width: 240px;
  max-height: 320px;
  background: var(--bg-panel-solid);
  border-radius: var(--r-row);
  box-shadow: var(--sh-hair), var(--sh-sink);
  overflow: hidden;
}

.rsel__viewport {
  padding: var(--sp-1);
  max-height: 320px;
  overflow-y: auto;
}

/* Nhãn nhóm — Radix `Label` tự không cho focus/chọn, không cần disable tay. */
.rsel__group-label {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px var(--pad-chip) 5px;
  margin-top: 2px;
  border-bottom: 1px solid var(--line-soft);
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  color: var(--text-mid);
  user-select: none;
}

.rsel__group-label:first-child {
  margin-top: 0;
}

.rsel__item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 6px var(--pad-chip);
  margin: 1px 0;
  border-radius: var(--r-sm);
  font-size: var(--fs-cap);
  color: var(--text-hi);
  cursor: pointer;
  outline: none;
  user-select: none;
}

.rsel__item[data-highlighted] {
  background: var(--bg-hover);
}

.rsel__item[data-disabled] {
  color: var(--text-faint);
  cursor: default;
}

/* Thanh dẫn nối các đồng hồ cùng nhóm, thay ký hiệu "└". Vị trí `left` do
   nơi gọi đặt theo độ sâu. */
.rsel__rail {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--line-mid);
}

/* Chấm trạng thái đồng hồ, thay việc chỉ dựa vào chữ mờ để báo disable
   (rule 06 §4). Màu do nơi gọi đặt theo `enabled`. */
.rsel__dot {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--st-idle);
}

.rsel__check {
  margin-left: auto;
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--accent);
}

.rsw {
  width: 34px;
  height: var(--h-btn-sm);
  padding: 2px;
  border: none;
  border-radius: var(--r-pill);
  background: var(--st-idle);
  cursor: pointer;
  transition: background 0.15s;
}

.rsw[data-state='checked'] {
  background: var(--accent);
}

.rsw[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

.rsw__thumb {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: var(--r-pill);
  background: var(--text-hi);
  transition: transform 0.15s;
}

.rsw__thumb[data-state='checked'] {
  transform: translateX(14px);
}

/* ══════════════════════════════════════════════════════════════════════
   Hộp thoại NHÓM CAMERA — CameraGroupDialog.tsx (màn An ninh), làm lại 08/09
   ══════════════════════════════════════════════════════════════════════ */

/* Câu hỏi + hàng nút của hộp thoại — trước 06/10 nằm chung khối hộp xác nhận cảnh báo (`.ackdlg`, đã gỡ vì tường không
   còn xác nhận cảnh báo); giữ tên lớp để không đổi JSX của hộp nhóm camera. */
.ackdlg__what {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-sub);
  font-weight: var(--fw-strong);
  line-height: 1.3;
  color: var(--text-hi);
  overflow-wrap: anywhere;
}

.ackdlg__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
  margin-top: var(--sp-1);
}

/* Hộp thoại nhóm camera: `camera-group-dialog.css` (20/10). */

/* Nút ✕ của Dialog (11/09) — góc trên phải, cùng khuôn `.tbtn`. Hộp thoại to
   trên tường phải đóng được bằng chuột, không chỉ Esc/bấm ra ngoài. */
.rdlg__close {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-3);
}

/* Hộp thoại video (xem lại): nền ĐẶC — nền kính mặc định để lộ ô camera phía
   sau xuyên qua những vùng trống của hộp thoại, nhìn như vỡ bố cục. */
.rdlg--solid {
  background: var(--bg-void);
}
/**
 * Các khối riêng của màn Tổng quan: cột thiết bị, ô camera, ô nước thải mini.
 *
 * Vì sao là CSS chứ không phải inline style như trước: ba khối này lặp N lần
 * trong lưới kéo-thả, mà react-grid-layout render lại children liên tục lúc
 * kéo. Object style inline tạo mới mỗi lần render là rác cho GC và làm `memo`
 * mất tác dụng. Class tĩnh thì trình duyệt tính một lần.
 *
 * Mọi màu/cỡ ở đây đều là token (rule 06 §2) — không một hex nào.
 */

/* ══════════════════════════════════════════════════════════════════════
   Cột thiết bị theo phân hệ — card `dev-*`
   ══════════════════════════════════════════════════════════════════════ */

.bcol {
  display: flex;
  flex-direction: column;
  gap: var(--gap-row);
  min-height: 0;
  cursor: pointer;
}

.bcol__empty {
  padding: var(--pad-row);
  font-size: var(--fs-cap);
  color: var(--text-faint);
}

/* Một dòng thiết bị: "viên rời" — nền nhạt hơn panel, bo góc, không viền.
   Đây là mẫu dòng danh sách chung của hệ VSS. */
.bitem {
  padding: 6px 9px 7px;
  background: var(--bg-row);
  border-radius: var(--r-md);
  min-width: 0;
  transition: background 0.15s;
}

.bitem:hover {
  background: var(--bg-row-hover);
}

.bitem__head {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  min-width: 0;
}

.bitem__name {
  flex: 1;
  font-family: var(--font-cond);
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-title);
  line-height: var(--lh-label);
  color: var(--text-hi);
  min-width: 0;
}

/* Chấm trạng thái chỉ hiện khi KHÁC bình thường — dòng nào cũng có chấm thì
   chấm không còn nghĩa. Kèm chữ ở badge đầu cột cho người mù màu. */
.bitem__dot {
  width: var(--dot-size);
  height: var(--dot-size);
  border-radius: var(--r-pill);
  flex: 0 0 auto;
  box-shadow: 0 0 8px -2px currentColor;
}

.bitem__meta {
  font-size: var(--fs-cap);
  color: var(--text-lo);
  letter-spacing: var(--track-chip);
  min-width: 0;
}

/* ══════════════════════════════════════════════════════════════════════
   Thẻ KPI Cảnh báo — dòng chân tách số theo mức (`AlarmLevelMix`, 20/10)
   ══════════════════════════════════════════════════════════════════════ */

.alm-mix__n {
  font-weight: var(--fw-value);
  color: var(--text-mid);
}

.alm-mix__n--warn {
  color: var(--st-warn);
}

/* ══════════════════════════════════════════════════════════════════════
   Dải SỰ KIỆN đáy màn Tổng quan (23/09)
   ══════════════════════════════════════════════════════════════════════ */

/* Card sự kiện nổi trên bản đồ cao THEO NỘI DUNG (không có chiều cao định sẵn), mà `.card-feed`
   gốc là `flex: 1 1 0` ⇒ co về 0 và thân card biến mất. Ở đây phải là `auto`. */
.card-feed--overview {
  flex: 1 1 auto;
  /* Mép đáy mờ dần: thẻ cuối bị cắt ngang thì người đứng xa vẫn biết danh sách CÒN, không
     phải đã hết. Mặt nạ chỉ lấy độ trong, `black` ở đây không phải màu hiển thị. */
  mask-image: linear-gradient(to bottom, black calc(100% - var(--sp-6)), transparent);
}

/* ── Thẻ sự kiện màn Tổng quan (20/10) ─────────────────────────────────────────
   Khuôn chung `card-feed__card` (list.css) giữ nguyên cho 6 màn kia; ở đây chỉ đổi
   những gì card hẹp nổi trên bản đồ cần:
     · NGUỒN là chữ chính (dòng đầu, sáng), nội dung xuống dòng hai (nhạt hơn). */
.card-feed--overview .card-feed__card-body {
  padding: var(--sp-2) var(--sp-3);
}

/* Nguồn (tên camera/thiết bị) là thứ phân biệt các thẻ — cho xuống tối đa 2 dòng thay vì cắt "…"
   ở cột hẹp (29/10). Khuôn chung `list.css` của các màn khác vẫn một dòng. */
.card-feed--overview .card-feed__card-source {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  color: var(--text-hi);
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-value);
  white-space: normal;
  overflow-wrap: anywhere;
}

.card-feed--overview .card-feed__card-time {
  justify-self: end;
  color: var(--text-mid);
  font-size: var(--fs-body);
}

.card-feed--overview .card-feed__card-msg {
  color: var(--text-mid);
  font-size: var(--fs-body);
  font-weight: var(--fw-body);
}

/* Card trải hết bề ngang màn, nên một thẻ ăn trọn 1920px sẽ giãn rỗng tuếch.
   Xếp thẻ thành lưới `--ev-cols` cột, đọc THEO HÀNG — mới nhất trên-trái, sang
   phải, cuộn xuống là cũ dần. Đây là thứ tự DUY NHẤT ăn khớp với cuộn dọc; xếp
   theo cột kiểu báo giấy phải tràn ngang mới đủ chỗ, mà trên tường không ai
   cuộn ngang được. Cuộn do `.card-feed` bọc ngoài lo — KHÔNG lồng vùng cuộn thứ
   hai (screen-grids.css §4). */
.evgrid {
  display: grid;
  grid-template-columns: repeat(var(--ev-cols, 3), minmax(0, 1fr));
  gap: var(--sp-1) var(--sp-4);
  /* Dòng bám ĐỈNH: ít sự kiện thì các dòng đứng sát nhau ở trên, không dàn đều
     ra cả chiều cao card thành những khe trống vô nghĩa. */
  align-content: start;
}

/* ══════════════════════════════════════════════════════════════════════
   Ô camera theo VÙNG — `ZoneCameraGrid`, dùng ở màn Bản đồ lẫn Tổng quan
   ══════════════════════════════════════════════════════════════════════ */

.zcamgrid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Hàng cao ĐÚNG bằng ô 16:9, không co theo panel (27/10): `.zcam` có `overflow: hidden` nên chiều cao tối
     thiểu tự động của nó là 0 — panel thấp hơn tổng các ô thì hàng bị nén ~60px, ô 16:9 tràn đè lên ô dưới.
     Khu nhiều camera thì thân panel (`scroll-y`) cuộn, không ép ô. */
  grid-auto-rows: max-content;
  align-content: start;
  flex: none;
  gap: var(--sp-2);
}

.zcam {
  position: relative;
  /* Khung hình giữ đúng 16:9 của luồng camera; `contain` bên trong lo phần
     hình không méo. */
  aspect-ratio: 16 / 9;
  background: var(--bg-void);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm);
  overflow: hidden;
}

.zcam__video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  background: var(--bg-void);
}

.zcam__label {
  position: absolute;
  left: 5px;
  bottom: 4px;
  max-width: calc(100% - 10px);
  padding: 1px 6px;
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  color: var(--text-hi);
  background: color-mix(in srgb, var(--bg-void) 78%, transparent);
  border-radius: var(--r-sm);
  /* Tên camera BE trả có thể dài ("Camera .NT.SANH TRUOC NDH") — xuống dòng, không cắt "…". */
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Ô chưa có luồng (vượt số phiên mở cùng lúc, hoặc camera mất tín hiệu): giữ khung + tên. */
.zcam__none {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--sp-2);
  text-align: center;
  font-size: var(--fs-micro);
  color: var(--text-lo);
}

.zcamgrid__empty {
  padding: var(--sp-3);
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

.zcam__live {
  position: absolute;
  top: 4px;
  right: 6px;
  font-size: var(--fs-micro);
  font-weight: var(--fw-label);
  color: var(--st-crit);
  text-shadow: 0 0 8px color-mix(in srgb, var(--st-crit) 80%, transparent);
}

/* ══════════════════════════════════════════════════════════════════════
   Băng sự kiện chạy ngang ở thanh đáy (23/08)

   Vòng chạy dựa trên MỘT quy ước: danh sách được render HAI lần liền nhau,
   băng dịch trái đúng 50% rồi quay về 0. Vì nửa sau giống hệt nửa đầu, mắt
   không thấy điểm nối. Không đo đạc bằng JS, không timer — GPU lo hết, hợp
   với máy chạy 24/7.
   ══════════════════════════════════════════════════════════════════════ */

.ioc-tick {
  display: flex;
  align-items: stretch;
  width: 100%;
  height: 100%;
  min-width: 0;
  overflow: hidden;
}

.ioc-tick__badge {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 0 var(--sp-3);
  background: var(--bg-raised);
  font-size: var(--fs-cap);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--text-mid);
  white-space: nowrap;
}

.ioc-tick__led {
  width: 5px;
  height: 5px;
  border-radius: var(--r-pill);
  background: var(--st-crit);
  box-shadow: var(--glow-crit);
  /* Nhấp nháy: một trong hai ngoại lệ động-vô-hạn của rule 06 §4. */
  animation: ioc-tick-blip 1.8s ease-in-out infinite;
}

@keyframes ioc-tick-blip {
  50% {
    opacity: 0.2;
  }
}

.ioc-tick__view {
  flex: 1 1 auto;
  position: relative;
  min-width: 0;
  overflow: hidden;
  /* Chữ mờ dần ở hai mép thay vì bị cắt phựt — mép cứng làm mắt giật. */
  /* `#000` ở đây KHÔNG phải màu hiển thị mà là KÊNH ALPHA của mask (đen = giữ,
     trong suốt = xoá). Đổi sang token màu là sai nghĩa — đừng "dọn" chỗ này. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 22px, #000 calc(100% - 22px), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 22px, #000 calc(100% - 22px), transparent);
}

.ioc-tick__track {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  display: flex;
  align-items: center;
  white-space: nowrap;
  will-change: transform;
  animation-name: ioc-tick-roll;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* Rê chuột thì dừng để đọc cho hết câu. */
.ioc-tick__view:hover .ioc-tick__track {
  animation-play-state: paused;
}

@keyframes ioc-tick-roll {
  to {
    transform: translateX(-50%);
  }
}

.ioc-tick__empty {
  display: flex;
  align-items: center;
  height: 100%;
  padding-left: var(--sp-4);
  font-size: var(--fs-cap);
  color: var(--text-faint);
}

.ioc-tk {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0 18px;
  font-size: var(--fs-cap);
  color: var(--text-mid);
}

.ioc-tk__dot {
  width: 5px;
  height: 5px;
  border-radius: var(--r-pill);
  flex: 0 0 auto;
}

.ioc-tk b {
  font-family: var(--font-num);
  font-weight: var(--fw-value);
  color: var(--text-lo);
}

.ioc-tk em {
  font-style: normal;
  font-weight: var(--fw-label);
  color: var(--text-hi);
}

.ioc-tk u {
  font-family: var(--font-num);
  text-decoration: none;
  font-weight: var(--fw-value);
}

.ioc-tick__stats {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  padding: 0 var(--sp-3);
  background: var(--bg-raised);
}

@media (prefers-reduced-motion: reduce) {
  .ioc-tick__track {
    animation: none;
  }

  .ioc-tick__led {
    animation: none;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   Thanh đáy — khối NỔI, hàng cuối của khung (mẫu S07 `.tick`)

   Trước đây đây là một thanh HÀN vào mép dưới màn (`border-top` + nền đặc) và
   toàn bộ style nằm inline trong `Footer.tsx`. Nay nó bo góc, trôi trên nền
   như mọi card khác, và style về đúng chỗ của nó (rule 06 §2).
   ══════════════════════════════════════════════════════════════════════ */

.ioc-footer {
  display: flex;
  align-items: center;
  min-width: 0;
  overflow: hidden;
  border-radius: var(--r-panel);
  background: linear-gradient(180deg, var(--bg-panel-solid), var(--bg-void));
  box-shadow: var(--sh-hair), var(--sh-lite), var(--sh-float);
  user-select: none;
  z-index: var(--z-tick);
}

.ioc-footer__group {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
  white-space: nowrap;
}

.ioc-footer__item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.ioc-footer__label {
  font-size: var(--fs-micro);
  font-weight: var(--fw-title);
  letter-spacing: var(--track-label);
  color: var(--text-faint);
}

.ioc-footer__value {
  font-family: var(--font-num);
  font-size: var(--fs-micro);
  font-weight: var(--fw-title);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
}

/* Kẻ 1px thay cho ký tự "|" — không phụ thuộc font để canh cho thẳng. */
.ioc-footer__divider {
  width: 1px;
  height: 10px;
  flex: 0 0 auto;
  background-color: var(--line-soft);
}
/* ══════════════════════════════════════════════════════════════════════
   S04 · NƯỚC THẢI — style của ba card đặc thù màn này
   ══════════════════════════════════════════════════════════════════════

   VÌ SAO LÀ FILE CSS chứ không phải style inline trong component (như trước):

   1. `@container` không viết được bằng inline style. Card phán quyết phải ĐỔI
      BỐ CỤC theo hình dạng ô người dùng kéo — cao thì chữ to xếp chồng, dẹt
      thì một hàng ngang. Chỉ container query làm được, và nó phải nằm trong
      stylesheet.
   2. Cùng lý do với `overview-cards.css`: react-grid-layout dựng lại children
      liên tục lúc kéo. Object style inline tạo mới mỗi lần render là rác GC
      trên màn treo 24/7.

   Mọi giá trị lấy từ `tokens.css`. Không hex, không px tự chế cho màu/cỡ chữ.
   ══════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════
   ① Card phán quyết tuân thủ (WwVerdictCard)
   ══════════════════════════════════════════════════════════════════════ */

/* ⚠ CONTAINER NẰM Ở KHUNG PANEL, KHÔNG Ở `.wwv` — đọc kỹ trước khi "dọn".
   `@container` chỉ áp cho HẬU DUỆ của container, nên một phần tử tự khai
   `container-type` thì KHÔNG BAO GIỜ khớp truy vấn của chính nó. Đó là lý do
   dạng dải ngang viết 16/09 chưa từng chạy: `.wwv` vừa là container vừa là
   thứ cần đổi bố cục (phát hiện 20/09 khi đo trên trình duyệt — băng cao 64px
   mà vẫn ở dạng cột). Nay thân panel giữ vai container ngoài, `.wwv` vẫn là
   container riêng để `cqh`/`cqw` của chữ bên trong đo theo chính nó. */
.wwv-host > .tp__body {
  container-type: size;
}

.wwv {
  flex: 1;
  min-height: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5);
  /* Đơn vị đo của chữ phán quyết (cqh/cqw) — đo theo chính ô này. */
  container-type: size;
}

.wwv__eyebrow {
  color: var(--text-mid);
}

/* Chữ phán quyết.

   Giãn dòng 1,25 chứ KHÔNG 0,92 như mẫu: chữ HOA tiếng Việt có dấu nặng dưới
   "Ạ" và dấu hỏi trên "Ẩ" — dòng sát thì dấu nặng của "ĐẠT" nằm ngay trên chữ
   U dòng dưới, đọc thành "ĐAT QỦY" (chụp 16/09). Cùng lý do với `--lh-label`.

   `--fw-title` (700) chứ không 800: `main.tsx` chỉ nạp Inter 400/500/600/700.
   Xin 800 thì trình duyệt tự kéo đậm giả, nét nhoè — trên tường 3–5m thấy rõ. */
.wwv__big {
  font-family: var(--font-ui);
  font-size: clamp(var(--fs-hero), min(27cqh, 16cqw), calc(var(--fs-hero-xl) * 1.5));
  font-weight: var(--fw-title);
  line-height: 1.25;
  letter-spacing: -0.02em;
  white-space: pre-line;
}

.wwv__exceeded {
  font-size: var(--fs-body-lg);
  color: var(--st-crit);
}

.wwv__sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-5);
  margin-top: var(--sp-2);
  font-size: var(--fs-body-lg);
  color: var(--text-mid);
}

.wwv__val {
  color: var(--text-hi);
  font-weight: var(--fw-title);
}

/* Chấm trạng thái chỉ hiện ở dạng dải — ở dạng cao thì chính chữ to đã là tín
   hiệu, thêm chấm là thừa. */
.wwv__dot {
  display: none;
}

/* ── Dạng DẢI: ô thấp (≤ 150px) — DỰ PHÒNG TỪ 30/09 ──────────────────────
   20/09–30/09 băng nằm ở hàng đầu màn cao `clamp(64px, 8vh, 92px)` nên dạng
   này là thường trực. Từ 30/09 card nằm trong NGĂN PHẢI, ô ~1/3 chiều cao ngăn
   (> 150px) ⇒ dạng cột chữ to bên trên là thường trực; dạng dải giữ lại cho
   lúc người dùng kéo ô trong ngăn thấp xuống.
   Hai chữ "ĐẠT QUY CHUẨN" cao 132px ăn 7% mặt tường là quá đắt cho một câu
   trả lời có/không. Dẹt xuống thì xếp ngang: chấm · phán quyết · chi tiết
   vượt · (đẩy phải) tỉ lệ đạt + truyền Sở. */
@container (max-height: 150px) {
  .wwv {
    flex-direction: row;
    align-items: center;
    flex-wrap: nowrap;
    gap: var(--sp-3);
    padding: 0 var(--sp-4);
  }

  .wwv__dot {
    display: inline-block;
    flex: 0 0 auto;
  }

  /* Nhãn phân hệ nhường chỗ cho số liệu khi hẹp — tên trạm đã có ở tiêu đề màn. */
  .wwv__eyebrow {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Chữ phán quyết về một dòng: `pre-line` của chuỗi i18n ("ĐẠT\nQUY CHUẨN")
     phải bị vô hiệu, nếu không dải cao gấp đôi. */
  .wwv__big {
    flex: 0 0 auto;
    font-size: clamp(var(--fs-sub), 46cqh, var(--fs-num-lg));
    line-height: var(--lh-label);
    white-space: nowrap;
  }

  .wwv__exceeded {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--fs-body);
  }

  .wwv__sub {
    flex: 0 0 auto;
    margin-top: 0;
    margin-left: auto;
    flex-wrap: nowrap;
    font-size: var(--fs-body);
  }
}

/* Dải rất hẹp: bỏ nhãn phân hệ trước, số liệu giữ đến cùng. */
@container (max-height: 150px) and (max-width: 900px) {
  .wwv__eyebrow {
    display: none;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   ② Chỉ tiêu tức thời so với ngưỡng (QcvnBulletList)
   ══════════════════════════════════════════════════════════════════════ */

.wwb {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* Hàng chia đều chiều cao card. Cột: tên · thanh · (số + ngưỡng) · trạng thái.
   Ngưỡng nằm CHUNG CỘT với số đo chứ không lơ lửng trên thanh như trước — nhãn
   ngưỡng cũ cỡ `--fs-cap` (10px) treo trên vạch, từ 3m không ai đọc được. */
.wwb__row {
  flex: 1 1 0;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 9em) minmax(0, 1fr) minmax(0, 7.5em) auto;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-2);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s;
}

.wwb__row:hover {
  background: var(--bg-row-hover);
}

.wwb__row[aria-pressed='true'] {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--line-accent);
}

.wwb__name {
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-strong);
  color: var(--text-hi);
  line-height: var(--lh-label);
}

/* ── Thanh bullet ──────────────────────────────────────────────────────── */

.wwb__track {
  position: relative;
  height: var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--bg-well);
  box-shadow: var(--sh-hair);
  overflow: visible;
}

/* Vùng ĐẠT tô nền mờ: mắt đọc "đang nằm trong hay ngoài vùng an toàn" trước cả
   khi đọc số. Dùng chung sắc độ với `.mt-meter__band` của metric.css. */
.wwb__band {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--st-good) 20%, transparent);
}

.wwb__fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  border-radius: var(--r-sm);
  background: var(--wwb-tone, var(--accent-effluent));
  box-shadow: 0 0 10px -2px var(--wwb-tone, var(--accent-effluent));
  transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Vạch ngưỡng: chỉ còn VẠCH, số đã chuyển xuống cột bên phải. */
.wwb__tick {
  position: absolute;
  top: -4px;
  bottom: -4px;
  width: 3px;
  margin-left: -1.5px;
  border-radius: 1px;
  background: var(--st-crit);
  box-shadow: var(--glow-crit);
}

/* ── Cột số + ngưỡng ───────────────────────────────────────────────────── */

.wwb__read {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  min-width: 0;
}

.wwb__value {
  font-size: var(--fs-num);
  font-weight: var(--fw-title);
  line-height: var(--lh-num);
  white-space: nowrap;
  color: var(--wwb-tone, var(--text-hi));
}

.wwb__unit {
  margin-left: 4px;
  font-family: var(--font-ui);
  font-size: var(--fs-cap);
  font-weight: var(--fw-body);
  color: var(--text-lo);
}

/* Ngưỡng dạng chữ ("≤ 150 mg/L", "5,5 – 9") — đọc được, và nói rõ chiều của
   ngưỡng, thứ mà một vạch trên thanh không nói được. */
.wwb__limit {
  font-size: var(--fs-cap);
  color: var(--text-faint);
  white-space: nowrap;
}

/* ══════════════════════════════════════════════════════════════════════
   ③ Ô camera trạm (EffluentCameraCard)
   ══════════════════════════════════════════════════════════════════════ */

.wwc {
  flex: 1;
  min-height: 0;
  min-width: 0;
  display: grid;
  align-content: center;
  justify-content: center;
}

/* Cửa sổ xem một camera (WwtpCameraDialog, 19/10): hộp CỐ ĐỊNH 80% × 80% màn, thân là
   cột flex — thanh ◄ n/m ► · khung video lấp phần còn lại. Mặc định `.rdlg__body` cao
   theo nội dung, khung video đo ra 0. */
.wwpl .rdlg__body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  overflow: hidden;
}

.wwpl__bar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.wwpl__spacer {
  flex: 1;
}

/* Hai lớp để thắng `.campb__stage { height: 100% }` bất kể thứ tự nạp CSS. */
.wwpl .wwpl__stage {
  flex: 1;
  height: auto;
}

.wwpl__offline {
  color: var(--st-crit);
  font-size: var(--fs-cap);
}

/* Nút bấm phủ trọn ô, nằm trên cùng. Bọc cả ô trong <button> thì trình đọc màn
   hình phải đọc hết sơ đồ + nhãn mới tới tên vị trí. */
.wwc__hit {
  position: absolute;
  inset: 0;
  z-index: 3;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}

.wwc__hit:hover {
  box-shadow: inset 0 0 0 1px var(--line-mid);
}

/* Không `border` — luật VSS `--panel-border-w: 0px`: ngăn cách bằng tầng màu +
   bóng, không bằng đường kẻ. `--sh-hair` là "viền ảo" thay thế. */
.wwc__tile {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  border-radius: var(--r-md);
  background: var(--bg-void);
  box-shadow: var(--sh-hair);
  overflow: hidden;
}

/* Cửa xả là nơi lấy mẫu đo QCVN — người trực nhìn ô này đầu tiên. Nhấn bằng
   vòng accent + quầng sáng, vẫn không dùng `border`. */
.wwc__tile--outfall {
  box-shadow:
    inset 0 0 0 2px var(--accent-effluent),
    var(--sh-halo);
}

.wwc__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Sơ đồ công đoạn vẽ nền ô khi chưa có luồng. KHÔNG phải ảnh camera giả —
   nét đơn sắc, rõ ràng là bản vẽ. Nó trả lời "ô này soi công đoạn nào của dây
   chuyền", thứ mà một hình chữ nhật đen không trả lời được. */
.wwc__scheme {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  color: var(--st-idle);
  opacity: 0.55;
}

.wwc__status {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  text-align: center;
  padding: 0 var(--sp-2);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  color: var(--text-lo);
}

.wwc__status--offline {
  color: var(--st-crit);
}

.wwc__status--nofeed {
  color: var(--text-lo);
}

.wwc__overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* Trên video + sơ đồ, dưới nút bấm. */
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 7px;
  background: linear-gradient(0deg, color-mix(in srgb, var(--bg-void) 88%, transparent) 0%, transparent 100%);
}

/* Tên vị trí xuống dòng, không cắt "…" (lệnh 16/09). */
.wwc__name {
  font-size: var(--fs-cap);
  font-weight: var(--fw-title);
  color: var(--text-hi);
  line-height: var(--lh-label);
}

/* ══════════════════════════════════════════════════════════════════════
   ④ Dụng cụ thí nghiệm trong dải KPI (QcvnVesselIcon)
   ══════════════════════════════════════════════════════════════════════
   Lệnh 21/09: 6 ô chỉ tiêu bỏ thanh đo ngang 4px (`.mt-meter`), thay bằng
   ống nghiệm · bình tam giác · cốc đong · nhiệt kế · giấy quỳ. Hình học và
   bảng phân công dụng cụ nằm ở `screens/wastewater/qcvn-vessel-shapes.ts`;
   bề rộng ô hình khai ở khối S05 của `screen-grids.css`.                */

/* Mức dịch TRƯỢT MỀM mỗi khi số đo đổi thay vì nhảy giật — cùng đường cong
   với `.mt-meter__fill` mà nó thay thế. Dùng `transform` chứ không phải
   thuộc tính hình học `y`: transform chắc chắn chạy trên compositor, còn
   `y`/`height` của SVG thì tuỳ engine. */
.qvessel__body {
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Bọt khí nổi từ đáy dịch lên mặt.
   ⚠ ANIMATION LẶP VÔ HẠN — ngoại lệ thứ 4 của rule 06 §4, user duyệt rõ
   21/09 (đã ghi vào luật). Thuần CSS, chạy trên GPU compositor: 6 ô × 3–6
   bọt là chuyện nhỏ, nhưng 6 timer JS đổi toạ độ thì không.
   Quãng đường `--qv-rise` tính bằng ĐƠN VỊ VIEWBOX: transform trên phần tử
   SVG làm việc trong hệ toạ độ cục bộ nên `px` ở đây = đơn vị viewBox, tự co
   theo cỡ ô hình. */
.qvessel__bubble {
  animation: qvessel-rise var(--qv-dur, 3.6s) linear var(--qv-delay, 0s) infinite;
}

@keyframes qvessel-rise {
  0% {
    transform: translateY(0);
    opacity: 0;
  }
  15% {
    opacity: 0.85;
  }
  80% {
    opacity: 0.5;
  }
  100% {
    transform: translateY(calc(var(--qv-rise, 16) * -1px));
    opacity: 0;
  }
}

/* Người khai báo hạn chế chuyển động: bọt đứng yên (vẫn thấy được, chỉ
   không bay), mức dịch nhảy thẳng tới số mới. */
@media (prefers-reduced-motion: reduce) {
  .qvessel__body {
    transition: none;
  }

  .qvessel__bubble {
    animation: none;
  }
}
/**
 * Công tắc BÓNG ĐÈN của ma trận Chiếu sáng — xem `screens/lighting/components/
 * LampSwitch.tsx` để biết vì sao control này là một bóng đèn chứ không phải một
 * công tắc gạt.
 *
 * LUẬT HÌNH HỌC của file này: bật và tắt phải chiếm ĐÚNG MỘT chỗ như nhau.
 * Quầng sáng đi bằng `drop-shadow` (vẽ ra ngoài hộp, không cộng vào layout) và
 * tia đi bằng lớp phủ `position: absolute` — không thứ nào được phép đổi kích
 * thước hộp. Ba bóng nằm sát nhau trong một ô bảng: chỉ cần một pixel xê dịch
 * lúc bật là người trực bấm trượt sang bóng kế bên.
 */

.lampsw {
  /* VÀNG ĐÈN, đúng màu cột đèn ĐANG SÁNG trên Sơ đồ tuyến (`LAMP_ON_COLOR`) —
     bảng và bản đồ nói cùng một thứ tiếng về cái gì đang sáng.
     Đổi từ trắng ấm sang đây 21/09 (lệnh người dùng "màu nổi hơn"): trắng ấm
     trên nền phiến tối gần như không khác bóng đã tắt, phải nhìn kỹ mới thấy —
     mà đây là thứ người trực liếc từ 3–5m. */
  --lampsw-on: var(--accent-light);
  /* Cùng màu cột đèn TẮT trên Sơ đồ tuyến (`LAMP_OFF_COLOR`) — bảng và bản đồ
     nói cùng một thứ tiếng về cái gì đang tắt. */
  --lampsw-off: var(--st-idle);

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--lampsw-off);
  cursor: pointer;
  /* ≤200ms cho một lượt bật/tắt — đúng hạn của rule 06 §4. KHÔNG có animation
     lặp: tia hiện ra rồi đứng yên. */
  transition: color 0.2s, filter 0.2s;
}

/* Hai lớp quầng: một lõi gắt sát bóng, một vầng rộng loang ra — bóng đèn thật
   sáng như vậy, và ở cỡ 34px thì một lớp nhoè đều trông như bị mờ nét. */
.lampsw--on {
  color: var(--lampsw-on);
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--lampsw-on) 85%, transparent))
    drop-shadow(0 0 12px color-mix(in srgb, var(--lampsw-on) 45%, transparent));
}

/* Gợi ý bấm được: bóng đang TẮT ấm lên một nửa khi rê chuột. Loại trừ
   `.lampsw--on` tường minh — chọn lọc theo :hover nặng hơn `.lampsw--on` nên
   không loại thì rê chuột lên bóng đang sáng sẽ làm nó TỐI đi. */
.lampsw:not(.lampsw--on):not(.lampsw--locked):hover {
  color: color-mix(in srgb, var(--lampsw-on) 55%, var(--lampsw-off));
}

/* Khoá: vẫn nhìn thấy, vẫn rê chuột đọc được lý do, nhưng nói rõ là không bấm được. */
.lampsw--locked {
  opacity: 0.4;
  cursor: not-allowed;
}

.lampsw__bulb {
  position: relative;
  z-index: 1;
}

.lampsw__rays {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.2s;
  pointer-events: none;
  stroke: currentColor;
  stroke-width: 6;
  stroke-linecap: round;
}

.lampsw--on .lampsw__rays {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .lampsw,
  .lampsw__rays {
    transition: none;
  }
}

/**
 * Bảng phiếu sự cố — màn S08 bản tường "CHỈ NHÌN" (02/10: đơn giản, thoáng, dễ nhìn từ 3–5 m).
 *
 *   .tkb   ba làn: chờ tiếp nhận · đang xử lý · tạm dừng
 *   .tkl   một làn: đầu làn (tên · số to) + vài thẻ, không cuộn
 *   .tkc   một thẻ phiếu (không bấm được — thao tác ở app CMS)
 *   .alf   (05/10) bảng cảnh báo thô ở cột phải — thay ô "Theo phân hệ"
 *
 * THOÁNG: ít thẻ, đệm vừa, chữ cùng thang các màn khác; phân tách bằng TẦNG MÀU chứ không bằng đường
 * kẻ. Màu chỉ ở chỗ có chuyện — nhãn mức, nền đỏ nhạt khi Khẩn cấp/trễ hạn, dòng hạn đỏ/cam. Không vạch
 * màu mép thẻ (user 02/10: nhãn mức là đủ).
 * Chữ dài XUỐNG DÒNG, không cắt "…" (lệnh 16/09).
 */

/* Thang chữ S08 — PHÂN CẤP từ to xuống nhỏ, mỗi bậc nhỏ hơn bậc trên:
     tiêu đề card (CardHeader, ≈1,3× cỡ thân)  >  tên làn / tiêu đề cột (`head`)  >  nội dung chính (`body`)  >  dòng phụ (`note`).
     Mọi cỡ chữ của ba làn phiếu và bảng cảnh báo lấy từ đây — muốn to/nhỏ cả màn chỉ sửa ba dòng này. */
.scr--alm {
  --s8-fs-head: calc(var(--fs-body-lg) * 1.08);
  --s8-fs-body: var(--fs-body-lg);
  --s8-fs-note: var(--fs-body);
}

/* ══ Bảng ba làn ═══════════════════════════════════════════════════════ */

.tkb {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-3);
  padding: var(--sp-3);
}

/* Chưa đọc được phiếu (BE chưa có API, mất kết nối): nói thẳng, không vẽ ba làn trống giả. */
.tkb__state {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-8);
  font-size: var(--fs-body-lg);
  color: var(--text-lo);
}

.tkl {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  border-radius: var(--r-row);
  background: var(--bg-well);
  box-shadow: var(--sh-hair);
  overflow: hidden;
}

.tkl__head {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
}

.tkl__name {
  font-family: var(--font-ui);
  font-size: var(--s8-fs-head);
  font-weight: var(--fw-strong);
  line-height: var(--lh-label);
  color: var(--text-mid);
  white-space: nowrap;
}

.tkl__count {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-num);
  font-weight: var(--fw-title);
  line-height: 1;
  letter-spacing: var(--track-num);
  color: var(--text-hi);
}

/* Làn trống: số 0 lùi về màu nhạt — không có việc không phải tin cần gào lên. */
.tkl__count--zero {
  color: var(--text-faint);
}

/* Không cuộn: làn chỉ giữ vài thẻ, phần thừa gói vào dòng "+n phiếu khác". */
.tkl__list {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: 0 var(--sp-2) var(--sp-2);
}

/* Nút mở/thu gọn danh sách của làn. */
.tkl__more {
  flex: 0 0 auto;
  padding: var(--sp-1) var(--sp-2);
  border: 0;
  border-radius: var(--r-row);
  background: var(--bg-row);
  font-family: inherit;
  font-size: var(--s8-fs-body);
  font-weight: var(--fw-label);
  color: var(--text-mid);
  text-align: center;
  cursor: pointer;
}

.tkl__more:hover,
.tkl__more:focus-visible {
  color: var(--text-hi);
  background: var(--bg-hover);
}

/* Làn đã mở hết: cuộn dọc; nút "Thu gọn" dính đáy để không phải cuộn tới cuối mới thu được. */
.tkl__list--open {
  overflow-y: auto;
}

.tkl__list--open .tkl__more {
  position: sticky;
  bottom: 0;
  background: var(--bg-panel-solid);
  box-shadow: 0 -6px 8px -4px var(--bg-panel-solid);
}

.tkl__empty {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-3);
  text-align: center;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-lo);
}

/* Không có phiếu chờ tiếp nhận là TIN TỐT — màu đạt, không phải màu xám của ô rỗng. */
.tkl__empty--good {
  color: var(--st-good);
}

/* Khung chờ đúng hình thẻ thật: làn không nhảy chiều cao lúc dữ liệu về. */
.tkl__skel {
  flex: 0 0 auto;
  height: clamp(72px, calc(var(--ui-px) * 84), 120px);
  border-radius: var(--r-row);
  background: var(--bg-row);
  box-shadow: var(--sh-hair);
}

/* ══ Thẻ phiếu ═════════════════════════════════════════════════════════ */

.tkc {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--pad-row);
  border-radius: var(--r-row);
  background: var(--bg-row);
}

/* Khẩn cấp hoặc đã trễ hạn: tô nền nhạt cả thẻ để mắt bắt được ngay khi lướt qua. */
.tkc--hot {
  background: color-mix(in srgb, var(--st-crit) 10%, var(--bg-row));
}

.tkc__top {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

.tkc__tag {
  display: inline-block;
  margin-right: var(--sp-1);
  padding: 0 var(--sp-1);
  border-radius: var(--r-sm);
  background: var(--st-crit);
  font-size: var(--s8-fs-note);
  font-weight: var(--fw-title);
  letter-spacing: var(--track-label);
  line-height: 1.3;
  color: var(--text-hi);
  white-space: nowrap;
  vertical-align: 1px;
}

.tkc__code {
  margin-left: auto;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--s8-fs-note);
  color: var(--text-lo);
  white-space: nowrap;
}

.tkc__title {
  font-size: var(--s8-fs-body);
  font-weight: var(--fw-strong);
  line-height: 1.25;
  color: var(--text-hi);
  /* Luôn chừa ĐỦ HAI dòng: mọi thẻ cao bằng nhau dù tên sự cố ngắn hay dài. */
  min-height: 2.5em;
  overflow: hidden;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

.tkc__src {
  font-size: var(--s8-fs-note);
  line-height: var(--lh-body);
  color: var(--text-mid);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tkc__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-top: 2px;
  font-size: var(--s8-fs-note);
  line-height: var(--lh-label);
}

.tkc__who {
  min-width: 0;
  color: var(--text-mid);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Chưa ai nhận: chữ cam — việc chưa có người cầm. */
.tkc__who--none {
  color: var(--st-warn);
}

.tkc__clock {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-strong);
  white-space: nowrap;
}

.tkc__clock--mid { color: var(--text-mid); }
.tkc__clock--good { color: var(--st-good); }
.tkc__right {
  display: flex;
  flex: 0 0 auto;
  align-items: baseline;
  gap: var(--sp-2);
}
.tkc__clock--warn { color: var(--st-warn); }
.tkc__clock--crit { color: var(--st-crit); }

/* ══ Card "Cảnh báo" — BẢNG cảnh báo thô (`alarms.list`), kiểu bảng tính ══════════════════════
   Thay ô "Theo phân hệ" (05/10): cột phải của S08. Có hàng tiêu đề DÍNH ĐỈNH, kẻ ô hai chiều, dòng sọc xen kẽ,
   bảy cột (giờ · mức · nội dung · nơi · số đo/ngưỡng · trạng thái · phiếu). MỚI NHẤT Ở ĐẦU BẢNG; mọi cảnh báo
   trong khoảng giờ qua nằm trong một bảng CUỘN DỌC (người trực tự cuộn; để yên thì tự về đầu — xem AlarmFeedCard).
   Mức độ LUÔN có nhãn chữ (StatusBadge); vạch màu đầu dòng chỉ là phụ.
   ⛔ Mọi dòng có chiều cao CỐ ĐỊNH (`--alf-row-h`, nội dung tối đa hai dòng): màn có luật
   `.scr * { min-height: 0 }` nên dòng co được sẽ bị nén đè chữ. */
.alm-feed {
  --alf-row-h: clamp(52px, calc(var(--ui-px) * 58), 84px);
  --alf-head-h: clamp(32px, calc(var(--ui-px) * 36), 48px);
  --alf-fs: var(--s8-fs-body);
  --alf-fs-sub: var(--s8-fs-note);
}

/* Chưa xác nhận: con số cần người để mắt tới nên tô đỏ, có kèm chữ. */
.alf-unack {
  padding: 1px var(--sp-2);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--st-crit) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--st-crit) 40%, transparent);
  color: var(--st-crit);
  font-size: var(--fs-body);
  font-weight: var(--fw-label);
  white-space: nowrap;
}

.alf__state {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-6);
  color: var(--text-lo);
  font-size: var(--fs-body-lg);
  text-align: center;
}

/* Không có cảnh báo nào là TIN TỐT — màu đạt, không phải màu xám của ô rỗng. */
.alf__state--good {
  color: var(--st-good);
}

/* Khung chứa bảng: cao theo card, cuộn dọc khi dài hơn. */
.alf-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

.alf-t {
  width: 100%;
  table-layout: fixed;
  /* `separate` (không phải `collapse`): hàng tiêu đề `position: sticky` chỉ giữ được đường kẻ khi các ô tự mang viền. */
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--alf-fs);
  color: var(--text-mid);
}

.alf-c--time { width: 10%; }
.alf-c--level { width: 15%; }
.alf-c--content { width: 19%; }
.alf-c--source { width: 16%; }
.alf-c--reading { width: 15%; }
.alf-c--state { width: 13%; }
.alf-c--ticket { width: 12%; }

.alf-t th {
  position: sticky;
  top: 0;
  z-index: 1;
  height: var(--alf-head-h);
  padding: 0 var(--sp-2);
  overflow: hidden;
  border-top: 1px solid var(--line-soft);
  background:
    linear-gradient(0deg, color-mix(in srgb, var(--accent) 10%, transparent), color-mix(in srgb, var(--accent) 10%, transparent)),
    var(--bg-thead, var(--bg-panel-solid));
  color: color-mix(in srgb, var(--accent) 45%, var(--text-mid));
  font-size: var(--s8-fs-head);
  font-weight: var(--fw-title);
  letter-spacing: normal;
  text-align: left;
  text-overflow: ellipsis;
  text-transform: none;
  white-space: nowrap;
  line-height: 1.2;
  font-family: var(--font-ui);
}

.alf-th--n {
  text-align: right;
}

.alf-t td {
  height: var(--alf-row-h);
  padding: 0 var(--sp-2);
  overflow: hidden;
  vertical-align: middle;
}

/* Kẻ ô hai chiều: mỗi ô mang viền phải + dưới, ô đầu thêm viền trái. */
.alf-t th,
.alf-t td {
  border-right: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
}

.alf-t th:first-child,
.alf-t td:first-child {
  border-left: 1px solid var(--line-soft);
}

/* Dòng sọc xen kẽ như bảng tính. */
.alf-t tbody tr:nth-child(even) td {
  background: color-mix(in srgb, var(--text-hi) 3%, transparent);
}

/* Dòng Nghiêm trọng ửng đỏ cả dòng (thay vạch mép trái, 20/10) — phụ, nhãn chữ ở cột "Mức"
   mới là chính. Đứng SAU luật sọc xen kẽ, cùng độ ưu tiên ⇒ thắng nhờ thứ tự. */
.alf-t tbody tr.alf-tr--crit td {
  background: color-mix(in srgb, var(--st-crit) 7%, transparent);
}

/* Đã đóng: lùi màu nhưng vẫn đọc được. */
.alf-tr--done {
  opacity: 0.6;
}

.alf-td--time {
  white-space: nowrap;
}

.alf-time {
  display: block;
  color: var(--text-hi);
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-value);
}

.alf-date {
  display: block;
  color: var(--text-faint);
  font-size: var(--alf-fs-sub);
  font-style: normal;
}

/* Nội dung xuống dòng, tối đa hai dòng — không cắt "…" giữa câu (lệnh 16/09). */
.alf-content {
  margin: 0;
  overflow: hidden;
  color: var(--text-hi);
  font-size: var(--alf-fs);
  font-weight: var(--fw-value);
  line-height: 1.25;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

.alf-src__domain,
.alf-src__device {
  display: block;
  overflow: hidden;
  font-size: var(--alf-fs-sub);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.alf-src__domain {
  color: var(--text-mid);
}

.alf-src__device {
  color: var(--text-lo);
}

.alf-td--n {
  color: var(--text-hi);
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-value);
  text-align: right;
  white-space: nowrap;
}

.alf-tr--crit .alf-td--n {
  color: var(--st-crit);
}

.alf-tr--warn .alf-td--n {
  color: var(--st-warn);
}

.alf-state {
  white-space: nowrap;
  font-size: var(--alf-fs-sub);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.alf-state--open {
  color: var(--st-warn);
}

.alf-state--ack {
  color: var(--st-info);
}

.alf-state--cleared {
  color: var(--text-lo);
}

.alf-td--id {
  overflow: hidden;
  color: var(--text-mid);
  font-family: var(--font-num);
  font-size: var(--alf-fs-sub);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ══ Hình nhỏ bên phải năm ô số (`MetricTile.aside`) ═══════════════════════════════════════════
   Mỗi hình vẽ từ số backend ĐÃ gửi (xem ticket-kpi.ts). Chữ cỡ phụ của S08; màu lấy token trạng thái / màu phân hệ.
   Ở thẻ dẹt (`mt--strip`) `aside` là MỘT CỘT của lưới thẻ, rộng = hệ số × chiều cao thẻ, tối đa 42% bề ngang (metric.css).
   Hình S08 rộng hơn hình vuông mặc định nên nâng hệ số lên 2,2 (như rule `--kpi-aside-k` của sparkline/đồng hồ nửa vòng) —
   cột vẫn bị chặn ở 42% nên số lớn không bị lấn. Hình luôn vừa cột (`width: 100%`), không tự định bề rộng. */
.mt--strip:has(> .mt__aside > .kva) {
  --kpi-aside-k: 2.2;
}

/* metric.css ép MỌI <svg> trong cột hình phủ kín cột (position:absolute; inset:0) — đúng cho icon trang trí, sai cho
   vòng % và dấu tick của S08 (bị phóng to và trôi ra mép thẻ). Trả về kích thước tự nhiên. */
.mt--strip .mt__aside .kva svg {
  position: static;
  inset: auto;
  width: auto;
  height: auto;
  flex: none;
}
.kva {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-1);
  width: 100%;
  min-width: 0;
  color: var(--text-mid);
  font-size: var(--s8-fs-note);
}

.kva--none {
  align-items: center;
  color: var(--st-good);
}

.kva--chips {
  align-items: stretch;
}

.kva-chip {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-1);
  min-width: 0;
  padding: 1px var(--sp-2);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--st-crit) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--st-crit) 40%, transparent);
  color: var(--st-crit);
  white-space: nowrap;
}

.kva-chip b {
  font-family: var(--font-num);
  font-weight: var(--fw-value);
}

.kva-chip em {
  min-width: 0;
  overflow: hidden;
  color: var(--text-mid);
  font-style: normal;
  text-overflow: ellipsis;
}

.kva-more {
  color: var(--text-lo);
}

.kva-text,
.kva-legend {
  text-align: right;
  line-height: 1.25;
}

.kva-text {
  white-space: nowrap;
}

.kva-legend b {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-value);
}

.kva-leg {
  white-space: nowrap;
}

/* Chú giải ba đoạn xuống dòng được (flex-wrap) — hai mục liền nhau trong JSX không có khoảng trắng nên inline sẽ không bao giờ ngắt. */
.kva-legend {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  column-gap: var(--sp-2);
}

.kva-leg--late b { color: var(--st-crit); }
.kva-leg--risk b { color: var(--st-warn); }
.kva-leg--ok b { color: var(--st-good); }

.kva-track,
.kva-stack {
  display: flex;
  height: 8px;
  overflow: hidden;
  border-radius: var(--r-pill);
  background: var(--bg-well);
}

.kva-stack {
  gap: 2px;
}

.kva-stack > i {
  display: block;
  min-width: 3px;
}

.kva-fill {
  display: block;
  height: 100%;
  min-width: 2px;
  border-radius: var(--r-pill);
}

.kva-fill--good, .kva-seg--ok { background: var(--st-good); }
.kva-fill--warn, .kva-seg--risk { background: var(--st-warn); }
.kva-fill--crit, .kva-seg--late { background: var(--st-crit); }

.kva--done {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
}

.kva-ring__track {
  fill: none;
  stroke: var(--bg-well);
  stroke-width: 6;
}

.kva-ring__fill {
  fill: none;
  stroke: var(--st-good);
  stroke-linecap: round;
  stroke-width: 6;
}

.kva-ring__text {
  fill: var(--text-hi);
  font-family: var(--font-num);
  font-size: 13px;
  font-weight: 600;
  text-anchor: middle;
  dominant-baseline: central;
}

.kva-cols {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

.kva-bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 36px;
}

.kva-bars > i {
  display: block;
  width: 5px;
  border-radius: 1px;
  background: var(--st-good);
}

.kva-bars > .kva-bar--zero {
  background: var(--line-soft);
}

.kva-cap {
  color: var(--text-lo);
  font-size: var(--fs-micro);
  white-space: nowrap;
}

/* Lớp báo động KHẨN CẤP (spec 261001 §11) — dáng HUD báo động (lệnh 03/10). Chỉ token; KHÔNG chặn thao
   tác (pointer-events none toàn lớp). Chuyển động lặp là ngoại lệ rule 06 §4: viền đỏ thở 1,6 s (không
   chớp > 3 lần/giây), sọc trượt, nhãn nhấp nháy — CSS thuần, chỉ transform/opacity (GPU compositor),
   tắt hết khi `prefers-reduced-motion`. */
.emg {
  --emg-stripe: var(--sp-5);
  --emg-edge: 3px;
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: grid;
  place-items: center;
  pointer-events: none;
  /* Phủ tối cả màn — dashboard vẫn nhìn xuyên được, nhưng mắt bị kéo về thẻ báo động. */
  background: color-mix(in srgb, var(--bg-void) 62%, transparent);
}

/* Loé đỏ MỘT lần lúc bật — gây chú ý ngay, không lặp. */
.emg__flash {
  position: absolute;
  inset: 0;
  background: var(--st-crit);
  opacity: 0;
  animation: emg-flash 0.6s ease-out 1;
}

/* Viền đỏ + quầng tối ở mép màn, thở chậm. */
.emg__vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at center, transparent 40%, color-mix(in srgb, var(--st-crit) 38%, transparent) 100%);
  box-shadow:
    inset 0 0 0 var(--emg-edge) var(--st-crit),
    inset 0 0 var(--sp-8) var(--sp-3) color-mix(in srgb, var(--st-crit) 55%, transparent);
  animation: emg-breathe 1.6s ease-in-out infinite;
}

/* ── Dải sọc cảnh báo ─────────────────────────────────────────────────── */
/* Dải trên phủ đúng hàng điều hướng, dải dưới nằm ngay trên thanh sự kiện — giờ ở header và dòng
   sự kiện vẫn đọc được trong lúc báo động. */
.emg__band {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--h-nav);
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--bg-void);
  border-block: 2px solid var(--st-crit);
  box-shadow: var(--glow-crit);
}
/* Khớp lưới `.shell` (index.css): lề + khoảng hàng đều là `--gutter`. */
.emg__band--top { top: calc(var(--gutter) * 2 + var(--h-masthead)); }
.emg__band--bottom { bottom: calc(var(--gutter) * 2 + var(--h-footer)); }

/* Rộng hơn màn đúng một chu kỳ sọc ngang (2 × bề rộng sọc × √2) để trượt liền mạch. */
.emg__stripes {
  position: absolute;
  inset: 0 calc(var(--emg-stripe) * -2.8285) 0 0;
  background: repeating-linear-gradient(
    -45deg,
    var(--st-crit) 0 var(--emg-stripe),
    var(--bg-void) var(--emg-stripe) calc(var(--emg-stripe) * 2)
  );
  opacity: 0.85;
  will-change: transform;
  animation: emg-slide 1.1s linear infinite;
}

.emg__plate {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  height: 100%;
  padding: 0 var(--sp-6);
  background: var(--bg-void);
  border-inline: 2px solid var(--st-crit);
  color: var(--st-crit);
  font-family: var(--font-cond);
  font-size: var(--fs-sub);
  font-weight: 800;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  text-shadow: 0 0 var(--sp-2) color-mix(in srgb, var(--st-crit) 80%, transparent);
}
.emg__plate-icon {
  width: var(--fs-sub);
  height: var(--fs-sub);
}

/* ── Thẻ giữa: ngoặc góc HUD → khung vát góc (viền đỏ) → thân thẻ ───────── */
.emg__hud {
  position: relative;
  max-width: 64%;
  padding: var(--sp-3);
  /* Bốn ngoặc góc vẽ bằng 8 vạch gradient — một phần tử, không thêm DOM. */
  background:
    linear-gradient(var(--st-crit), var(--st-crit)) top left / var(--sp-8) var(--emg-edge) no-repeat,
    linear-gradient(var(--st-crit), var(--st-crit)) top left / var(--emg-edge) var(--sp-8) no-repeat,
    linear-gradient(var(--st-crit), var(--st-crit)) top right / var(--sp-8) var(--emg-edge) no-repeat,
    linear-gradient(var(--st-crit), var(--st-crit)) top right / var(--emg-edge) var(--sp-8) no-repeat,
    linear-gradient(var(--st-crit), var(--st-crit)) bottom left / var(--sp-8) var(--emg-edge) no-repeat,
    linear-gradient(var(--st-crit), var(--st-crit)) bottom left / var(--emg-edge) var(--sp-8) no-repeat,
    linear-gradient(var(--st-crit), var(--st-crit)) bottom right / var(--sp-8) var(--emg-edge) no-repeat,
    linear-gradient(var(--st-crit), var(--st-crit)) bottom right / var(--emg-edge) var(--sp-8) no-repeat;
  animation: emg-in 0.35s cubic-bezier(0.2, 1.4, 0.4, 1) both;
}

.emg__frame {
  --emg-bevel: var(--sp-6);
  padding: 2px;
  background: var(--st-crit);
  clip-path: polygon(var(--emg-bevel) 0, 100% 0, 100% calc(100% - var(--emg-bevel)), calc(100% - var(--emg-bevel)) 100%, 0 100%, 0 var(--emg-bevel));
  filter: drop-shadow(0 0 var(--sp-4) color-mix(in srgb, var(--st-crit) 70%, transparent));
}

.emg__card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-8) var(--sp-5);
  background: linear-gradient(180deg, color-mix(in srgb, var(--st-crit) 18%, var(--bg-panel-solid)), var(--bg-panel-solid) 60%);
  clip-path: polygon(var(--emg-bevel) 0, 100% 0, 100% calc(100% - var(--emg-bevel)), calc(100% - var(--emg-bevel)) 100%, 0 100%, 0 var(--emg-bevel));
  text-align: center;
}
/* Vạch quét mờ như màn radar — tĩnh. */
.emg__card::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(0deg, color-mix(in srgb, var(--st-crit) 7%, transparent) 0 1px, transparent 1px 4px);
  pointer-events: none;
}

.emg__icon {
  width: var(--fs-hero);
  height: var(--fs-hero);
  color: var(--st-crit);
  filter: drop-shadow(0 0 var(--sp-2) color-mix(in srgb, var(--st-crit) 85%, transparent));
}

.emg__tag {
  padding: var(--sp-1) var(--sp-4);
  border-radius: var(--r-sm);
  background: var(--st-crit);
  color: var(--bg-void);
  font-family: var(--font-cond);
  font-size: var(--fs-sub);
  font-weight: 800;
  letter-spacing: 0.24em;
  animation: emg-blink 1.2s steps(2, jump-none) infinite;
}

.emg__title {
  color: var(--text-hi);
  font-family: var(--font-cond);
  font-size: var(--fs-hero);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  text-wrap: balance;
  text-shadow: 0 0 var(--sp-4) color-mix(in srgb, var(--st-crit) 75%, transparent);
}

/* Tiêu đề rất dài (tên khu, lô, nhà máy) hạ một bậc để không quá 3 dòng — cấm cắt "…". */
.emg__title--long {
  font-size: var(--fs-num-lg);
}

/* Vị trí là câu hỏi thứ hai của người trực ("ở đâu?") — đủ to, đủ sáng để đọc từ 5 m. */
.emg__src {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--text-hi);
  font-size: var(--fs-num);
  font-weight: 600;
}
.emg__src-icon {
  flex: none;
  width: var(--fs-num);
  height: var(--fs-num);
  color: var(--st-crit);
}

.emg__timer {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-2);
  padding: var(--sp-1) var(--sp-5);
  border-block: 1px solid color-mix(in srgb, var(--st-crit) 55%, transparent);
  color: var(--st-crit);
}
.emg__timer-icon {
  width: var(--fs-num-lg);
  height: var(--fs-num-lg);
}
.emg__timer-value {
  font-family: var(--font-num);
  font-size: var(--fs-hero);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 var(--sp-3) color-mix(in srgb, var(--st-crit) 80%, transparent);
}

.emg__meta {
  color: var(--text-hi);
  font-size: var(--fs-sub);
}

/* Cảnh báo khẩn cấp khác có thể còn nặng hơn — chip phải nổi, không được thành chú thích nhỏ. */
.emg__more {
  margin-top: var(--sp-3);
  padding: var(--sp-1) var(--sp-4);
  border: 2px solid var(--st-crit);
  border-radius: var(--r-sm);
  background: var(--st-crit-bg);
  color: var(--st-crit);
  font-family: var(--font-cond);
  font-size: var(--fs-sub);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* ── Chuyển động ─────────────────────────────────────────────────────── */
@keyframes emg-flash {
  0% { opacity: 0.45; }
  100% { opacity: 0; }
}

@keyframes emg-breathe {
  0%, 100% { opacity: 0.5; }
  50% { opacity: 1; }
}

@keyframes emg-slide {
  to { transform: translateX(calc(var(--emg-stripe) * -2.8285)); }
}

@keyframes emg-in {
  from { opacity: 0; transform: scale(1.08); }
  to { opacity: 1; transform: scale(1); }
}

@keyframes emg-blink {
  50% { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .emg__flash,
  .emg__vignette,
  .emg__stripes,
  .emg__hud,
  .emg__tag {
    animation: none;
  }
}

/* ── Dải "Đang xử lý phiếu khẩn cấp" (InduX Q23) ─────────────────────────── */
/* Nổi trên khung, không chiếm chỗ của màn nào; đỏ đứng yên — lớp báo động ở trên mới là thứ chuyển động. */
.emg-strip {
  position: fixed;
  top: var(--sp-2);
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  max-width: 70vw;
  padding: var(--sp-1) var(--sp-4);
  border-radius: var(--r-pill);
  background: var(--st-crit);
  color: var(--bg-void);
  font-family: var(--font-cond);
  font-size: var(--fs-sub);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}

.emg-strip__icon {
  flex: none;
  width: 1.1em;
  height: 1.1em;
}

.emg-strip__more {
  opacity: 0.85;
}

/**
 * Màn Quan trắc môi trường (S06) — các card riêng của màn.
 *
 *   .envq  VN_AQI 24 giờ (chip trạm · cột theo thang màu · thang chú giải)
 *   .envp  bảng chỉ tiêu không khí (dùng chung cho tên/đơn vị chỉ tiêu của bảng nước mặt)
 *   .envw  khí tượng & la bàn gió
 *   .envn  tiếng ồn ranh KCN
 *   .envs  nước mặt nguồn tiếp nhận
 *   .envk  khí thải ống khói · .envc lịch tuân thủ (ngăn phải)
 *
 * Bảng chỉ tiêu, ống khói, chất thải, tuân thủ dùng KHUÔN 1 `card-table`; cảnh báo dùng KHUÔN 2
 * `card-feed` (list.css). File này chỉ còn giữ các ô con đặc thù của màn.
 *
 * Chất liệu theo hệ VSS: tách lớp bằng tầng màu và bóng, không kẻ khung. Màu thang (VN_AQI,
 * VN_WQI) và màu trạng thái chỉ xuất hiện ở chỗ có số; khung và nền giữ trung tính.
 * Chữ dài XUỐNG DÒNG, không cắt "…" (lệnh 16/09).
 */

/* ══ VN_AQI 24 giờ ══════════════════════════════════════════════════════ */

.envq {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.envq__stations {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  grid-auto-flow: column;
  gap: var(--sp-2);
}

/* Chip trạm: viền mảnh màu MỨC AQI hiện tại, số AQI to — vừa là bộ chọn vừa là số đọc. */
.envq__chip {
  --envq-c: var(--text-faint);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'name val'
    'lvl  val';
  align-items: center;
  column-gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: none;
  border-radius: var(--r-row);
  background: var(--bg-well);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--envq-c) 35%, transparent);
  font: inherit;
  text-align: left;
  color: inherit;
  cursor: pointer;
  transition: background 0.15s;
}

.envq__chip:hover {
  background: var(--bg-row-hover);
}

.envq__chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.envq__chip--on,
.envq__chip--on:hover {
  background: var(--bg-row-sel);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.envq__chip-name {
  grid-area: name;
  font-size: var(--fs-body);
  font-weight: var(--fw-strong);
  line-height: 1.3;
  color: var(--text-hi);
  overflow-wrap: anywhere;
}

.envq__chip-lvl {
  grid-area: lvl;
  font-size: var(--fs-cap);
  line-height: 1.3;
  color: var(--text-mid);
}

.envq__chip-val {
  grid-area: val;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-num);
  font-weight: var(--fw-title);
  line-height: 1;
  color: var(--envq-c);
}

.envq__chart {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* Thang chú giải 6 mức: một dải liền, mỗi mức một ô màu + khoảng số + tên. */
.envq__scale {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.envq__band {
  --envq-c: var(--text-faint);
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 3px var(--sp-1) 2px;
  border-top: 3px solid var(--envq-c);
  background: color-mix(in srgb, var(--envq-c) 10%, transparent);
  text-align: center;
}

.envq__band-range {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-cap);
  color: var(--text-hi);
}

.envq__band-name {
  font-size: var(--fs-cap);
  line-height: 1.2;
  color: var(--text-mid);
  overflow-wrap: anywhere;
}

/* ══ Bảng chỉ tiêu (không khí · nước mặt) — ô con trong khuôn `card-table` ═══════ */

.envp__unit {
  margin-left: var(--sp-1);
  font-size: var(--fs-cap);
  font-weight: var(--fw-body);
  color: var(--text-lo);
}

.envp__avg {
  color: var(--text-mid);
  font-size: var(--fs-body);
  white-space: nowrap;
}

.envp__lim {
  color: var(--text-mid);
  font-family: var(--font-num);
  font-size: var(--fs-body);
}

.envp__pct {
  color: var(--text-mid);
  font-family: var(--font-num);
  font-size: var(--fs-body);
}

/* ══ Khí tượng & la bàn gió ═════════════════════════════════════════════ */

.envw {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--sp-2);
}

.envw__rose {
  width: 100%;
  height: 100%;
  min-height: 0;
}

.envw__ring {
  fill: none;
  stroke: var(--line-mid);
  stroke-width: 1.2;
}

.envw__ring--in {
  stroke: var(--line-soft);
  stroke-dasharray: 2 3;
}

.envw__tick {
  stroke: var(--line-mid);
  stroke-width: 1.2;
}

.envw__card {
  fill: var(--text-hi);
  font-family: var(--font-cond);
  font-size: 15px;
  font-weight: var(--fw-title);
}

.envw__trail {
  fill: var(--accent-2);
}

.envw__arrow {
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linecap: round;
}

.envw__head {
  fill: var(--accent);
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 60%, transparent));
}

.envw__hub {
  fill: var(--bg-panel-solid);
  stroke: var(--line-mid);
}

.envw__speed {
  fill: var(--text-hi);
  font-family: var(--font-num);
  font-size: 14px;
  font-weight: var(--fw-title);
}

.envw__speed-u {
  fill: var(--text-lo);
  font-size: 9px;
}

.envw__stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-1) var(--sp-2);
}

.envw__dir {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px var(--sp-2);
}

.envw__dir-v {
  font-size: var(--fs-body-lg);
  font-weight: var(--fw-strong);
  color: var(--text-hi);
}

.envw__dir-deg,
.envw__stat-u {
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

.envw__stat {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-key);
  background: var(--bg-well);
}

.envw__stat-v {
  display: flex;
  align-items: baseline;
  gap: 3px;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sub);
  font-weight: var(--fw-title);
  line-height: 1.2;
  color: var(--text-hi);
}

/* Dòng trạng thái dưới khối số: chỉ còn "Mất dữ liệu" (màu lỗi) khi số hết hạn. */
.envw__meta {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px var(--sp-2);
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

.envw__lost {
  color: var(--st-crit);
  font-weight: var(--fw-strong);
}

.envw__info {
  display: inline-flex;
  padding: 0;
  border: 0;
  background: transparent;
  align-items: center;
  color: var(--text-lo);
  cursor: help;
}

/* ══ Tiếng ồn ranh KCN ══════════════════════════════════════════════════ */

.envn {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  gap: var(--gap-row);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Một điểm đo: tên (trái, xuống dòng được) · số · thanh so ngưỡng · LAmax · đường 24h. */
.envn__row {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) auto minmax(0, 1fr) minmax(0, 0.9fr);
  grid-template-areas:
    'name val meter spark'
    'name max meter spark';
  align-items: center;
  column-gap: var(--sp-3);
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-row);
  background: var(--bg-row);
}

.envn__name {
  grid-area: name;
  font-size: var(--fs-body);
  font-weight: var(--fw-strong);
  line-height: 1.3;
  color: var(--text-hi);
  overflow-wrap: anywhere;
}

.envn__val {
  grid-area: val;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sub);
  font-weight: var(--fw-title);
  line-height: 1.1;
  color: var(--text-hi);
  text-align: right;
}

.envn__unit {
  margin-left: 3px;
  font-size: var(--fs-cap);
  font-weight: var(--fw-body);
  color: var(--text-lo);
}

.envn__max {
  grid-area: max;
  font-family: var(--font-num);
  font-size: var(--fs-cap);
  color: var(--text-lo);
  text-align: right;
  white-space: nowrap;
}

.envn__meter {
  --envn-w: 0%;
  --envn-lim: 100%;
  --envn-c: var(--st-good);
  grid-area: meter;
  position: relative;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--bg-raised);
}

.envn__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--envn-w);
  border-radius: inherit;
  background: var(--envn-c);
}

/* Vạch ngưỡng của khung giờ hiện tại — vẽ ĐÈ lên phần lấp để vẫn thấy khi số vượt. */
.envn__lim {
  position: absolute;
  top: -4px;
  bottom: -4px;
  left: var(--envn-lim);
  width: 2px;
  border-radius: 1px;
  background: var(--text-hi);
  transform: translateX(-1px);
}

.envn__spark {
  grid-area: spark;
  height: 30px;
  min-width: 0;
}

/* ══ Nước mặt nguồn tiếp nhận ═══════════════════════════════════════════ */

.envs {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.envs__heads {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-2);
  padding: 0 0 var(--sp-1);
}

.envs__head {
  --envs-c: var(--text-faint);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'pos wqi'
    'lvl wqi';
  align-items: center;
  column-gap: var(--sp-2);
  padding: 3px var(--sp-3);
  border-radius: var(--r-row);
  background: var(--bg-well);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--envs-c) 35%, transparent);
}

.envs__pos {
  grid-area: pos;
  font-size: var(--fs-body);
  font-weight: var(--fw-strong);
  color: var(--text-hi);
}

.envs__lvl {
  grid-area: lvl;
  font-size: var(--fs-cap);
  color: var(--envs-c);
}

.envs__wqi {
  grid-area: wqi;
  display: flex;
  align-items: baseline;
  gap: 4px;
}

.envs__wqi-v {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-num);
  font-weight: var(--fw-title);
  line-height: 1;
  color: var(--envs-c);
}

.envs__wqi-u {
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

/* Ngày lấy mẫu là thứ phân biệt số định kỳ với số tức thời — không được là dòng mờ nhỏ nhất màn. */
.envs__foot {
  flex: 0 0 auto;
  padding: var(--sp-1) 0 0;
  font-size: var(--fs-body);
  line-height: 1.4;
  color: var(--text-mid);
}

/* ══ Cảnh báo môi trường ═════════════════════════════════════════════════ */

/* Cảnh báo ĐÃ XỬ LÝ: số đọc lùi về xám — cả ô toàn số màu khi không còn gì mở trông như đang
   có sự cố. Chỉ màn này: `card-feed` còn chở thẻ của màn khác (sự kiện AI…) với nghĩa khác. */
.scr--env .card-feed__card--good .card-feed__card-reading {
  color: var(--text-lo);
}

/* ══ Khí thải ống khói ══════════════════════════════════════════════════ */

.envk__lim {
  display: block;
  font-size: var(--fs-cap);
  font-weight: var(--fw-body);
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-lo);
}

.envk__name {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.envk__factory {
  display: block;
  font-weight: var(--fw-strong);
  line-height: 1.3;
  color: var(--text-hi);
  white-space: normal;
  overflow-wrap: anywhere;
}

.envk__stack {
  display: block;
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

/* ══ Lịch tuân thủ — ô con trong khuôn `card-table` ═══════════════════════ */

.envc__main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.envc__name {
  font-size: var(--fs-body);
  font-weight: var(--fw-strong);
  line-height: 1.3;
  color: var(--text-hi);
  overflow-wrap: anywhere;
}

.envc__last {
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

.envc__due {
  display: grid;
  justify-items: end;
  gap: 2px;
}

.envc__date {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-body);
  color: var(--text-hi);
}

.envc__left {
  font-size: var(--fs-cap);
  color: var(--text-lo);
}

/**
 * Ngăn "Chỉ số ảnh hưởng chi phí" màn Năng lượng (S11, 15/10).
 *
 *   .cdr-grid   lưới 2 cột × 3 hàng, chia đều chiều cao ngăn
 *   .cdr-block  một chỉ số: nhãn · số to · dòng ngữ cảnh
 *
 * Màu trạng thái CHỈ ở con số (BE chấm `warn`/`crit`); khung trung tính, KHÔNG vạch màu mép
 * trái (user bỏ 15/10). Chữ dài xuống dòng, không cắt "…" (lệnh 16/09).
 */

.cdr-grid {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: minmax(0, 1fr);
  gap: var(--sp-2);
}

.cdr-block {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-height: 0;
  padding: var(--sp-3);
  border-radius: var(--r-row);
  background: var(--bg-well);
  box-shadow: var(--sh-hair);
  overflow: hidden;
}

.cdr-block__title {
  margin: 0;
  color: var(--text-mid);
  font-size: var(--fs-label);
  font-weight: var(--fw-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.cdr-block__value {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: var(--sp-1);
}

.cdr-block__value strong {
  font-family: var(--font-num);
  font-size: var(--fs-kpi);
  font-weight: var(--fw-value);
  font-variant-numeric: tabular-nums;
  color: var(--text-hi);
  line-height: 1.1;
}

.cdr-block--warn .cdr-block__value strong { color: var(--st-warn); }
.cdr-block--crit .cdr-block__value strong { color: var(--st-crit); }

.cdr-block__value em {
  font-style: normal;
  font-size: var(--fs-body);
  color: var(--text-lo);
}

.cdr-block__lines {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 0;
  color: var(--text-mid);
  font-size: var(--fs-body);
  line-height: 1.35;
}

.cdr-block__lines p {
  margin: 0;
}

/* Dòng điểm đo gây ra: tên bên trái (xuống dòng được), số bên phải. */
.cdr-block__row {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-2);
}

.cdr-block__row b {
  flex: 0 0 auto;
  font-family: var(--font-num);
  font-weight: var(--fw-value);
  font-variant-numeric: tabular-nums;
  color: var(--text-hi);
}

/* Ô lẻ cuối (năm chỉ số trong lưới hai cột) trải hết bề ngang. */
.cdr-block--wide {
  grid-column: 1 / -1;
}
* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  /* 100% rather than 100vw: 100vw counts the scrollbar gutter and produces a
     phantom horizontal scrollbar on a wall display. */
  width: 100%;
  height: 100%;
  background: var(--bg-void);
  color: var(--text-hi);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;
  user-select: none;
}

#root {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

button {
  font-family: inherit;
  cursor: pointer;
}

/* ══════════════════════════════════════════════════════════════════════
   Shell
   ══════════════════════════════════════════════════════════════════════ */

.shell {
  width: 100%;
  height: 100%;
  display: grid;
  /* BỐN hàng cố định theo mẫu S07: thanh lệnh · dải phân hệ · vùng màn ·
     băng sự kiện. 20/09 từng rút xuống ba hàng (gộp tên màn vào thanh lệnh);
     21/09 trả lại hàng riêng theo ảnh mẫu người dùng — chip tên màn cần một
     hàng của nó để đọc được từ 3–5m.
     Mẫu S07 còn một hàng nữa cho dải KPI; ở app này KPI là CARD trong lưới
     kéo-thả nên nó sống trong hàng `1fr`, không tách riêng. */
  grid-template-rows: var(--h-masthead) var(--h-nav) minmax(0, 1fr) var(--h-footer);
  gap: var(--gutter);
  /* Lề khung chuyển từ `.shell__main` lên đây: mọi thanh đều NỔI, cách mép
     màn và cách nhau đúng một --gutter (mẫu S07 `.app`). */
  padding: var(--gutter);
  overflow: hidden;
  position: relative;
  isolation: isolate;
  background: var(--bg-app);
}

/* Ambient backdrop: a blueprint grid plus a cold zenith wash, tinted by the
   active screen's accent. Pure CSS, so it costs nothing on a 24/7 display. */
.shell::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  /* Lưới ở mức "thì thầm": nhìn từ 3m gần như không thấy, chỉ đủ phá cảm giác
     nền chết. Đậm hơn nữa là nó cạnh tranh với số liệu — thứ duy nhất đáng
     nhìn trên tường. Giá trị ở token để chỉnh một chỗ. */
  background-image:
    linear-gradient(rgba(255, 255, 255, var(--bg-grid-opacity)) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, var(--bg-grid-opacity)) 1px, transparent 1px);
  background-size: var(--bg-grid-size) var(--bg-grid-size);
}

.shell::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* Ba vệt sáng thay một vệt: vệt 1 đi theo accent của màn (đổi màn là đổi
     tông cả trang), hai vệt phụ lệch tâm để nền không đối xứng cứng đờ.
     Cộng một vệt tối ở đáy để chân màn hình lún xuống, đỡ chói mắt ca đêm. */
  background:
    radial-gradient(ellipse 90% 48% at 22% -8%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 60%),
    radial-gradient(ellipse 70% 44% at 88% 4%, color-mix(in srgb, var(--accent-vision) 6%, transparent), transparent 62%),
    radial-gradient(ellipse 80% 40% at 50% 108%, color-mix(in srgb, var(--accent-effluent) 4%, transparent), transparent 60%),
    radial-gradient(ellipse 70% 50% at 50% 118%, rgba(0, 0, 0, 0.55), transparent 60%);
  transition: background 0.5s ease;
}

/* The screen canvas. Screens fill it exactly; nothing here scrolls. */
.shell__main {
  min-height: 0;
  min-width: 0;
  position: relative;
  overflow: hidden;
}

/* Every screen root is a grid that consumes 100% of the canvas. This is the
   rule that eliminates the dead space the previous layout left below its
   side columns. */
.screen {
  width: 100%;
  height: 100%;
  display: grid;
  gap: var(--gutter);
  min-width: 0;
  min-height: 0;
}

/* ── Wall mode ────────────────────────────────────────────────────────── */
/* Wall mode giữ ĐỦ cả thanh lệnh lẫn dải phân hệ: người đi ngang qua tường
   cần đọc được ngay mình đang đứng trước màn nào, và một wall bị set nhầm màn
   phải đổi được tại chỗ. Lề nay do `.shell` giữ nên ở đây không còn gì để
   chỉnh — khối này cố ý để trống, xoá hẳn thì lần sau lại có người thêm lại. */

/* ── Boot screen ──────────────────────────────────────────────────────── */

.boot {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-5);
  background: var(--bg-void);
}

.boot__ring {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 2px solid rgba(0, 229, 255, 0.18);
  border-top-color: var(--accent-command);
  animation: spin 0.9s linear infinite;
}

.boot__text {
  font-size: var(--fs-label);
  font-weight: 800;
  letter-spacing: var(--track-wide);
  color: var(--accent-command);
  text-transform: uppercase;
}

.boot__bar {
  width: 240px;
  height: 2px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
  border-radius: 2px;
}

.boot__bar span {
  display: block;
  width: 40%;
  height: 100%;
  background: var(--accent-command);
  box-shadow: 0 0 10px var(--accent-command);
  animation: t-sweep 1.4s ease-in-out infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes ioc-title-sheen {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
/**
 * Template "renew" (16/09) — CHỈ ghi đè GIÁ TRỊ các token đã khai trong `tokens.css` (rule 07 §1).
 *
 * Bật theo màn: `data-template="renew"` trên `.dgs` (khoá `__view:template` trong `screen_texts`).
 * Hướng thiết kế: bề mặt tối sâu, bo góc lớn; bỏ vạch màu chạy ngang đầu panel (màu phân hệ đã
 * nằm ở chấm tiêu đề); đầu panel trong suốt, ngăn với thân bằng một đường mảnh; đầu cao hơn
 * một chút cho thoáng; kính mờ nhẹ cho card nổi trên bản đồ (8px — GPU tường yếu, bản đồ 3D vẽ
 * lại liên tục phía sau).
 */
[data-template='renew'] {
  --r-panel: 18px;
  --rail-h: 0px;
  --h-panel-head: 30px;
  --blur-glass: 8px;
  --grad-head: linear-gradient(180deg, transparent, transparent);
  --sh-head: inset 0 -1px 0 var(--line-soft);
  --grad-panel: linear-gradient(
    180deg,
    color-mix(in srgb, var(--bg-panel-hi) 94%, transparent) 0%,
    color-mix(in srgb, var(--bg-panel-solid) 94%, transparent) 55%,
    color-mix(in srgb, var(--bg-void) 90%, transparent) 100%
  );
  --grad-kpi: linear-gradient(
    180deg,
    color-mix(in srgb, var(--bg-panel-hi) 94%, transparent),
    color-mix(in srgb, var(--bg-void) 90%, transparent)
  );
}
