# sub-frontend-designer-L2 — LĂNG-KÍNH: PANEL DANH SÁCH

> Run: 2026-08-05-S174-khkk-style-audit · Lane L2 (ensemble /fable-clone frontend-designer)
> Nhiệm vụ: quét style/UX **panel danh sách** KHKK (GĐ2) đối chiếu Duyệt NCC / PE (GĐ1, golive = CHUẨN).
> Đầu ra: spec thi-hành-được cho Opus 5 MAX.
> STATUS: KHUNG RỖNG ghi @lượt-1 TRƯỚC KHI ĐỌC FILE — append theo tiến-độ đọc, KHÔNG gom cuối.

---

## 0. Phạm vi + file soi

| # | File | Vai |
|---|---|---|
| A | `fe-admin/src/components/pe/PeListPanel.tsx` | CHUẨN (GĐ1 golive) |
| B | `fe-admin/src/components/khkk/KhkkListPanel.tsx` | Đối chiếu (GĐ2) |
| C | `fe-admin/src/components/khkk/KhkkWorkListPanel.tsx` | Đối chiếu (GĐ2) |

Đã đọc đủ 3 file @lượt-2/3. LOC: A=264 · B=375 · C=305 (`wc -l`).

---

## 1. Inventory cấu trúc 3 file

### A — `PeListPanel.tsx` (CHUẨN, 264 dòng)
Shell `<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">` (:92).
1. **Header filter block** `space-y-2 border-b border-slate-200 p-3` (:94)
   - Hàng nhãn: `text-[11px] font-semibold uppercase tracking-wide text-slate-500` "Danh sách phiếu" + **pill đếm** `rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600` (:96-101)
   - Ô tìm: `Search` icon absolute `left-2.5 top-2.5 h-4 w-4 text-slate-400` + `Input className="pl-8"` (:103-111)
   - Ô lọc: `Select` "Tất cả trạng thái" + map `PeDisplayStatus` (:117-122); nhánh `editableOnly` thay Select bằng **banner lọc-cố-định** `rounded border border-slate-200 bg-slate-50 px-2 py-1.5 text-[11px]` (:112-115)
2. **Body** `flex-1 overflow-y-auto` (:127)
   - loading: 6 skeleton `h-16 animate-pulse rounded-md bg-slate-100` (:128-134) — ⚠️ KHÔNG có `motion-reduce:animate-none`
   - empty: `<EmptyState icon={ClipboardCheck} title description>` bọc `p-6` (:135-147)
   - **KHÔNG có nhánh `isError`** (:127-236 — 0 hit `isError`)
   - list `<ul className="divide-y divide-slate-100">` (:148)
3. **Card phiếu** = `<li className="group relative">` + `<button className="block w-full px-3 py-2.5 pr-9 text-left transition hover:bg-slate-50">` + selected `bg-brand-50 ring-1 ring-inset ring-brand-200` (:150-156). ⚠️ KHÔNG có `focus-visible:*`.
   - hàng 1: chips (`PeUrgentChips` + `PeFinalizeChip`) + tiêu đề `text-[13px] font-medium text-slate-900 truncate` ⟂ badge phải `shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium` (**rounded, KHÔNG border**) (:158-190)
   - hàng 2: `mt-0.5 text-[11px] text-slate-500` — `font-mono` mã · `·` · projectName (:165-169)
   - hàng 2b (điều kiện): NCC trúng `mt-0.5 truncate text-[11px] text-emerald-600` "✓ …" (:173-181)
   - hàng 3: `mt-1 flex items-center justify-between text-[11px]` — chip loại `rounded bg-slate-100 px-1.5 py-0.5 text-slate-600` ⟂ `<SlaTimer>` (:192-197)
   - hàng 4 (điều kiện): `mt-1 text-[10px] text-brand-600` "✓ Đã tạo HĐ" (:198-200)
   - **pencil overlay** `absolute right-2 top-2 rounded p-1.5` 3-state bright/dim/active (:208-232)
4. **Nút đáy** sticky `border-t border-slate-200 bg-white p-2` + `<Button className="w-full justify-center gap-1.5 text-xs">` + `Plus h-3.5 w-3.5` "Thêm mới" (:239-248)
5. **Phân trang: KHÔNG CÓ** — `pageSize: 50` cứng (:76), 0 hit `page`.

### B — `KhkkListPanel.tsx` (375 dòng)
Shell `<aside>` **giống hệt A** (:254).
1. Header filter `space-y-2 border-b border-slate-200 p-3` (:257) — ⚠️ **KHÔNG có hàng nhãn + pill đếm** (A:96-101 vắng mặt)
   - Ô tìm: **giống hệt A** (:258-266) — icon/`pl-8`/`Search` khớp
   - Ô lọc: `Select` + `aria-label="Lọc trạng thái"` (A KHÔNG có aria-label) (:267-276)
   - 2 dòng ghi-chú `text-[10px] italic text-slate-400` (flatMode + !canPe) (:277-287)
2. Body `flex-1 overflow-y-auto` (:290) — **2 chế độ**: `PipelineTreePanel` (cây, :291-298) ⟂ danh-sách phẳng (:300-344)
   - loading 8 skeleton **có** `motion-reduce:animate-none` (:300-306)
   - **có nhánh `isError` + nút "Thử lại"** (:307-318) — A KHÔNG có
   - empty: **KHÔNG dùng `EmptyState`** mà tự vẽ `icon-chip` inline + text (:319-330)
3. Card `KhkkPlanCard` (:98-163) — export riêng, 3 hàng (chi tiết §2)
4. **Nút đáy: KHÔNG CÓ**
5. **Phân trang: CÓ** — `border-t px-3 py-2 text-[11px]` + "N kế hoạch — Trang x / y" + 2 nút `←`/`→` (:347-372), `PAGE_SIZE = 20` (:42)

### C — `KhkkWorkListPanel.tsx` (305 dòng)
Shell `<aside className="flex min-h-0 flex-col overflow-hidden border-r border-slate-200 bg-white">` (:142) — thêm `min-h-0`.
- Bố cục **2 TẦNG 50/50** qua `<Tier>` (:48-75): header tầng `border-b bg-slate-50/80 px-3 py-2` + icon 5×5 + title `text-[11px] font-bold uppercase tracking-wide text-slate-600` + pill đếm `rounded-full bg-slate-100 px-2 py-0.5 text-[10px]`
- **KHÔNG có ô tìm, KHÔNG có ô lọc, KHÔNG phân trang, KHÔNG nút đáy** (cố ý, :298-302)
- Card tầng-trên (PE): `px-3 py-2` · mã `font-mono text-[10px] text-brand-700` · tên `text-[12px] font-medium text-slate-800` · selected `bg-amber-50 ring-1 ring-inset ring-amber-300` (:189-211)
- Card tầng-dưới (KHKK): `px-3 py-2` · mã `font-mono text-[10px] text-violet-700` · badge `text-[9px]` · tên `text-[12px] font-medium text-slate-800` · selected `bg-violet-50 ring-1 ring-inset ring-violet-300` (:252-282)
- loading = **chữ "Đang tải…"** `p-3 text-[11px] text-slate-500` (:160, :230) — KHÁC skeleton của A/B
- error = "Không tải được danh sách." + "Thử lại" `text-[10px]` (:161-172, :231-242)
- empty = `<p className="p-3 text-[11px] italic text-slate-400">` (:173-177, :243-248) — KHÁC A (`EmptyState`) và KHÁC B (icon-chip)
- gate quyền: `!canKhkk` → khối `Lock` giữa cột (:143-151)

