:root {
  --sidebar-w: 320px;
}

.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  grid-template-rows: 100vh;
  /* ép DÚNG 1 hàng cao 100vh - thiếu dòng này, hàng grid tự auto-size
    theo nội dung DÀI NHẤT trong 2 cột (thường là #main), khiến cả #sidebar lẫn #main bị kéo cao
    hơn viewport và TRANG (thay vì từng cột) mới là thứ thật sự cuộn - đây là nguyên nhân "cuộn
    sidebar và layout chính dính vào nhau". */
  height: 100vh;
  /* 2026-08-21 (theo yêu cầu người dùng: "chuyển động mở đóng sidebar mượt mà hơn") - trước đây
     đóng/mở CHỈ toggle class .sidebar-collapsed (0px <-> var(--sidebar-w)) không hề có transition
     -> nhảy khựng tức thì. grid-template-columns ĐỘNG VIÊN được (browser hiện đại nội suy mượt
     giữa 2 giá trị length khi SỐ LƯỢNG track không đổi - ở đây luôn 2 track) - cách rẻ nhất, không
     cần JS đo/animate width bằng tay. */
  transition: grid-template-columns 280ms cubic-bezier(0.25, 0.8, 0.25, 1);
}

.app-shell.sidebar-resizing {
  cursor: col-resize;
  user-select: none;
}

/* Lúc đang KÉO TAY (mousemove liên tục), applySidebarState() gọi lại rất nhiều lần/giây - giữ
   transition ở đây sẽ làm sidebar "trễ nhịp" đuổi theo chuột (rubber-band lag), phải TẮT hẳn
   animation trong lúc kéo, chỉ animate khi bấm nút đóng/mở (đổi state 1 LẦN, không liên tục). */
.app-shell.sidebar-resizing {
  transition: none;
}

.app-shell.sidebar-collapsed {
  grid-template-columns: 0px 1fr;
}

/* ---- Sidebar (tương đương sidebar Streamlit: mode + filter + model options, kéo giãn +
   đóng/mở được — xem app.js phần "Sidebar resize/collapse") ---- */
#sidebar {
  position: relative;
  min-width: 0;
  min-height: 0;
  /* cho phép grid item CO LẠI đúng bằng chiều cao hàng (100vh) thay vì tự auto-
    size theo nội dung - bắt buộc phải có để overflow-y:auto phía dưới thực sự phát huy tác dụng. */
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border);
  padding: 16px 14px;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  gap: 18px;
  transition: padding 280ms cubic-bezier(0.25, 0.8, 0.25, 1);
}

.app-shell.sidebar-resizing #sidebar {
  transition: none;
}

/* đồng bộ với .app-shell - tắt lúc kéo tay */
.app-shell.sidebar-collapsed #sidebar {
  padding: 16px 0;
  border-right: none;
}

/* Nội dung sidebar MỜ DẦN theo đúng nhịp đóng/mở (280ms, khớp transition grid-template-columns
   ở trên) thay vì bị BÓP MÉO/xuống dòng loạn xạ khi cột co hẹp dần rồi biến mất đột ngột lúc
   min-width:0 chạm - opacity fade cho cảm giác "tan biến" mượt, che luôn phần chữ bị bóp trong
   lúc cột đang thu hẹp (vẫn có mặt trong DOM, chỉ ẩn thị giác). pointer-events tắt để không bấm
   nhầm vào nút ẩn lúc đang/đã đóng. */
#sidebar>* {
  transition: opacity 160ms ease;
}

.app-shell.sidebar-collapsed #sidebar>* {
  opacity: 0;
  pointer-events: none;
}

/* Tay kéo giãn — dải mỏng ở mép phải sidebar, hiện khi hover/kéo để không rối mắt lúc bình thường. */
#sidebar-resize {
  position: absolute;
  top: 0;
  right: -4px;
  bottom: 0;
  width: 8px;
  cursor: col-resize;
  z-index: 25;
}

#sidebar-resize::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 3px;
  width: 2px;
  background: transparent;
  border-radius: 2px;
}

#sidebar-resize:hover::after,
.app-shell.sidebar-resizing #sidebar-resize::after {
  background: var(--accent);
}

.app-shell.sidebar-collapsed #sidebar-resize {
  display: none;
}

/* Nút đóng/mở — "tab" bán nguyệt gắn liền mép sidebar, nằm giữa chiều cao màn hình (dễ với
   tay hơn nút nhỏ ở góc trên). Nửa tròn lồi vào phần nội dung chính, cạnh phẳng áp sát sidebar. */
#sidebar-toggle {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 30;
  width: 18px;
  height: 44px;
  border-radius: 0 999px 999px 0;
  border: 1px solid var(--border);
  border-left: none;
  background: var(--bg-elevated);
  color: var(--fg-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 12px;
  box-shadow: var(--shadow);
  padding: 0;
  /* "left" set qua inline style bằng JS (applySidebarState() - theo đúng mép sidebar, xem
     app.js) - animate CHÍNH property đó để tab bán nguyệt trượt THEO ĐÚNG NHỊP với mép sidebar
     đang thu/giãn, không bị "nhảy cóc" tới vị trí mới trước khi sidebar kịp đóng xong. */
  transition: left 280ms cubic-bezier(0.25, 0.8, 0.25, 1);
}

.app-shell.sidebar-resizing #sidebar-toggle {
  transition: none;
}

/* tắt lúc kéo tay - lý do y hệt #sidebar */
#sidebar-toggle:hover {
  color: var(--fg);
  border-color: var(--accent);
}

#sidebar-toggle .bi {
  transition: transform 280ms cubic-bezier(0.25, 0.8, 0.25, 1);
}

#sidebar-toggle.flipped .bi {
  transform: rotate(180deg);
}

#sidebar h1 {
  font-size: 15px;
  margin: 0 0 2px;
  letter-spacing: .2px;
}

#sidebar .subtitle {
  font-size: 11.5px;
  color: var(--fg-muted);
  margin: 0 0 6px;
}

.field-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.field-group label.group-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--fg-muted);
  font-weight: 600;
}

.seg-control {
  display: flex;
  flex-wrap: wrap;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.seg-control button {
  flex: 1 1 25%;
  min-width: 0;
  padding: 7px 3px;
  background: var(--bg-elevated);
  color: var(--fg-muted);
  border: none;
  cursor: pointer;
  font-size: 11.5px;
  border-right: 1px solid var(--border);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.seg-control button:last-child {
  border-right: none;
}

.seg-control button.active {
  background: var(--accent);
  color: var(--accent-fg);
  font-weight: 600;
}

/* 2026-09-04 (theo yêu cầu người dùng, ảnh chụp thật - "padding cho đỡ bị dính nhau") - #page-
   tabs giờ có 4 nút (thêm "Search ASR") với nhãn DÀI hơn hẳn #mode-control (KIS/Q&A/TRAKE/
   Temporal, .seg-control dùng chung) - flex 25%/nút của .seg-control gốc làm chữ bị cắt
   ("Tìm...", "Sea..."). Override RIÊNG cho #page-tabs: chia 2 nút/hàng (2x2) thay vì 4/hàng,
   thêm padding + viền ngang phân tách 2 hàng rõ ràng (border-right gốc chỉ phân tách NGANG,
   không đủ khi xuống hàng). */
#page-tabs button {
  flex: 1 1 46%;
  padding: 9px 8px;
  font-size: 12.5px;
  border-bottom: 1px solid var(--border);
}

#page-tabs button:nth-last-child(-n+2) {
  border-bottom: none;
}

/* 2026-08-24 (theo yêu cầu người dùng: "thêm tổng thời gian chạy và thời gian chạy cho từng
   level") - thời gian tầng, hiện MỜ HƠN nhãn tầng để không cạnh tranh thị giác với tên/số lượng
   (thông tin phụ trợ), tabular-nums để các tab không giật chữ khi số đổi giữa các lần chạy. */
.seg-control button .dbg-tab-time {
  margin-left: 5px;
  opacity: .65;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
}

input[type="text"],
input[type="number"],
input[type="date"],
select,
textarea {
  width: 100%;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg);
  color: var(--fg);
  outline: none;
}

textarea {
  resize: vertical;
  min-height: 52px;
}

input[type="text"]:disabled,
input[type="number"]:disabled,
input[type="date"]:disabled,
select:disabled,
textarea:disabled {
  opacity: .45;
  cursor: not-allowed;
  background: var(--bg-elevated);
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

/* 2026-08-23 (theo yêu cầu người dùng: "thiết kế các checkbox này đẹp hơn giúp mình nhé") -
   TRƯỚC ĐÂY dùng checkbox mặc định trình duyệt (align-items:center) - với nhãn dài xuống 2 dòng
   (vd "Tách câu thành từng mệnh đề...") checkbox bị kéo xuống GIỮA khối 2 dòng thay vì nằm ngay
   đầu dòng 1, nhìn lệch hẳn (đúng như ảnh chụp). Đổi align-items:flex-start (neo theo đỉnh, đúng
   chỗ dòng đầu) + tự vẽ lại checkbox (appearance:none) bằng token màu của app thay vì để hệ điều
   hành vẽ - vuông bo góc, viền rõ, khi tick chuyển nền --accent + dấu tick vẽ bằng border-trick
   (không cần ảnh/SVG rời) - nhất quán với các nút bấm khác trong app, tương phản đúng theo theme. */
.checkbox-row {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  font-size: 12.5px;
  color: var(--fg);
  cursor: pointer;
  line-height: 1.45;
  padding: 3px 0;
}

.checkbox-row input[type="checkbox"] {
  -webkit-appearance: none;
  appearance: none;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  border: 1.5px solid var(--border);
  border-radius: 4px;
  background: var(--bg-elevated);
  cursor: pointer;
  display: inline-grid;
  place-content: center;
  transition: background 150ms ease, border-color 150ms ease;
}

.checkbox-row input[type="checkbox"]:hover {
  border-color: var(--accent);
}

.checkbox-row input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
}

.checkbox-row input[type="checkbox"]:checked::after {
  content: "";
  width: 4px;
  height: 8px;
  border: solid var(--accent-fg);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-1px, -1px);
}

.checkbox-row input[type="checkbox"]:focus-visible {
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* min-width:0 tránh input[type=date] (icon lịch gốc trình duyệt) ép 2 cột lệch nhau/cấn icon
   ở mép - lỗi "lõm" đã gặp khi sidebar hẹp. */
.row-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.row-2>* {
  min-width: 0;
}

input[type="date"] {
  padding-right: 6px;
}

/* input[type=date] KHÔNG hỗ trợ placeholder (trình duyệt luôn tự hiện "dd-----yyyy", bỏ qua
   placeholder đặt sẵn) - dùng nhãn nhỏ phía trên thay thế để rõ đây là "từ ngày"/"đến ngày". */
.date-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.date-label {
  font-size: 10.5px;
  color: var(--fg-muted);
  padding-left: 1px;
}

.field-note {
  font-size: 11px;
  color: var(--fg-muted);
  margin: 0;
  line-height: 1.4;
}

.slider-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.slider-row input[type="range"] {
  flex: 1;
}

.slider-row output {
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--fg-muted);
  width: 34px;
  text-align: right;
}

/* 2026-08-23 (theo yêu cầu người dùng: "làm lại màu sắc cho những thành phần này thành màu
   tương phản trắng đen trong các mode nhé", ảnh chụp track/thumb mặc định của trình duyệt gần
   như vô hình trên nền sáng) - TRƯỚC ĐÂY input[type=range] KHÔNG có style riêng, dùng nguyên
   giao diện mặc định hệ điều hành (track xám nhạt viền mảnh, thumb xanh) - độ tương phản với nền
   --bg/--bg-elevated quá thấp để nhìn rõ. Track/thumb giờ style THẲNG bằng token --fg/--border/
   --accent nên LUÔN tương phản đúng theo theme (áng sáng: track xám đậm rõ + thumb đen/trắng
   viền rõ; theme tối: ngược lại) - style riêng cho từng engine (-webkit-/-moz-) vì range input
   là 1 trong số ít phần tử KHÔNG style chung được qua 1 selector duy nhất giữa các trình duyệt. */
