@charset "UTF-8";
/* Không viền, không nền: panel giờ nằm TRONG tab (`tab-content-harness`), mà tab đã là một cái
   khung rồi — vẽ thêm một khung nữa là hai khung lồng nhau. Bản trước có `border` + `background:
   #fff` vì panel nằm trong hộp description vàng nhạt của Redmine, và ô `gap` (#fbf3de) gần như
   trùng với nền đó; lý do ấy mất cùng lúc với lần dời vào tab (nền tab là trắng). Đo bằng mắt
   2026-09-14 trên rh7p2web ở cả hai chỗ. */
.harness-panel { margin: 4px 0; }
.harness-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 0 0 8px; }
/* Khối gộp hai ticket cùng sống (chủ dự án 2026-10-01): phần Test nối ngay dưới phần phát triển,
   một đường kẻ đứt tách hai phase — vẫn MỘT khối, chỉ là hai băng bước. */
.harness-panel--gop .harness-head--test { margin-top: 14px; padding-top: 10px; border-top: 1px dashed #ced4da; }
.harness-gop-nhom { margin: 10px 0 4px; font-size: 0.95em; }
.harness-qa-phase { margin-inline-end: 4px; }
.harness-chip { display: inline-block; padding: 1px 7px; border-radius: 10px;
                background: #e6f1fb; border: 1px solid #cfe2f5; font-size: 11px; }
/* Chip ĐỎ cho con số hỏng/chặn của khung "Test run". View chỉ gắn class này khi con số > 0 — một
   chip đỏ "Hỏng: 0" đứng thường trực dạy người đọc bỏ qua màu đỏ, đúng lúc màu đó cần có nghĩa. */
.harness-chip--do { background: #fbe6e6; border-color: #f0c9c9; color: #8a2222; font-weight: bold; }
/* Hàng chip tổng kết: sát tiêu đề khung, trên ô nhập. `gap` cho cả hàng thay vì margin từng chip —
   chip nào ẩn (chặn = 0) thì không để lại khoảng trống. */
.harness-test-tom { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
                    margin: 4px 0 8px; }
/* Cụm nút "Thêm testcase" + "Báo lỗi" ở CUỐI hàng chip, căn phải (chủ dự án 2026-10-01).
   `margin-inline-start: auto` đẩy cả cụm sang mép phải mà không cần thêm khung bọc hàng chip. */
.harness-test-nut { margin-inline-start: auto; display: flex; gap: 6px; align-items: center; }
.harness-test-nut .harness-bug { display: inline-block; }
/* Dòng testcase chạy tay: nền khác một chút (chủ dự án 2026-10-01), thay cho cột "Manual only".
   Tô trên `td`, không trên `tr`: vằn odd/even của lõi đặt nền trên `tr` với độ ưu tiên ngang
   bằng, nên tô trên `tr` thì thắng hay thua tuỳ thứ tự nạp file. Giữ màu hover của lõi. */
table.harness-test-chi-tiet tbody tr.harness-test-tay > td { background-color: #cfe2fb; }
table.harness-test-chi-tiet tbody tr.harness-test-tay:hover > td { background-color: #b9d4f8; }
/* Chú thích màu dưới bảng: ô mẫu cùng màu với dòng chạy tay. */
.harness-test-chu-thich { margin: 4px 0; font-size: 0.9em; color: var(--oc-gray-7); }
.harness-test-chu-thich .mau { display: inline-block; width: 12px; height: 12px; margin-inline-end: 4px;
                               vertical-align: -2px; background: #cfe2fb; border: 1px solid #9ec2f2; }
.harness-tc-them .tabular input[type="text"],
.harness-tc-them .tabular textarea { width: 95%; box-sizing: border-box; }
/* Bảng chi tiết testcase trong popup: mượn NGUYÊN `table.list` của lõi (vằn odd/even lõi tự kẻ
   theo `nth-child`), cộng hai class của bảng bộ testcase ở khối xem artifact — `harness-ca-cuon`
   là khối cuộn ngang, `harness-ca` là cỡ chữ và canh trên. Hai bảng hiện CÙNG một bộ dữ liệu, để
   chúng đọc ra hai kiểu là bắt QC học hai lần. Mấy dòng dưới là phần riêng của popup. */
/* `table.list td` của lõi căn GIỮA và canh GIỮA theo chiều dọc — đúng cho cột số liệu, sai cho
   precondition và expected. Bề ngang thì để bảng tự chia theo nội dung rồi cuộn ngang, không khai
   cứng: số cột thay đổi theo từng bản 09-test-cases (bản trước 2026-09-21 chỉ có bốn khoá).

   `table.list.harness-test-chi-tiet` chứ không `.harness-test-chi-tiet`: chọn tử một-class-một-thẻ
   YẾU HƠN `table.list td` của lõi, nên bản đầu của mấy dòng này KHÔNG có tác dụng gì — đo bằng
   `getComputedStyle` 2026-09-22, ra đúng `center`. Cùng cái bẫy đang làm `.harness-ca td
   { vertical-align: top }` của bảng bộ testcase chết lặng, xem ghi chú ở đó. */
table.list.harness-test-chi-tiet td { text-align: start; vertical-align: top;
                                      overflow-wrap: anywhere; }
/* TIÊU ĐỀ căn theo NỘI DUNG, không mặc kệ mỗi bên một kiểu (chủ dự án 2026-09-22). Đo trước khi
   sửa: `th` ra `center` còn `td` ra `start`, cộng thêm lệch 2px vì `th` thụt 4px mà `td` thụt 2px
   — nên chữ tiêu đề đứng lệch hẳn khỏi cột chữ bên dưới nó.

   Chọn căn TRÁI cho cả hai: ba cột dài nhất (precondition, steps, expected) là văn xuôi nhiều
   dòng, mà văn xuôi căn giữa thì mỗi dòng bắt đầu một chỗ. Một mép trái duy nhất cho cả bảng là
   thứ mắt dò xuống được. */
table.list.harness-test-chi-tiet th { text-align: start; }
table.list.harness-test-chi-tiet th,
table.list.harness-test-chi-tiet td { padding-inline-start: 4px; }
table.list.harness-test-chi-tiet td.name { font-weight: bold; white-space: nowrap; }
/* Ô dữ liệu testcase: GIỮ dấu xuống dòng của file gốc, vẫn tự ngắt dòng khi hết chỗ (`pre-wrap`,
   không `pre`) — `precondition` và `expected` hay là khối nhiều dòng trong YAML, và HTML thì nuốt
   hết. `pre-wrap` chứ không `pre-line` là để khớp `.harness-gap-q` và `.harness-artifact-raw`: cả
   file này giữ chữ do người/agent gõ bằng `pre-wrap`, và nó giữ luôn phần thụt lề trong giá trị.
   Chỉ ô này, không cả bảng: ô tên/kết quả/nhập tay mang xuống dòng của chính template. */
table.list.harness-test-chi-tiet td.harness-test-o { white-space: pre-wrap; }
/* Ba cột nới rộng thêm (chủ dự án 2026-09-22). Đo trước khi sửa, ở dialog 80vw trên màn 1614px:
   `req` 87px, `type` 39px, `result` 58px — `type` hẹp hơn chữ "Positive", và ô kết quả phải chứa
   cả chip, tên người và giờ nên 58px bẻ nó thành bốn dòng.

   `min-inline-size` chứ không `inline-size`: bảng vẫn tự chia theo nội dung (số cột đổi theo từng
   bản 09-test-cases), mấy dòng này chỉ đặt SÀN. Dùng `em` nên sàn đi theo cỡ chữ của bảng. */
table.list.harness-test-chi-tiet .harness-cot-req { min-inline-size: 8em; }
table.list.harness-test-chi-tiet .harness-cot-type { min-inline-size: 7em; }
table.list.harness-test-chi-tiet .harness-test-kq { min-inline-size: 11em; }
/* `priority` đủ chỗ cho chữ "Medium" trên MỘT dòng (chủ dự án 2026-09-22): cột tự chia ra 54px,
   mà "Medium" cộng hai bên thụt lề là 62px nên nó bị bẻ đôi. */
table.list.harness-test-chi-tiet .harness-cot-priority { min-inline-size: 6em; }
/* Và sàn cho ba cột CHỮ, cùng lượt: bảng chỉ rộng bằng khung, nên nới ba cột trên xong thì đúng ba
   cột QC cần đọc bị bóp lại (đo: precondition 160→95px, expected 151→75px, dòng đầu cao 488px).
   Cho chúng sàn rồi để bảng tràn ra và CUỘN NGANG — chủ dự án đã chốt cuộn ngang khi bảng dài. */
table.list.harness-test-chi-tiet .harness-cot-precondition,
table.list.harness-test-chi-tiet .harness-cot-expected { min-inline-size: 15em; }
table.list.harness-test-chi-tiet .harness-cot-steps { min-inline-size: 13em; }
/* `note` của 09-test-cases cũng là cột CHỮ: thiếu sàn thì nó bị bóp còn một chữ mỗi dòng (#282812). */
table.list.harness-test-chi-tiet .harness-cot-note { min-inline-size: 15em; }
/* Ô nhập không được hẹp hơn cái dropdown trong nó: bảng chia cột theo nội dung, mà ô này gần như
   không có nội dung nên nó bị bóp còn vài chữ.

   `vertical-align: middle` — cột này là hai NÚT BẤM, không phải văn xuôi (chủ dự án 2026-09-22).
   Dán chúng lên mép trên của một dòng cao bốn dòng chữ thì mắt phải nhảy lên đầu dòng mỗi lần. */
/* `nowrap` + sàn 12em: dropdown 98px cộng cây bút 28px cộng thụt lề là 144px, mà sàn cũ 10em chỉ
   chừa 130px — nên cây bút rơi xuống dòng dưới (đo: select ở y=111, bút ở y=138). Hai thứ của CÙNG
   một lượt nhập phải đứng cùng hàng (chủ dự án 2026-09-22). */
table.list.harness-test-chi-tiet td.harness-test-nhap { min-inline-size: 12em;
                                                        vertical-align: middle;
                                                        white-space: nowrap; }
/* Cột "Xem note" cũng là MỘT NÚT, cùng luật với ô nhập ngay trên. Thiếu nó thì `overflow-wrap:
   anywhere` của bảng (dòng đầu file này, đặt cho văn xuôi của precondition/expected) bẻ nhãn nút
   xuống TỪNG KÝ TỰ: bảng chia cột theo nội dung, thấy chữ nào cũng ngắt được nên nó bóp cột còn
   26px và nút cao 190px (chủ dự án 2026-09-23, đo trên rhreal). `nowrap` là thứ nói với bảng rằng
   cột này có một bề ngang tối thiểu thật. */
table.list.harness-test-chi-tiet td.harness-test-loi { white-space: nowrap;
                                                       vertical-align: middle; }
/* Dòng thao tác dưới ô chọn kết quả (2026-10-19): Ghi chú · Sửa · Xoá cùng một kiểu — icon + chữ
   nhỏ dạng link, một dòng, không xuống dòng. Xoá màu đỏ của chip đỏ. Nút là `button` nên phải gỡ
   khung mặc định; nền vàng "ghi chú đã có chữ" ở dưới vẫn thắng vì chọn tử của nó nặng hơn. */
.harness-test-thao-tac { display: flex; gap: 10px; margin-block-start: 4px; font-size: 0.9em;
                         white-space: nowrap; }
.harness-test-thao-tac button { background: none; border: 0; margin: 0; padding: 1px 3px;
                                border-radius: 3px; font: inherit; line-height: inherit;
                                color: var(--oc-blue-8, #1971c2); cursor: pointer; }
.harness-test-thao-tac button, .harness-test-thao-tac a {
  display: inline-flex; align-items: center; gap: 2px; }
.harness-test-thao-tac .harness-tc-xoa { color: #8a2222; }
.harness-ghi-chu-nut { vertical-align: middle; margin-inline-start: 4px; padding: 2px 4px;
                       line-height: 1; cursor: pointer; }
/* Cây bút ĐỔI MẶT khi ô ghi chú đã có chữ: đóng cửa sổ xong mà nút vẫn y như cũ thì không có gì
   nói rằng ghi chú đã dính vào lượt gửi. `:placeholder-shown` là cách hỏi "ô còn rỗng không" mà
   không cần JS — ô ghi chú luôn có chữ gợi ý nên phép hỏi này luôn trả lời được. */
td.harness-test-nhap:has(textarea:not(:placeholder-shown)) .harness-ghi-chu-nut {
  background: #fff8dc; border-color: #d4b106; }
/* Cửa sổ ghi chú mượn khung của popup artifact nhưng KHÔNG mượn kích thước: 80vw × 80vh cho một ô
   sáu dòng là một cửa sổ rỗng. `block-size: auto` để nó cao đúng phần nội dung.

   `dialog.harness-ghi-chu-hop` — thêm tên THẺ — vì `.harness-artifact-dialog` nằm SAU trong file
   này: hai chọn tử cùng một class thì cái đứng sau thắng, và bản đầu của hai dòng dưới ra đúng
   1212×740 khi đo. Cùng họ với hai lần specificity đã cắn trong bảng này.

   `fit-content` chứ KHÔNG `auto`: trình duyệt đặt `inset: 0` cho dialog modal, mà một hộp
   `position: fixed` bị neo cả mép trên lẫn mép dưới thì `block-size: auto` KÉO GIÃN ra đầy màn
   hình thay vì co theo nội dung — đo được 890px cho một cửa sổ có 146px nội dung, và ép
   `style.blockSize = 'auto'` tận tay cũng y hệt. */
dialog.harness-ghi-chu-hop { inline-size: min(32em, 90vw); block-size: fit-content;
                             white-space: normal; }
dialog.harness-ghi-chu-hop textarea { inline-size: 100%; box-sizing: border-box; }
/* Cửa sổ danh sách bình luận. Rộng hơn cửa sổ ghi chú vì nó chứa ảnh và khối bằng chứng; cao thì
   vẫn `fit-content` cho tới trần của trình duyệt, và `max-block-size` để danh sách dài thì CUỘN
   trong khung thay vì đẩy nút Đóng ra ngoài — cùng cái bẫy đã ghi ở `.harness-test-chi-tiet-khung`. */
dialog.harness-note-hop { inline-size: min(46em, 92vw); block-size: fit-content; white-space: normal; }
.harness-note-ds { overflow-y: auto; min-block-size: 0; max-block-size: 60vh; }
/* Mỗi bình luận một khối có mép trái — nhiều người viết vào cùng một testcase thì ranh giới giữa
   hai lời phải nhìn ra ngay, không phải đọc mới biết. */
.harness-note-muc { border-inline-start: 3px solid var(--oc-gray-3); padding-inline-start: 8px;
                    margin-block-end: 10px; }
.harness-note-dau { margin: 0 0 4px; display: flex; gap: 6px; align-items: baseline;
                    flex-wrap: wrap; }
/* Nhãn vòng test: chip VIỀN, không nền — đứng cạnh chip kết quả mà trông khác loại, để "Staging"
   không đọc ra như một kết quả thứ hai. */
.harness-note-chang { background: transparent; font-weight: bold; }
/* Ảnh và video người ta đẩy lên không được kéo rộng cửa sổ, và cao quá nửa màn hình thì một cái
   ảnh chụp toàn trang đẩy mọi bình luận sau nó xuống dưới tầm nhìn. */
.harness-note-file { margin-block-start: 6px; }
.harness-note-file img, .harness-note-file video { max-inline-size: 100%; max-block-size: 50vh;
                                                   block-size: auto; display: block; }
.harness-note-muc .wiki img, .harness-note-muc .wiki video { max-inline-size: 100%; block-size: auto; }
/* Khung nạp mảnh AJAX là một flex item của `<dialog>` cao 80vh. `min-block-size: 0` KHÔNG thừa —
   cùng cái bẫy đã ghi ở `.harness-artifact-wrap`: mặc định flex item không co nhỏ hơn nội dung, nên
   thiếu dòng này thì bảng mười ba dòng đẩy hàng nút Đóng ra ngoài cửa sổ. Cuộn DỌC ở đây, cuộn
   NGANG để `.harness-ca-cuon` bên trong lo, để không có hai thanh cuộn ngang lồng nhau. */
.harness-test-chi-tiet-khung { flex: 1; min-block-size: 0; overflow-y: auto; }
/* Ảnh lỗi QC dán vào không được kéo rộng cả bảng. */
.harness-test-chi-tiet .wiki { overflow-wrap: anywhere; }
.harness-test-chi-tiet .wiki img { max-inline-size: 100%; }
/* Bằng chứng agent gửi: giữ xuống dòng thật, nhưng đừng để một dòng log dài kéo cả bảng. */
.harness-test-bang-chung { white-space: pre-wrap; overflow-wrap: anywhere; max-block-size: 12em;
                           overflow: auto; font-size: 11px; margin: 4px 0 0; }
.harness-attempts { color: #666; font-size: 11px; margin-left: 6px; }

/* MỘT hàng, ô bằng nhau, tối thiểu 88px; hẹp hơn thì băng cuộn ngang (2026-10-15). DAG test mọc tới
   11 bước (chặng master), và `flex: 1 1 0; min-width: 0` cũ ép 11 ô vào bề ngang bất kỳ — màn hẹp là
   mỗi ô vài chục px, chữ gãy từng từ. Không cho xuống hàng: một ô lẻ ở hàng hai đọc ra như một băng
   khác, mà panel đã có hai băng (phát triển + test) chồng nhau. `harness.js` cuộn tới ô đang chạy. */
/* Ngữ pháp sơ đồ của archify (tt-a1i/archify, theme sáng của `assets/template.html`), chủ dự án chọn
   2026-10-16: mỗi bước là một NODE viền theo ngữ nghĩa, nối nhau bằng MŨI TÊN của main path — đoạn đã
   đi qua liền màu "xong", đoạn chưa tới đứt xám. Gap 14px là chỗ của mũi tên.

   MÀU thì KHÔNG lấy của archify mà lấy Open Color của theme Redmine (`--oc-*` kèm hex dự phòng, như
   spec §7), chủ dự án chọn cùng ngày sau khi đo trên trang thật: cyan/emerald của archify lệch tông với
   xanh header và link của theme, ngay cạnh chip "B3 · Spec". Mỗi trạng thái có ba bậc — viền (≥ 3:1
   trên trắng), dấu/chữ đậm (≥ 4,5:1 trên nền của nó), nền nhạt. teal = xong, blue = đang ở đây (cùng
   xanh của link), yellow = chờ người, red = bị trả lại, gray = chưa tới. */
.harness-flow { --hf-chu: var(--oc-gray-9, #212529); --hf-chu-phu: var(--oc-gray-7, #495057);
                --hf-xam: var(--oc-gray-6, #868e96); --hf-xam-nen: var(--oc-gray-1, #f1f3f5);
                --hf-xong: var(--oc-teal-8, #099268); --hf-xong-dam: var(--oc-teal-9, #087f5b);
                --hf-xong-nen: var(--oc-teal-0, #e6fcf5);
                --hf-dang: var(--oc-blue-7, #1c7ed6); --hf-dang-dam: var(--oc-blue-9, #1864ab);
                --hf-dang-nen: var(--oc-blue-0, #e7f5ff);
                --hf-cho: var(--oc-yellow-9, #e67700); --hf-cho-dam: #8a6d1f;
                --hf-cho-nen: var(--oc-yellow-0, #fff9db);
                --hf-tra: var(--oc-red-7, #f03e3e); --hf-tra-dam: var(--oc-red-9, #c92a2a);
                --hf-tra-nen: var(--oc-red-0, #fff5f5);
                display: grid; grid-auto-flow: column; grid-auto-columns: minmax(88px, 1fr); gap: 14px;
                overflow-x: auto; padding: 4px 4px 6px; }
.harness-cell { position: relative; min-width: 0; border: 1.5px solid var(--hf-xam); border-radius: 6px;
                overflow-wrap: anywhere; padding: 5px 4px; text-align: center; font-size: 11px;
                text-decoration: none; color: var(--hf-chu); background: #fff; }
/* Ô có PR là `<a>`, và luật `a, a:link, a:visited` của theme (0,1,1) thắng `.harness-cell` (0,1,0):
   tên bước ở ô có PR ra màu link xanh, ô không có PR thì đen — xanh đó đọc nhầm thành "đang ở đây".
   Luật này chỉ để thắng độ ưu tiên. */
.harness-flow a.harness-cell { color: var(--hf-chu); }
/* Mũi tên VÀO ô này, từ ô liền trước — `+` nên ô đầu không có. Màu theo ô TRƯỚC: đoạn đã đi qua là
   đoạn rời một ô xong. `::before` của ô là chỗ trống duy nhất: `::after` là vệt viền của `.dang`.
   Hình mũi tên là MỘT `clip-path` (thân 2px + đầu 6×8px), kích thước nguyên pixel. Bản đầu ghép đầu
   mũi tên từ hai tam giác gradient cao 3,5px: phóng to thì đúng, nhưng ở cỡ 1:1 khử răng cưa biến nó
   thành một vệt mờ lệch khỏi thân (chủ dự án chụp màn hình 2026-10-16). Hai lớp nền: lớp đầu tô kín
   vùng đầu mũi tên, lớp sau là thân LẶP 6px — lúc có job, dời `background-position` của lớp này 6px
   là gạch đứt trôi liền mạch (`harness-edge-flow`); `clip-path` giữ nguyên hình nên đầu đứng yên. */
.harness-cell + .harness-cell::before {
  --c: var(--hf-xam);
  content: ""; position: absolute; top: calc(50% - 4px); left: -14px; width: 14px; height: 8px;
  clip-path: polygon(0 3px, calc(100% - 6px) 3px, calc(100% - 6px) 0, 100% 4px,
                     calc(100% - 6px) 8px, calc(100% - 6px) 5px, 0 5px);
  background: linear-gradient(var(--c), var(--c)) 100% 0 / 6px 100% no-repeat,
              linear-gradient(90deg, var(--c) 3px, transparent 3px) 0 0 / 6px 100% repeat-x; }
.harness-cell.done + .harness-cell::before {
  --c: var(--hf-xong);
  background-image: linear-gradient(var(--c), var(--c)), linear-gradient(var(--c), var(--c)); }
.harness-cell .ten { display: block; font-weight: bold; }
.harness-cell .who { display: block; color: var(--hf-chu-phu); overflow: hidden;
                     text-overflow: ellipsis; white-space: nowrap; }
/* Dòng người DUYỆT. Màu hổ phách là màu "đang chờ người" của cả file này (`.harness-cell.gap`,
   `.harness-gap-head`) — nhưng màu chỉ là lớp thứ hai: chuỗi dịch đã mang sẵn chữ "duyệt:" ở đầu,
   nên người mù màu vẫn phân biệt được nó với dòng `.who` ngay trên. Cắt ellipsis cùng lý do với
   `.who`: login dạng email dài hơn bề ngang một ô chín-cột. */
.harness-cell .duyet { display: block; color: #8a6d1f; overflow: hidden;
                       text-overflow: ellipsis; white-space: nowrap; }
/* Sigil trước tên bước — kiểu ký hiệu góc node của archify. Mỗi trạng thái có MỘT dấu đơn sắc riêng
   (✓ xong, ▸ đang ở đây, ? chờ trả lời gap, ↩ bị trả lại) cùng lý do `.who` hiện chữ: màu một mình
   thì người mù màu không thấy. Màu dấu là bản ĐẬM của màu viền để đạt tương phản trên nền nhạt. */
.harness-cell.done { background: var(--hf-xong-nen); border-color: var(--hf-xong); }
.harness-cell.done .ten::before { content: "✓\00a0"; color: var(--hf-xong-dam); }
/* Viền 2px CỘNG dấu ▸ trước tên bước: màu một mình thì người mù màu không thấy ô nào đang chạy.
   Dấu này TRƯỚC ĐÂY không tồn tại — comment cũ khai là có mà markup thì không, và ô `now` chỉ phân
   biệt được bằng màu. Bỏ mã bước đi thì ô cũng mất nốt chữ đơn sắc cuối cùng, nên thêm thật. */
.harness-cell.now  { background: var(--hf-dang-nen); border: 2px solid var(--hf-dang);
                     padding: 4.5px 3.5px; }
.harness-cell.now .ten::before { content: "▸\00a0"; color: var(--hf-dang-dam); }
.harness-cell.gap  { background: var(--hf-cho-nen); border-color: var(--hf-cho); }
.harness-cell.gap .ten::before { content: "?\00a0"; color: var(--hf-cho-dam); }
/* "Đã làm rồi, bị trả lại" — KHÁC hẳn `todo` ("chưa tới"), nên phải khác cả màu lẫn dấu. Dấu `↩`
   là lớp thứ hai vì màu thì người mù màu không thấy, cùng lý do `.who` hiện chữ thay vì đổi màu. */
.harness-cell.needs_info { background: var(--hf-tra-nen); border-color: var(--hf-tra); }
.harness-cell.needs_info .ten::after { content: " ↩"; color: var(--hf-tra-dam); }
/* "Chờ duyệt" — bước hiện tại có artifact `in-review` (chủ dự án 2026-09-30). Họ hổ phách như
   `.gap` vì cùng là "đang chờ người", nhưng vàng hơn, viền 2px như `.now` và nhịp sáng: đây là
   chỗ duy nhất trên băng mà một cú bấm của người đọc làm ticket chạy tiếp. Nhãn chữ `.can-duyet`
   nằm trong ô nên màu không phải tín hiệu duy nhất; #8a6d1f trên yellow-0 đạt khoảng 4,6:1. */
.harness-cell.cho_duyet { background: var(--hf-cho-nen); border: 2px solid var(--hf-cho);
                          padding: 4.5px 3.5px;
                          animation: harness-cho-duyet 2.4s ease-out infinite; }
.harness-cell .can-duyet { display: block; font-weight: bold; color: var(--hf-cho-dam); }
/* Nhịp "radar" của archify (`archify-radar-live`): vòng loé ra rồi tắt hẳn ở 55%, nghỉ tới hết chu kỳ —
   không đổi kích thước ô, chỉ `box-shadow`. */
@keyframes harness-cho-duyet {
  0%       { box-shadow: 0 0 0 0 rgba(230, 119, 0, .52); }
  55%, 100% { box-shadow: 0 0 0 .32rem rgba(230, 119, 0, 0); }
}
@media (prefers-reduced-motion: reduce) { .harness-cell.cho_duyet { animation: none; } }
/* "Chờ phát triển" — T1 của phase test khi ticket phát triển chưa đóng (chủ dự án 2026-10-01).
   Xám, viền gạch đứt, KHÔNG nhịp sáng: không có gì chạy và người đọc chưa bấm được gì ở đây. */
.harness-cell.cho_dev { background: var(--hf-xam-nen); border: 2px dashed var(--hf-xam);
                        padding: 4.5px 3.5px; color: var(--hf-chu-phu); }
.harness-cell .cho-dev { display: block; font-weight: bold; color: var(--hf-chu-phu); }
.harness-cell .dot { display: block; font-weight: bold; color: var(--hf-chu-phu); }
/* "Chưa tới" vẽ viền ĐỨT như node chưa đi qua của archify; mảnh hơn `cho_dev` (1.5px, nền trắng)
   để hai thứ không lẫn — `cho_dev` còn có nhãn chữ riêng. Viền gray-6 chứ không nhạt hơn: bản trước
   (#cbd5e1) chỉ 1,48:1 trên trắng, ô gần như không có khung. Chữ gray-7, không gray-6: 3,3:1 là đủ
   cho viền nhưng không đủ cho chữ 11px. */
.harness-cell.todo { background: #fff; border-style: dashed; color: var(--hf-chu-phu); }

/* Ô bước hiện tại có VỆT SÁNG CHẠY VÒNG QUANH VIỀN khi có job thật trong hàng đợi
   (`Live.dang_chay?`), không phải khi runner "được phép chạy" (chủ dự án 2026-10-16: thay cho đường
   ray chạy dưới băng — chuyển động phải nằm ở BƯỚC đang chạy, không ở cả băng). Vệt là `::after`
   phủ đúng lên viền: nền conic xoay theo `--hf-goc`, mask khoét phần ruột nên chữ và màu nền của ô
   giữ nguyên — KHÔNG đổi kích thước, vì ô co giãn làm cả băng chín ô nhảy theo. `::after` trước đây
   là chấm "●"; chấm đó nhường chỗ, và vệt vẫn là một HÌNH (không chỉ màu) khi chuyển động bị tắt.

   Bám `.dang` chứ KHÔNG `.now` (chủ dự án 2026-09-23: "giao diện đang không có animation job đang
   chạy"): `status` của ô trả `gap` thay cho `now` khi bước hiện tại có gap, nên bản trước mất luôn
   ô để nhấp nháy đúng vào lúc máy đang làm. Trong vòng sửa của DAG test ca đó là ca THƯỜNG — server
   tự mở defect ở T2 rồi kéo con trỏ về đúng T2. `.now` vẫn giữ MÀU của ô.

   `@property` để trình duyệt nội suy được góc; trình duyệt cũ không biết nó thì vệt đứng yên ở 0°,
   vẫn đọc được là "ô này đang chạy". */
@property --hf-goc { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
.harness-panel.job-running .harness-cell.dang::after {
  content: ""; position: absolute; inset: -2px; padding: 2px; border-radius: 6px; pointer-events: none;
  background: conic-gradient(from var(--hf-goc), transparent 0 40%, rgba(28, 126, 214, .25) 60%,
                             var(--hf-dang) 99.5%, transparent 99.5%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  animation: harness-vien-chay 1.6s linear infinite; }
@keyframes harness-vien-chay { to { --hf-goc: 360deg; } }
/* Vệt chỉ thấy được khi khác màu viền: viền `now` là `--hf-dang`, trùng đầu vệt, nên vệt chạy mà mắt
   không thấy (#288690 trên pro, 2026-10-16). Lúc có job, viền nhạt xuống blue-2 để vệt nổi lên. Ô
   `gap` viền hổ phách, đã khác màu vệt, giữ nguyên. */
.harness-panel.job-running .harness-cell.now.dang { border-color: var(--oc-blue-2, #a5d8ff); }
/* Mũi tên dẫn VÀO ô đang chạy thành gạch đứt xanh trôi về phía ô — `archify-edge-flow` của archify,
   nhưng lặp mãi chứ không một lượt: ở đây nó trả lời "máy có đang làm không", câu đó còn đúng tới khi
   job dừng. Chỉ dời lớp thân (lớp thứ hai, ô lặp 6px); đầu mũi tên đứng yên. */
.harness-panel.job-running .harness-cell + .harness-cell.dang::before {
  --c: var(--hf-dang);
  background-image: linear-gradient(var(--c), var(--c)), linear-gradient(90deg, var(--c) 3px, transparent 3px);
  animation: harness-edge-flow .4s linear infinite; }
@keyframes harness-edge-flow { to { background-position: 100% 0, 6px 0; } }
@media (prefers-reduced-motion: reduce) {
  .harness-panel.job-running .harness-cell.dang::after,
  .harness-panel.job-running .harness-cell + .harness-cell.dang::before { animation: none; }
}

/* Khối "đang chạy". Từ 2026-09-29 nó nằm trong tab log: summary bị giấu, `open` đi theo tab được
   chọn (server đặt, `chon_tab` đổi); JS đọc `open` để poll nhanh hay chậm, và vẽ dòng. */
.harness-live { margin: 6px 0 0; font-size: 12px; }
.harness-live-lines { list-style: none; margin: 6px 0 0; padding: 6px 8px; max-height: 340px;
                      overflow-y: auto; background: #f7f9fb; border: 1px solid #e2e8ee;
                      border-radius: 4px; font-family: monospace; line-height: 1.5; }
/* `pre-wrap`, KHÔNG `nowrap`: từ 2026-09-14 dòng stream mang cả lời agent — nhiều câu, có xuống
   dòng, có danh sách đánh số. `nowrap` + ellipsis cắt chúng còn một dòng cụt ở mép phải, tức là
   thứ người mở khối này ra để đọc thì họ không đọc được. Dòng tool vẫn ngắn nên không đổi hình. */
.harness-live-lines li { white-space: pre-wrap; overflow-wrap: anywhere; margin: 2px 0; }
.harness-live-pick { margin: 6px 0 0; color: #666; }
.harness-live-pick select { font-size: 12px; margin-left: 4px; }
/* Tin gửi cho agent — spec chen-context §5. Màu theo trạng thái: "không kịp" phải đọc được từ xa. */
.harness-msgs { margin: 6px 0 0; }
.harness-msgs-tieu-de { margin: 0; color: #666; }
.harness-msgs ul { list-style: none; margin: 2px 0 0; padding: 0; }
.harness-msg { white-space: pre-wrap; overflow-wrap: anywhere; margin: 2px 0; }
.harness-msg.cho { color: #666; }
.harness-msg.khong_kip { color: #b3261e; }
.harness-msg-form { margin: 6px 0 0; }
.harness-msg-form textarea { width: 100%; box-sizing: border-box; font-size: 12px; }

/* Đường ray ĐỨNG YÊN (chủ dự án 2026-10-16): chuyển động "đang chạy" đã dời vào viền ô bước hiện
   tại — xem `harness-vien-chay`. Ray chỉ còn khác nhau bằng độ đậm giữa có job và không. */
.harness-rail { height: 3px; margin: 6px 0 10px; border-radius: 2px;
                background-image: repeating-linear-gradient(90deg, var(--oc-blue-7, #1c7ed6) 0 8px, transparent 8px 16px); }
.harness-rail.stopped { opacity: .35; }

/* `>` là BẮT BUỘC, không phải cho gọn: popup "Bắt đầu Harness" nằm TRONG `.harness-start`, nên một
   luật con-cháu kéo luôn `<form>` của popup thành hàng ngang — tiêu đề dạt trái, khung ô ra giữa,
   nút sang phải. Luật này chỉ dành cho hàng nút một dòng, không dành cho form trong `<dialog>`. */
.harness-start > form { display: flex; align-items: center; gap: 6px; margin: 1em 0; }
.harness-start label { color: #666; font-size: 12px; }
.harness-switch { display: inline-flex; gap: 6px; margin-left: 8px; }
.harness-inline { display: inline; }
.harness-inline input[type="submit"] { font-size: 11px; padding: 1px 8px; }

.harness-gap { background: #fbf3de; border-radius: 4px; padding: 8px 10px; margin: 8px 0; }
.harness-gap--escalated { border-left: 4px solid #b34141; }
.harness-gap-head { font-size: 12px; color: #8a6d1f; margin: 0 0 4px; }
.harness-gap-esc { color: #b34141; font-weight: bold; }
/* Nút "Chép link" (2026-10-13) đứng cuối dòng đầu gap, cỡ chữ của dòng đó — nút phụ, không tranh
   chỗ với ô trả lời. */
.harness-gap-link { margin-inline-start: 8px; font-size: 11px; padding: 0 6px; }
/* Câu mà link trỏ tới: viền cam để người nhận link thấy ngay câu nào là của mình giữa một chùm.
   scroll-margin: trình duyệt cuộn neo sát mép trên, viền bị cắt mất nửa. */
.harness-gap:target { outline: 3px solid #e8a33d; outline-offset: 2px; scroll-margin-top: 40px; }
tr.harness-qa-item:target { scroll-margin-top: 40px; }
tr.harness-qa-item:target > td { background: #fbf3de; }
/* `<pre>` nên phải kéo về font của trang — mặc định của trình duyệt là monospace cỡ nhỏ, mà đây là
   văn bản người đọc chứ không phải log. Giữ lại đúng một thứ của `<pre>`: xuống dòng thật. */
.harness-gap-q, .harness-qa-q, .harness-qa-a { margin: 0 0 6px; white-space: pre-wrap;
                 overflow-wrap: anywhere; font-family: inherit; font-size: inherit; }
.harness-gap-opts { list-style: none; margin: 0 0 6px; padding: 0; }
.harness-gap-opts li { margin: 3px 0; }

/* Ô trả lời. `resize: vertical` chứ không `both`: kéo ngang làm textarea trườn ra khỏi panel
   và đẩy vỡ layout issue của Redmine, mà chiều cao mới là thứ người ta thật sự cần thêm. */
.harness-gap-answer, .harness-qa-them-o { display: block; width: 100%; max-width: 60em;
                                          resize: vertical; }
.harness-qa-them-o { margin: 0 0 6px; }
/* Bộ đếm ký tự dưới ô chữ hỏi–đáp (spec 2026-10-02 §7). Đổi màu khi còn ≤ 100 — đủ sớm để người gõ
   rút gọn thay vì chạm trần giữa câu. */
.harness-dem { display: block; font-size: 0.85em; margin: -4px 0 6px; }
/* Hai class: `.harness-muted` (khai báo SAU trong file) cùng độ ưu tiên sẽ đè màu đỏ — đo trên rhreal. */
.harness-dem.harness-dem--it { color: #c0392b; }
.harness-gap-nguoi { margin: 0 0 4px; font-weight: bold; }
/* Danh sách gợi ý người trong popup (`nap_goi_y` trong harness.js) — thay widget của `<datalist>`,
   thứ trượt khỏi ô nhập khi nằm trong `<dialog>`. Toạ độ do JS đặt theo ô nhập. */
.harness-goi-y { position: fixed; z-index: 10; margin: 0; padding: 2px 0; list-style: none;
                 max-block-size: 16em; overflow-y: auto; box-sizing: border-box;
                 background: var(--oc-white, #fff); border: 1px solid var(--oc-gray-4, #ced4da);
                 border-radius: 3px; box-shadow: 0 2px 6px rgba(0, 0, 0, .15); }
.harness-goi-y li { padding: 3px 8px; cursor: pointer; }
.harness-goi-y li.now, .harness-goi-y li:hover { background: var(--oc-blue-0, #e7f5ff); }
.harness-goi-y li span { margin-inline-start: 8px; color: #888; }
.harness-gap-send { margin-top: 4px; }
.harness-gap-phim { margin-left: 8px; font-size: 0.9em; }

/* Nút gửi khoá khi chưa đủ câu trả lời. Trình duyệt tự làm mờ nút `disabled`, nhưng con trỏ
   vẫn là mũi tên thường — người ta rê vào, bấm, không có gì xảy ra, và không có gì nói vì sao. */
#harness-gap-submit:disabled { cursor: not-allowed; }
/* Gộp cả chùm gap vào một form: khoảng cách giữa các hộp phải do chính hộp giữ, vì form không
   còn nằm trong từng hộp nữa. */
.harness-gap + .harness-gap { margin-top: 10px; }
.harness-gap-opts label { cursor: pointer; }

/* Hỏi–đáp: bảng `table.list` của lõi. Lõi canh giữa ô của `table.list`, mà hai cột đầu là văn bản
   nhiều dòng — canh giữa thì mỗi dòng lệch một mép. Hai cột người/lúc không xuống dòng, để phần
   rộng dồn cho câu hỏi và câu trả lời. */
.harness-qa { margin: 8px 0; }
/* `table.list` ĐỨNG TRƯỚC, cùng cách `harness-test-chi-tiet`: `table.list td` của lõi là (0,1,2),
   thắng một `.harness-qa-bang td` (0,1,1) — đo 2026-09-29 trên rhreal, bản đầu ra chữ canh giữa. */
table.list.harness-qa-bang td, table.list.harness-qa-bang th { text-align: start; vertical-align: top; }
.harness-qa-bang .harness-qa-by, .harness-qa-bang .harness-qa-at { white-space: nowrap; }
/* Bấm dòng mở cả câu hỏi ở dòng `.harness-qa-day` ngay dưới (2026-10-12). */
.harness-qa-bang tr.harness-qa-item { cursor: pointer; }
.harness-qa-mo { border: 0; background: none; padding: 0 4px 0 0; margin: 0; cursor: pointer;
                 color: var(--oc-gray-6, #868e96); font-size: 1.5em; line-height: 1; display: inline-block; transition: transform .15s; }
.harness-qa-mo::before { content: "\203A"; }
.harness-qa-dang-mo .harness-qa-mo { transform: rotate(90deg); }
.harness-qa-dang-mo td, tr.harness-qa-day td { background: var(--oc-gray-0, #f8f9fa); }
tr.harness-qa-day td { padding: 4px 8px 10px 24px; }
.harness-qa-full { white-space: pre-wrap; overflow-wrap: anywhere; font-family: inherit; font-size: inherit;
                   margin: 0 0 8px; padding: 8px 10px; background: var(--oc-white, #fff); border: 1px solid var(--oc-gray-3, #dee2e6); border-radius: 3px; }
.harness-qa-chep-bao { margin-inline-start: 6px; color: var(--oc-gray-7, #495057); }
/* Bảng người phụ trách ở tab Cài đặt: cùng lý do, cột "Hiện tại" không có class `name`. */
table.list.harness-nguoi-bang td { text-align: start; }

/* Thanh tab con của panel (`_subtabs`, 2026-09-29). Nút TRẦN, gỡ khung nút của lõi: tab là nhãn để
   chọn, không phải việc để bấm — nút thật (Thêm Q&A, Xem chi tiết) đứng ở cuối thanh, và hai loại
   trông giống nhau thì mắt phải đọc chữ mới tách được. Tab đang chọn: chữ đậm màu hơn CỘNG gạch dưới
   2px — không chỉ bằng màu. Viền trong suốt ở mọi tab để đổi tab không làm cả thanh nhảy 2px. */
.harness-subtabs { display: flex; align-items: flex-end; flex-wrap: wrap; gap: 2px;
                   margin: 12px 0 6px; border-bottom: 1px solid var(--oc-gray-3); }
.harness-subtab-list { display: flex; flex-wrap: wrap; gap: 2px; }
.harness-subtab { border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px;
                  border-radius: 0; background: none; font: inherit; padding: 5px 10px;
                  color: var(--oc-gray-6); cursor: pointer; }
.harness-subtab:hover { background: none; color: var(--oc-gray-8); }
.harness-subtab[aria-selected="true"] { color: var(--oc-gray-8); font-weight: bold;
                                        border-bottom-color: var(--color-current-marker, #1c7ed6); }
.harness-subtab-actions { margin-inline-start: auto; align-self: center; }
.harness-subtab-actions button { font-size: 11px; padding: 1px 8px; }
/* Pane nhật ký: khối `<details>` giữ nguyên vì harness.js đọc `open` của nó, nhưng summary thì
   thành thừa — nhãn tab đã nói đúng câu đó. Tab log chọn là khối mở (server và `chon_tab` lo). */
.harness-subpanel details.harness-live > summary { display: none; }

/* Dòng "panel đã cũ". JS chỉ chèn nó khi việc tự nạp lại BỊ CHẶN vì người đang gõ dở — nên nó
   hiếm, và khi nó hiện thì nó là thứ quan trọng nhất trên panel. */
.harness-stale { margin: 0 0 8px; padding: 6px 10px; border-radius: 4px;
                 background: #fff4d6; border-left: 4px solid #d99a00; font-size: 12px; }

/* ─── dashboard (pha 5B) ─────────────────────────────────────────────────────────────────────── */
.harness-group { border: 1px solid #ddd; border-radius: 6px; padding: 8px 12px; margin: 1em 0; }
.harness-group h3 { margin: 0 0 6px; font-size: 14px; }
/* 4,5em và monospace là kích thước của MÃ ("B10"). Từ 2026-09-15 nhãn là TÊN bước — "Ảnh hưởng",
   và khi một mã mang hai nghĩa thì "Yêu cầu / Mục tiêu" — nên cột phải rộng ra và font phải là font
   của chữ. Cho xuống dòng chứ KHÔNG ellipsis: `title` đang giữ mã bước, nên chữ bị cắt là chữ mất
   hẳn chứ không phải chữ rê chuột ra đọc được. */
.harness-bar-label { flex: 0 0 11em; font-size: 12px; overflow-wrap: anywhere; }
.harness-bar-value { flex: 0 0 4em; text-align: right; font-size: 12px; color: #444; }
/* "Đang ở đâu" + "Nghẽn" cùng một hàng, dải xếp chồng + danh sách xếp hạng (2026-09-24, phương án B).
   Nhãn/giá trị giữ class `harness-bar-*` để test tên bước, mã trong title, đơn vị vẫn đo được. */
.harness-diag { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
                gap: 12px; margin: 1em 0; }
.harness-diag > .harness-group { margin: 0; }
.harness-dai { display: flex; height: 14px; border-radius: 3px; overflow: hidden;
               background: var(--oc-gray-1, #f1f3f5); margin: 4px 0 8px; }
.harness-doan { display: block; height: 100%; min-width: 2px; }
.harness-chu-giai, .harness-xep-hang { list-style: none; padding: 0; margin: 0; }
.harness-chu-giai li, .harness-xep-hang li { display: flex; align-items: center; gap: 8px; margin: 2px 0;
                                             font-size: 12px; }
.harness-chu-giai .harness-bar-label, .harness-xep-hang .harness-bar-label { flex: 1 1 auto; }
.harness-chu-giai .harness-bar-value, .harness-xep-hang .harness-bar-value { flex: 0 0 auto; }
.harness-o-mau { flex: 0 0 10px; height: 10px; border-radius: 2px; }
.harness-xep-hang { counter-reset: harness-hang; }
.harness-xep-hang li::before { counter-increment: harness-hang; content: counter(harness-hang);
                               flex: 0 0 1.4em; text-align: right; color: var(--oc-gray-6, #868e96); }
/* ponytail: 10 màu xoay vòng theo thứ hạng; hơn 10 bước cùng có ticket thì hai đoạn trùng màu — chú
   giải vẫn in tên và số nên không mất dữ liệu. Thêm màu khi DAG thật sự có nhiều bước đông cùng lúc. */
.harness-mau-0 { background: var(--oc-blue-6, #228be6); }
.harness-mau-1 { background: var(--oc-teal-6, #12b886); }
.harness-mau-2 { background: var(--oc-orange-6, #fd7e14); }
.harness-mau-3 { background: var(--oc-grape-6, #be4bdb); }
.harness-mau-4 { background: var(--oc-yellow-6, #fab005); }
.harness-mau-5 { background: var(--oc-cyan-7, #1098ad); }
.harness-mau-6 { background: var(--oc-pink-6, #e64980); }
.harness-mau-7 { background: var(--oc-green-7, #37b24d); }
.harness-mau-8 { background: var(--oc-indigo-6, #4c6ef5); }
.harness-mau-9 { background: var(--oc-gray-6, #868e96); }
.harness-note { font-size: 11px; color: #777; margin: 4px 0 0; }
.harness-muted { color: #888; }

/* Dòng "có lượt nào đang chạy không". Ca `hong` KHÔNG chỉ đổi màu: người mù màu phải đọc được nó,
   nên chuỗi dịch đã mang sẵn "⚠" ở đầu — màu chỉ là lớp thứ hai. */
.harness-runner-status { margin: 4px 0 0; font-size: 12px; color: #777; }
.harness-runner-status.hong { color: #b34141; }
/* "Chờ dev" / "chờ duyệt lại nguồn": ticket đứng im cho tới khi NGƯỜI làm gì đó ở chỗ khác (đóng
   ticket dev, bấm "Deployed to JackFruit") — chữ xám thì không ai thấy (chủ dự án 2026-10-29).
   Hổ phách + nhấp nháy như `.cho_duyet`, KHÔNG đỏ: đỏ ở panel này là "lượt chạy hỏng". */
.harness-runner-status.cho_phat_trien, .harness-runner-status.cho_nguon {
  font-weight: bold; color: #8a6d1f; background: #fff9db;
  border: 2px solid #e67700; border-radius: 3px; padding: 4px 8px;
  animation: harness-cho-nhay 1.2s ease-in-out infinite;
}
@keyframes harness-cho-nhay {
  50% { background: #ffd8a8; border-color: #fff9db; }
}
@media (prefers-reduced-motion: reduce) {
  .harness-runner-status.cho_phat_trien, .harness-runner-status.cho_nguon { animation: none; }
}

/* Issue đổi sau khi chốt 00-context. Đậm hơn `.harness-runner-status` một bậc — cùng chỗ, cùng cỡ
   chữ, nhưng nó KHÔNG phải trạng thái định kỳ mà là một việc người phải quyết. Không dùng đỏ:
   đỏ ở panel này đang có nghĩa "lượt chạy hỏng", mà đây không phải lỗi của ai. */
.harness-doi-issue {
  margin: 4px 0 0; font-size: 12px; color: #8a6d1f;
  border-left: 3px solid #e3c65a; padding-left: 6px;
}

/* Dòng log gần nhất dài và không xuống dòng: cắt bằng ellipsis chứ không cho nó kéo bảng rộng ra
   ngoài màn hình. `max-width` chứ không `width` để cột còn co được trên màn hẹp. */
.harness-runners td:last-child {
  max-width: 40em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.harness-runner-buoc { font-weight: bold; margin-right: 6px; }

/* Mã job: cùng ngôn ngữ với lệnh chẩn đoán trên server, nên để font đều như mã.
   `white-space: nowrap` vì "#78" bị xuống dòng thành "#" và "78" là đọc ra hai thứ. */
.harness-runner-job { font-family: monospace; white-space: nowrap; }

/* Đội hình worker. Danh sách chứ không bảng: mỗi worker là một dòng ngắn, và số worker thường là
   một chữ số — dựng bốn cột tiêu đề cho ba dòng là thừa khung hơn là thừa chữ. */
.harness-workers-head { margin: 0 0 4px; font-weight: bold; font-size: 12px; }
.harness-workers { list-style: none; margin: 0 0 10px; padding: 0; font-size: 12px; }
.harness-workers li { margin: 2px 0; }
.harness-workers a { margin-left: 4px; }
.harness-worker-ten { font-family: monospace; margin-right: 6px; }
.harness-worker-thay { margin-left: 6px; }
/* "Không worker nào đang trực" là tin XẤU, không phải một dòng trạng thái nhạt: job xếp hàng sẽ
   nằm im và không có gì khác trên trang nói ra điều đó. Chuỗi dịch đã mang sẵn "⚠" ở đầu — màu chỉ
   là lớp thứ hai, cho người mù màu vẫn đọc được. */
.harness-workers-trong { color: #b34141; }
/* Băng "công tắc runner toàn cục TẮT" (spec 2026-09-24 §3.2 R1). Nền và viền theo băng cảnh báo
   của spec §7 (`--oc-*` kèm hex dự phòng). CHỮ dùng #8a6d1f của `.harness-doi-issue` thay cho
   `--oc-yellow-9`: #e67700 trên nền yellow-0 chỉ đạt khoảng 2,8:1, dưới mức 4,5:1 cho chữ 12px;
   #8a6d1f đạt khoảng 4,6:1. Chuỗi dịch đã mang "⚠" ở đầu nên màu không phải tín hiệu duy nhất. */
.harness-runner-tat {
  margin: 0 0 8px; padding: 6px 8px; font-size: 12px; color: #8a6d1f;
  background: var(--oc-yellow-0, #fff9db);
  border-left: 3px solid var(--oc-yellow-7, #f59f00);
}

/* Ghi chú "lease quá hạn" cạnh tên worker của một job (2026-10-07). Cùng bảng màu cảnh báo vàng với
   `.harness-runner-tat` — chữ #8a6d1f đạt tương phản chữ nhỏ, và không dựa vào màu làm tín hiệu duy nhất
   vì câu đã nêu số phút. */
.harness-runner-het-han {
  display: block; margin-top: 2px; font-size: 12px; color: #8a6d1f;
}

/* Dòng tóm tắt còn lại trên trang báo cáo. `hong` khi KHÔNG worker nào trực — cùng luật với
   `.harness-workers-trong`: đó là tin xấu, không phải một dòng trạng thái nhạt. */
.harness-runners-tomtat.hong a { color: #b34141; font-weight: bold; }

.harness-pr { margin: 0; font-size: 12px; }
.harness-review-approved          { color: #2e7d32; font-weight: bold; }
.harness-review-changes_requested { color: #b34141; font-weight: bold; }
.harness-review-pending           { color: #8a6d1f; }
.harness-review-none              { color: #888; }
/* Băng "đang chờ duyệt" (chủ dự án 2026-09-30: nút duyệt nằm cuối dòng PR thì "rất khó để ý").
   Nền hổ phách là màu "đang chờ người" của cả file — cùng nền với `.harness-gap`, nên ai đã quen
   khối gap đọc ra ngay "tới lượt người". Viền trái đậm theo `.harness-runner-tat`. Tiêu đề mang
   sẵn "⏳" nên màu không phải tín hiệu duy nhất. */
.harness-pr.cho-duyet { background: #fbf3de; border-left: 4px solid var(--oc-yellow-7, #f59f00);
                        border-radius: 4px; padding: 8px 10px; margin: 8px 0; }
.harness-can-duyet { margin: 0 0 4px; font-size: 13px; color: #8a6d1f; }
/* Tên người một dòng riêng: đứng sau tiêu đề dài thì nó bị bẻ đôi giữa chừng, mà "chờ AI" là
   thứ người ta đọc để biết đi hỏi ai. */
.harness-can-duyet .harness-cho-duyet { display: block; margin-top: 2px; }
/* Nút là HÀNH ĐỘNG nên khác màu hẳn với băng: xanh lá của `.harness-review-approved`, chữ trắng
   đạt khoảng 5:1. Một hàng riêng, không lẫn cuối dòng PR. Tiền tố `.harness-pr` để thắng luật
   `button:hover` của lõi Redmine (độ ưu tiên 0,1,1 cao hơn một class trần). */
.harness-pr .harness-duyet-mo { display: block; margin: 8px 0 0; padding: 5px 14px; font-size: 13px;
                                font-weight: bold; color: #fff; background: #2e7d32;
                                border: 1px solid #1b5e20; border-radius: 4px; cursor: pointer; }
.harness-pr .harness-duyet-mo::before { content: "✓ "; }
.harness-pr .harness-duyet-mo:hover { background: #1b5e20; }
.harness-pr .harness-duyet-mo:focus-visible { outline: 2px solid #378add; outline-offset: 2px; }
/* Popup duyệt: mỗi ngày review một dòng. Nút bỏ ẩn khi chỉ còn một dòng (vẫn giữ chỗ để cột không nhảy). */
.harness-duyet-ngay { margin: 0 0 4px; }
.harness-duyet-ngay:only-child .harness-bo-ngay { visibility: hidden; }
/* "Bắt đầu test" là nút CHÍNH (chủ dự án 2026-10-01): việc tiếp theo của cả issue, nên nền đặc
   nổi hơn mọi nút viền xung quanh. Màu xanh dương, không xanh lá: xanh lá đã là "Duyệt bước này",
   hai việc khác nhau không được trông như một. Cùng kiểu ở MỌI chỗ nút này hiện. */
.harness-test-open { padding: 4px 14px; font-weight: bold; color: #fff; background: #1d6fd8;
                     border: 1px solid #1559b0; border-radius: 4px; cursor: pointer;
                     box-shadow: 0 1px 2px rgba(0, 0, 0, .15); }
.harness-test-open::before { content: "▶ "; font-size: 0.85em; }
.harness-test-open:hover { background: #1559b0; }
.harness-test-open:focus-visible { outline: 2px solid #378add; outline-offset: 2px; }

/* Nút "Chuyển sang Bug" của ticket spike đã kết luận lỗi thật (chủ dự án 2026-10-07: "nổi bật hơn").
   Cam đậm, chữ trắng, cỡ chữ và đệm lớn hơn nút xanh của "Bắt đầu test" — khác MÀU để không lẫn với nút
   bắt đầu thường, và là việc kế tiếp duy nhất của issue nên được phép to hơn các nút phụ. */
.harness-bug-open { padding: 7px 18px; font-size: 1rem; font-weight: bold; color: #fff; background: #d9480f;
                    border: 1px solid #b53b0c; border-radius: 4px; cursor: pointer;
                    box-shadow: 0 2px 4px rgba(0, 0, 0, .25); }
.harness-bug-open::before { content: "🐞 "; }
.harness-bug-open:hover { background: #b53b0c; }
.harness-bug-open:focus-visible { outline: 2px solid #f08c4a; outline-offset: 2px; }
/* "Chuyển sang Improvement": cùng cỡ nổi bật (cũng là việc kế tiếp duy nhất của issue), khác màu và biểu
   tượng để không đọc nhầm thành nút báo lỗi. */
.harness-toi-uu-open { padding: 7px 18px; font-size: 1rem; font-weight: bold; color: #fff; background: #2b8a3e;
                       border: 1px solid #237032; border-radius: 4px; cursor: pointer;
                       box-shadow: 0 2px 4px rgba(0, 0, 0, .25); }
.harness-toi-uu-open::before { content: "⚡ "; }
.harness-toi-uu-open:hover { background: #237032; }
.harness-toi-uu-open:focus-visible { outline: 2px solid #69db7c; outline-offset: 2px; }

/* Popup chọn chế độ gate. `<dialog>` của trình duyệt không mang theo giao diện Redmine nào, nên
   mượn lại đúng những thứ Redmine đã có: `h3.title`, `.box`, `p.buttons`, `table.list`, và các
   biến màu `--oc-*`. Ở đây chỉ còn phần khung mà không class nào của Redmine phủ. Không gõ mã màu
   trần: theme tối hay theme riêng của công ty đổi biến, còn mã trần thì không đổi theo. */
.harness-runner-dialog {
  padding: 0 10px 10px;
  border: 1px solid var(--oc-gray-3);
  border-radius: 5px;
  background: var(--oc-white);
  color: var(--oc-gray-7);
  /* BỀ NGANG CỐ ĐỊNH, không để `<dialog>` co theo nội dung (chủ dự án 2026-09-23: "popup UX và
     layout này lệch lạc quá"). Bản trước chỉ chặn hai đầu `34em … 90vw`, mà `<dialog>` là
     shrink-to-fit: bảng `table.list` bên trong xin 100% nên popup "Bắt đầu test" nở đúng tới trần
     90vw — đo được 1333px — trong khi ba ô nhập bên dưới đứng yên ở 320/420px. Hai khối cùng một
     form mà một cái rộng gấp ba cái kia là chỗ lệch người ta nhìn thấy ngay.

     48em ≈ 768px: đủ cho bảng ba cột của popup "Bật runner", và đủ chỗ cho cột nhãn 180px của
     `.tabular` mà ô nhập vẫn còn hơn 500px. */
  inline-size: min(48em, 92vw);
}
.harness-runner-dialog::backdrop { background: rgba(0, 0, 0, .35); }
.harness-runner-dialog .box { margin-block-end: 0; }
.harness-runner-dialog h3.title { margin-block: 10px 6px; }
.harness-runner-dialog input[type='radio'] + label { margin-inline-start: 4px; }
/* Nhãn chế độ dài (Auto) xuống dòng thì dòng hai thụt theo chữ, không chạy về dưới nút radio. */
.harness-runner-dialog .box > input[type='radio'] + label {
  display: inline-block; vertical-align: top; max-inline-size: calc(100% - 3em);
}
.harness-runner-dialog .buttons { margin-block: 10px 0; text-align: end; }
/* MỘT MÉP PHẢI cho mọi ô nhập, và nó trùng mép phải của bảng phía trên. `.tabular p` của lõi đã
   chừa sẵn 180px bên trái cho nhãn, nên `100%` ở đây là "hết phần còn lại" — ba ô cao thấp khác
   nhau nhưng thẳng hàng hai bên.

   Bản trước cho `.env` rộng 420px còn ba ô kia 320px và ghi là "cố ý lệch hàng" để token dài không
   bị wrap. Lý do đó mất hiệu lực từ 2026-09-23: khung đã cố định 48em nên ô nào cũng hơn 500px,
   rộng hơn cả 420px cũ. Con số cứng chỉ còn để lại đúng cái lệch chủ dự án chỉ ra.

   `max-inline-size` phải khai lại: lõi cắt trần `input`/`select` ở 95%, nên `100%` không có nó thì
   hai ô ra 95% còn `textarea` ra 100% — lại lệch, chỉ là lệch ít hơn.

   `[type=…]` kê riêng, không dùng `.tabular input` trần: trong popup "Bật runner" `input` còn là
   hai radio chọn chế độ gate, và một radio rộng hết hàng là một dấu chấm to. Cái giá của nó là mỗi
   kiểu ô mới thêm vào popup phải kê thêm một dòng ở đây. */
.harness-runner-dialog .tabular input[type='text'],
.harness-runner-dialog .tabular input[type='url'],
.harness-runner-dialog .tabular select,
.harness-runner-dialog .tabular textarea {
  inline-size: 100%;
  max-inline-size: 100%;
  box-sizing: border-box;
}
.harness-runner-dialog .tabular textarea { font-family: monospace; }
.harness-gate-list { inline-size: 100%; margin-block-start: 6px; }
/* Tiêu đề căn theo nội dung. `table.list th` của lõi căn giữa, mà mọi ô dưới nó căn trái — cùng
   chỗ lệch đã sửa cho bảng chi tiết testcase hôm 2026-09-22. */
.harness-gate-list th { text-align: start; }
/* Cột nhãn của bảng khớp cột nhãn của `.tabular` bên dưới, để ô nhập của hai khối bắt đầu ở CÙNG
   MỘT mép trái — bảng "ai làm vai gì" và ba ô "test ở đâu" đọc ra thành một biểu mẫu, không phải
   hai thứ dán cạnh nhau.

   166 không phải con số đẹp, nó là phép trừ: lõi chừa 180px cho nhãn (`.tabular p`
   `padding-inline: 180px 0`), còn ô bảng ăn thêm 10px padding phải + 2px padding trái của ô kế +
   2px viền. Nhãn nào dài hơn 166px thì bảng tự nới và mép lại lệch — `inline-size` trên `td` chỉ
   là gợi ý; ba nhãn hiện tại ở cả ba thứ tiếng đều ngắn hơn. */
.harness-gate-list td.name { text-align: start; font-weight: bold; inline-size: 166px; }
/* … và mép PHẢI cũng thế: `table.list td` của lõi chừa 10px cuối ô, nên ô nhập trong bảng dừng
   sớm hơn ba ô dưới đúng 10px. Bỏ ở ô cuối là hai khối khép cùng một đường. */
.harness-gate-list td:last-child { padding-inline-end: 0; }
.harness-gate-list input { inline-size: 100%; box-sizing: border-box; }
/* Chọn nhiều người duyệt (2026-10-08): mỗi người một dòng ô + nút ×, nút + ở dưới để thêm. */
.harness-nhieu-nguoi { text-align: start; }
.harness-nguoi-dong { display: flex; align-items: center; gap: 4px; margin-block-end: 4px; }
.harness-nguoi-dong input, .harness-nguoi-dong select { flex: 1 1 auto; min-inline-size: 0; }
.harness-nguoi-bo, .harness-nguoi-them { inline-size: auto !important; padding: 0 8px; line-height: 1.6; cursor: pointer; }
.harness-duyet-nhieu .da-duyet { color: #2a7a2a; }
.harness-duyet-nhieu .chua-duyet { color: #a15c00; }

/* Chọn AUTO thì không chỉ định ai, nên cả khối chọn người biến mất. MỘT dòng CSS thay cho một nắm
   JS, và nó vẫn đúng khi JS hỏng hoặc bị CSP chặn. `~` đòi radio và khối là anh em ruột — test
   `test_radio_auto_la_anh_em_ruot_cua_khoi_chon_nguoi` ghim đúng chuyện đó.

   Đổi từ `gate_mode=bypass` sang `che_do=auto` (2026-09-23): popup bật runner — nơi có radio
   bypass — đã gỡ, việc chọn chế độ dời vào hai popup bắt đầu. */
.harness-runner-dialog input[name='che_do'][value='auto']:checked ~ .harness-gate-chon {
  display: none;
}

/* Công tắc "Runner on" chỉ dành cho manual (2026-10-09). Nó đứng sau hàng ngôn ngữ log, không còn là
   anh em đứng sau radio, nên dấu ngã không với tới — dùng `:has` trên khung chung. */
.harness-runner-dialog .box:has(input[name='che_do'][value='auto']:checked) .harness-runner-switch {
  display: none;
}

/* Chặng test bỏ tick (2026-10-15): chỉ còn dòng tiêu đề có ô tick, hai ô URL/.env biến mất. Cùng
   lối `:has` ở trên — không JS. */
.harness-chang-khoi:not(:has(> legend input[type='checkbox']:checked)) > p { display: none; }
/* Tên chặng nằm trong `<legend>`, mà `.tabular label` của lõi kéo mọi nhãn sang trái 180px vào cột
   nhãn của `.tabular p`. `<legend>` không có cột đó, nên tên chặng lòi ra ngoài mép popup và bị cắt
   (chủ dự án 2026-10-16). Trả nhãn về dòng thường, cạnh ô tick.
   Khung không padding ngang, lề -1px: ô nhập bên trong thẳng mép với bảng người phía trên (202–672 trên
   khung 48em) thay vì thụt 9px vì viền + padding. Chặng bỏ tick bỏ luôn viền: chỉ còn dòng ô tick,
   không còn một khung rỗng. */
.harness-chang-khoi { margin: 8px -1px 0; padding: 2px 0 4px; min-inline-size: 0; }
.harness-chang-khoi > legend { margin-inline-start: 6px; padding: 0 4px; }
.harness-chang-khoi > legend label {
  float: none; display: inline; inline-size: auto; margin: 0 0 0 4px; text-align: start; cursor: pointer;
}
.harness-chang-khoi:not(:has(> legend input[type='checkbox']:checked)) { border-color: transparent; padding-block: 0; }

/* Bảng chặng ở tab Cài đặt (2026-10-15): mỗi chặng một dòng. `table.list td` của lõi căn giữa — ở đây
   căn trái như mọi ô nhập liệu. Ô tick đổi dòng bằng `:has`: bỏ tick chặng đang bật thì URL gạch ngang
   (lưu là xoá), tick chặng đang tắt thì chữ "Đang tắt" nhường chỗ cho hai ô nhập. */
.harness-chang-bang { inline-size: 100%; margin-block: 6px; }
table.list.harness-chang-bang td { text-align: start; vertical-align: top; padding: 6px 8px; }
table.list.harness-chang-bang td.name { inline-size: 140px; white-space: nowrap; font-weight: bold; }
.harness-chang-bang td.name label { margin-inline-start: 4px; cursor: pointer; }
.harness-chang-url { word-break: break-all; }
.harness-chang-nhap label { display: block; margin-block: 4px 2px; font-weight: normal; }
.harness-chang-nhap label:first-child { margin-block-start: 0; }
.harness-chang-nhap input[type='url'],
.harness-chang-nhap textarea { inline-size: 100%; box-sizing: border-box; }
.harness-chang-nhap textarea { font-family: monospace; }
.harness-chang-dong:not(:has(input[type='checkbox']:checked)) .harness-chang-nhap,
.harness-chang-dong:has(input[type='checkbox']:checked) .harness-chang-tat { display: none; }
.harness-chang-dong:not(:has(input[type='checkbox']:checked)) .harness-chang-url {
  text-decoration: line-through;
  color: #999;
}
.harness-test-chang .buttons { margin-block: 8px 0; }

/* Hàng "Ngôn ngữ log của agent": chừa lề cuối cho nhãn để hai lựa chọn không dính vào nhau. Luật
   `input[type='radio'] + label` của popup chỉ chừa lề đầu; hàng này còn dùng ở tab Cài đặt, nơi
   không có luật popup nào. */
.harness-log-lang label { margin-inline: 4px 14px; }

/* Thanh tab của `/harness/workflow`. Không dùng `render_tabs` của lõi nên cũng không dùng CSS của
   nó — ba tab này là LINK, và tab đang mở đã in bằng <strong> để đọc được không cần màu. */
.harness-tabs { list-style: none; margin: 0 0 12px; padding: 0 0 5px; border-bottom: 1px solid #ddd; }
.harness-tabs li { display: inline-block; margin-right: 16px; }

.harness-pick-project { margin: 0 0 10px; }
.harness-pick-project label { margin-right: 6px; }
/* Hai ô chọn (dự án, kỳ) chung một hàng: khoảng cách sau mỗi ô để nhãn "Sprint:" không dính vào ô dự án. */
.harness-pick-project select { margin-right: 12px; }

/* Dòng tiêu đề kỳ của tab Sprint (spec 2026-09-24 §3.1 S0): tên kỳ bên trái, giờ lấy số liệu bên
   phải; màn hẹp thì xuống dòng chứ không đẩy trang cuộn ngang. */
.harness-ky { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px;
              margin: 0 0 10px; color: var(--oc-gray-7, #495057); }

/* Năm thẻ số (§7). `auto-fit` + `minmax(190px, 1fr)`: 1280px ra năm thẻ một hàng, 768px ra 3 + 2,
   dưới 480px một cột — không cần media query nào. Khung KHÔNG tô nền theo trạng thái: màu chỉ nằm
   ở dòng so sánh và luôn đi kèm mũi tên trong chữ. Nhãn không `uppercase`: chữ Nhật không có hoa. */
.harness-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
                gap: 12px; margin: 0 0 4px; }
.harness-kpi { border: 1px solid var(--oc-gray-3, #dee2e6); border-radius: 6px; padding: 10px 12px;
               background: var(--oc-white, #fff); }
.harness-kpi p { margin: 0; }
.harness-kpi-nhan { font-size: 12px; color: var(--oc-gray-7, #495057); }
.harness-kpi .harness-kpi-so { margin: 2px 0; font-size: 24px; font-weight: 700;
                               font-variant-numeric: tabular-nums; color: var(--oc-gray-9, #212529); }
.harness-kpi-phu, .harness-kpi-so-sanh { font-size: 12px; color: var(--oc-gray-7, #495057); }
.harness-kpi-phu.harness-muted { color: var(--oc-gray-6, #868e96); }
.harness-kpi .harness-kpi-so-sanh { margin-top: 4px; }
/* Tốt / xấu / trung tính theo CỰC TÍNH của chỉ số (`DashboardQuery::CUC_TINH`), không theo chiều mũi
   tên: Lead time giảm là tốt, Hoàn tất giảm là xấu, Phiên luôn trung tính. Đứng SAU
   `.harness-kpi-phu` để thắng nó ở cùng độ đặc hiệu. */
.harness-tot   { color: var(--oc-green-8, #2f9e44); }
.harness-xau   { color: var(--oc-red-8, #e03131); }
.harness-trung { color: var(--oc-gray-7, #495057); }

/* Bảng Cần xử lý và Đã xong: cột Issue dùng đúng mẹo `max-width: 0` của bảng tab Issue (ghi chú dài
   ở `.harness-issues td:first-child`) — tiêu đề thật dài 120+ ký tự phải bị cắt, không kéo bảng ra
   ngoài màn hình. */
.harness-can td:first-child, .harness-xong td:first-child { max-width: 0; width: 40%; }
/* Bảng rộng cuộn TRONG khung của nó ở tablet; trang thì không cuộn ngang (spec §7). */
.harness-cuon { overflow-x: auto; }
/* Hàng kỳ đang chọn: đậm CỘNG dấu tam giác trong chữ — không chỉ bằng màu. */
.harness-theo-ky tr.harness-dang-chon td { font-weight: bold; }
.harness-theo-ky progress { inline-size: 8em; vertical-align: middle; }

/* Tiêu đề issue thật dài 120+ ký tự (Nhật + Việt ngăn bằng `|`) và nó kéo bảng ra ngoài màn hình —
   cùng bài học với cột log của bảng runner, khác cách xử lý: bảng kia giấu HẲN tiêu đề vào
   `title=`, còn đây là một trang DANH SÁCH nên tiêu đề phải đọc được. Cắt bằng ellipsis, bản đầy
   đủ vẫn ở `title=`.

   FLEX chứ không `inline-block` + `max-width`: bản đầu dùng `max-width: 34em` và nó RỚT DÒNG ngay
   khi cột hẹp hơn 34em — một `inline-block` không co xuống dưới bề rộng đó, nên trình duyệt đẩy nó
   xuống dòng dưới thay vì cắt. Mã issue nằm một dòng, tiêu đề một dòng, mỗi hàng cao gấp đôi. Đo
   bằng mắt trên rhreal 2026-09-16.

   `min-width: 0` là mấu chốt và trông như thừa: mặc định `min-width` của một flex item là
   `auto` = bề rộng nội dung, nên `overflow: hidden` không bao giờ có hiệu lực và nó lại đẩy cột
   rộng ra. Đặt về 0 mới cho phép nó co lại rồi cắt.

   Không còn con số `em` nào: ô lấy đúng bề rộng bảng chia cho, mã issue giữ nguyên, tiêu đề co
   theo. Đó là lý do bỏ `max-width` chứ không phải chỉnh nó nhỏ lại — một con số đoán sẵn thì luôn
   sai ở một bề rộng màn hình nào đó.

   VÀ `max-width: 0` trên chính cái Ô, không chỉ trên flex item bên trong: `min-width: 0` mới chỉ
   CHO PHÉP phần tử co, nhưng ô bảng ở `table-layout: auto` vẫn nở theo nội dung max-content và đẩy
   năm cột kia ra khỏi màn hình. `max-width: 0` cộng một `width` phần trăm là công thức quen để một
   ô bảng chịu cắt: nó nhận đúng phần bề rộng được chia, không đòi thêm. Đo bằng mắt 2026-09-16 —
   bản chỉ có flex làm bảng tràn ngang, có thanh cuộn dưới cùng. */
.harness-issues td:first-child { max-width: 0; width: 45%; }
.harness-issues-vande { display: flex; align-items: baseline; gap: 6px; }
.harness-issues-subject {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.harness-issues-loc { margin: 0 0 10px; }
.harness-issues-loc select { margin-right: 12px; }
.harness-issues-loc label { margin-right: 6px; }
/* Runner đang bật là thông tin VẬN HÀNH — nó nói máy đang được phép tự chạy vào ticket của người
   thật. In đậm chứ không chỉ đổi màu, cùng luật với mọi chỗ khác trong file này. */
.harness-issues-runner.on { font-weight: bold; }

/* Trang `/harness/workflow` ẩn header và menu toàn cục của Redmine — chủ dự án chốt 2026-09-16,
   cùng lần bỏ thanh menu dự án: trang này là một bảng điều khiển đọc liên tục, và ba tầng thanh
   điều hướng phía trên chiếm gần một phần ba màn hình trước khi thấy dòng dữ liệu đầu tiên.

   `body.controller-harness_workflows` KHÔNG PHẢI trang trí — ĐỪNG GỠ. `harness.css` được nạp qua
   `view_layouts_base_html_head` (xem `Harness::ViewHook`), tức nó có mặt ở MỌI trang Redmine. Một
   luật `#header { display: none }` không phạm vi sẽ xoá thanh tìm kiếm và ô nhảy dự án khỏi toàn
   bộ hệ thống — mọi trang, mọi người dùng — và triệu chứng không chỉ về plugin này.

   `#top-menu` GIỮ NGUYÊN, cố ý: nó là đường ra khỏi trang (Trang chính, Dự án, Harness workflow,
   Quản trị). Ẩn nốt nó là nhốt người dùng trong một trang không có lối về. Cái mất ở đây chỉ là ô
   tìm kiếm và ô nhảy dự án, cả hai còn nguyên ở mọi trang khác.

   Chỉ áp từ 900px trở lên (chủ dự án 2026-09-24, Q8). Dưới 900px Redmine (`responsive.css`,
   `max-width: 899px`) dời điều hướng vào menu flyout, và nút mở menu đó nằm TRONG `#header`. Ẩn nó
   ở tablet là mất lối điều hướng duy nhất. Neo `body.controller-…` vẫn bắt buộc bên trong
   `@media`: media query chỉ giới hạn bề rộng, không giới hạn trang. */
@media (min-width: 900px) {
  body.controller-harness_workflows #header,
  body.controller-harness_workflows #main-menu { display: none; }
}

/* Nút "Báo lỗi" + hàng nút "Đã sửa xong · TC-n". Popup dùng `.box.tabular` của lõi nên nhãn và cột
   tự theo Redmine; chỉ nới ô chữ cho gần kín cột như ô Mô tả của form New issue — các bước tái hiện
   là nhiều dòng, và một ô hẹp là thứ làm người ta viết "lỗi" rồi bấm gửi. */
.harness-bug form { display: inline-block; margin-inline-end: 6px; }
.harness-bug-dialog form { display: block; }
.harness-bug-dialog .tabular input[type="text"],
.harness-bug-dialog .tabular textarea { width: 95%; box-sizing: border-box; }

/* Ticket đã đóng: băng bước read-only, mờ đi để không trông như đang chạy. CHỈ băng bước, không cả
   panel (2026-09-29): nút Báo lỗi đã lên dòng đầu, và nó là việc CÒN LÀM ĐƯỢC trên ticket đóng —
   làm mờ nó là bảo người ta đó là nút chết. Băng phát triển có độ mờ riêng ở `.harness-phase--xong`. */
.harness-panel--dong > .harness-flow { opacity: .78; }

/* Băng phát triển đã xong, vẽ trên băng test (`_dev_strip`). Cùng độ mờ với ticket đã đóng. Tắt
   vệt viền: ô B12 là "bước hiện tại" của ticket đó, và luật `.job-running .dang` bám cả panel —
   thiếu các dòng này thì một bước đã đóng có vệt chạy như đang chạy mỗi khi job test chạy. Mũi tên vào
   ô đó cũng trôi theo (`harness-edge-flow`), nên trả nó về hình đoạn đã đi qua: phase xong là mọi
   bước đã xong. */
/* Mờ CHỈ băng bước, không cả dòng tên phase: nút Chạy lại nằm trên dòng đó và vẫn bấm được — cùng
   lý lẽ với `.harness-panel--dong` ở trên. */
.harness-phase--xong { margin-bottom: 8px; }
.harness-phase--xong > .harness-flow { opacity: .78; }
.harness-phase-nhan { display: flex; align-items: center; gap: 8px; font-size: .85em; color: #666; margin-bottom: 4px; }
.harness-panel.job-running .harness-phase--xong .harness-cell.dang::after { content: none; }
.harness-panel.job-running .harness-phase--xong .harness-cell.now.dang { border-color: var(--hf-dang); }
.harness-panel.job-running .harness-phase--xong .harness-cell + .harness-cell.dang::before {
  --c: var(--hf-xong); animation: none;
  background-image: linear-gradient(var(--c), var(--c)), linear-gradient(var(--c), var(--c)); }

/* Câu trả lời của nút panel, in ngay trong panel thay vì ở đỉnh trang issue — nơi người vừa bấm
   không còn nhìn. Xem `Harness::VePanel.don_flash`.

   Màu và icon KHÔNG tự vẽ: khối này mang đúng class `flash error|warning|notice` của Redmine nên
   nó LÀ flash của Redmine, chỉ khác chỗ đứng. Bản đầu gõ ba bộ mã màu trần — vừa lệch với flash
   thật ngay trên cùng trang, vừa đứng im khi đổi theme.

   `pre-line`: một vài câu là nhiều lỗi ghép bằng ký tự xuống dòng (ô nhập kết quả ca chạy tay), và
   gộp chúng thành một dòng dài là bắt người đọc tự tách ra. */
.harness-flash { white-space: pre-line; margin-block: 0 8px; }

/* Khối XEM TRƯỚC artifact: danh sách bên trái, nội dung bên phải, một cửa sổ xem đủ.

   Cùng luật với `.harness-runner-dialog` ở trên — mượn `h3.title`, `.box`, `p.buttons` và các biến
   `--oc-*` của Redmine, ở đây chỉ còn phần khung mà không class nào của Redmine phủ. Bản đầu gõ mã
   màu trần (#f0f4f8, #e6f1fb) và trông như một cửa sổ của phần mềm khác dán vào giữa trang.

   80% màn hình, CỐ ĐỊNH cả hai chiều (chủ dự án 2026-09-21). Bản trước cắt trần ở 1100px nên màn
   rộng vẫn còn một tài liệu nằm trong một cột hẹp. Cố định chứ không `max-`: cửa sổ co giãn theo
   độ dài từng artifact thì mỗi lần bấm sang mục khác là cả khung nhảy một cỡ, và mắt phải tìm lại
   chỗ đang đọc. */
.harness-artifact-dialog {
  padding: 0 10px 10px;
  border: 1px solid var(--oc-gray-3);
  border-radius: 5px;
  background: var(--oc-white);
  color: var(--oc-gray-7);
  inline-size: 80vw;
  block-size: 80vh;
}
/* `[open]` là BẮT BUỘC. `display: flex` đặt thẳng lên `<dialog>` sẽ ghi đè `display: none` của
   trình duyệt, và popup hiện thường trực giữa trang — hỏng thấy ngay nhưng rất dễ gõ nhầm.
   Cột dọc để `.harness-artifact-wrap` ăn hết phần giữa, tiêu đề và nút giữ nguyên chỗ. */
.harness-artifact-dialog[open] { display: flex; flex-direction: column; }
.harness-artifact-dialog::backdrop { background: rgba(0, 0, 0, .35); }
.harness-artifact-dialog h3.title { margin-block: 10px 6px; }
.harness-artifact-dialog .buttons { margin-block: 10px 0; text-align: end; }

/* `flex: 1` ăn hết chiều cao còn lại giữa tiêu đề và hàng nút. `min-block-size: 0` KHÔNG phải
   thừa: mặc định một flex item không co nhỏ hơn nội dung của nó, nên thiếu dòng này thì hai cột
   bên trong phình theo tài liệu và `overflow-y` của chúng không bao giờ chạy — cửa sổ tràn ra
   ngoài thay vì cuộn trong. */
.harness-artifact-wrap {
  display: flex; gap: 12px; align-items: stretch;
  flex: 1 1 auto; min-block-size: 0;
}
/* Cột trái mang đúng dáng `#sidebar` của Redmine: nền xám nhạt, chữ nhỏ, vạch ngăn một phía. */
.harness-artifact-list {
  flex: 0 0 200px; margin: 0; padding: 0; list-style: none; overflow-y: auto;
  font-size: 0.8125rem;
  background: var(--oc-gray-0);
  border-inline-end: 1px solid var(--oc-gray-4);
}
.harness-artifact-list li { margin: 0; }
/* Nút trải hết bề ngang cột: một nút vừa khít chữ thì vùng bấm nhảy theo độ dài tên bước, và
   người ta bấm trượt vào khoảng trống bên cạnh. */
.harness-artifact-pick {
  display: block; inline-size: 100%; text-align: start; border: 0; background: none;
  padding: 5px 8px; cursor: pointer; font: inherit; color: var(--oc-blue-8);
}
.harness-artifact-pick:hover { background: var(--oc-gray-2); }
/* Ô chọn version nằm dưới nút bước, hẹp bằng cột: nhãn dài (workstream · trạng thái · giờ) bị cắt bằng
   dấu ba chấm của trình duyệt thay vì đẩy cột trái rộng ra. */
.harness-artifact-ver {
  display: block; box-sizing: border-box; inline-size: calc(100% - 16px); margin: 0 8px 6px;
  font-size: 0.75rem; text-overflow: ellipsis;
}
/* Mục đang xem: một vạch màu bên mép, dấu hiệu Redmine dùng cho mục hiện tại — không phải một
   mảng nền lạ. `--color-current-marker` là biến của lõi. */
.harness-artifact-pick.now {
  background: var(--oc-white); font-weight: bold; color: var(--oc-gray-8);
  box-shadow: inset 3px 0 0 var(--color-current-marker);
}
.harness-artifact-view { flex: 1 1 auto; overflow-y: auto; padding-inline: 4px; }
/* Thống kê một runner (dialog của tab Runner): danh sách số liệu gọn, bảng hẹp, thanh biểu đồ bằng
   khối ký tự — không dùng style nội tuyến vì CSP của Redmine có thể chặn câm. */
/* Khung cuộn ăn hết chiều cao giữa tiêu đề và nút Close của dialog (flex cột), không còn khoảng trống
   dưới nội dung. Thẻ số to ở đầu, rồi lưới hai cột các khối có khung (chủ dự án 2026-10-07: bản đầu là một
   cột dài các tiêu đề + bảng, "lộn xộn"). Dưới 800px lưới về một cột. */
.harness-runner-chi-tiet-view { flex: 1 1 auto; min-block-size: 0; overflow-y: auto; padding-inline: 4px; }
.harness-rt-pham-vi { margin-block: 0 12px; }
.harness-rt-the-hang { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.harness-rt-the {
  display: flex; flex-direction: column; gap: 2px; padding: 10px 12px;
  border: 1px solid var(--oc-gray-3); border-radius: 5px; background: var(--oc-gray-0);
}
.harness-rt-the .nhan { font-size: 0.8em; color: var(--oc-gray-6); }
.harness-rt-the .so { font-size: 1.7em; line-height: 1.2; color: var(--oc-gray-9); }
.harness-rt-the .phu { font-size: 0.8em; color: var(--oc-gray-6); }
.harness-rt-ghi-chu { margin-block: 6px 14px; font-size: 0.8em; }
.harness-rt-luoi { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: start; }
.harness-rt-luoi-duoi { margin-block-start: 12px; }
.harness-rt-khoi { padding: 8px 12px 10px; border: 1px solid var(--oc-gray-3); border-radius: 5px; min-inline-size: 0; }
.harness-rt-khoi h4 { margin: 0 0 6px; padding: 0; border: 0; font-size: 0.95em; color: var(--oc-gray-8); }
.harness-rt-cot { display: flex; flex-direction: column; gap: 12px; min-inline-size: 0; }
.harness-rt-luoi-duoi .harness-rt-khoi-rong { grid-column: 1 / -1; }
.harness-rt-bang { inline-size: 100%; border-collapse: collapse; font-size: 0.9em; }
.harness-rt-bang td { padding: 3px 6px; border-block-end: 1px solid var(--oc-gray-2); vertical-align: middle; }
.harness-rt-bang tr:last-child td { border-block-end: 0; }
.harness-rt-bang td.so { text-align: end; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Bảng job gần đây: bố cục cố định để cột thẳng hàng giữa các dòng; cột Bước ăn phần còn lại và cắt bằng
   dấu ba chấm thay vì đẩy các cột số ra xa nhau. Có hàng tiêu đề vì "thời gian" và "token" đứng cạnh nhau
   đều là số — không có nhãn thì không ai biết cột nào là cột nào. */
.harness-rt-gan-day { table-layout: fixed; }
.harness-rt-gan-day th {
  padding: 3px 6px; text-align: start; font-size: 0.8em; font-weight: normal; color: var(--oc-gray-6);
  border-block-end: 1px solid var(--oc-gray-4);
}
.harness-rt-gan-day th.so { text-align: end; }
.harness-rt-gan-day .job { inline-size: 4.5em; }
.harness-rt-gan-day .issue { inline-size: 6em; }
.harness-rt-gan-day .tt { inline-size: 6.5em; }
.harness-rt-gan-day .gio { inline-size: 6em; }
.harness-rt-gan-day .tok { inline-size: 7.5em; }
.harness-rt-gan-day td.buoc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.harness-rt-gan-day td.gio, .harness-rt-gan-day td.tok { color: var(--oc-gray-6); }
/* Biểu đồ cột (SVG): rộng theo khối, kẹp 520px để chữ trong viewBox không phóng quá cỡ trên màn rộng. */
.harness-rt-do-thi { display: block; inline-size: 100%; max-inline-size: 520px; block-size: auto; margin-inline: auto; }
.harness-rt-do-thi .truc { stroke: var(--oc-gray-4); stroke-width: 1; }
.harness-rt-do-thi .cot rect { fill: var(--oc-blue-6); }
.harness-rt-do-thi .cot:hover rect { fill: var(--oc-blue-8); }
.harness-rt-do-thi text { font-size: 10px; fill: var(--oc-gray-6); }
.harness-rt-do-thi .gia-tri { fill: var(--oc-gray-8); font-weight: bold; }
.harness-rt-do-thi .trong .ngay { fill: var(--oc-gray-5); }
.harness-rt-khoang { margin: 2px 0 0; text-align: center; font-size: 0.8em; }
.harness-rt-ds { margin: 0; padding: 0; list-style: none; }
.harness-rt-ds li { padding: 3px 0; }
.harness-rt-ten { font-family: monospace; }
.harness-rt-st { font-size: 0.85em; }
.harness-rt-st-done { color: var(--oc-green-8, #2f9e44); }
.harness-rt-st-failed, .harness-rt-st-blocked { color: var(--oc-red-8, #e03131); }
.harness-rt-st-running, .harness-rt-st-queued { color: var(--oc-blue-8); }
.harness-rt-st-cancelled { color: var(--oc-gray-6); }
@media (max-width: 800px) {
  .harness-rt-luoi { grid-template-columns: minmax(0, 1fr); }
}
/* Lõi Redmine có `#history p.nodata { display: none; }` (ẩn placeholder "chưa có ghi chú" của tab
   Notes cũ). Tab Harness sống trong CÙNG `#history` vì `issue_history_tabs` là chỗ duy nhất Redmine
   7 cho thêm tab (xem Harness::IssuesHelperPatch) — luật đó vô tình khớp luôn mọi `.nodata` trong
   panel này, nuốt mất cả thông báo "Pick an item..." lẫn thông báo lỗi đọc GitHub. Ghi đè cụ thể
   hơn (id + hai class) để thắng luật của lõi (id + tag + class). */
#history #harness-panel .nodata { display: block; }
.harness-artifact-head {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin-block-end: 8px; padding-block-end: 6px;
  border-block-end: 1px solid var(--oc-gray-4);
}
/* Ảnh trong artifact không được đẩy ngang cột. */
.harness-artifact-body img { max-inline-size: 100%; }

/* Bảng bộ ca của `09-test-cases`. Cuộn NGANG chứ không ép xuống dòng: mười cột trong một khung
   đã hẹp thì mỗi ô còn vài chữ, và bảng thành thứ phải giải mã. Ngược với `.harness-artifact-raw`
   ngay dưới — ở đó dòng dài là văn xuôi nên xuống dòng mới đúng. */
.harness-ca-tieu-de { margin-block: 14px 6px; }
.harness-ca-cuon { overflow-x: auto; }
.harness-ca { font-size: 0.8125rem; }
/* `top`: ô `steps` cao vài dòng, canh giữa làm id và kỳ vọng trôi xuống lưng chừng. */
.harness-ca td { vertical-align: top; }

/* File thô. `pre-wrap` chứ không để cuộn ngang: dòng dài trong một cột hẹp bắt người đọc kéo
   qua kéo lại từng dòng một, và raw thường được mở đúng lúc đang dò một chi tiết. */
.harness-artifact-raw { white-space: pre-wrap; word-break: break-word; margin: 0;
                        font-size: 0.8125rem; }

/* Ô đính kèm file trong hộp gap — thay được cho ô chữ. */
.harness-gap-tep { margin: 6px 0 0; }
.harness-gap-tep label { display: block; font-weight: 600; }

/* Xoá dữ liệu harness của issue (tab Cài đặt, chỉ admin): tách khỏi phần cài đặt bằng một đường kẻ, nút đỏ. */
.harness-xoa-du-lieu { margin-top: 1.5em; padding-top: .5em; border-top: 1px solid #ddd; }
.harness-xoa-nhac { color: #888; }
.harness-xoa-nut { color: #c00; }

/* Tab Giới thiệu của `/harness/workflow`: menu mục bên trái, bài bên phải. Menu bám theo khi cuộn để
   bài dài vẫn chuyển mục được. Dưới 900px (ngưỡng responsive của Redmine) gộp về một cột. */
.harness-hd { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 24px; align-items: start; }
.harness-hd-menu { position: sticky; top: 12px; }
.harness-hd-menu ol { margin: 0; padding: 0 0 0 22px; border-inline-start: 3px solid #e5e5e5; }
.harness-hd-menu li { padding: 5px 4px; line-height: 1.35; }
.harness-hd-menu li.now { background: #eef5fb; border-radius: 4px; }
.harness-hd-menu .harness-bell-tai { margin-top: 14px; padding: 10px 12px; border: 1px solid #d7e6f3;
                                     border-radius: 6px; background: #f6fafd; }
.harness-hd-menu .harness-bell-tai p { margin: 6px 0 0; }
.harness-hd-bai { max-width: 860px; }
.harness-hd-video { margin: 6px 0 0; padding: 0; list-style: none; }
.harness-hd-video li { padding: 2px 0; }
.harness-hd-video-muc { margin: 0 0 18px; padding: 10px 12px; border: 1px solid #d7e6f3; border-radius: 6px;
                        background: #f6fafd; }
.harness-hd-bai h3 { margin-top: 1.6em; padding-bottom: 4px; border-bottom: 1px solid #eee; }
.harness-hd-bai h3:first-child { margin-top: 0; }
.harness-hd-bai .harness-hd-dan { font-size: 1.05em; color: #444; }
.harness-hd-hinh { margin: 14px 0 22px; }
.harness-hd-hinh img { display: block; max-width: 100%; height: auto; border: 1px solid #ddd;
                       border-radius: 4px; box-shadow: 0 1px 4px rgba(0, 0, 0, .08); }
.harness-hd-hinh figcaption { margin-top: 6px; color: #555; }
.harness-hd-hinh figcaption ol { margin: 4px 0 0; padding-left: 22px; }
.harness-hd-so { width: 100%; height: auto; margin: 10px 0 18px; }
.harness-hd-the { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px;
                  margin: 12px 0 18px; }
.harness-hd-the > div { padding: 10px 12px; border: 1px solid #e3e3e3; border-radius: 6px; background: #fafafa; }
.harness-hd-the h4 { margin: 0 0 6px; }
.harness-hd-the p { margin: 0; }
.harness-hd-meo { padding: 10px 14px; border-inline-start: 4px solid #f0ad4e; background: #fff8ec; margin: 14px 0; }
.harness-hd-vidu { padding: 12px 16px; border: 1px solid #e3e3e3; border-radius: 6px; margin: 0 0 16px; }
.harness-hd-vidu h4 { margin: 0 0 8px; }
.harness-hd-vidu dl { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; margin: 0; }
.harness-hd-vidu dt { font-weight: bold; color: #555; }
.harness-hd-vidu dd { margin: 0; }
.harness-hd-chuyen { display: flex; justify-content: space-between; gap: 12px; margin-top: 28px;
                     padding-top: 12px; border-top: 1px solid #eee; }
.harness-hd-chuyen .harness-hd-sau { margin-left: auto; }
@media (max-width: 899px) {
  .harness-hd { grid-template-columns: 1fr; }
  .harness-hd-menu { position: static; }
}
.harness-hd-menu .harness-bell-tai .nodata { margin: 6px 0 0; padding: 6px 8px; font-size: .9em; text-align: start; }
.harness-hd-bai table.list td { text-align: start; padding: 5px 8px; }
.harness-hd-bai table.list td:first-child { white-space: nowrap; font-weight: bold; }