---

## 2. Bảng đối chiếu 10 trục

> 🔴 **Đính chính phạm vi CHUẨN (đo được, quan trọng cho §4):** `PeListPanel.tsx` **KHÔNG phải** panel đang chạy ở màn Duyệt NCC golive. Màn golive là `pages/pe/PurchaseEvaluationsListPage.tsx` — nó **tự vẽ `<aside>` panel-1 inline** (`:399-455`), có CÂY 4 tầng inline (`:472-…`). `PeListPanel.tsx` chỉ được dùng ở màn **Thao tác** (`PurchaseEvaluationWorkspacePage`). Vậy CHUẨN có **2 mặt**: (A1) panel màn-danh-sách = `PurchaseEvaluationsListPage:399-455` · (A2) panel màn-thao-tác = `PeListPanel.tsx`. Đối chiếu đúng cặp: **B ⟂ A1**, **C ⟂ A2**.

| Trục | A1 `PurchaseEvaluationsListPage` (golive) | A2 `PeListPanel` | B `KhkkListPanel` | C `KhkkWorkListPanel` | Verdict |
|---|---|---|---|---|---|
| **Vỏ panel** | `aside flex flex-col overflow-hidden border-r border-slate-200 bg-white` (:399) | y hệt (:92) | y hệt (:254) | + `min-h-0` (:142) | ✅ KHỚP |
| **Hàng nhãn + pill đếm trong panel** | KHÔNG (đếm nằm ở header trang `:386-388`) | CÓ "Danh sách phiếu" + pill (:96-101) | **KHÔNG** — và header trang B để pill = "Giai đoạn 2" (`KhkkListPage:128-130`) **CHỨ KHÔNG PHẢI SỐ ĐẾM** | pill đếm mỗi tầng (:66-70) | 🔴 **DELTA-1**: KHKK là màn DUY NHẤT không cho biết "đang có bao nhiêu phiếu" ở chế-độ cây |
| **Ô tìm** | 1 ô, `Search` `left-2.5 top-2.5 h-4 w-4`, `Input pl-8`, ph. "Tìm mã / tên gói thầu / dự án…" (:401-409) | y hệt (:103-111) | **2 Ô CHỒNG NHAU ở chế-độ cây** — ô host (`:258-266`, `Input` h-8 rounded-lg) + ô ruột cây (`PipelineTreePanel:236-244`, `input` thô h-8 rounded-**md** ph. "Lọc dự án / hạng mục / mã phiếu…") | KHÔNG có (cố ý) | 🔴 **DELTA-2 (nặng nhất)** |
| **Ô lọc trạng thái** | `Select` (:437-453) + 2 banner thay-thế: đỏ "Phiếu đã xóa" (:427-431), hổ phách "Lọc cố định: Đã gửi duyệt" (:432-435) | `Select` (:117-122) + banner xám lọc-cố-định (:112-115) | `Select` + `aria-label` (:267-276). **KHÔNG có banner cho `deletedView`** — vào leaf "Đã xóa" thì `Select` vẫn hiện dù server bỏ qua `phase` (`:209`) | KHÔNG | 🔴 **DELTA-3**: mất banner ngữ-cảnh + `Select` chết-lặng ở view Đã xóa |
| **Nhãn trạng thái (từ vựng)** | "Nháp · Đã gửi duyệt · **Cần chỉnh sửa lại** · Đã duyệt · Từ chối" (`purchaseEvaluation.ts:83-89`) | ↑ | "**Đang soạn thảo** · **Chờ duyệt** · Đã duyệt · **Trả lại** · Từ chối" (`khkk.ts:49-55`) | ↑ | 🟡 **DELTA-4**: 3/5 nhãn khác chữ cho cùng ý |
| **Badge trạng thái (hình)** | `rounded px-1.5 py-0.5 text-[10px] font-medium` + `bg-X-100 text-X-700`, **KHÔNG viền** (`PeListPanel:183-190` + `purchaseEvaluation.ts:91-97`) | ↑ | `rounded-**md** **border** px-1.5 py-0.5 text-[10px] font-medium` + `bg-X-100 text-X-**800** border-X-300` (`:136-143` + `khkk.ts:58-64`) | như B nhưng `text-[9px]` (:270-277) | 🔴 **DELTA-5**: 3 biến-thể badge cho cùng 1 hệ trạng-thái |
| **Card — bố cục hàng** | 4 hàng: tiêu-đề+badge · mã·dự-án · chip-loại+SLA · "✓ Đã tạo HĐ" | ↑ (:158-200) | 3 hàng: tiêu-đề+badge · mã+N?+ngày · người-soạn+tiền (:121-160) | 2 hàng, không badge tầng-trên (:200-210) | 🟡 hợp lý (khác dữ-liệu) |
| **Card — cỡ chữ** | tiêu-đề `text-[13px] font-medium text-slate-900`; phụ `text-[11px] text-slate-500`; mã `font-mono` | ↑ | tiêu-đề `text-[13px]` ✅; phụ `text-[11px]` ✅; mã `font-mono` ✅ | tiêu-đề **`text-[12px] text-slate-800`**; phụ **`text-[10px]`**; mã `font-mono text-[10px]` | 🔴 **DELTA-6**: C nhỏ hơn A2 nguyên 1 nấc (12/10 vs 13/11) |
| **Card — padding + selected** | `px-3 py-2.5` · `bg-brand-50 ring-1 ring-inset ring-brand-200` | ↑ (:154-156) | `px-3 py-2.5` ✅ + `bg-brand-50 ring…brand-200` ✅ + **`focus-visible:ring-2 …brand-500/60`** (A KHÔNG có) (:117-118) | **`px-3 py-2`** + ring **amber-300 / violet-300** (:196-198, :258-261) | 🟡 B tốt hơn A (giữ) · 🔴 **DELTA-7**: C dùng 2 màu chọn khác brand |
| **Chip nhóm N1-N8** | — | — | `rounded border border-violet-200 bg-violet-50 px-1 **text-[9px]** font-semibold text-violet-700`, **không `py`** (:147-151) | y hệt (:265-269) | 🟡 **DELTA-8**: 9px + thiếu `py-0.5` ⇒ lệch baseline với mã `font-mono` bên cạnh; là chữ NHỎ NHẤT toàn màn |
| **Số tiền** | không có trên card | không có | `tabular-nums`, `text-emerald-700` khi `frozen` / `text-brand-800` (:157-159); `formatVnd` = `toLocaleString('vi-VN') + ' đ'` (:44-47) | không có | 🟡 **DELTA-9**: nghĩa của màu (đã chốt/tạm tính) **không có chú giải** ở panel |
| **Khoảng cách khối lọc** | `space-y-2 … p-3` (:400) | y hệt (:94) | y hệt (:257) | không có khối lọc | ✅ KHỚP |
| **Empty-state** | `<EmptyState icon title description>` bọc `p-6` (:465-469) | y hệt (:135-147) | **tự vẽ**: `p-6 text-center text-sm` + `icon-chip` inline style (:319-330) | `<p className="p-3 text-[11px] italic text-slate-400">` (:173-177, :243-248) | 🔴 **DELTA-10**: 3 hình empty khác nhau; chỉ A dùng component chung `EmptyState` |
| **Loading** | 6 skeleton `h-16 animate-pulse rounded-md bg-slate-100`, **thiếu** `motion-reduce` (:458-463) | y hệt, thiếu `motion-reduce` (:128-134) | 8 skeleton **có** `motion-reduce:animate-none` (:300-306) | **chữ "Đang tải…"** `p-3 text-[11px]` (:160, :230) | 🔴 **DELTA-11**: C tụt hạng (chữ thay skeleton) · 🟢 B hơn A về a11y |
| **Error** | **KHÔNG có nhánh `isError`** (0 hit) | **KHÔNG** (0 hit) | CÓ + nút "Thử lại" (:307-318) | CÓ ×2 + "Thử lại" `text-[10px]` (:161-172, :231-242) | 🟢 KHKK hơn — **đừng hạ theo A**; ngược lại nên nâng A |
| **Phân trang** | KHÔNG (pageSize 50 cứng, `:84/:102`) | KHÔNG (pageSize 50, `:76`) | CÓ, chỉ ở `flatMode`: "N kế hoạch — Trang x / y" + `←`/`→` (:347-372), `PAGE_SIZE=20` (:42) | KHÔNG (30 phiếu mới nhất, `:40`) | 🔴 **DELTA-12**: nút `←`/`→` là **ký tự trần**, không phải `lucide` icon (FD3) + không `aria-label` |
| **Nút đáy** | KHÔNG (nút "Lập kế hoạch"/"Tạo" nằm ở header trang) | CÓ sticky `Button w-full` + `Plus` (:239-248) | KHÔNG — nút "Lập kế hoạch" ở header trang (`KhkkListPage:133-136`) | KHÔNG (cố ý, có lý-lẽ ghi tại :298-302) | ✅ hợp lý |
| **Trạng thái thiếu-quyền** | — | — | dòng chữ `text-[10px] italic slate-400` (:282-287) | khối `Lock` giữa cột (:143-151) | 🟡 **DELTA-13**: 2 hình cho cùng 1 việc, bản B mờ hơn ghi-chú thường |