/* 2026-08-23 tiếp (theo yêu cầu người dùng: "cho mình cái màu đường phía bên trái có màu fill
   giống như thanh cũ nhé, nhưng giờ là màu tương phản trắng đen") - giao diện mặc định cũ có 1
   đoạn "fill" bên trái nút kéo (thường tô xanh) để biết đã kéo tới đâu; bản style riêng phía
   trên đã BỎ MẤT phần này (track chỉ 1 màu --border đồng nhất). Khôi phục lại phần fill nhưng
   đổi màu sang --fg (tương phản đen/trắng đúng theme, không cố định xanh) - biến CSS --range-
   fill (%) do range_fill.js tính và ghi đè inline (Chromium/WebKit không có pseudo-element công
   khai đại diện phần đã kéo như Firefox nên phải tự vẽ gradient theo %). */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 20px;
  background: transparent;
  cursor: pointer;
  --range-fill: 0%;
}

input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(to right, var(--fg) var(--range-fill), var(--border) var(--range-fill));
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--fg);
  border: 2px solid var(--bg);
  box-shadow: 0 0 0 1px var(--border);
}

input[type="range"]::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: var(--border);
}

/* Firefox CÓ pseudo-element riêng cho phần "đã kéo" - không cần JS tính % như Chromium/WebKit. */
input[type="range"]::-moz-range-progress {
  height: 6px;
  border-radius: 999px 0 0 999px;
  background: var(--fg);
}

input[type="range"]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--fg);
  border: 2px solid var(--bg);
  box-shadow: 0 0 0 1px var(--border);
}

input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px var(--accent-soft), 0 0 0 1px var(--accent);
}

input[type="range"]:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px var(--accent-soft), 0 0 0 1px var(--accent);
}

.btn {
  padding: 8px 12px;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  cursor: pointer;
  font-size: 13px;
}

.btn:hover {
  border-color: var(--accent);
}

.btn.primary {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
  font-weight: 600;
}

.btn.primary:hover {
  filter: brightness(1.08);
}

.btn.subtle {
  background: transparent;
  border-color: transparent;
  color: var(--fg-muted);
}

.btn:disabled {
  opacity: .5;
  cursor: default;
}

/* 2026-08-24 (theo yêu cầu người dùng: "xây dựng thêm chức năng interrupt... cho cả tab Tìm
   kiếm và Debug") - nút chính đổi sang trạng thái "đang chạy, bấm để huỷ" (xem app.js::
   currentSearchAbort / debug_tab.js::currentDebugAbort) - màu err để rõ ràng khác hẳn trạng
   thái "sẵn sàng bấm để chạy" (primary/accent), không lẫn lộn 2 hành vi trái ngược nhau. */
.btn.primary.cancelable {
  background: color-mix(in srgb, var(--err) 88%, black);
  border-color: var(--err);
  color: #fff;
}

.btn.primary.cancelable:hover {
  filter: brightness(1.1);
}

/* 2026-08-21 (theo yêu cầu người dùng: "dùng icon đẹp và hiện đại hơn" - đổi toàn bộ emoji sang
   bootstrap-icons, người dùng tự thêm CDN vào index.html) - icon đứng CẠNH chữ trong nút (không
   phải icon-only) cần margin-right nhẹ để không dính sát chữ; nút CHỈ CÓ icon (.icon-btn, thẻ
   kết quả) thì icon tự canh giữa theo text-align mặc định của <button>, không cần chỉnh thêm. */
.btn .bi {
  margin-right: 5px;
  vertical-align: -1px;
}

/* Nút CHỈ có icon (không chữ, xem .icon-btn ở khu action mỗi card + nút đổi theme) - bỏ margin
   để icon canh giữa đúng tâm nút, không lệch sang trái. */
.icon-btn .bi {
  margin-right: 0;
}

/* Nút đổi sáng/tối — góc trên phải sidebar, icon đổi ☀️/🌙 theo đúng theme đang hiện (xem app.js). */
#theme-toggle {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 5;
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: 999px;
  font-size: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ---- Main column ---- */
/* 2026-08-21 (bug thật: "màn hình nhỏ thì input chiếm hết màn hình, không tới được kết quả") -
   TRƯỚC ĐÂY chỉ #results tự cuộn riêng (flex:1 + overflow-y:auto), còn #main thì overflow:
   hidden - khi vùng input phía trên (canvas/mốc TRAKE cao) cao HƠN CẢ viewport, #results bị ép
   xuống 0px, KHÔNG CÁCH NÀO cuộn tới được. Đổi #main thành 1 VÙNG CUỘN DUY NHẤT (toàn bộ input
   + kết quả cuộn CHUNG theo thứ tự), đảm bảo luôn cuộn tới được kết quả bất kể input dài bao
   nhiêu - #results không cần tự cuộn riêng nữa (bỏ flex:1/overflow-y:auto, xem dưới). */
#main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

/* 2026-08-28 (theo yêu cầu người dùng: "làm sticky phần input này cho mọi mode để khi kéo lên
   kéo xuống xem kết quả vẫn có thể sửa input và nộp bài") - GHIM cả khối nhập liệu lên đầu vùng
   cuộn #main. Ghim CẢ KHỐI (không chỉ #topbar) vì input thật sự nằm ở chỗ khác nhau tuỳ mode:
   KIS -> ô truy vấn trong #topbar; Q&A -> #qa-box (cũng trong #topbar); TRAKE/Temporal -> thẻ
   Mốc trong #anchors-box (NGOÀI #topbar) - chỉ ghim #topbar thì 2 mode sau vẫn phải cuộn ngược
   lên mới sửa được mốc.
   2026-08-28 (mở rộng, theo yêu cầu người dùng - ảnh chụp thanh lọc video_id + thanh "N kết quả
   + Tự động điền") - GỘP THÊM #result-filter-box/#steplog/#results-bar vào cùng khối ghim (vốn
   trước đó nằm NGOÀI, cuộn mất theo #results) - đây cũng là input/hành động cần bấm liên tục
   trong lúc đang cuộn xem lưới kết quả (lọc theo video_id, bấm Tự động điền), không riêng ô
   truy vấn. CHỈ #results (lưới ảnh, phần dài nhất) là còn cuộn tự do bên dưới khối ghim.
   max-height + overflow-y: chặn trường hợp khối nhập liệu CAO HƠN CẢ MÀN HÌNH (Q&A nhiều field,
   hoặc TRAKE mở canvas riêng cho từng mốc) - nếu để cao tự do, phần ghim sẽ che hết vùng kết quả
   và không cách nào cuộn tới được (đúng lớp bug đã từng gặp/ghi ở #main ngay trên). Đánh đổi đã
   biết: khi khối vượt ngưỡng và bắt đầu tự cuộn, dropdown chọn nhãn trong canvas (.box-edit-
   dropdown) có thể bị cắt ở mép dưới - hiếm gặp, và gấp canvas lại là hết. */
#input-sticky {
  position: sticky;
  top: 0;
  z-index: 20;
  flex: 0 0 auto;
  background: var(--bg);
  max-height: 60vh;
  overflow-y: auto;
  overflow-x: hidden;
  /* Bóng đổ nhẹ để khối ghim tách bạch với lưới kết quả trôi bên dưới khi cuộn. */
  box-shadow: 0 6px 10px -6px rgba(0, 0, 0, .45);
}

#topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

/* Khoảng cách tối đa giữa 2 mốc (TRAKE/Temporal) — ngang hàng với nút Tìm kiếm, ngay trong
   topbar (thay vì sidebar như v3, theo yêu cầu người dùng). */
#max-gap-box {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 170px;
  flex-shrink: 0;
}

#max-gap-box .qa-label span {
  cursor: help;
  opacity: .8;
}

/* Lọc video_id — chuyển từ sidebar "Bộ lọc" lên topbar (2026-09-24, theo yêu cầu người dùng),
   cạnh #max-gap-box, cùng pattern cột dọc label+input. */
#video-ids-box {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 110px;
  flex-shrink: 0;
}

/* Q&A có nhiều dòng field cao hơn 1 input thường - ghim nút Tìm kiếm lên đầu thay vì căn giữa
   theo chiều cao cả khối, đỡ trông lệch. */
#topbar:has(#qa-box:not(.hidden)) {
  align-items: flex-start;
  padding-top: 14px;
  padding-bottom: 14px;
}

#query-input {
  flex: 1;
}

/* Mốc TRAKE/Temporal — hàng thẻ cuộn ngang, mỗi mốc 1 canvas OCR/Object RIÊNG (giống hệt v3:
   st.container(key="trake_row", horizontal=True) - moi mốc 1 the co canvas rieng). */
#anchors-box {
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

#anchors-row {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  padding-bottom: 4px;
  margin-bottom: 10px;
}

.anchor-card {
  flex: 0 0 auto;
  width: 360px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
}

.anchor-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.anchor-card-head h3 {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
}

.anchor-card-close {
  padding: 4px 9px;
  font-size: 13px;
}

.anchor-card input[type="text"] {
  margin-bottom: 10px;
}

.anchor-relation {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
  flex-wrap: wrap;
}

.anchor-relation-label {
  font-size: 11px;
  color: var(--fg-muted);
}

/* 2026-08-21 (theo yêu cầu người dùng: "cho canvas này đóng vào mở ra như bên KIS, mỗi mốc có
   cái đóng mở riêng") - Y HỆT #canvas-toggle/#canvas-panel bên KIS/Q&A (xem ghi chú đầy đủ ở đó)
   - chỉ đổi tên lớp để KHÔNG áp CSS toàn cục #canvas-toggle (dành riêng cho canvas KIS/Q&A) lên
   đây, vì mỗi mốc TRAKE/Temporal cần trạng thái đóng/mở ĐỘC LẬP với nhau (lưu ở a.canvasOpen,
   xem app.js::renderAnchorCards) - dùng max-height (không phải grid-template-rows:0fr, đã xác
   nhận qua bug thật với canvas KIS: 0fr không co đúng khi có canvas kích thước cố định lồng bên
   trong nested grid). */
.anchor-canvas-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  text-align: left;
  padding: 7px 0;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--fg);
  font: inherit;
  font-size: 12px;
  margin-bottom: 6px;
}

.anchor-canvas-toggle:hover {
  color: var(--accent);
}

.anchor-canvas-toggle .act-caret {
  color: var(--fg-muted);
  font-size: 9px;
}

.anchor-canvas-toggle .act-title {
  font-weight: 600;
  flex: 1;
}

.anchor-canvas-toggle .act-badge {
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 999px;
  padding: 1px 8px;
  font-size: 10.5px;
  font-weight: 600;
}

.anchor-canvas-panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height 320ms cubic-bezier(0.25, 0.8, 0.25, 1);
}

.anchor-canvas-panel.open {
  max-height: 500px;
  overflow-y: auto;
}

.anchors-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

#qa-box {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.qa-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.qa-label {
  font-size: 12.5px;
  color: var(--fg-muted);
}

#qa-box .checkbox-row {
  line-height: 1.4;
}

.qa-stepper {
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 220px;
}

.qa-stepper input[type="number"] {
  flex: 1;
  -moz-appearance: textfield;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.qa-stepper input[type="number"]::-webkit-inner-spin-button,
.qa-stepper input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.qa-stepper .btn {
  padding: 6px 12px;
  font-size: 14px;
  line-height: 1;
}

.qa-stepper input:disabled,
.qa-stepper .btn:disabled {
  opacity: .45;
}

#result-filter-box {
  padding: 10px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

#result-filter {
  max-width: 340px;
}

/* Chỉ còn dùng làm dòng thông báo lỗi/trạng thái chung (không còn hiện chi tiết từng bước xử
   lý) - tự ẩn hẳn (không chiếm khoảng trống) khi rỗng, xem app.js::renderSteplog. */
#steplog:empty {
  display: none;
}

#steplog {
  font-size: 12px;
  color: var(--fg-muted);
  padding: 8px 18px;
  border-bottom: 1px solid var(--border);
}

#steplog .err {
  color: var(--err);
}

#steplog .dres-correct { color: var(--ok); }
#steplog .dres-wrong { color: var(--err); }
#steplog .dres-indeterminate { color: var(--warn); }

#results-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

#results-count {
  font-size: 12.5px;
  color: var(--fg-muted);
}

#autofill-btn:disabled {
  opacity: .55;
}

#results {
  padding: 16px 18px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 12px;
  align-content: start;
}

.card {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  cursor: default;
}

.card img {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: cover;
  background: var(--border);
  display: block;
}

/* TRAKE/Temporal — layout giống hệt v3: card RỘNG hết hàng (grid-column: 1/-1), timeline trực
   quan chấm điểm từng mốc theo đúng vị trí thời gian, rồi 1 cột/mốc bên dưới. */
.card-wrap-chain {
  grid-column: 1 / -1;
}

.card-chain {
  cursor: default;
}

.chain-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 10px 12px 0;
  font-weight: 600;
  font-size: 13px;
}

.chain-header .score {
  font-weight: 400;
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
}

/* 2026-08-23 (theo yêu cầu người dùng: "làm thêm 1 cảnh báo ở ngoài này... cảnh báo chỉ mất khi
   người dùng điều chỉnh đúng lại thứ tự") - xem app.js::renderChainCard::updateOrderWarning() -
   chỉ hiện khi frame_ids KHÔNG tăng dần đúng thứ tự giữa các mốc, tự ẩn khi đã sửa đúng lại. */
.chain-order-warning {
  margin: 8px 12px 0;
  padding: 6px 10px;
  border-radius: 6px;
  font-size: 11.5px;
  line-height: 1.4;
  background: color-mix(in srgb, var(--err) 12%, transparent);
  border: 1px solid var(--err);
  color: var(--err);
}

.chain-order-warning.hidden {
  display: none;
}

/* 2026-08-23 (bug thật: "mốc 2 phải LỚN HƠN mốc 1" hoá ra là dòng backfill theo THIẾT KẾ, không
   phải lỗi người dùng chỉnh sai) - biến thể "info" (xanh, không phải đỏ) khi is_backfill=true,
   để không tạo cảm giác báo lỗi/trách người dùng cho 1 việc họ không làm sai. */
.chain-order-warning.info {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: var(--accent);
  color: var(--accent);
}

/* 2026-08-23 (theo yêu cầu người dùng: hiện is_backfill cho người dùng biết) - badge nhỏ đánh
   dấu "dòng bù thêm, không khớp lọc/chuỗi 100%" (xem app.js::backfillBadge). */
.backfill-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 10px;
  font-weight: 600;
  color: var(--accent);
  background: var(--accent-soft);
  padding: 1px 6px;
  border-radius: 999px;
  vertical-align: middle;
  cursor: help;
}

.chain-track {
  margin: 8px 12px;
}

.chain-timeline {
  position: relative;
  height: 34px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.chain-dot {
  position: absolute;
  top: 4px;
  transform: translateX(-50%);
  text-align: center;
  white-space: nowrap;
}

.chain-dot-mark {
  display: block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--err);
  margin: 0 auto 2px;
}

.chain-dot-label {
  font-size: 10px;
  color: var(--fg-muted);
}

/* Hàng mốc — quay lại flex thường (KHÔNG canh theo % trùng chấm timeline nữa, cách đó lệch/tràn
   card khi mốc gần 2 đầu timeline) - chỉ cuộn ngang bình thường như trước. */
.chain-anchors {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  margin-top: 10px;
}

.chain-anchor {
  flex: 0 0 auto;
  width: 220px;
}

.chain-anchor .chain-img {
  width: 220px;
  aspect-ratio: 16/9;
  object-fit: cover;
  display: block;
  cursor: zoom-in;
  border-radius: 6px;
  background: var(--border);
}

.chain-anchor-caption {
  font-size: 11.5px;
  color: var(--fg-muted);
  margin: 6px 0;
  line-height: 1.4;
}

.chain-anchor-text {
  color: var(--fg);
  font-weight: 500;
}

.chain-anchor-caption code {
  font-family: var(--font-mono);
  background: var(--accent-soft);
  color: var(--accent);
  padding: 0 4px;
  border-radius: 3px;
}

.chain-anchor-actions {
  display: flex;
  gap: 4px;
}

.chain-anchor-actions .btn {
  flex: 1;
  padding: 5px 6px;
  font-size: 13px;
}

.chain-anchor .vlm-result {
  margin-top: 6px;
}

/* Nộp + Playback nằm CHUNG 1 hàng (trước đây .add-btn width:100% đẩy nút 🎯 xuống hàng riêng,
   thừa 1 dòng trống), CHIA ĐỀU bề ngang — flex-basis:0 để 2 nút bằng nhau đúng nghĩa, không bị
   lệch theo bề rộng nội dung sẵn có của từng nút. */
.chain-footer {
  padding: 0 12px 12px;
  display: flex;
  gap: 6px;
  align-items: stretch;
}

.chain-footer .btn {
  flex: 1 1 0;
}

.card .meta {
  padding: 6px 8px;
  font-size: 11px;
  color: var(--fg-muted);
  display: flex;
  justify-content: space-between;
  gap: 4px;
}

.card .meta .score {
  font-variant-numeric: tabular-nums;
}

/* 2026-08-21 (theo yêu cầu người dùng: "khi lọc bằng ASR... chữ được khớp sẽ được bôi màu trong
   câu tương ứng để người dùng dễ dùng") - hiện NGUYÊN câu ASR khớp ngay dưới mỗi card, đoạn
   khớp bôi màu bằng <mark> (không dùng nền vàng mặc định của trình duyệt - lệch tông với theme
   tối/sáng của app, dùng màu accent sẵn có cho nhất quán). */
.asr-match {
  padding: 6px 8px;
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--fg-muted);
  border-top: 1px dashed var(--border);
  background: var(--bg);
}

.asr-match .bi {
  color: var(--fg-muted);
  margin-right: 2px;
}

.asr-match mark {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 700;
  padding: 0 2px;
  border-radius: 3px;
}

/* 2026-09-04 (theo yêu cầu người dùng: "search ngữ nghĩa cho ASR") - badge % độ liên quan
   (cosine similarity) cho kết quả CHẾ ĐỘ SEMANTIC ở tab Search ASR - KHÔNG có khái niệm "đoạn
   khớp trong câu" như <mark> nên hiện % thay thế, cùng tinh thần màu accent với mark. */
.asr-score-badge {
  display: inline-flex;
  align-items: center;
  font-weight: 700;
  color: var(--accent);
  background: var(--accent-soft);
  padding: 0 5px;
  border-radius: 999px;
  font-variant-numeric: tabular-nums;
}

.qa-answer-input {
  margin: 0 8px 8px;
  width: calc(100% - 16px);
  font-size: 11.5px;
  padding: 6px 8px;
}

.card .actions {
  display: flex;
  gap: 4px;
  padding: 0 8px 8px;
}

.card .actions .btn {
  flex: 1;
  padding: 7px 6px;
  font-size: 15px;
}

/* Nút chỉ icon — tên hiện thành tooltip khi hover (data-tooltip), thay vì ghi chữ luôn trong nút */
.icon-btn {
  position: relative;
}

.icon-btn::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--fg);
  color: var(--bg);
  font-size: 11px;
  font-weight: 500;
  padding: 4px 8px;
  border-radius: 6px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
  z-index: 30;
}

.icon-btn:hover::after {
  opacity: 1;
}

.icon-btn:disabled::after {
  display: none;
}

/* 2026-08-23 (theo yêu cầu người dùng: "bỏ vùng màu trắng ở ngoài, chỉ giữ lại icon có màu
   tương phản phù hợp") - #theme-toggle CHỈ có class "icon-btn" (không có "btn"), nên KHÔNG được
   .btn reset kiểu dáng - vẫn là <button> mặc định của trình duyệt (nền trắng/xám + viền + bo góc
   hệ điều hành, đúng như ảnh chụp "vùng trắng" quanh icon). Reset thẳng về icon trần, không nền/
   viền, chỉ tô màu icon theo token (tương phản đúng theme sáng/tối), có hover nhẹ để vẫn nhận ra
   là nút bấm được. */
#theme-toggle {
  background: transparent;
  border: none;
  padding: 6px;
  border-radius: 50%;
  color: var(--fg-muted);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 150ms ease, background 150ms ease;
}

#theme-toggle:hover {
  color: var(--fg);
  background: var(--bg-elevated);
}

/* Đã nộp — nút "📤 Nộp" chuyển thành "✅", bấm lại để hoàn tác (xem app.js::addToSubmission) */
.add-btn.submitted {
  background: var(--ok);
  border-color: var(--ok);
  color: #fff;
}

.card .rank {
  position: absolute;
  margin: 6px;
  font-size: 10.5px;
  background: rgba(0, 0, 0, .55);
  color: #fff;
  padding: 1px 6px;
  border-radius: 5px;
}

.card-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
}

/* Kết quả VLM Verify — hiện ngay dưới card khi bấm, xem app.js::vlmVerify */
.vlm-result {
  margin: -4px 8px 8px;
  padding: 6px 8px;
  border-radius: 7px;
  background: var(--accent-soft);
  font-size: 11.5px;
  color: var(--fg);
  line-height: 1.4;
  word-break: break-word;
}

.vlm-result.err {
  background: transparent;
  border: 1px solid var(--err);
  color: var(--err);
}

/* 2026-08-23 (theo yêu cầu người dùng: "cho mình dòng này nằm giữa layout") - #results là grid
   (xem #search-spinner phía dưới, cùng vấn đề) nên #empty-state mặc định chỉ chiếm ô ĐẦU TIÊN
   (~210px, top-left) chứ KHÔNG tự căn giữa cả vùng dù đã có text-align:center - grid-column:1/-1
   cho nó CHIẾM HẾT bề ngang, rồi flex center để "nằm giữa layout" đúng nghĩa (cả ngang lẫn dọc). */
#empty-state {
  grid-column: 1 / -1;
  color: var(--fg-muted);
  padding: 40px;
  text-align: center;
  min-height: 260px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 2026-08-21 (theo yêu cầu người dùng: "spinner quay ở giữa chứ không phải ghi dòng chữ đang
   tìm kiếm") - #results là grid (xem dưới) nên 1 item DUY NHẤT mặc định chỉ chiếm ô đầu tiên
   (~210px, top-left) chứ KHÔNG tự căn giữa cả vùng - grid-column:1/-1 cho nó CHIẾM HẾT bề ngang
   (span mọi cột hiện có), rồi flex center CẢ ngang lẫn dọc bên trong đúng nghĩa "ở giữa". */
#search-spinner {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 260px;
  color: var(--fg-muted);
  font-size: 42px;
}

#search-spinner .bi {
  animation: aic-spin 0.9s linear infinite;
}

@keyframes aic-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  #search-spinner .bi {
    animation-duration: 2.4s;
  }
}

/* ---- Floating submission bubble ---- */
/* 2026-08-22 (theo yêu cầu người dùng: "đưa nút submit xuống cuối góc phải") - GÓC DƯỚI PHẢI
   màn hình, panel MỞ NGƯỢC LÊN TRÊN (không phải xuống dưới) - neo gần đáy màn hình mà panel mở
   xuống sẽ tràn ra ngoài viewport, xem ghi chú "top:100%" -> "bottom:100%" ở #sub-panel. */
#sub-bubble {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 40;
}

#sub-pill {
  border-radius: 999px;
  padding: 8px 14px;
  box-shadow: var(--shadow);
}

/* width: max-content — tự nới theo dòng dài nhất (vd "L22_V004 · frame 36900") thay vì cắt chữ
   trong 1 khổ cố định, chặn 2 đầu bằng min/max để không quá hẹp lúc rỗng hay quá rộng khi có
   video_id dài bất thường. */
#sub-panel {
  /* 2026-08-21 (bug thật: "vị trí nó bị vào giữa rồi") - display:none -> opacity/transform để
     animate được, nhưng panel (dù ẩn) vẫn NẰM TRONG LUỒNG bên trong #sub-bubble (position:fixed;
     right:20px), kéo giãn bubble cha rộng ra theo panel -> neo theo right đẩy dạt cả khối vào
     giữa màn hình. Fix: position:absolute cho panel (bubble cha đã position:fixed, tự làm
     containing block) - không còn đóng góp kích thước vào bubble cha nữa. */
  position: absolute;
  bottom: 100%;
  right: 0;
  width: max-content;
  min-width: 340px;
  max-width: min(92vw, 480px);
  max-height: 75vh;
  overflow-y: auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
  margin-bottom: 8px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(8px) scale(0.98);
  transform-origin: bottom right;
  transition: opacity 200ms cubic-bezier(0.25, 0.8, 0.25, 1),
    transform 200ms cubic-bezier(0.25, 0.8, 0.25, 1),
    visibility 0s linear 200ms;
}