---

## 3. Delta chi tiết — xếp theo mức đau (mỗi mục: bằng-chứng → sửa gì)

**MIRROR đã kiểm:** `sha256sum` 2 app → `KhkkListPanel` `ad9073db…314d` KHỚP · `KhkkWorkListPanel` `fcb97650…0e26` KHỚP ⇒ **mọi sửa phải áp 2 app** (`fe-admin` + `fe-user`), giữ SHA cặp.

### 🔴 DELTA-2 — HAI Ô TÌM CHỒNG NHAU (nặng nhất, thấy ngay bằng mắt)
Ở chế-độ cây (`flatMode === false`), panel render:
- ô 1 = host `KhkkListPanel.tsx:260-265` — `Input` (h-8 **rounded-lg** border-slate-**300** text-**sm**), ph. "Tìm mã kế hoạch / mã phiếu / gói thầu…"
- ô 2 = ruột cây `PipelineTreePanel.tsx:235-244` (icon `:236`, `<input>` `:237-243`) — `<input>` thô (h-8 **rounded-md** border-slate-**200** text-**[12px]**), ph. "Lọc dự án / hạng mục / mã phiếu…", cách ô 1 đúng 1 `p-2`+`p-3` ≈ 20px.
Hai ô **trái nghĩa nhau**: gõ ô 1 ⇒ `flatMode` bật (`:201`) ⇒ **cây + ô 2 biến mất giữa lúc đang gõ**; gõ ô 2 ⇒ lọc trong cây (`PipelineTreePanel:145-153`), ép mở hết node (`:180`).
→ **Sửa:** 1 ô DUY NHẤT. Nâng `PipelineTreePanel` nhận `searchTerm`/`onSearchTermChange` (controlled) + ẩn ô ruột khi `hideHeader` (cùng lý-lẽ đã dùng để ẩn header ở `:225-232`). Host giữ 1 `Input`.

### 🔴 DELTA-5 — 3 biến-thể badge cho cùng 1 hệ trạng-thái
`purchaseEvaluation.ts:91-97` (`rounded`, không viền, `text-X-700`) ⟂ `khkk.ts:58-64` (`rounded-md`+`border`+`text-X-800`) ⟂ `KhkkWorkListPanel.tsx:270-277` (`text-[9px]`).
→ **Sửa:** chọn 1 khuôn. Khuyến nghị **giữ khuôn KHKK có viền** (viền giúp badge không chìm khi nền card đổi sang `bg-brand-50` lúc chọn — PE đang chìm thật) và **nâng PE lên khuôn đó**, KHÔNG hạ KHKK. Nếu owner muốn "khác gì cũng phải giống golive" thì đổi ngược, nhưng phải đổi ở `khkk.ts` (1 chỗ) chứ đừng vá tại card.
→ Bắt buộc: `KhkkWorkListPanel:272` `text-[9px]` → `text-[10px]` (bằng B) — không có lý do để cùng badge nhỏ hơn ở màn khác.

### 🔴 DELTA-6 — card màn Thao tác nhỏ hơn 1 nấc so với chuẩn A2
`KhkkWorkListPanel:205` tiêu-đề `text-[12px] text-slate-800` + `:201-203` phụ `text-[10px]` ⟂ chuẩn `PeListPanel:160` `text-[13px] text-slate-900` + `:165` `text-[11px]`. Padding `py-2` (`:196`) ⟂ `py-2.5` (`PeListPanel:154`).
→ **Sửa:** đưa C về `text-[13px]/text-slate-900` + `text-[11px]` + `py-2.5`. Cột C rộng 300-340px (`KhkkCreatePage:180`) — thừa chỗ, không phải ép nhỏ vì hẹp.