#sub-panel.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0) scale(1);
  transition: opacity 200ms cubic-bezier(0.25, 0.8, 0.25, 1),
    transform 200ms cubic-bezier(0.25, 0.8, 0.25, 1),
    visibility 0s linear 0s;
}

.sp-title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 14px;
}

.sp-title {
  margin: 0;
  font-size: 19px;
  line-height: 1.25;
  font-weight: 700;
}

#sp-close {
  flex-shrink: 0;
  padding: 5px 9px;
}

.sp-field {
  margin-bottom: 12px;
}

.sp-field-label {
  display: block;
  font-size: 12.5px;
  color: var(--fg-muted);
  margin-bottom: 5px;
}

.sp-field-label span {
  cursor: help;
  opacity: .8;
}

#sub-panel .sp-field input {
  width: 100%;
}

.sp-new-btn {
  width: 100%;
  text-align: left;
  margin-bottom: 14px;
  background: var(--accent-soft);
  border-color: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

.sp-new-btn:hover {
  border-color: var(--accent);
}

#sub-panel .sp-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-bottom: 14px;
}

#sub-panel .sp-actions .btn {
  padding: 9px;
}

.sp-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 4px;
  border-radius: 6px;
  font-size: 12.5px;
  border-bottom: 1px solid var(--border);
}

.sp-item .sp-icon {
  flex-shrink: 0;
}

.sp-item .sp-tag {
  flex-shrink: 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--accent);
  background: var(--accent-soft);
  padding: 1px 5px;
  border-radius: 4px;
}

.sp-item .sp-label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  background: none;
  border: none;
  color: var(--fg);
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 5px;
  font-size: inherit;
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.sp-item .sp-label:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

/* Đáp án Q&A hiện ngay trong danh sách nộp bài — Q&A của BTC nộp kèm đáp án nên phải soát được
   bằng mắt là dòng nào đã có/chưa có đáp án trước khi xuất CSV. */
.sp-item .sp-answer {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11.5px;
  color: var(--fg-muted);
  font-style: italic;
}

.sp-item .sp-answer.empty {
  color: var(--err);
  font-style: normal;
}

.sp-item .btn {
  padding: 4px 8px;
  font-size: 12px;
  flex-shrink: 0;
}

/* Nháy sáng card khi nhảy tới từ danh sách nộp bài (jumpToResult, xem app.js) */
@keyframes card-flash {

  0%,
  100% {
    box-shadow: none;
  }

  20%,
  60% {
    box-shadow: 0 0 0 3px var(--accent);
  }
}

.card-wrap.flash .card {
  animation: card-flash 1.2s ease;
}

/* Card ảnh bấm được — con trỏ gợi ý phóng to (xem app.js::openLightbox) */
.card img {
  cursor: zoom-in;
}

/* ---- Form nổi nhập câu trả lời Q&A (chỉ hiện khi bấm Nộp, Enter mới thực sự nộp) ---- */
#qa-answer-modal {
  position: fixed;
  inset: 0;
  z-index: 110;
  background: rgba(8, 9, 11, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
}

.qam-box {
  width: 360px;
  max-width: 92vw;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
}

.qam-title {
  font-size: 12.5px;
  color: var(--fg-muted);
  margin-bottom: 8px;
}

#qam-input {
  width: 100%;
}

.qam-hint {
  font-size: 11px;
  color: var(--fg-muted);
  margin-top: 8px;
}

/* ---- Lightbox phóng to/thu nhỏ ảnh (giống nút mở rộng của st.image bên Streamlit) ---- */
#lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(8, 9, 11, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

#lightbox img {
  max-width: 90vw;
  max-height: 90vh;
  object-fit: contain;
  transform-origin: center center;
  transition: transform 0.12s ease-out;
  cursor: zoom-in;
  user-select: none;
  -webkit-user-drag: none;
}

#lightbox img.zoomed {
  cursor: zoom-out;
}

#lightbox-close {
  position: fixed;
  top: 18px;
  right: 22px;
  z-index: 101;
  width: 36px;
  height: 36px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .25);
  background: rgba(255, 255, 255, .08);
  color: #fff;
  font-size: 15px;
  cursor: pointer;
}

#lightbox-close:hover {
  background: rgba(255, 255, 255, .18);
}

#lightbox-hint {
  position: fixed;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 11.5px;
  color: rgba(255, 255, 255, .55);
}

/* ---- Video player nổi (đoạn shot) — góc dưới trái, đối xứng với bubble nộp bài ở trên phải ---- */
#video-player {
  position: fixed;
  bottom: 20px;
  left: 20px;
  z-index: 40;
  width: 360px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.vp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 12.5px;
  font-weight: 600;
}

#vp-close {
  padding: 3px 8px;
  font-size: 12px;
}

#vp-body {
  background: #000;
}

#vp-video {
  width: 100%;
  max-height: 260px;
  display: block;
}

/* ---- Canvas vẽ khung OCR/Object (giống objects_canvas bên v3) ---- */
#canvas-section {
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

/* Thanh gấp gọn — mặc định canvas ĐÓNG (tuỳ chọn, ít dùng) nên chỉ tốn 1 dòng thay vì ~240px
   trống như trước. Badge "N khung" cho biết đang có bộ lọc vẽ mà không cần bung ra. */
#canvas-toggle {
  width: 100%;
  display: flex;
  align-items: baseline;
  gap: 10px;
  text-align: left;
  padding: 9px 18px;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--fg);
  font: inherit;
  font-size: 13px;
}

#canvas-toggle:hover {
  background: var(--bg-elevated);
}

#canvas-toggle .ct-caret {
  color: var(--fg-muted);
  font-size: 10px;
}

#canvas-toggle .ct-title {
  font-weight: 600;
}

#canvas-toggle .ct-sub {
  color: var(--fg-muted);
  font-size: 11.5px;
  flex: 1;
  min-width: 0;
}

#canvas-badge {
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 999px;
  padding: 2px 9px;
  font-size: 11px;
  font-weight: 600;
  white-space: nowrap;
}

/* 2026-08-21 (theo yêu cầu người dùng: "tương tự làm cái này cho mượt mà" - đã làm mượt sidebar
   + 2 bubble nổi, giờ tới panel gấp gọn này) - TRƯỚC ĐÂY toggle class .hidden (display:none <->
   block) - bật tắt tức thì, không animate được (lý do y hệt #sub-panel). Nhưng đây
   là panel CHIẾM CHỖ THẬT trong luồng trang (đẩy nội dung bên dưới xuống khi mở), không phải
   panel nổi che lên trên như 2 bubble kia - opacity+transform không đủ (nội dung dưới nó phải
   TỰ DÃN/CO chỗ theo animation, không nhảy khựng), cần animate CHIỀU CAO thật.

   2026-08-21 (bug thật: "vẫn bị" - đã thử kỹ thuật grid-template-rows: 0fr<->1fr trước đó, xác
   nhận qua ảnh chụp thật VẪN lộ nội dung bị cắt dở dù CSS served đúng, không phải cache trình
   duyệt) - kỹ thuật đó phụ thuộc row track co đúng 0 khi CÓ nested grid/canvas kích thước cố
   định bên trong (.cp-inner lại là 1 grid khác, con của nó - #canvas-global-mount chứa <canvas>
   width/height cố định bằng JS - có min-height mặc định riêng, không bị min-height:0 của
   .cp-inner "lan truyền" xuống) - dễ vỡ, khó chẩn đoán tiếp tại sao đúng lý thuyết mà vẫn lộ.
   Đổi sang max-height (kỹ thuật CŨ HƠN nhưng ĐÁNG TIN CẬY hơn nhiều, không phụ thuộc chi tiết
   nested layout bên trong) - overflow:hidden CHÍNH #canvas-panel + max-height 0 <-> 1 giá trị ĐỦ
   LỚN (không cần khớp tuyệt đối, chỉ cần lớn hơn chiều cao thật) - animate max-height LUÔN clip
   đúng theo overflow bất kể cấu trúc bên trong phức tạp thế nào. */
#canvas-panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height 320ms cubic-bezier(0.25, 0.8, 0.25, 1);
}

#canvas-panel.open {
  max-height: 700px;
  overflow-y: auto;
}

/* phòng khi danh sách khung đã vẽ dài hơn 700px thật (vd vẽ rất nhiều khung) - cuộn thay vì bị cắt mất */
/* 2026-08-21 (theo yêu cầu người dùng: "cho cái canvas này vào giữa được không") - cột phải
   TRƯỚC ĐÂY là 1fr (chiếm HẾT phần còn lại của bề ngang trang) trong khi nội dung thật (AND/OR +
   vài dòng khung đã vẽ) hẹp hơn nhiều -> cột phải bị kéo giãn rỗng, cả khối 2 cột dồn về sát
   trái, để trống mênh mông bên phải. Đổi cột 2 sang max-content (tự co theo ĐÚNG nội dung, không
   chiếm thừa) + justify-content:center trên .cp-inner (canh CẢ KHỐI 2 cột vào giữa .cp-inner khi
   tổng bề rộng 2 cột nhỏ hơn container, thay vì luôn dính mép trái). */
/* 2026-08-23 (theo yêu cầu người dùng: "nút chọn mode OCR hay Object + hoàn tác + xoá hết + chú
   thích sẽ nằm bên trái của canvas, nhờ đó canvas sẽ fit và to hơn") - cột 1 TRƯỚC ĐÂY giới hạn
   360px cho CẢ modebar+canvas+toolbar xếp CHỒNG DỌC (canvas bị ép nhỏ theo đúng 360px) - tăng
   trần cột 1 lên rộng hơn hẳn (canvas giờ có chỗ giãn ra) vì bên trong nó (#canvas-global-mount)
   đã đổi bố cục sang 2 CỘT: cụm điều khiển hẹp bên trái + canvas chiếm phần còn lại bên phải
   (xem #canvas-global-mount dưới) - canvas.js::updateScale() đã tự co giãn theo bề rộng
   .canvas-stage-wrap sẵn (scale theo tỉ lệ khung hình gốc), không cần sửa gì thêm ở JS. */
#canvas-panel>.cp-inner {
  display: grid;
  grid-template-columns: minmax(0, 640px) minmax(0, max-content);
  justify-content: center;
  gap: 18px;
  padding: 4px 18px 14px;
  align-items: start;
}

#canvas-side {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
  max-width: 420px;
}

#canvas-box-list {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.cbl-label,
.cbl-empty {
  font-size: 11px;
  color: var(--fg-muted);
}

.cbl-label {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .02em;
}

.cbl-empty {
  line-height: 1.5;
  max-width: 46ch;
}

.cbl-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 9px;
}

.cbl-idx {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--border);
  color: var(--fg-muted);
  font-size: 10.5px;
  display: grid;
  place-items: center;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 900px) {
  #canvas-panel>.cp-inner {
    grid-template-columns: 1fr;
  }
}

#spatial-op-row {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 10px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

/* Trong panel canvas (KIS/Q&A) hàng AND/OR là 1 phần của cột phải, không còn là 1 dải riêng
   chạy ngang hết bề rộng nữa. */
#canvas-side-op #spatial-op-row {
  padding: 0;
  border-bottom: none;
  background: none;
}

#spatial-op-row .qa-label {
  margin: 0;
}

#spatial-op-row .qa-label span {
  cursor: help;
  opacity: .8;
}

.canvas-radio {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12.5px;
  cursor: pointer;
}

.canvas-radio input {
  width: auto;
}

.canvas-modebar {
  display: flex;
  gap: 6px;
  margin-bottom: 8px;
}

.canvas-mode-btn {
  border: 2px solid transparent;
  border-radius: 6px;
  padding: 4px 10px;
  font: 600 11px system-ui, sans-serif;
  cursor: pointer;
  color: #111;
}

.canvas-mode-ocr {
  background: #22c55e;
}

.canvas-mode-object {
  background: #f5c518;
}

.canvas-mode-btn.active {
  border-color: var(--fg);
  box-shadow: 0 0 0 2px rgba(120, 120, 120, 0.35);
}

.canvas-mode-btn:not(.active) {
  opacity: .45;
}

.canvas-stage-wrap {
  width: 100%;
  max-width: 360px;
  position: relative;
  overflow: hidden;
}

/* ---- Bố cục RIÊNG cho canvas TOÀN CỤC (KIS/Q&A, #canvas-global-mount) - cụm điều khiển
   (modebar/toolbar, vốn xếp DỌC phía trên+dưới canvas trong canvas.js) chuyển sang CỘT HẸP bên
   TRÁI, canvas chiếm phần còn lại bên phải (to hơn). Scope theo ID #canvas-global-mount (KHÔNG
   phải class dùng chung) nên canvas RIÊNG từng mốc TRAKE (.anchor-canvas-mount, thẻ mốc hẹp
   ~360px, không đủ chỗ cho bố cục 2 cột) vẫn giữ NGUYÊN bố cục dọc cũ. */
#canvas-global-mount {
  display: grid;
  column-gap: 16px;
  align-items: start;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "modebar stage" "toolbar stage";
}

#canvas-global-mount .canvas-modebar {
  grid-area: modebar;
  flex-direction: column;
  align-items: stretch;
  margin-bottom: 0;
}

#canvas-global-mount .canvas-toolbar {
  grid-area: toolbar;
  flex-direction: column;
  align-items: flex-start;
  margin-top: 0;
}

#canvas-global-mount .canvas-stage-wrap {
  grid-area: stage;
  max-width: none;
}

@media (max-width: 640px) {
  #canvas-global-mount {
    grid-template-columns: 1fr;
    grid-template-areas: "modebar" "stage" "toolbar";
  }

  #canvas-global-mount .canvas-modebar {
    flex-direction: row;
  }
}

.canvas-stage {
  position: relative;
  background: #1c1c1e;
  border: 1px solid #3a3a3c;
  border-radius: 8px;
  overflow: hidden;
  cursor: crosshair;
  user-select: none;
  transform-origin: top left;
  box-sizing: border-box;
}

.canvas-grid {
  display: block;
}

.box-editor {
  position: absolute;
  display: flex;
  gap: 3px;
  z-index: 10;
}

.box-edit-input {
  background: #2c2c2e;
  color: #4ade80;
  border: 1px solid #22c55e;
  border-radius: 4px;
  font: 600 12px/1.4 system-ui, sans-serif;
  padding: 2px 6px;
  outline: none;
}

.box-edit-search-wrap {
  position: relative;
}

.box-edit-search {
  background: #2c2c2e;
  color: #fde68a;
  border: 1px solid #f5c518;
  border-radius: 4px;
  font: 600 12px system-ui, sans-serif;
  padding: 2px 6px;
  outline: none;
  width: 160px;
}

.box-edit-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 2px;
  z-index: 30;
  background: #2c2c2e;
  border: 1px solid #f5c518;
  border-radius: 4px;
  max-height: 160px;
  overflow-y: auto;
  width: 220px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, .5);
}

.box-edit-dropdown-item {
  padding: 4px 8px;
  font: 500 12px system-ui, sans-serif;
  color: #fde68a;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.box-edit-dropdown-item.highlight,
.box-edit-dropdown-item:hover {
  background: #f5c518;
  color: #111;
}

.box-edit-dropdown-empty {
  padding: 4px 8px;
  font: 500 12px system-ui, sans-serif;
  color: #888;
}

.box-edit-count {
  background: #2c2c2e;
  color: #fde68a;
  border: 1px solid #f5c518;
  border-radius: 4px;
  font: 600 12px system-ui, sans-serif;
  padding: 2px 2px;
  outline: none;
  width: 34px;
  text-align: center;
}

.box-del {
  position: absolute;
  width: 16px;
  height: 16px;
  background: #d33;
  color: #fff;
  border-radius: 50%;
  font: 700 11px/16px system-ui, sans-serif;
  text-align: center;
  cursor: pointer;
  z-index: 9;
}

.canvas-toolbar {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  align-items: center;
  flex-wrap: wrap;
}

.canvas-toolbar .field-note {
  margin: 0;
}

/* ---- 🎬 Playback (xem/chỉnh frame trước khi nộp) — xem playback.js ---- */
#playback-modal {
  position: fixed;
  inset: 0;
  z-index: 120;
  background: rgba(8, 9, 11, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
}

/* 2026-09-19 (theo yêu cầu người dùng: "scale phóng to phần filmstrip, tăng chiều ngang của khung
   playback lên và tăng chiều dọc nếu có thể") - 1000px -> tối đa 1400px (không quá 96% cửa sổ),
   cao tối đa 96vh (trước 92vh, lề modal 20px -> 12px). Ô filmstrip = bề rộng dải / 5 với tỉ lệ 16:9
   nên rộng ra thì ô CAO ra theo (1000px: ô ~180x101, 1400px: ô ~263x148). */
.pb-box {
  width: 100%;
  max-width: min(1400px, 96vw);
  max-height: 96vh;
  overflow-y: auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 12px 16px;
}

.pb-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
}

.pb-head h2 {
  margin: 0;
  font-size: 16px;
}

.pb-timeline {
  position: relative;
  height: 34px;
  margin: 4px 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.pb-timeline:empty {
  display: none;
  margin: 0;
  border: none;
  height: 0;
}

.pb-dot {
  position: absolute;
  top: 4px;
  transform: translateX(-50%);
  text-align: center;
  white-space: nowrap;
}

.pb-dot-mark {
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--err);
  margin: 0 auto 2px;
}

.pb-dot-active .pb-dot-mark {
  width: 14px;
  height: 14px;
  border: 3px solid var(--ok);
  background: var(--err);
}

.pb-dot-label {
  font-size: 11px;
  color: var(--fg-muted);
}

.pb-anchor-picker {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}

.pb-pick-btn.active {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

/* Thanh điều khiển (nudge/gõ số/slider/revert) gộp CHUNG 1 khối, đứng TRÊN 2 khung hình — để
   khung Frame xem thử và Video gốc nằm ngang hàng nhau ngay bên dưới (theo yêu cầu người dùng:
   "align lại 2 cái màn hình frame và video nằm ngang với nhau"), không còn xen kẽ control ở giữa. */
/* 2026-09-19 (theo yêu cầu người dùng: "giảm khoảng trắng theo chiều dọc... laptop thông thường
   thì phải cuộn") - nudge/gõ số + slider/frame gốc từ 2 HÀNG xếp chồng gộp thành 1 HÀNG (tự xuống
   dòng nếu cửa sổ hẹp). Chỉ ghi đè trong phạm vi .pb-controls (.slider-row/.pb-goto-row còn dùng
   chung ở tab "Xem video"). */
.pb-controls {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 10px;
  margin-bottom: 8px;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
}

.pb-controls .pb-nudge-row {
  display: flex;
  gap: 6px;
  align-items: stretch;
  flex: 0 0 auto;
}

.pb-controls .pb-nudge-row>.btn {
  flex: 0 0 auto;
  min-width: 56px;
}

.pb-controls .pb-goto-row {
  display: flex;
  gap: 6px;
  flex: 0 0 auto;
}

.pb-controls .pb-goto-row input {
  flex: 0 0 84px;
  width: 84px;
  min-width: 0;
}

.pb-controls .slider-row {
  flex: 1 1 280px;
  min-width: 0;
}

.slider-row #pb-revert {
  flex: 0 0 auto;
}

#pb-confirm-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* 2026-08-23 (theo yêu cầu người dùng: "kiểm tra lại thứ tự frame qua từng mốc" - cảnh báo khi
   frame đang chọn vi phạm thứ tự với mốc trước/sau, xem playback.js::orderViolation) - #pb-
   confirm-status không nằm trong #steplog nên KHÔNG ăn theo "#steplog .err" sẵn có. */
#pb-confirm-status .err {
  color: var(--err);
  font-weight: 500;
}

/* Lịch sử frame ĐÃ CHỐT của mốc đang chọn — bấm 1 mốc cũ để quay lại NGAY (không cần xác nhận
   lại), giữ NGUYÊN các mốc sau nó trong lịch sử (không phải undo-stack xoá tương lai). */
#pb-history-box {
  margin: 0;
}

.pb-history-label {
  display: block;
  font-size: 11px;
  color: var(--fg-muted);
  margin-bottom: 6px;
}

#pb-history {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.pb-history-chip {
  padding: 4px 9px;
  font-size: 11.5px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg-muted);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}

.pb-history-chip:hover {
  border-color: var(--accent);
  color: var(--fg);
}

.pb-history-chip.current {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
  font-weight: 600;
  cursor: default;
}

#pb-confirm-row code {
  font-family: var(--font-mono);
  background: var(--accent-soft);
  color: var(--accent);
  padding: 0 4px;
  border-radius: 3px;
}

/* 2026-09-19 (theo yêu cầu người dùng: "Bỏ phần hiển thị frame ở bên trái của phần play video...
   xuống hàng thêm filmstrip") - khung "Frame xem thử" đã bỏ, Video gốc đứng 1 mình ở giữa (khống
   chế bề rộng để không quá to so với các khối còn lại), FILMSTRIP nằm ngay bên dưới. */
/* 2026-09-19 (giảm khoảng trắng dọc): Video (trái) + khối xác nhận/lịch sử/nộp (phải) CÙNG 1 hàng.
   Bề rộng video bị chặn thêm theo CHIỀU CAO cửa sổ: calc((96vh - 200px - <cao filmstrip>) * 16/9).
   200px = phần còn lại của khung (tiêu đề + thanh điều khiển + nhãn video + lề, đo thật ~160px +
   dư địa); <cao filmstrip> = --pb-fs-h do playback.js đặt theo chiều cao THẬT của dải (đổi theo mức
   phóng to/thu nhỏ khung ảnh) -> filmstrip to lên thì video tự co bớt, laptop thấp (768px, kể cả có
   thanh địa chỉ) vẫn không phải cuộn. Màn hình cao thì vẫn tối đa 640px; sàn 240px (phóng to cực
   đại trên màn hình rất thấp thì chấp nhận cuộn thay vì thu video về 0). */
.pb-body {
  display: flex;
  gap: 16px;
  justify-content: center;
  align-items: stretch;
}

.pb-col-video {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  flex: 0 0 auto;
  width: min(640px, 58%, max(240px, calc((96vh - 200px - var(--pb-fs-h, 165px)) * 16 / 9)));
}

/* Nhiều mốc (TRAKE/Temporal): thêm timeline + hàng chọn mốc (~70px) -> chừa thêm 65px cho video. */
.pb-box.multi .pb-col-video {
  width: min(640px, 58%, max(240px, calc((96vh - 265px - var(--pb-fs-h, 165px)) * 16 / 9)));
}

.pb-col-side {
  flex: 1 1 260px;
  min-width: 240px;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 23px;
  /* = chiều cao nhãn "VIDEO GỐC" + khoảng cách, để cột phải ngang mép trên video */
}

.pb-col-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--fg-muted);
  text-transform: uppercase;
}

.pb-col-video video {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: contain;
  border-radius: 8px;
  background: #000;
  display: block;
  border: 1px solid var(--border);
}

/* ---- 🎞️ Filmstrip (xem playback.js, phần "Filmstrip") ----
   Dải frame chạy ngang, KÉO được bằng chuột như canvas; ~5 khung hiện cùng lúc (bề rộng 1 ô =
   bề rộng dải / 5, JS đặt --fs-cell-w); mỗi ô = [khung ảnh 16:9][dải màu ngăn cách]; các ô chỉ
   được dựng cho phần đang nhìn thấy (dải có thể dài hàng nghìn ô). Màu dải xoay vòng 5 màu trầm
   (khớp tông theme, không loè loẹt) - đúng 5 màu = 5 ô liên tiếp, dễ định hướng khi lướt nhanh. */
.pb-filmstrip-box {
  margin-top: 8px;
}

.pb-filmstrip-head {
  display: flex;
  align-items: baseline;
  gap: 6px 12px;
  flex-wrap: wrap;
  margin-bottom: 4px;
}

.pb-filmstrip-head .field-note {
  margin: 0;
}

.pb-filmstrip-hint {
  flex: 1 1 auto;
}

/* gợi ý thao tác nằm CHUNG dòng tiêu đề (trước là 1 đoạn riêng dưới dải) */
.pb-fs-zoom {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  align-self: center;
}