### 🔴 DELTA-10 — 3 hình empty-state
A dùng `<EmptyState>` (`EmptyState.tsx` — icon trong `rounded-xl bg-slate-100 p-2.5`, title `text-[13px] font-semibold slate-700`, desc `text-xs slate-500`) · B tự vẽ `icon-chip` + `text-sm` (`KhkkListPanel:319-330`) · C chỉ 1 dòng `italic slate-400` (`:173-177`, `:243-248`).
→ **Sửa:** cả B và C dùng `<EmptyState>`. B: `icon={Inbox}` + title "Không có kế hoạch khớp bộ lọc" + description nói **cách thoát** ("Xoá từ khoá hoặc chọn Tất cả trạng thái"). C: title ngắn + description giữ nguyên câu giải-thích đang có (câu đó tốt, chỉ sai vỏ).

### 🔴 DELTA-11 — loading màn Thao tác tụt hạng
`KhkkWorkListPanel:160`/`:230` = chữ "Đang tải…" ⟂ chuẩn skeleton `h-16 animate-pulse` (`PeListPanel:128-134`).
→ **Sửa:** skeleton 4-6 dòng, **kèm `motion-reduce:animate-none`** (KHKK list đã làm đúng ở `:303`, PE golive đang THIẾU — xem DELTA-14).

### 🔴 DELTA-12 — nút phân trang là ký tự trần
`KhkkListPanel:360`/`:368` in `←` `→` bằng text, không `aria-label`, không icon set. Trái FD3 (icon set nhất quán = lucide) + FD5 (nút không có tên đọc được).
→ **Sửa:** `<ChevronLeft className="h-3.5 w-3.5" />` / `<ChevronRight …>` + `aria-label="Trang trước"` / `"Trang sau"`; giữ `disabled:opacity-40` (đã có).

### 🔴 DELTA-15 (MỚI, FD5 — ĐO ĐƯỢC) — chữ `slate-400` cho thông tin BẮT BUỘC ĐỌC
Tương-phản `#94a3b8` trên nền trắng = **2.56:1** (tự tính theo WCAG relative-luminance) ⇒ **trượt AA 4.5:1, trượt cả ngưỡng 3:1**. Đang dùng cho đúng những câu người dùng CẦN đọc nhất:
- `KhkkListPanel:278` "Đang lọc — danh sách lấy thẳng từ máy chủ…" (`text-[10px] italic text-slate-400`)
- `KhkkListPanel:283` "Bạn chưa có quyền xem phiếu Duyệt NCC…"
- `KhkkListPanel:230` "Gói này chưa có kế hoạch ký kết."
- `KhkkWorkListPanel:174`, `:244`, `:290`
- (systemic, KHÔNG phải lỗi riêng KHKK) `PipelineTreePanel:278` "Cây tải 200 phiếu mới nhất…"
→ **Sửa:** `text-slate-400` → `text-slate-500` (`#64748b` = **4.76:1**, PASS AA) cho MỌI dòng chữ mang thông tin; giữ `slate-400` chỉ cho icon trang trí. Và nâng `text-[10px]` → `text-[11px]` cho 2 dòng ghi-chú của B (chúng đang là chữ nhỏ nhất + mờ nhất trong khi giải thích hành-vi lạ nhất của màn).

### 🟡 DELTA-1 — mất số đếm
`KhkkListPage:128-130` pill header = "Giai đoạn 2 · <tên nhóm>" ⟂ golive `PurchaseEvaluationsListPage:386-388` pill = **số phiếu**. Ở chế-độ cây, B **không nơi nào** cho biết đang có bao nhiêu kế hoạch (pill của cây bị `hideHeader` ẩn, `PipelineTreePanel:210-219`).
→ **Sửa:** thêm pill số vào header trang B, đứng CẠNH pill "Giai đoạn 2" (2 pill khác vai, đừng gộp): flat = `list.data.total`; tree = tổng `stage2Count`.

### 🟡 DELTA-3 — mất banner ngữ-cảnh ở view "Đã xóa"
Golive có 2 banner thay chỗ `Select` (`:427-435`: đỏ cho Đã-xóa, hổ-phách cho Chờ-tôi). B vào `?view=deleted` vẫn hiện `Select` trạng-thái dù server **bỏ qua** `phase` (`KhkkListPanel:209` `phase: deletedView ? undefined : …`) ⇒ ô lọc **chết câm**: đổi giá trị, danh sách không đổi.
→ **Sửa:** `deletedView` ⇒ thay `Select` bằng banner đỏ đúng khuôn golive: `rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[11px] text-red-700` + câu "🗑 Phiếu đã xóa — chỉ xem."

### 🟡 DELTA-4 — từ vựng trạng thái lệch
`khkk.ts:49-55` "Đang soạn thảo/Chờ duyệt/Trả lại" ⟂ `purchaseEvaluation.ts:83-89` "Nháp/Đã gửi duyệt/Cần chỉnh sửa lại".
→ **Sửa (đề-xuất, cần owner gật vì là chữ hiển thị):** thống nhất theo golive — `1:'Nháp'`, `2:'Đã gửi duyệt'`, `98:'Cần chỉnh sửa lại'`. 1 chỗ sửa, 0 rủi ro máy (label thuần hiển thị). Nếu owner muốn giữ chữ KHKK thì đảo lại — nhưng ĐỪNG để 2 bộ chữ song song, cùng 1 người dùng đi qua GĐ1→GĐ2 trong 1 phút.

### 🟡 DELTA-7 — màu "đang chọn" của C
`KhkkWorkListPanel:197` `ring-amber-300` + `:260` `ring-violet-300` ⟂ chuẩn `ring-brand-200`+`bg-brand-50`.
→ **Sửa:** giữ màu accent cho **đầu tầng** (`Tier` icon `:60`, đang đúng) nhưng trạng-thái CHỌN của mọi card về `bg-brand-50 ring-brand-200` — "đang chọn" là 1 khái niệm, không nên đổi màu theo tầng, người dùng phải học 2 lần.

### 🟡 DELTA-8 — chip nhóm N1-N8
`KhkkListPanel:147-151` `text-[9px] font-semibold px-1` **không có `py`** ⇒ hộp chip cao hơn chữ, lệch baseline với `font-mono` bên cạnh; 9px là cỡ chữ nhỏ nhất toàn bộ 3 file.
→ **Sửa:** `text-[10px] px-1.5 py-0.5 leading-none` + `title={khkkGroupMenuLabel(n)}` (hàm đã tồn tại, `KhkkListPage:129` đang dùng) — "N5" một mình không nói được gì.

### 🟡 DELTA-9 — số tiền đổi màu không chú giải
`KhkkListPanel:157-159`: `frozen` (phase = DaDuyet) → `text-emerald-700`, còn lại `text-brand-800`. Người dùng không có cách nào biết 2 màu = "đã chốt" ⟂ "tạm tính".
→ **Sửa:** `title={frozen ? 'Giá trị đã duyệt (đã chốt)' : 'Giá trị đề xuất (tạm tính)'}` + thêm 1 ký-tự phân biệt (vd `≈` trước số khi chưa chốt). Rẻ, không tốn chỗ.

### 🟡 DELTA-13 — 2 hình cho "thiếu quyền"
B = dòng chữ mờ (`:282-287`) · C = khối `Lock` căn giữa (`:143-151`). C đúng hơn (cột trắng phải nói ra lý do).
→ **Sửa:** B dùng lại đúng khuôn C (icon `Lock` + `text-[11px] text-slate-500`), đặt ngay trên chỗ đáng lẽ là cây.

### 🟢 DELTA-14 (nâng CHUẨN, không hạ KHKK) — 2 chỗ KHKK làm ĐÚNG hơn golive
1. **Nhánh `isError` + "Thử lại"**: B `:307-318`, C `:161-172`/`:231-242` — golive `PurchaseEvaluationsListPage` và `PeListPanel` **0 hit `isError`** ⇒ mất mạng = màn trắng im lặng, không phân biệt được với "chưa có phiếu".
2. **`motion-reduce:animate-none`** trên skeleton: B `:303` có; A1 `:461` + A2 `:131` **không** (FD5).
→ **Sửa:** port 2 thứ này **NGƯỢC** sang PE. Đây là chỗ duy nhất trong bài mà "đồng bộ" nghĩa là sửa bên golive.

### ✅ Đã KHỚP — không đụng
Vỏ `<aside>` · lưới host 3-panel `lg:grid-cols-[400px_1fr_360px]` (`KhkkListPage:140` = `PurchaseEvaluationsListPage:397`) · khối lọc `space-y-2 border-b p-3` · vị trí + kích thước icon `Search` · `Input`/`Select` cùng primitive (`h-8 rounded-lg`) · card `px-3 py-2.5` + selected `bg-brand-50 ring-brand-200` (B) · `divide-y divide-slate-100` · header trang (icon 5×5 + `h1 text-base font-semibold` + pill).
🟢 B còn **hơn** A ở `focus-visible:ring-2 ring-inset ring-brand-500/60` trên card (`:117`) — A **không có** focus ring ⇒ **giữ B, port sang A** (FD5).

---

## 4. Câu hỏi owner O5 — `flatMode` (KhkkListPanel:202)
Nghi vấn: filter bật ⇒ panel đổi CÂY → DANH-SÁCH-PHẲNG ⇒ 3 leaf (Đang duyệt / Đã duyệt / Đã xóa) trông khác leaf "Danh sách".

### 4.1 XÁC NHẬN nghi vấn — 4/4 mắt xích, đo được

| Leaf menu | URL (`Layout.tsx`) | Suy ra | `flatMode` (`KhkkListPanel:201`) | HÌNH thấy |
|---|---|---|---|---|
| Danh sách (`Khkk_List`) | `:41` `/khkk/list?group=1` | phase=null, deleted=false | **false** | 🌳 CÂY |
| Đang duyệt (`Khkk_Pending`) | `:43` `…&filter=ChoDuyet` | `FILTER_TO_PHASE` (`KhkkListPage:46-52`) → phase=2 (`:61`,`:86`) | **true** | ▤ PHẲNG |
| Đã duyệt (`Khkk_Approved`) | `:44` `…&filter=DaDuyet` | phase=3 | **true** | ▤ PHẲNG |
| Đã xóa (`Khkk_Deleted`) | `:45` `…&view=deleted` | `deletedView=true` (`:62`) | **true** | ▤ PHẲNG |

⇒ **3/4 leaf KHÔNG BAO GIỜ thấy cây, ngay từ lần vẽ đầu**, không cần người dùng gõ gì. Người dùng đọc thành "menu lọc sai" vì thứ họ so sánh là leaf "Danh sách" — thứ DUY NHẤT còn cây.

**Đo thêm — cái GIỐNG và cái KHÁC giữa 2 hình:**
- **Card GIỐNG HỆT**: cả 2 nhánh cùng render `KhkkPlanCard` (tree qua `stage2Content` `:239-244`, flat qua `:335-340`) ⇒ khác biệt **0% ở thẻ phiếu**.
- **Khác 100% ở phần khung**: tree = [ô tìm host][Select][**ô tìm thứ 2**][📅Năm>📁Dự án>🧱Hạng mục>📁GĐ2] · flat = [ô tìm host][Select][ghi-chú italic 10px][card phẳng][thanh phân trang]. ⇒ "trông khác hẳn" **không phải do phiếu**, mà do **giàn giáo + 1 ô tìm thừa + 1 thanh phân trang**.
- Máy lọc thì ĐÚNG: tree mode `enabled: flatMode` = false (`:217`) ⇒ query `khkk-list` **không chạy**, dữ liệu cây đến từ `pipeline-khkk-index` (`usePipelineStages:90-102`, `pageSize=200`, KHÔNG nhận filter).

### 4.2 Đường 1 — GIỮ CÂY KHI LỌC (sửa `flatMode`)

**Được:** 1 hình duy nhất cho cả 4 leaf; giữ ngữ-cảnh "phiếu này thuộc gói/dự án nào" — đúng lý do cây sinh ra; hết cảnh gõ-phím-làm-đổi-màn. Khuôn có sẵn, golive đã chạy: `PurchaseEvaluationsListPage` cho search+phase vào **chính query nuôi cây** (`:75-102`) rồi dựng `yearGroups` từ kết quả ĐÃ LỌC (`:220-262`), + `hideEmpty={filterActive}` (`:58`, `:524`) để không đổ folder rỗng. Cơ chế `hideEmpty` đã tồn tại ở `PipelineStageFolders:314-325`.