.pb-fs-zoom .icon-btn {
  padding: 2px 6px;
  line-height: 1;
}

.pb-fs-zoom .icon-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

#pb-fs-zoom-lbl {
  min-width: 46px;
  text-align: center;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

#pb-preview-caption {
  margin-left: auto;
  white-space: nowrap;
}

.pb-filmstrip {
  --fs-sep-w: 10px;
  --fs-cell-w: 190px;
  --fs-pad: 6px;
  --fs-frame-h: calc((var(--fs-cell-w) - var(--fs-sep-w)) * 9 / 16);
  position: relative;
  overflow-x: auto;
  overflow-y: hidden;
  background: #0e1116;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-y;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}

.pb-filmstrip::-webkit-scrollbar {
  display: none;
}

.pb-filmstrip.grabbing {
  cursor: grabbing;
}

.pb-filmstrip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.pb-filmstrip-track {
  position: relative;
  height: calc(var(--fs-frame-h) + var(--fs-pad) * 2);
}

.pb-fs-cell {
  position: absolute;
  top: var(--fs-pad);
  height: var(--fs-frame-h);
  width: var(--fs-cell-w);
  --fs-sep: hsl(210 42% 50%);
}

.pb-fs-cell[data-sep="1"] {
  --fs-sep: hsl(160 36% 42%);
}

.pb-fs-cell[data-sep="2"] {
  --fs-sep: hsl(36 56% 50%);
}

.pb-fs-cell[data-sep="3"] {
  --fs-sep: hsl(350 44% 54%);
}

.pb-fs-cell[data-sep="4"] {
  --fs-sep: hsl(268 34% 58%);
}

.pb-fs-frame {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: calc(var(--fs-cell-w) - var(--fs-sep-w));
  background: #1a2029;
  border-radius: 4px;
  overflow: hidden;
}

.pb-fs-cell:not(.loaded):not(.failed) .pb-fs-frame {
  animation: pb-fs-shimmer 1.3s ease-in-out infinite;
}

@keyframes pb-fs-shimmer {

  0%,
  100% {
    background: #1a2029;
  }

  50% {
    background: #232b37;
  }
}

.pb-fs-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity 0.15s;
  pointer-events: none;
  -webkit-user-drag: none;
}

.pb-fs-cell.loaded .pb-fs-frame img {
  opacity: 1;
}

.pb-fs-cell.failed .pb-fs-frame::before {
  content: "\26A0";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--warn);
  font-size: 18px;
}

.pb-fs-num {
  position: absolute;
  left: 4px;
  bottom: 3px;
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 10.5px;
  line-height: 1.35;
  font-variant-numeric: tabular-nums;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  pointer-events: none;
}

/* Dải màu ngăn cách - kéo dài HẾT chiều cao dải (kể cả phần đệm trên/dưới khung) như vạch chia phim. */
.pb-fs-sep {
  position: absolute;
  right: 0;
  width: var(--fs-sep-w);
  top: calc(var(--fs-pad) * -1);
  bottom: calc(var(--fs-pad) * -1);
  background: var(--fs-sep);
}

/* Khung GẦN NHẤT frame đang xem thử - viền nổi bật (đường playhead đỏ nằm đúng vị trí chính xác). */
.pb-fs-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 4px;
  pointer-events: none;
  border: 2px solid transparent;
  transition: border-color 0.12s;
}

.pb-fs-cell.near .pb-fs-frame::after {
  border-color: var(--err);
}

.pb-fs-playhead {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  z-index: 3;
  pointer-events: none;
  background: var(--err);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
}

.pb-fs-playhead::before {
  content: "";
  position: absolute;
  top: 0;
  left: -5px;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 8px solid var(--err);
}

.pb-fs-confirmed {
  position: absolute;
  bottom: 0;
  width: 3px;
  height: 14px;
  margin-left: -1.5px;
  z-index: 2;
  pointer-events: none;
  background: var(--ok);
  border-radius: 2px 2px 0 0;
  display: none;
}

/* Nút Nộp/ô câu trả lời đẩy xuống ĐÁY cột phải (ngang mép dưới video) - margin-top:auto. */
#pb-submit-area {
  margin-top: auto;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

#pb-submit-area .field-note {
  margin: 0 0 8px;
}

#pb-submit-area .qa-field {
  margin-bottom: 8px;
}

/* Cửa sổ hẹp: cột phải rơi xuống dưới video như bố cục cũ. */
@media (max-width: 900px) {
  .pb-body {
    flex-direction: column;
    align-items: center;
  }

  .pb-col-video {
    width: 100%;
  }

  .pb-col-side {
    width: 100%;
    max-width: none;
    padding-top: 0;
  }
}

/* ---- Tab "Xem video" (theo yêu cầu người dùng: "chức năng xem video trực tiếp trên app... một
   tab riêng, có chức năng search, chọn frame muốn tới, keyframe tương ứng được trích ra ở
   local") - xem static/js/video_tab.js. ---- */
/* #search-sidebar bọc quanh mọi field-group CŨ (mode/model/filter/...) - PHẢI tự lặp lại
   display:flex+gap:18px của #sidebar cha, vì gộp chúng vào 1 div con làm mất khoảng cách gap
   GIỮA các field-group (gap chỉ áp dụng cho CON TRỰC TIẾP của flex container). */
#search-sidebar {
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-height: 0;
}

/* ---- Stage / Event workspace --------------------------------------------------- */
#stage-events-sidebar {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.stage-events-heading strong {
  display: block;
  font-size: 13px;
}

.stage-events-heading span {
  display: block;
  margin-top: 3px;
  color: var(--fg-muted);
  font-size: 11px;
  line-height: 1.35;
}

#stage-events-sidebar #anchors-box {
  padding: 0;
  border: 0;
  background: transparent;
}

#stage-events-sidebar #anchors-row {
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: visible;
}

#stage-events-sidebar .anchor-card {
  width: 100%;
  min-width: 0;
  padding: 10px;
  border-radius: 8px;
}

#stage-events-sidebar .anchor-card-head {
  margin-bottom: 7px;
}

#stage-events-sidebar .anchor-card input[type="text"] {
  margin-bottom: 7px;
}

.event-tool-label {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 1px 0 4px;
  color: var(--fg-muted);
  font-size: 10.5px;
  font-weight: 650;
  text-transform: uppercase;
}

/* Thử (2026-09-24, theo yêu cầu người dùng: "thử đặt icon này vào placeholder của input search
   xem thử... làm tương tự cho OCR và ASR") - icon kính lúp (bi-search) BÊN TRONG ô input Search/
   OCR/ASR (không thể nhét icon THẬT vào thuộc tính placeholder HTML - chỉ nhận text thuần), neo
   trái bằng position:absolute + đệm trái cho input để chữ không đè lên icon. title="Query"/"OCR"/
   "ASR" trên icon hiện tooltip khi hover, phân biệt 3 icon giống hệt nhau. */
.anchor-text-field {
  position: relative;
  display: flex;
  align-items: stretch;
}
.anchor-text-field input {
  flex: 1;
  padding-left: 28px;
}
.anchor-text-icon {
  position: absolute;
  left: 10px;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  line-height: 1;
  color: var(--fg-muted);
  font-size: 13px;
  pointer-events: none;
}

#stage-events-sidebar .anchors-actions {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 6px;
  margin-top: 8px;
}

#topbar {
  align-items: center;
  gap: 8px;
}

#stage-tabs {
  display: flex;
  gap: 4px;
  align-items: center;
  flex: 1 1 auto;
  overflow-x: auto;
  scrollbar-width: thin;
}

.stage-tab {
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg-elevated);
  color: var(--fg-muted);
  padding: 7px 13px;
  white-space: nowrap;
  cursor: pointer;
}

.stage-tab:hover {
  color: var(--fg);
  border-color: var(--accent);
}

.stage-tab.active {
  color: var(--accent-fg);
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 650;
}

.stage-tab.dirty:not(.active)::after {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 6px;
  border-radius: 50%;
  background: var(--warn);
  vertical-align: middle;
}

/* 2026-09-24 (theo yêu cầu người dùng: "xóa nút x trực tiếp ở stage đó") - nút "×" xoá riêng
   từng Stage, chỉ hiện khi có >1 Stage (xem renderStageTabs trong app.js). */
.stage-tab-close {
  display: inline-block;
  margin-left: 8px;
  padding: 0 4px;
  border-radius: 4px;
  color: inherit;
  opacity: .6;
  font-weight: 700;
}

.stage-tab-close:hover {
  opacity: 1;
  background: rgba(0, 0, 0, .12);
}

#add-stage {
  flex: 0 0 auto;
}

#max-gap-box {
  flex: 0 1 170px;
  min-width: 100px;
}

#video-ids-box {
  flex: 0 1 100px;
  min-width: 85px;
}

/* 2026-09-24 (theo yêu cầu người dùng: "Chuyển phần chọn mô hình lên thanh trên đầu luôn nhé") -
   chuyển từ sidebar lên #topbar, cạnh #video-ids-box/#max-gap-box - kích thước gọn tương tự. */
#topbar #dense-model {
  flex: 0 0 auto;
  width: 90px;
}

/* Kết quả chỉ là keyframe: không card, không metadata/action/score, không khoảng trắng trang trí. */
#result-filter-box,
#results-bar {
  display: none !important;
}

#results {
  --result-thumb-w: 220px;
  --result-thumb-h: 124px;
  padding: 3px;
  gap: 3px;
  grid-template-columns: repeat(auto-fill, var(--result-thumb-w));
  justify-content: start;
}

.result-thumb {
  position: relative;
  width: var(--result-thumb-w);
  min-width: 0;
  overflow: hidden;
  background: transparent;
  border: 0;
  border-radius: 0;
  cursor: pointer;
  line-height: 0;
}

.result-thumb img,
.result-chain img {
  width: var(--result-thumb-w);
  height: var(--result-thumb-h);
  object-fit: cover;
  display: block;
  background: var(--border);
}

/* Mỗi ảnh trong 1 chuỗi Temporal/TRAKE là 1 ô riêng (frame_id khác nhau/mốc) - cần wrapper
   position:relative RIÊNG để neo nhãn đúng góc của ĐÚNG ảnh đó, không phải neo theo cả .result-
   chain (sẽ dồn hết nhãn về góc chuỗi thay vì từng ảnh). */
.result-chain-item {
  position: relative;
  line-height: 0;
}

/* 2026-09-24 (theo yêu cầu người dùng: "hiển thị Video_id + Frame_id nằm trên góc trái trên
   cùng của frame được hiển thị ra, nằm chồng lên trên frame luôn... chọn màu sao cho nhìn được
   nhưng không che đi nội dung của frame quá nhiều") - nền đen mờ (rgba, không phải màu đặc) +
   chữ trắng: tương phản đủ đọc được trên MỌI ảnh (sáng lẫn tối) mà vẫn thấy được nội dung frame
   bên dưới lớp nền, khác hẳn 1 khối màu đặc sẽ che mất góc ảnh. pointer-events:none để không
   chặn click/mở Playback trên thẻ kết quả.
   font-variant-numeric: tabular-nums giữ chữ số frame_id thẳng cột, dễ đọc lướt qua khi lướt
   nhiều kết quả liên tiếp. */
.result-thumb-label {
  position: absolute;
  top: 4px;
  left: 4px;
  max-width: calc(100% - 8px);
  padding: 2px 6px;
  background: rgba(0, 0, 0, .55);
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
  border-radius: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
  z-index: 1;
}

.result-thumb:hover,
.result-thumb:focus-visible,
.result-chain:hover,
.result-chain:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.result-chain {
  /* Mỗi kết quả Temporal là MỘT hàng/chuỗi riêng. Các ảnh trong hàng lần lượt tương ứng Event
     1..N; không trộn những frame của hai chuỗi liên tiếp thành lưới KIS. */
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(var(--event-count), var(--result-thumb-w));
  grid-auto-rows: var(--result-thumb-h);
  justify-content: start;
  width: max-content;
  max-width: 100%;
  overflow-x: auto;
  gap: 3px;
  min-width: 0;
  margin-bottom: 6px;
  cursor: pointer;
  background: transparent;
}

#pb-submit-area {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

#pb-submit-area .field-note {
  margin: 0;
}

.pb-submit-buttons {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 7px;
}

.pb-submit-kind {
  min-width: 0;
}

.pb-submit-kind.dres-correct {
  background: var(--ok);
  border-color: var(--ok);
  color: #fff;
}

.pb-submit-kind.dres-wrong {
  background: var(--err);
  border-color: var(--err);
  color: #fff;
}

.pb-submit-kind.dres-indeterminate {
  background: var(--warn);
  border-color: var(--warn);
  color: #fff;
}

.pb-vqa-submit {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.pb-vqa-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 7px;
}

.pb-vqa-row input {
  min-width: 0;
}

@media (max-width: 760px) {
  #results {
    --result-thumb-w: 176px;
    --result-thumb-h: 99px;
  }

  #topbar {
    flex-wrap: wrap;
  }

  #stage-tabs {
    flex-basis: 100%;
  }

  #max-gap-box {
    flex: 1 1 140px;
  }

  #video-ids-box {
    flex: 1 1 100px;
  }

  #topbar #dense-model {
    flex: 1 1 100px;
    width: auto;
  }

  .result-chain {
    grid-column: 1 / -1;
    overflow-x: auto;
  }
}

#video-sidebar {
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-height: 0;
  flex: 1;
}

#video-sidebar .field-group {
  flex-shrink: 0;
}

/* 2026-08-23 (bug thật: "trình bày giống bên tab tìm kiếm được không bạn" - #debug-sidebar THIẾU
   HẲN rule này, khiến mọi .field-group bị dồn sát nhau không có khoảng cách, khác hẳn 2 sidebar
   kia - cùng lý do đã ghi ở #search-sidebar phía trên: gap chỉ áp dụng cho CON TRỰC TIẾP của
   flex container, phải tự khai báo lại ở MỖI sidebar riêng). */
#debug-sidebar {
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-height: 0;
}

/* 2026-09-04 (theo yêu cầu người dùng, ảnh chụp thật - "vẫn còn hơi dính nhau") - CÙNG bug/lý
   do đã ghi ở #search-sidebar/#debug-sidebar phía trên: #asr-sidebar THIẾU HẲN rule này (gap
   chỉ áp dụng cho CON TRỰC TIẾP của flex container, không tự kế thừa) khiến các .field-group bị
   dồn sát nhau. */
#asr-sidebar {
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-height: 0;
}

#vt-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.vt-list-loading {
  font-size: 12px;
  color: var(--fg-muted);
  padding: 10px 4px;
  text-align: center;
}

.vt-list-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
  padding: 8px 10px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}

.vt-list-item:hover {
  border-color: var(--accent);
}

.vt-list-item.active {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.vt-list-vid {
  font-size: 11.5px;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.vt-list-title {
  font-size: 12px;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-height: 1.35;
}

.vt-list-author {
  font-size: 10.5px;
  color: var(--fg-muted);
}

/* #main (cha) đã tự overflow-y:auto - #video-main KHÔNG tự cuộn riêng (tránh cuộn lồng cuộn),
   chỉ cần padding cho nội dung. #search-main cũng vậy - display:contents để KHÔNG chèn thêm 1
   box flex column nữa (giữ nguyên hành vi layout CŨ của #main trước khi có wrapper này). */
#search-main {
  display: contents;
}

#video-main {
  padding: 18px;
}

#vt-empty {
  color: var(--fg-muted);
  padding: 60px 20px;
  text-align: center;
}

#vt-player-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 14px;
  flex-wrap: wrap;
}

#vt-title {
  font-size: 16px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

#vt-meta {
  font-size: 12px;
  color: var(--fg-muted);
}

.vt-body {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

@media (max-width: 900px) {
  .vt-body {
    grid-template-columns: 1fr;
  }
}

.vt-col-video,
.vt-col-frame {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

.vt-col-video video {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: contain;
  border-radius: 8px;
  background: #000;
  display: block;
  border: 1px solid var(--border);
}

#vt-frame-img {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: cover;
  border-radius: 8px;
  background: var(--border);
  display: block;
  border: 1px solid var(--border);
}

.vt-controls {
  display: flex;
  gap: 8px;
  align-items: stretch;
}

.vt-controls>.btn {
  flex: 0 0 auto;
  min-width: 64px;
}

.vt-controls .pb-goto-row {
  display: flex;
  gap: 8px;
  flex: 1;
}

.vt-controls .pb-goto-row input {
  flex: 1;
  min-width: 0;
}

#vt-download-frame {
  align-self: flex-start;
}

/* ---- Tab "Search ASR" (2026-09-04, theo yêu cầu người dùng: "trực quan lên để làm công việc
   tìm kiếm, trực quan video, frame và text tương ứng") - danh sách đoạn transcript dạng card
   NGANG (thumbnail trái + text phải, khác lưới ảnh #results của KIS/QA) vì nội dung chính ở đây
   là CÂU CHỮ dài, không phải ảnh - xem asr_tab.js. ---- */
#asr-main {
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 0;
}

#asr-empty {
  color: var(--fg-muted);
  padding: 60px 20px;
  text-align: center;
}

#asr-results {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.asr-result-item {
  display: flex;
  gap: 12px;
  text-align: left;
  padding: 0;
  cursor: pointer;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  align-items: stretch;
}

.asr-result-item:hover {
  border-color: var(--accent);
}

.asr-result-thumb {
  flex: 0 0 140px;
  width: 140px;
  aspect-ratio: 16/9;
  object-fit: cover;
  background: var(--border);
  display: block;
}

.asr-result-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.asr-result-meta {
  display: flex;
  gap: 8px;
  align-items: baseline;
  padding: 8px 10px 0;
  font-size: 11px;
  color: var(--fg-muted);
}

.asr-result-vid {
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.asr-result-time {
  font-variant-numeric: tabular-nums;
}

.asr-result-item .asr-match {
  border-top: none;
  padding: 6px 10px 8px;
}

/* Player box - Ở TRÊN danh sách khi chọn 1 đoạn (không thay thế danh sách, xem index.html) -
   bố cục video/frame TÁI DÙNG .vt-body/.vt-col-video/.vt-col-frame (giống hệt tab Xem video). */
#asr-player-box {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
}

#asr-player-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}

#asr-player-title {
  font-size: 16px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

#asr-player-meta {
  font-size: 12px;
  color: var(--fg-muted);
  flex: 1;
}

#asr-player-video {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: contain;
  border-radius: 8px;
  background: #000;
  display: block;
  border: 1px solid var(--border);
}

#asr-player-frame {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: cover;
  border-radius: 8px;
  background: var(--border);
  display: block;
  border: 1px solid var(--border);
}

#asr-player-text {
  margin-top: 12px;
}

#asr-player-text .asr-match {
  border-top: 1px dashed var(--border);
  border-radius: 6px;
  font-size: 12.5px;
}

/* ---- Floating warmup-status bubble (2026-08-22, "thêm dấu hiệu spinner đang load... đặt chỗ
   nào đó cho gọn") — GÓC DƯỚI TRÁI, đối xứng với #sub-bubble ở góc phải (cùng cơ chế pill+panel-
   đổ-lên, xem ghi chú đầy đủ ở #sub-panel phía trên). Mặc định "hidden" (gỡ hẳn
   khỏi layout qua display:none — KHÔNG cần animate xuất hiện lần đầu, chỉ cần biến mất êm khi
   xong, xem warmup.js) - JS gỡ class này ngay khi có ít nhất 1 bước warmup để theo dõi. */
#warmup-bubble {
  position: fixed;
  bottom: 20px;
  left: 20px;
  z-index: 40;
}

#warmup-bubble.hidden {
  display: none;
}

/* 2026-08-23 (theo yêu cầu người dùng: "làm lại màu sắc... thành màu tương phản trắng đen") -
   TRƯỚC ĐÂY dùng class "btn subtle" (nền trong suốt + chữ var(--fg-muted), viền trong suốt) -
   nổi trên nền trang trực tiếp (không có --bg-elevated phía sau như panel) nên gần như vô hình,
   đúng như ảnh chụp. Ghi đè nền/viền/chữ RÕ RÀNG bằng token, tự đổi đúng theo theme sáng/tối. */
#warmup-pill {
  border-radius: 999px;
  padding: 8px 14px;
  box-shadow: var(--shadow);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-variant-numeric: tabular-nums;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--fg);
}

#warmup-pill .bi-arrow-repeat {
  color: var(--accent);
}

#warmup-pill .bi-arrow-repeat {
  animation: aic-spin 1.1s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  #warmup-pill .bi-arrow-repeat {
    animation-duration: 3s;
  }
}

#warmup-panel {
  position: absolute;
  bottom: 100%;
  left: 0;
  width: max-content;
  min-width: 300px;
  max-width: min(92vw, 420px);
  max-height: 60vh;
  overflow-y: auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
  margin-bottom: 8px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(8px) scale(0.98);
  transform-origin: bottom left;
  transition: opacity 200ms cubic-bezier(0.25, 0.8, 0.25, 1),
    transform 200ms cubic-bezier(0.25, 0.8, 0.25, 1),
    visibility 0s linear 200ms;
}

#warmup-panel.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0) scale(1);
  transition: opacity 200ms cubic-bezier(0.25, 0.8, 0.25, 1),
    transform 200ms cubic-bezier(0.25, 0.8, 0.25, 1),
    visibility 0s linear 0s;
}

#warmup-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.warmup-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  padding: 5px 2px;
  border-bottom: 1px solid var(--border);
}

.warmup-row:last-child {
  border-bottom: none;
}

.warmup-row-name {
  overflow-wrap: anywhere;
}

.warmup-row.warmup-pending {
  color: var(--fg-muted);
}

.warmup-row.warmup-loading {
  color: var(--accent);
}

.warmup-row.warmup-loading .bi {
  animation: aic-spin 0.9s linear infinite;
}

.warmup-row.warmup-ready {
  color: var(--fg-muted);
}

.warmup-row.warmup-ready .bi {
  color: #2e9e5b;
}

.warmup-row.warmup-error {
  color: var(--err);
}

/* ============================================================ Tab Debug (2026-08-23, theo yêu
   cầu người dùng: "tạo cho mình 1 tab Debug... trực quan, dễ nhìn dễ xem chứ không phải chỉ in
   ra các dòng chữ vô tri") - xem debug_tab.js. Mode KIS/Q&A trong sidebar bị disable (chỉ giữ để
   nhất quán hình dạng với sidebar Tìm kiếm) - dùng .btn:disabled sẵn có, không cần CSS riêng. */
.dbg-mode-btn.active {
  background: var(--accent);
  color: var(--accent-fg);
  font-weight: 600;
}

/* 2026-08-23 (theo yêu cầu người dùng, ảnh chụp thẻ Mốc thật - "bạn chưa đưa các chức năng này
   vào") - #dbg-anchors giờ tái dùng NGUYÊN .anchor-card/.anchor-canvas-* (đã style sẵn cho
   sidebar Tìm kiếm thật) - không cần CSS riêng cho khung mốc debug nữa. */

/* 2026-08-23 (theo yêu cầu người dùng: "cứ làm như thế này, mắc gì phải đổi" - #topbar/#anchors-
   box THẬT nằm ở main, KHÔNG phải sidebar) - #debug-main đổi sang display:contents (giống hệt
   #search-main) để #dbg-topbar/#dbg-anchors-box trở thành CON TRỰC TIẾP của #main (flex column),
   tự dãn full-width sát viền như #topbar/#anchors-box thật - không còn bị bó trong padding:18px
   của khối cha nữa (padding đó dời vào #dbg-results-area, CHỈ áp cho phần kết quả bên dưới). */
#debug-main {
  display: contents;
}

#dbg-topbar {
  display: flex;
  align-items: center;
  gap: 10px 18px;
  flex-wrap: wrap;
  padding: 10px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

#dbg-max-gap-box {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 320px;
  flex-shrink: 0;
}

/* 2026-08-24 (theo yêu cầu người dùng: "thêm phần thanh kéo cho gap penalty per second") - mirror
   #dbg-max-gap-box, width lớn hơn 1 chút vì label dài hơn (ghi rõ chỉ áp dụng thuật toán nào). */
#dbg-gap-rate-box {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 380px;
  flex-shrink: 0;
}

#dbg-anchors-box {
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

#dbg-anchors {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  padding-bottom: 4px;
  margin-bottom: 10px;
}

#dbg-run {
  flex-shrink: 0;
}