**Mất — 3 cái, cái thứ 3 là chí tử:**
1. **Số đếm nói dối.** Lọc client trên cửa-sổ 200 ⇒ leaf "Đã duyệt" hiện 37 trong khi máy chủ có 120. Golive không dính vì cây golive dựng từ *chính* kết quả server đã lọc; cây KHKK thì không.
2. **Mất phân trang thật** (`total`/`totalPages` `:221-222`, `PAGE_SIZE=20`) — cây không có "trang 2".
3. 🔴 **XƯƠNG CÂY KHÔNG PHẢI CỦA KHKK.** Năm>Dự án>Hạng mục dựng từ `pipeline-pe-index` = **200 phiếu PE** (`PipelineTreePanel:22` `PE_INDEX_PAGE_SIZE = 200`, dùng ở `:124`), rồi card KHKK mới treo vào nhánh PE cha (`usePipelineStages:123-134` `khkkByPeId`). Kế hoạch có PE cha nằm ngoài 200 PE đó ⇒ **không có cành để treo ⇒ vô hình**, dù server trả về. Cho lọc chạy trong cây KHÔNG gỡ được điều này — nó là ràng buộc kiến-trúc, không phải tuỳ chọn hiển thị. Đây đúng thứ R-11 đang che.
   - 🔸 Hệ quả phụ đã có thật hôm nay: `group` **không** nằm trong điều-kiện `flatMode` (`:201`) và được lọc client (`:227`, `:251`) ⇒ leaf **nhóm N2-N8 "Danh sách"** đang chịu đúng rủi ro "phiếu cũ vô hình" mà R-11 chống. Có xoa dịu một phần: cờ `truncated` (`usePipelineStages:156`) → in cảnh báo trên folder (`PipelineStageFolders:301`) — cảnh báo, không phải đường đi tới phiếu.

### 4.3 Đường 2 — GIỮ R-11 (lọc đi server, chuyển phẳng)

**Được:** đúng về DỮ LIỆU — mọi phiếu khớp đều tới được, kể cả ngoài 200; `total` thật; phân trang thật; số đếm không nói dối. Chi phí đã trả xong (code đang chạy).

**Mất:** đổi hình **IM LẶNG** do gõ phím; **không có đường quay lại cây** khi đang đứng ở 3 leaf trạng-thái (muốn thấy cây phải tự xoá filter trên URL — người dùng không làm được); mất ngữ-cảnh gói/dự án đúng lúc đang cần đối chiếu; và kéo theo **DELTA-2** (ô tìm thứ 2 biến mất giữa lúc gõ) — lỗi nặng nhất trong bài này lại là *hệ quả* của cơ chế đổi hình, không phải của việc lọc.

### 4.4 PHÁN + KHUYẾN NGHỊ

> **GIỮ MÁY của đường 2 (R-11, lọc đi server). SỬA phần HÌNH.**
> Lý do 1 câu: **mất phiếu là lỗi dữ-liệu, mất cây là lỗi tiện-nghi** — đường 1 đổi một lỗi tiện-nghi lấy một lỗi dữ-liệu, mà ràng buộc "xương cây = 200 PE" thì đường 1 KHÔNG gỡ được.
> Nhưng bug thật của O5 **không phải "cây hay phẳng"** — mà là **đổi hình không ai bấm, không ai báo, không quay lại được**. Đó là chỗ phải vá, và vá rẻ.

4 việc, xếp theo hiệu-quả/chi-phí:

**O5-1 (BẮT BUỘC) — gộp 2 ô tìm về 1.** Thêm prop controlled `searchTerm`/`onSearchTermChange` cho `PipelineTreePanel` (nay `useState` nội bộ `:112`), và khi `hideHeader` thì ẩn luôn ô tìm ruột (`:235-245`) — cùng lý-lẽ đã dùng để ẩn header. Ô host thành ô DUY NHẤT: chế-độ cây ⇒ lọc cây (client, đúng như `term` đang làm); chế-độ phẳng ⇒ đi server. Người dùng gõ 1 chỗ, không mất chỗ đang gõ.

**O5-2 (BẮT BUỘC) — nói ra chế-độ + cho đường quay lại.** Thay 2 dòng chữ mờ 10px (`:277-287`) bằng 1 hàng công-tắc đặt NGAY DƯỚI `Select`:
`[🌳 Cây] [▤ Danh sách]` — segmented, `role="group"` + `aria-pressed`, cao 28px, chữ `text-[11px] font-medium`, active = `bg-brand-50 text-brand-700 ring-1 ring-brand-200`.
Mặc định: leaf không-lọc = Cây · 3 leaf trạng-thái = Danh sách (**giữ nguyên hành vi hôm nay**), nhưng NGƯỜI DÙNG BẤM ĐỔI ĐƯỢC. Khi ở Danh sách vì bị lọc, kèm 1 chip "Đang lọc: Chờ duyệt ✕" — bấm ✕ = bỏ lọc + về cây. (Chip này cũng vá luôn DELTA-3: ở view Đã xóa thì chip đỏ "Đã xóa" thay cho `Select` chết câm.)
🔴 Ràng buộc kỹ-thuật để O5-2 không thành bẫy: khi người dùng CHỦ ĐỘNG chọn "Cây" trong lúc đang lọc, cây chỉ lọc trong 200 ⇒ **phải in đúng câu số**: "Cây đang lọc trong 200 phiếu mới nhất — máy chủ có N phiếu khớp" (N = `list.data.total`, đã có sẵn) + nút "Xem đủ ở dạng danh sách". Không có câu này thì đường 1 lẻn vào bằng cửa sau.

**O5-3 (NÊN) — cầu nối khi cây rỗng.** Cây (đã lọc) rỗng nhưng server có kết quả ⇒ hiện `EmptyState` + nút "Xem N phiếu ở dạng danh sách", **không tự nhảy**. Hôm nay chỗ này chỉ có 1 dòng `italic slate-400` (`:230`).

**O5-4 (NÊN) — kéo 2 hình gần nhau về mặt thị-giác.** Card đã giống hệt rồi ⇒ chỉ cần: (a) chế-độ phẳng thêm **hàng nhãn + pill đếm** như `PeListPanel:96-101` để cột không "cụt đầu" khi mất cây (vá luôn DELTA-1); (b) thanh phân trang dùng icon lucide + `aria-label` (DELTA-12); (c) mọi ghi-chú `slate-400`→`slate-500`, `text-[10px]`→`text-[11px]` (DELTA-15).

**Nếu owner bác và muốn 1 hình duy nhất:** thì phải chọn hình **PHẲNG cho cả 4 leaf** (bỏ cây khỏi panel 1, cây về đúng chỗ của nó là trang toàn-trình) — KHÔNG chọn "cây cho cả 4". Chọn cây cho cả 4 = chấp nhận giấu phiếu ngoài cửa-sổ 200 ở đúng 2 leaf mà kế-toán/lãnh-đạo hay tra nhất ("Đã duyệt", "Đã xóa"). Đó là đánh đổi tôi không khuyến nghị, và nếu vẫn chọn thì phải khai bằng chữ ngay trên cây, không được im.

---

## 5. SPEC thi-hành-được (cho Opus 5 MAX)

**Luật chung cho MỌI gói dưới đây**
- 🔴 **MIRROR ĐÔI**: `fe-admin/src/...` và `fe-user/src/...` phải giữ SHA256 bằng nhau (đã đo trước khi sửa: `KhkkListPanel` `ad9073db…`, `KhkkWorkListPanel` `fcb97650…`). Sửa xong chạy lại `sha256sum` cặp file → phải khớp.
- Chỉ đụng lớp trình-bày. **KHÔNG** đổi `queryKey`, tham số API, điều-kiện `enabled`, `OPEN_PHASES`, `readOnly`, quyền. Sửa xong `grep -c "api.get"` mỗi file phải KHÔNG đổi.
- Verify: `cd fe-admin && npm run build` **và** `cd fe-user && npm run build` — 0 TS error.
- Tailwind v4: dùng class có sẵn trong scale; accent teal/violet/amber **không có stop -800** (nhớ cũ, silent no-class).

### W-A — vá O5 (ưu tiên 1, chạm 3 file)
**A1. Gộp ô tìm** · `components/pipeline/PipelineTreePanel.tsx`
- Thêm 2 prop `searchTerm?: string` + `onSearchTermChange?: (v: string) => void`; `const search = searchTerm ?? innerSearch` (giữ `useState` nội bộ `:112` làm mặc định cho 6 host cũ ⇒ **0 đổi hành vi** ở PE/GĐ3/GĐ4).
- Ẩn khối ô tìm ruột (`:235-244`) khi `hideHeader === true` — chỉ KHKK truyền `hideHeader` (`KhkkListPanel:294`).
- `KhkkListPanel`: truyền `searchTerm={search}` + `onSearchTermChange={onSearchChange}` vào `PipelineTreePanel` (`:292-297`).
- **Nghiệm thu:** ở leaf "Danh sách", đếm `<input>` trong panel 1 = **1** (nay 2). Gõ 1 ký tự vào ô đó ⇒ cây co lại, **không** nhảy sang phẳng (vì `flatMode` sẽ do W-A2 quyết, không còn do `term`).

**A2. Công tắc chế-độ + chip lọc** · `pages/khkk/KhkkListPage.tsx` + `components/khkk/KhkkListPanel.tsx`
- HOST: thêm state theo ĐÚNG khuôn 3-trạng-thái đang có ở `:68-72`: `const [viewOverride, setViewOverride] = useState<'tree'|'flat'|undefined>(undefined)` — `undefined` = nghe theo URL. Reset về `undefined` trong khối đổi leaf `:80-85` (cùng chỗ `setChipPhase`).
- PANEL: đổi `:201` thành
  `const autoFlat = !canPe || deletedView || phase !== null` (🔴 **bỏ `term.length > 0`** — gõ chữ không còn được quyền đổi hình)
  `const flatMode = viewOverride ? viewOverride === 'flat' : autoFlat`
  `enabled: flatMode` (`:217`) giữ nguyên ⇒ ở chế-độ cây vẫn không gọi `khkk-list`. **NGOẠI LỆ**: khi `viewOverride === 'tree'` mà `autoFlat` đang true, cần `total` để in câu cảnh báo ⇒ đổi `enabled: flatMode || autoFlat` và **chỉ đọc `total`**, không render `items`.
- Công tắc đặt ngay sau `Select` (`:276`), ẩn khi `!canPe` (không có cây thì không có gì để bật):
  ```
  <div role="group" aria-label="Chế độ hiển thị" className="flex gap-1">
    <button type="button" aria-pressed={!flatMode} onClick={() => onViewChange('tree')}
      className={cn('inline-flex h-7 flex-1 items-center justify-center gap-1 rounded-md border px-2 text-[11px] font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/40',
        !flatMode ? 'border-brand-200 bg-brand-50 text-brand-700' : 'border-slate-200 text-slate-500 hover:bg-slate-50')}>
      <ListTree className="h-3.5 w-3.5" /> Cây
    </button>
    …bản sao cho 'flat' với <List className="h-3.5 w-3.5" /> Danh sách…
  </div>
  ```
  (icon `ListTree`/`List` từ `lucide-react` — **không dùng emoji**, FD3.)
- Chip lọc đang bật, thay cho 2 dòng chữ mờ `:277-287`: khi `phase !== null` → `<button>` chip `inline-flex items-center gap-1 rounded-full bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-amber-800 ring-1 ring-amber-200` nội dung `Đang lọc: {KHKK_PHASE_LABELS[phase]}` + `<X className="h-3 w-3" />`, `aria-label="Bỏ lọc trạng thái"`, onClick → `onPhaseChange(null)`.
- Khi `deletedView`: **thay** `Select` bằng banner đỏ khuôn golive (`PurchaseEvaluationsListPage:427-431`): `rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[11px] text-red-700` + "🗑 **Phiếu đã xóa** — chỉ xem. Không khôi phục, không thao tác."
- **Nghiệm thu:** vào `/khkk/list?group=1&filter=DaDuyet` → thấy PHẲNG + chip "Đang lọc: Đã duyệt ✕" + công tắc; bấm `Cây` → ra cây; bấm ✕ → về `Danh sách` không lọc, cây giữ nguyên.

**A3. Câu số khi cây bị lọc + cầu nối khi rỗng** · `KhkkListPanel.tsx`
- `viewOverride === 'tree' && autoFlat` ⇒ trên thân cây in: `Cây lọc trong 200 phiếu mới nhất — máy chủ có {total} phiếu khớp.` + `<button>` "Xem đủ ở dạng danh sách" (khuôn nút "Thử lại" `:310-316`). Class chữ: `px-3 py-1.5 text-[11px] text-slate-500` (**KHÔNG** `slate-400` — xem W-C).
- Cây rỗng sau lọc ⇒ `<EmptyState icon={Inbox} title="Không có kế hoạch khớp bộ lọc" description=… action={nút trên} />`.