#dbg-results-area {
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

#dbg-empty {
  color: var(--fg-muted);
  padding: 60px 20px;
  text-align: center;
}

#dbg-summary {
  font-size: 12.5px;
  color: var(--fg-muted);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 12px;
}

#dbg-summary b {
  color: var(--fg);
}

.dbg-loading {
  grid-column: 1/-1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 160px;
  color: var(--fg-muted);
  font-size: 15px;
}

.dbg-loading .bi {
  animation: aic-spin 0.9s linear infinite;
}

.dbg-err {
  padding: 10px 14px;
  border-radius: 8px;
  border: 1px solid var(--err);
  color: var(--err);
  background: color-mix(in srgb, var(--err) 10%, transparent);
  font-size: 13px;
}

.dbg-empty-note {
  color: var(--fg-muted);
  padding: 30px;
  text-align: center;
  font-size: 13px;
}

/* 2026-08-24 (theo yêu cầu người dùng: "thêm phần debug các bước xử lý query này vào UI Debug
   luôn nhé") - lộ bước chưng cất query (VI->EN qua LLM): câu dịch ra + so điểm VI thô vs EN chưng
   cất trực quan bằng 2 thanh có huy hiệu (trophy) đánh dấu bản THẮNG - đúng tinh thần "trực quan,
   không chỉ in ra các dòng chữ vô tri" đã áp dụng cho phần còn lại của tab Debug. */
#dbg-distill-panel {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 13px;
}

.dbg-distill-title {
  font-weight: 700;
  font-size: 12px;
  color: var(--fg-muted);
  display: flex;
  align-items: center;
  gap: 6px;
}

.dbg-distill-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 0;
  border-top: 1px solid var(--border);
}

.dbg-distill-row:first-of-type {
  border-top: none;
}

.dbg-distill-head {
  font-size: 12.5px;
  color: var(--fg);
}

.dbg-distill-en {
  font-size: 12px;
  color: var(--fg-muted);
}

.dbg-distill-note {
  font-size: 11.5px;
  color: var(--fg-muted);
  font-style: italic;
}

.dbg-distill-bars {
  display: flex;
  gap: 8px;
}

.dbg-distill-bar {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 8px;
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}

.dbg-distill-bar.win {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.dbg-distill-tag {
  font-size: 11px;
  color: var(--fg-muted);
}

.dbg-distill-bar.win .dbg-distill-tag {
  color: var(--accent);
  font-weight: 700;
}

.dbg-distill-score {
  font-weight: 700;
  font-size: 13px;
  margin-left: auto;
}

.dbg-distill-bar .bi-trophy-fill {
  color: var(--accent);
  font-size: 12px;
}

#dbg-results {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.dbg-video-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* align-items: center (đổi từ baseline 2026-08-24) - hàng này giờ có thêm NÚT Playback, baseline
   căn theo chữ khiến nút lệch hẳn lên so với nhãn/điểm bên cạnh. */
.dbg-video-head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.dbg-video-id {
  font-weight: 700;
  font-size: 14px;
}

.dbg-video-score {
  font-size: 12px;
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
  margin-left: auto;
}

/* 2026-08-24 (theo yêu cầu người dùng: "thêm chức năng playback vào debug tab") - nút mở dialog
   playback DÙNG CHUNG với tab Tìm kiếm (xem debug_tab.js::wirePlayback). Nhãn "đã chỉnh tay" đánh
   dấu RÕ frame do người dùng tự đổi trong playback, KHÔNG phải frame thuật toán chọn - tránh nhìn
   nhầm kết quả chẩn đoán (đúng tinh thần gap-honesty đã dùng cho nhãn is_backfill/Level). */
.dbg-playback-btn {
  flex-shrink: 0;
  padding: 4px 9px;
  font-size: 11.5px;
}

.dbg-playback-btn:hover {
  color: var(--accent);
}

.dbg-edited {
  display: inline-block;
  margin-left: 4px;
  padding: 1px 6px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  background: color-mix(in srgb, var(--warn) 18%, transparent);
  color: var(--warn);
}

/* 2026-08-23 (theo yêu cầu người dùng: "viết lại logic debug nhé, bạn đưa ra đủ kết quả từ level
   0 -> 3") - nhãn tầng nguồn gốc của từng dòng kết quả (xem debug_tab.js::LEVEL_META) - "ok"
   (Level 0, khớp chặt thật) dùng màu accent trung tính, "warn" (Level 1/2, đã nới lỏng) màu vàng
   cảnh báo nhẹ, "err" (Level 3, không khớp thật) màu đỏ - đúng mức độ tin cậy giảm dần. */
.dbg-level-badge {
  display: inline-flex;
  align-items: center;
  font-size: 10px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
  white-space: nowrap;
}

.dbg-level-badge.dbg-level-ok {
  background: var(--accent-soft);
  color: var(--accent);
}

.dbg-level-badge.dbg-level-warn {
  background: color-mix(in srgb, var(--warn) 18%, transparent);
  color: var(--warn);
}

.dbg-level-badge.dbg-level-err {
  background: color-mix(in srgb, var(--err) 15%, transparent);
  color: var(--err);
}

/* Mỗi mốc = 1 "làn" (lane) ngang, ứng viên định vị theo pts_time TRÊN CÙNG 1 TRỤC (để khoảng
   cách ngang giữa các chấm phản ánh ĐÚNG khoảng cách giây thật giữa các ứng viên - không dàn đều
   giả tạo như timeline v3 cũ). Đường nối SVG đè lên trên, vẽ SAU khi các chấm đã có toạ độ pixel
   thật (xem debug_tab.js::drawChosenPath). */
.dbg-lanes-wrap {
  position: relative;
}

.dbg-lanes-svg {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 2;
}

.dbg-path {
  stroke: var(--accent);
  stroke-width: 2;
  stroke-dasharray: 4 3;
  opacity: .85;
}

.dbg-path-joint {
  fill: var(--accent);
}

.dbg-lanes {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.dbg-lane {
  display: flex;
  align-items: center;
  gap: 10px;
}

.dbg-lane-label {
  flex: 0 0 auto;
  width: 150px;
  font-size: 11px;
  color: var(--fg-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dbg-lane-track {
  position: relative;
  flex: 1;
  height: 22px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
}

.dbg-dot {
  position: absolute;
  top: 50%;
  border-radius: 50%;
  background: var(--fg-muted);
  transform: translate(-50%, -50%);
  cursor: help;
  transition: opacity 120ms ease;
}

.dbg-dot-chosen {
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 3.5px var(--accent);
  z-index: 3;
}

/* 2026-08-24 (theo yêu cầu người dùng: "thêm debug các ảnh truy vấn được trong pool tương ứng với
   từng thuật toán... đầy đủ pool cho từng mốc... pool ảnh cùng video_id thu thập được sẽ được
   đánh dấu cùng màu... pool từng mốc không bị trộn lẫn với nhau") - xem debug_tab.js::
   renderPoolPreview. Toggle GẤP MẶC ĐỊNH (300 ảnh/mốc là nhiều, không nên đập thẳng vào mắt). */
.dbg-pool-toggle {
  width: 100%;
  text-align: left;
  padding: 10px 14px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--fg);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
}

.dbg-pool-toggle:hover {
  border-color: var(--accent);
}

.dbg-pool-toggle .bi-chevron-right {
  transition: transform 150ms ease;
  font-size: 11px;
}

.dbg-pool-toggle.open .bi-chevron-right {
  transform: rotate(90deg);
}

#dbg-pool-preview {
  margin-top: 8px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.dbg-pool-note {
  font-size: 11px;
  color: var(--fg-muted);
}

/* Mỗi mốc = 1 khối RIÊNG, không chung dòng với mốc khác - đúng yêu cầu "không bị trộn lẫn". */
.dbg-pool-lane {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.dbg-pool-lane-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--fg);
}

.dbg-pool-lane-count {
  font-weight: 400;
  color: var(--fg-muted);
  margin-left: 6px;
  font-size: 11px;
}

.dbg-pool-lane-track {
  display: flex;
  gap: 4px;
  overflow-x: auto;
  padding-bottom: 4px;
  scrollbar-width: thin;
}

.dbg-pool-thumb {
  flex: 0 0 auto;
  display: block;
  width: 56px;
  height: 32px;
  border-radius: 4px;
  border: 2px solid transparent;
  overflow: hidden;
  cursor: pointer;
}

.dbg-pool-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  background: var(--border);
}

.dbg-pool-neutral {
  border-color: var(--border);
  opacity: .75;
}

.dbg-pool-neutral:hover {
  opacity: 1;
  border-color: var(--fg-muted);
}

/* 14 màu tách biệt (hue cách đều ~26°) - đủ dùng cho cả 2 theme qua color-mix với nền, tránh cần
   khai riêng bảng màu sáng/tối. Gán ỔN ĐỊNH theo hash(video_id) trong JS, xem _poolColorIndex. */
.dbg-pool-color-0 {
  border-color: hsl(0 75% 55%);
}

.dbg-pool-color-1 {
  border-color: hsl(26 75% 55%);
}

.dbg-pool-color-2 {
  border-color: hsl(52 75% 50%);
}

.dbg-pool-color-3 {
  border-color: hsl(78 65% 45%);
}

.dbg-pool-color-4 {
  border-color: hsl(104 55% 45%);
}

.dbg-pool-color-5 {
  border-color: hsl(130 55% 48%);
}

.dbg-pool-color-6 {
  border-color: hsl(156 55% 45%);
}

.dbg-pool-color-7 {
  border-color: hsl(182 60% 45%);
}

.dbg-pool-color-8 {
  border-color: hsl(208 70% 55%);
}

.dbg-pool-color-9 {
  border-color: hsl(234 70% 65%);
}

.dbg-pool-color-10 {
  border-color: hsl(260 65% 65%);
}

.dbg-pool-color-11 {
  border-color: hsl(286 65% 62%);
}

.dbg-pool-color-12 {
  border-color: hsl(312 70% 60%);
}

.dbg-pool-color-13 {
  border-color: hsl(338 70% 58%);
}

.dbg-steps {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.dbg-step-card {
  flex: 1 1 160px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg);
}

.dbg-step-card img {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: cover;
  display: block;
  background: var(--border);
}

.dbg-step-info {
  padding: 2px 8px 8px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 11.5px;
}

.dbg-step-info b {
  font-size: 12px;
}

.dbg-step-info code {
  background: var(--accent-soft);
  color: var(--accent);
  padding: 0 4px;
  border-radius: 4px;
}

.dbg-penalty {
  color: var(--err);
  font-weight: 600;
}

.dbg-gap-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  width: fit-content;
  font-size: 10.5px;
  color: var(--fg-muted);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 7px;
}

/* ---- Toast thông báo (2026-09-24, theo yêu cầu người dùng: "hiển thị thông báo khi nộp thành
   công hay không thành công") - NỔI trên MỌI thứ (z-index > #playback-modal:120) vì nút submit
   DRES nằm trong modal Playback, #steplog trang chính bị che. Tạo lazy trong app.js::showToast. */
#toast-container {
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: min(360px, calc(100vw - 32px));
  pointer-events: none;
}

.toast {
  position: relative;
  pointer-events: auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-left: 4px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 11px 34px 11px 14px;
  animation: toast-in 0.18s ease-out;
}

.toast.toast-out {
  opacity: 0;
  transform: translateX(12px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

@keyframes toast-in {
  from { opacity: 0; transform: translateX(16px); }
  to { opacity: 1; transform: translateX(0); }
}

.toast-ok { border-left-color: var(--ok); }
.toast-err { border-left-color: var(--err); }
.toast-warn { border-left-color: var(--warn); }

.toast-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--fg);
}

.toast-ok .toast-title { color: var(--ok); }
.toast-err .toast-title { color: var(--err); }
.toast-warn .toast-title { color: var(--warn); }

.toast-line {
  margin-top: 3px;
  font-size: 11.5px;
  color: var(--fg-muted);
  word-break: break-word;
}

.toast-close {
  position: absolute;
  top: 6px;
  right: 8px;
  border: none;
  background: transparent;
  color: var(--fg-muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}

.toast-close:hover { color: var(--fg); }

@media (max-width: 560px) {
  #toast-container {
    left: 16px;
    right: 16px;
    max-width: none;
  }
}