### W-B — đồng bộ hình với golive (chạm 4 file)
| # | File | Sửa |
|---|---|---|
| B1 | `types/khkk.ts:58-64` ⟂ `types/purchaseEvaluation.ts:91-97` | thống nhất 1 khuôn badge. Khuyến nghị: PE **theo** KHKK (`rounded-md border`, `text-X-800`, `border-X-300`) vì có viền thì badge không chìm trên `bg-brand-50` lúc chọn. Sửa ở **map**, không sửa tại card. |
| B2 | `KhkkWorkListPanel.tsx:272` | `text-[9px]` → `text-[10px]` |
| B3 | `KhkkWorkListPanel.tsx:205,201-203,196` | tiêu-đề `text-[12px] text-slate-800`→`text-[13px] text-slate-900`; phụ `text-[10px]`→`text-[11px]`; `py-2`→`py-2.5` |
| B4 | `KhkkWorkListPanel.tsx:197,260` | ring chọn `amber-300`/`violet-300` → `bg-brand-50 ring-brand-200` (giữ accent ở đầu tầng `:60`) |
| B5 | `KhkkWorkListPanel.tsx:160,230` | chữ "Đang tải…" → 4 skeleton `h-12 animate-pulse rounded-md bg-slate-100 motion-reduce:animate-none` trong `space-y-2 p-3` |
| B6 | `KhkkWorkListPanel.tsx:173-177,243-248` + `KhkkListPanel.tsx:319-330` | dùng `<EmptyState>` (`@/components/EmptyState`), giữ nguyên CHỮ đang có |
| B7 | `KhkkListPanel.tsx:354-369` | `←`/`→` → `<ChevronLeft/>`/`<ChevronRight/>` `h-3.5 w-3.5` + `aria-label="Trang trước"/"Trang sau"` (thêm import lucide) |
| B8 | `KhkkListPanel.tsx:147-151` + `KhkkWorkListPanel.tsx:265-269` | chip nhóm: `text-[10px] px-1.5 py-0.5 leading-none` + `title={khkkGroupMenuLabel(n)}` |
| B9 | `KhkkListPanel.tsx:157-159` | thêm `title` giải nghĩa màu tiền (`'Giá trị đã duyệt (đã chốt)'` ⟂ `'Giá trị đề xuất (tạm tính)'`) |
| B10 | `KhkkListPanel.tsx:282-287` | khối thiếu-quyền dùng khuôn `Lock` của `KhkkWorkListPanel:143-151` |
| B11 | `pages/khkk/KhkkListPage.tsx:128-130` | thêm pill **số** cạnh pill "Giai đoạn 2" (2 pill riêng vai) |

### W-C — sàn a11y (FD5, chạm 3 file, thuần class)
- `text-slate-400` mang CHỮ → `text-slate-500` tại: `KhkkListPanel:230,278,283` · `KhkkWorkListPanel:174,244,290` · `PipelineTreePanel:278`. (Đo: `#94a3b8`=**2.56:1** trượt AA · `#64748b`=**4.76:1** đạt.) Giữ `slate-400` cho icon.
- `text-[10px]` của 2 ghi-chú `KhkkListPanel:278,283` → `text-[11px]`.

### W-D — NÂNG chuẩn golive (đừng hạ KHKK xuống)
- Thêm nhánh `isError` + nút "Thử lại" cho `PeListPanel.tsx` (sau `:134`) và `PurchaseEvaluationsListPage.tsx` (sau `:463`) — khuôn `KhkkListPanel:307-318`.
- Thêm `motion-reduce:animate-none` vào skeleton `PeListPanel:131` + `PurchaseEvaluationsListPage:461`.
- Thêm `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60` vào card `PeListPanel:154` (khuôn `KhkkListPanel:117`).
- ⚠️ Đây là file **golive** ⇒ tách commit riêng, và **nói trước với owner** (anh đã từng bác việc tự sửa màn Duyệt NCC — ghi ở `KhkkWorkListPanel.tsx:7-12`).

### W-E — chờ owner gật (KHÔNG tự làm)
- **DELTA-4** đồng bộ từ vựng trạng thái (`khkk.ts:49-55` → "Nháp / Đã gửi duyệt / Cần chỉnh sửa lại"). Chữ hiển thị = quyền owner.
- **B1** chọn chiều badge (PE theo KHKK, hay ngược lại).

### Thứ tự thi công đề-xuất
`W-C` (rẻ nhất, 0 rủi ro) → `W-B` (thuần hình) → `W-A` (có logic, cần soi kỹ) → `W-D` (đụng golive, tách commit) → `W-E` (sau khi owner gật).

---

## 6. Rubric verdict + scope check

### 6.1 FD2 — KHAI THẲNG: **KHÔNG CÓ SCREENSHOT** trong lượt này
Lượt này là **AUDIT read-only** (không giao sửa code) và cả 3 màn đều nằm sau `ProtectedRoute` + cần API HTTPS :5443 chạy — đúng **gotcha #3** trong ký-ức của tôi (vite proxy `http://` ⟂ API `https://` ⇒ login fail, kẹt `/login`). Thay bằng bằng-chứng ĐO ĐƯỢC: chuỗi class đọc thẳng từ đĩa + `sha256sum` mirror + tự tính tương-phản WCAG. **Không bịa "đã soi".** Khi thi công W-A/W-B thì FD2 loop là BẮT BUỘC (2 viewport 375/1440 + 4 leaf menu), và nên chụp trên **prod sau deploy** thay vì vật lộn dev-rig.

### 6.2 Rubric FD4 chấm HIỆN TRẠNG panel KHKK (không phải chấm bản sẽ sửa)
| Tiêu chí | Verdict | Căn cứ |
|---|---|---|
| Hierarchy | 🟡 | card 3 hàng rõ, nhưng chế-độ cây **không có tiêu-đề/số đếm** (DELTA-1) ⇒ cột cụt đầu |
| Spacing/rhythm | ✅ | `space-y-2 p-3`, `px-3 py-2.5`, `divide-y` khớp golive từng lớp |
| Typography | 🔴 | 4 nấc chữ trong 1 cột (13/11/10/**9**px); C nhỏ hơn chuẩn 1 nấc (DELTA-6, B2) |
| Color/contrast | 🔴 | `slate-400` = **2.56:1** cho 6 dòng chữ mang thông tin (DELTA-15) |
| Polish/states | 🟢 | hơn golive: có `isError`+Thử lại, `focus-visible` ring, `motion-reduce` |
| Responsive | 🟡 | `<lg` panel 1 chiếm cả màn (`KhkkListPage:140`) — ổn; nhưng cây `bodyMaxHClass="max-h-[28rem] lg:max-h-none"` (`:295`) ⇒ mobile cây kẹt 448px trong cột đã cuộn, cuộn lồng nhau (đúng bệnh đã vá ở GĐ3 F-1) — **cần soi bằng mắt khi thi công** |
| Detail | 🔴 | 2 ô tìm chồng nhau (DELTA-2); nút phân trang là ký tự trần (DELTA-12); chip nhóm lệch baseline (DELTA-8) |

**KẾT: CHƯA PASS** — 3 trục đỏ. Không trục nào cần đổi máy; toàn bộ nằm ở lớp trình-bày + 1 công-tắc chế-độ.

### 6.3 Scope check
✅ FE-only, **KHÔNG viết/sửa 1 dòng code nào** trong lượt này (audit thuần) · KHÔNG đụng BE/DB/logic · KHÔNG double-touch file của implementer-frontend · file duy nhất tôi ghi = chính sub-MD này.

---
END sub-frontend-designer-L2 — VERDICT=CHƯA-PASS (13 delta + 3 trục rubric đỏ) · O5=GIỮ-MÁY-R11-SỬA-HÌNH (bỏ `term` khỏi `flatMode` + gộp 2 ô tìm + công-tắc Cây/Danh-sách + chip lọc)
