Skip to content

DS-001 · Lớp component n12-* và luật tối giản ​

Một phần của DS-001. Lớp component n12-*: bong bóng, mascot, Term, accordion, popover, card ba tầng; luật tối giản SRC-048; class gắn lên phần tử phải có CSS.

5. Lớp component n12-* (REQ-UX-01/02/05/06) ​

App không tự chế các pattern này — dùng class từ index.css:

ClassVai trò
n12-card / n12-card-hoverKhối nội dung chuẩn (bg card, border, radius, shadow-1; hover nâng + shadow-2)
n12-btn + n12-btn-accent | n12-btn-primary | n12-btn-school | n12-btn-outline | n12-btn-ghost | n12-btn-smNút pill 44px; accent (coral) tối đa 1/màn; school = màu accent theme; busy dùng disabled + text đổi
n12-chip (+.is-active)Bộ chọn môn/lựa chọn — pill 40px
n12-tab (+.is-active)Tab điều hướng 44px, chỉ báo 3px
n12-inputInput/select 44px, focus ring ocean
n12-badge (+-solid -review -soft)Nhãn trạng thái nhỏ
n12-progress (+.is-school, <i style="width:x%">)Thanh tiến độ 14px, track muted, fill gradient ocean→seafoam (school: accent), có transition (REQ-UX-02)
n12-skeletonKhối shimmer khi tải (REQ-UX-06)
n12-heroGradient deep→accent + chữ trắng (REQ-UX-08)
n12-icon (+-lg)Khung tròn soft chứa emoji/SVG icon — đồng nhất icon toàn hệ (Q-108)
n12-emptyEmpty state: icon + 1 câu + 1 bước tiếp theo (REQ-UX-03)
n12-optionLựa chọn trắc nghiệm: card 48px, hover viền primary, đã chọn bg ocean-100

4b. Bong bóng mừng câu đúng .n12-bubbles (SRC-148) ​

Câu trả lời đúng được thưởng bằng chuyển động, không bằng thêm chữ: 6 bong bóng nổi từ đáy khối kết quả lên rồi tan trong 1.6s, một lượt duy nhất (không lặp — lặp thì mắt không rời ra để đọc phần giải thích). Dùng đúng bảng màu BUBBLE_ART của 5 bong bóng "mạng" ở đầu bài để learner nhận ra là cùng một vật. Khối chứa phải có position: relative + overflow: hidden. prefers-reduced-motion → không hiện bong bóng nào, lời khen bằng chữ vẫn đủ nghĩa.

Đi kèm là luật viết lời khen (pickPraise, Learning.tsx): không khen ngẫu nhiên khi có chuyện thật để nói. Thứ tự ưu tiên đi từ chuyện hiếm nhất — gặp lại câu từng sai mà đúng · tự nhận đoán mò mà trúng · câu khó · rồi mới tới câu ngắn xoay vòng. Khen chung chung nhiều màu vẫn là khen chung chung.

4c. Con vật dẫn .n12-mascot + bóng thoại .n12-say (SRC-149) ​

Lời khen sau câu đúng do một con vật nói, không phải hệ thống thông báo. Ba luật bắt buộc:

LuậtVì sao
Con vật phải khớp hình tròn learner vừa bấm (mòng biển = Học vượt · cua = Luyện tập/Luyện thêm · cá ngựa = Khám phá)Cùng một nhân vật đi suốt bài mới ra chất "có ai đó đang xem con làm"; đổi con vật giữa chừng thì chỉ là icon trang trí
Mỗi con một giọng riêng thật sự, không chỉ đổi tên con vậtNăm câu khen giống hệt nhau gắn ba con vật khác nhau vẫn là năm câu khen giống hệt nhau
Câu sai vẫn có con vật, nhưng ở tư thế khác (.n12-mascot-calm: không nhún, không bong bóng)SRC-149 từng bỏ hẳn con vật ở câu sai vì sợ thành trêu. SRC-163 đảo lại: phản hồi khô quá làm learner hết muốn làm câu tiếp. Nỗi lo cũ được giữ bằng cách bỏ phần ăn mừng, không phải bỏ nhân vật
Không bao giờ khen một câu saiTrẻ nhìn ra ngay, và sẽ thôi tin mọi lời khen khác. Câu nói khi sai làm ba việc: bình thường hoá cái bẫy · nói cái vừa thu được · giữ nhịp đi tiếp

Bóng thoại co theo chữ (flex: 0 1 auto), mũi nhọn dựng bằng hai lớp ::before/::after để viền liền mạch. Con vật nhô lên một nhịp rồi đứng yên — nhún mãi thì thành đồ trang trí động, kéo mắt khỏi phần giải thích. prefers-reduced-motion tắt animation.

4d. Tên khái niệm kèm tiếng Anh <Term> / .n12-term-en (SRC-150) ​

Tên khái niệm trong Learning Experience hiện tiếng Việt kèm tiếng Anh: Ý chính · Main idea. Tiếng Anh nhỏ hơn (0.8em) và nhạt hơn (opacity .62) — nó là chú thích, không phải bản dịch ngang hàng.

Đây không phải chức năng đổi ngôn ngữ. LangSwitch đổi cả giao diện sang một ngôn ngữ; Term hiện cả hai cùng lúc bất kể đang chọn ngôn ngữ nào, vì tiếng Anh ở đây đóng vai mã định danh: một khái niệm ở Việt Nam thường có vài cách gọi ("ý chính / ý chủ đạo / nội dung chính"), bản tiếng Anh thì chỉ có một.

Từ vựng nằm ở apps/learn/src/glossary.ts — chỉ nhận tên khái niệm, không nhận câu, bước làm bài hay mẹo thi; không có trong bảng thì im lặng chỉ hiện tiếng Việt. Chú sai một khái niệm còn tệ hơn không chú: learner sẽ mang từ sai đó đi tra sách tiếng Anh.

4e. Bảng màu hình vẽ — hai màu là hết (SRC-164) ​

Bảng DIAGRAM (apps/learn/src/explore/palette.ts) từng có 22 giá trị trải 6 họ màu; mỗi hình lấy vài màu tuỳ hứng. Hậu quả không chỉ là rối mắt: khi mọi thứ đều có màu riêng thì không còn màu nào nói được "chỗ này mới là chỗ cần nhìn".

Nay rút về hai màu + dải trung tính, mỗi màu có đúng một việc:

VaiMàuDùng cho
MAIN#1F7AB8đối tượng đang được học: đường, hình, thanh, tia đang xét
MARK#E4572Eđúng một thứ đang được nhấn trong hình
trung tínhink #17222B · muted #5A6B78 · line #C9D4DA · wash #EAF4FAtrục, lưới, nhãn, nền

Các key cũ (aqua571, green590, yellow364…) giữ nguyên tên làm bí danh nên 193 chỗ dùng không phải sửa. Luật giữ về sau: muốn thêm màu thì phải bỏ một màu — có test canh số giá trị và số họ màu.

Hai chỗ phải khớp tay với bảng này: --n12-figure / --n12-figure-mark trong tokens (dùng cho accent-color của thanh trượt) và hằng A1/A2/A3 trong figures/engine.tsx. Lưu ý thang --n12-ocean-* là thang trung tính (xám), không phải xanh — viết var(--n12-ocean-500, #1F7AB8) thì biến vẫn tồn tại nên hex dự phòng không bao giờ được dùng, và nét chính của hình ra màu xám.

4f. Tiến độ dạng khối .n12-steps (SRC-168) ​

Mỗi câu một ô, cách nhau 4px. Không dùng thanh liền cho việc học vì hai lý do:

  1. Thanh liền trả lời "được bao nhiêu phần trăm", nhưng câu learner thật sự hỏi là "còn mấy câu nữa thì xong". Khối đếm được bằng mắt; thanh liền thì không.
  2. Số câu của một phiên có thể tăng (câu sai được xếp lại xuống cuối). Với thanh liền, tiến độ đang 80% bỗng tụt còn 73% — nhìn như bị phạt. Với khối, đó chỉ là thêm một ô ở cuối hàng và phần đã làm không mất đi.

Ba trạng thái phân biệt bằng độ đậm (đã xong · đang làm · chưa tới), không bằng màu — hợp với hướng đơn sắc của hệ token. Ô của câu được xếp lại mang tông ấm nhạt để việc "số ô tăng lên" tự giải thích mà không cần thêm chữ. Bắt buộc có role="progressbar" và nhãn "Câu X trên Y".

4g. Accordion — cách mặc định để bày nhiều bước (SRC-533, SRC-540, SRC-123) ​

Giải thích có nhiều bước thì dùng Accordion, không dùng tab dọc và không dùng stepper +/−. Đây là luật ưu tiên chung, không riêng một màn (SRC-533). Lý do nằm ở khoảng cách giữa tiêu đề và nội dung của nó:

  • Tab dọc xếp bốn tiêu đề thành một cụm rồi đặt phần giải thích dưới cả cụm — mắt phải nhảy từ Bước 2 qua Bước 3, Bước 4 mới tới nội dung của chính Bước 2, và không có dấu hiệu nào nói nội dung đó thuộc bước nào.
  • Stepper +/− bắt learner đi tuần tự và giấu mất mục lục: không nhìn ra bài này có mấy bước, còn bao xa nữa thì hết.
  • Accordion mở nội dung ngay dưới tiêu đề đang chọn, các bước chưa mở vẫn thấy đủ mặt.

Ba ràng buộc bắt buộc:

  1. Các mục mở ĐỘC LẬP — bấm mục này không đóng mục kia (type="multiple" của shadcn Accordion, §5d). Người học đang so hai bước với nhau cần cả hai cùng mở; tự đóng hộ là giật mất một nửa phép so (SRC-540).
  2. Mặc định mở mục đầu, các mục sau đóng — màn không bao giờ mở ra ở trạng thái rỗng hoàn toàn.
  3. Mục dài đọc một lần rồi thôi (giới thiệu school: "dành cho ai · học thế nào · ra khỏi trường có gì") thì gom cả ba cụm vào MỘT accordion đóng sẵn, đặt dưới phần learner tới để dùng — phần học không được bị ba đoạn giới thiệu đẩy xuống dưới mép màn hình (SRC-123).
  4. Accordion nằm trong màn có thể đổi ngữ cảnh (đổi school, đổi bài) phải nhận key theo ngữ cảnh đó, nếu không trạng thái mở của ngữ cảnh trước dính sang ngữ cảnh sau (SRC-123).

Hình minh hoạ đi kèm accordion cũng theo luật "đọc được trước đã": word-web vẽ mỗi khái niệm là một THẺ có nền và viền, nét nối vẽ tới mép thẻ, thẻ đang chọn đổi màu đầy đủ và bấm được ngay trên hình — chữ trôi tự do quanh một hình tròn với nét nối mảnh thì gần như tàng hình trên nền sâu (SRC-540).

4h. Popover — mở tại chỗ, đọc được, và là nơi chứa lời giải thích (SRC-471, SRC-475, SRC-480, SRC-503, SRC-575) ​

Popover mở ngay tại điểm vừa click, neo dưới chính nút/thẻ vừa bấm, chỉ lùi vào khi chạm mép màn (đệm tối thiểu 8px mỗi cạnh, chiều cao trần 70% màn hình hoặc 420px). Popover bật giữa màn hình, cách chỗ bấm nửa trang, làm người đọc mất dấu chỗ mình vừa bấm và phải tự nối lại hai thứ (SRC-503). Ngoại lệ có chủ đích: modal giữa bài là điểm dừng cố ý, nó đứng giữa màn.

LuậtVì sao
Mọi con số hệ dùng để thúc learner (chuỗi ngày, điểm, số unit đã vững) phải mở được popover nói đúng hai điều: đang đếm cái gì, và làm gì thì nó nhúc nhíchTrước đó chúng chỉ có title — trên máy cảm ứng learner không có cách nào biết chúng nghĩa gì mà vẫn bị chúng thúc (SRC-480)
Lời giải thích dài ("vì sao lại là việc này", "con đang ở đâu") vào popover của một dấu i tròn nhỏ trên header card, không nằm trên mặt cardMặt card chỉ đủ chỗ cho lời hứa của hệ; hai link chữ trên card phá luật text budget §5b·3 (SRC-471)
Hành động đưa learner sang một thế giới khác (Bắt đầu học) mở popover xác nhận trước, mang tên bài + "vì sao đáng làm" + nút vào họcTrước khi bấm, learner cần biết mình đang tin vào cái gì; một tên Unit trần trụi như "Bảng nhân và chia" thì chưa có bối cảnh nào (SRC-475)
Nút trong popover nhỏ lại, bấm ra ngoài lớp phủ là ở lại, không có link "Ở lại đây"Nội dung mới là thứ phải nổi lên; một link để-không-làm-gì là một quyết định thừa bắt learner phải ra (SRC-503)
Popover mang chất liệu của nền nó mọc ra: trên bản đồ nền biển sâu thì popover là hộp TỐI, không phải thẻ trắngThẻ sáng mặc định (bg-card + chữ ocean-900) đặt trên theme biển sâu — nơi lớp chữ mục đã là gần trắng — cho ra trắng trên trắng, gần như không đọc được chữ nào (SRC-575)

Nhãn của cụm giải thích là "Cơ sở để hệ thống tính toán", không phải "Vì sao lại là việc này": nó bày VẾT TÍNH TOÁN của hệ, không phải một lời biện minh (SRC-475).

4i. Card ba tầng — một khuôn duy nhất (SRC-471) ​

Mức vững, Package và Module là ba tầng khác nhau của cùng một thứ nên dùng cùng một khuôn: header (tên + dải ô) có border-b nối liền hai mép card, thân card nằm dưới. Đường kẻ chạy hết bề ngang mới đọc ra là đây là nhãn của khối bên dưới; đường kẻ thụt vào hai đầu chỉ đọc ra là một gạch trang trí.

Tên card lớn viết thành câu hỏi của learner ("Em học chắc tới đâu?"), không phải thuật ngữ của hệ ("Mức vững"), và bỏ dòng phụ — tên đã là câu hỏi thì dòng giải thích bên dưới thành thừa.

4j. Dải cá SeaRow (SRC-535) ​

Trang trí biển là một component dùng chung, không phải dải emoji chép tay ở từng màn: SeaRow dựng 3-5 con cá xen bong bóng, chọn theo seed — mỗi màn một đàn khác nhau nhưng đứng yên theo màn, cùng một trang luôn thấy cùng một đàn chứ không nhảy lung tung mỗi lần render.

Có mặt ở bốn chỗ: đầu màn Home (card Việc hôm nay) · trên thẻ câu hỏi lúc đang làm bài (cỡ nhỏ hơn) · hero School Home (seed theo school) · màn kết quả. Trang trí thuần: aria-hidden, pointer-events: none, tôn trọng prefers-reduced-motion. Chép tay ở từng nơi là cách chắc chắn nhất để sáu màn có sáu mật độ cá khác nhau.

5b. LUẬT TỐI GIẢN — bắt buộc mọi page (SRC-048, REQ-UX-11/12) ​

Chủ dự án 2026-08-14: "Vô cùng ghét UI chi chít chữ". Luật áp cho MỌI page mới/sửa, mọi portal:

  1. Nền tràn viền, CHỮ có cột (sửa v0.4, SRC-160 — thay cho luật "full-width" cũ). Nền, header, hero vẫn trải hết chiều ngang; nhưng nội dung nằm trong .n12-page (mặc định --measure-app 960px), .n12-page-text (736px, cho màn đọc) hoặc .n12-page-wide (1216px, cho lưới thẻ). Vì sao đổi: luật cũ hiểu "full-width" là cả chữ cũng trải hết, nên trên màn 1900px một tiêu đề ba chữ nằm cách mũi tên 1400px — mắt phải quét ngang và khoảng trống ở giữa đọc ra là "chưa làm xong" chứ không phải "thoáng". Đây là nguyên nhân số một của nhận xét "lổn nhổn".

    Mức thứ tư .n12-page-full (max-width: none), thêm 10/9/2026 theo chỉ đạo chủ dự án. Luật cũ nói "không đẻ thêm mức đo thứ tư"; ngoại lệ này có lý do hẹp và kiểm được: nó dành cho vùng lưới dày, không dành cho chữ. Thang năng lực b21 có sáu bậc phải nằm cùng một dòng mới so ngang được, mà sáu cột không lọt vào 1216px. Điều kiện dùng: chữ dài trong trang ấy vẫn phải tự bó bằng max-w-* của nó — bỏ điều kiện này là dựng lại đúng cái lỗi SRC-160 đã chữa.

  2. Vô cùng ít element mỗi màn: 1 khối hero/hành động chính + tối đa ~4-6 cards. Nhiều hơn → tách màn/mở dần.

  3. Text budget: card điều hướng = icon + tên + ≤1 dòng phụ (≤6 từ). Mô tả đoạn văn bị CẤM trên card/list — chuyển vào màn chi tiết. Mỗi màn ≤1 câu giới thiệu (hoặc không có). Metadata ≤3 mẩu/dòng, phần còn lại vào chi tiết.

  4. Hình > chữ: icon lớn trong n12-icon, cờ/emoji cỡ lớn, progress bar, màu — thay cho câu mô tả. Card nào toàn chữ là sai.

  5. HOME = hub: trang home (Root learn, tab "Hôm nay", home portal khác) CHỈ chứa links/cards điều hướng + 1 hành động chính — không dữ liệu chi tiết, không form.

  6. School Home read-only theo cụm: giới thiệu school bằng CỤM thông tin (môn học, của con, cách học) — KHÔNG bắt nhập liệu; form đặt mục tiêu/lịch thi nằm trong phần học (panel Mục tiêu).

  7. List item: 1-2 dòng, cả card bấm được — không thêm dòng link "Chi tiết" riêng.

  8. Cấm em dash — trong mọi chuỗi UI (SRC-057): tiêu đề, mô tả, badge, <title>, meta description của cả 6 app. Thay bằng · (ngăn 2 nhãn cùng cấp), dấu phẩy (mệnh đề nối) hoặc : (giải thích). Comment trong code không tính vì không render.

  9. Card không có link chữ CTA (SRC-057): bỏ "Vào xem", "Khám phá X"; cả card bấm được, chỉ để mũi tên → căn đáy card (flex h-full flex-col + mt-auto) để mọi card cùng hàng thẳng nhau; tên đầy đủ giữ ở title/aria-label cho screen reader.

  10. Chân trang chung .n12-footer (SRC-210): mọi app dùng một SiteFooter (canonical ở packages/design-system/ui/SiteFooter.tsx) — một dòng căn giữa: © năm Nemo12 · nemo12.com · một câu định vị. Năm lấy theo đồng hồ chứ không viết cứng. Sau câu định vị là một cụm icon kênh mạng xã hội (Facebook, LinkedIn, YouTube, Substack), đọc từ một nguồn duy nhất packages/design-system/ui/socialLinks.ts (SRC-1237); chân trang nemo12.com, JSON-LD sameAs và socialLinks của bốn site VitePress đọc cùng mảng đó. Từ 05.10.2026 cụm icon có thêm X và Threads (SRC-1238); đó là tài khoản cá nhân của chủ dự án nên mang cờ personal và JSON-LD sameAs của Organization bỏ qua chúng. Nhãn đọc màn hình của icon là Nemo12 trên <kênh>; trong sân IELTS của learn luôn là Nemo12 on <kênh> (SRC-1241). Cấm chân trang nhiều cột, menu link, danh sách nhóm cộng đồng, newsletter: mọi đường đi thật đã nằm ở header và home-hub, còn chân trang nhiều cột là chỗ đầu tiên luật tối giản bị phá. App cần thêm đúng một link riêng thì truyền qua children.

  11. Nhãn nút và câu trạng thái viết ngắn nhất có thể (SRC-472): nút điều hướng ≤3 từ ("← Toàn cảnh", không "← Xem toàn cảnh"), tiêu đề trạng thái ≤4 từ ("Chờ chút đã", không "Chưa mở lúc này"), câu báo trạng thái tối đa hai câu ngắn. Kèm luật đơn vị: thời gian chờ từ 60 phút trở lên nói bằng GIỜ — "180 phút nữa" bắt trẻ làm một phép chia trước khi hiểu được câu trả lời.

  12. Một việc chỉ được một tín hiệu (SRC-293, đảo ở SRC-324): ô đang làm trên bản đồ đã to hơn các ô khác và đã có bong bóng "Bắt đầu", nên không thêm vòng tròn bao quanh. Ba tín hiệu chồng lên cùng một ý làm ý đó khó đọc hơn chứ không rõ hơn — trước khi thêm một dấu nhấn, đếm xem chỗ đó đã có mấy dấu.

  13. Cùng một tầng thì cùng một cách đọc (SRC-210): trong một bản đồ nhiều tầng (Package → Module → Unit), mọi tầng xếp điểm ở trên, tên ngay dưới, căn giữa; phân biệt tầng bằng CỠ vòng tròn (Package 40px · Module 32px · Experience 24px), không bằng cách xếp khác nhau. Mỗi tầng một kiểu đọc thì mắt phải đổi hướng mỗi lần xuống một tầng.

Đo "ít element" bằng CHỮ HIỆN RA, không bằng số thẻ DOM (SRC-911, 2026-09-20) ​

Luật tối giản ở §5b nói "ít element", và cách đo hiển nhiên — đếm thẻ trong DOM — nói dối: một <details> gập lại vẫn giữ trọn con của nó trong cây, và trong Chromium chúng vẫn khai offsetParent khác null. Đếm thẻ thì một trang đã gọn đi một phần ba vẫn ra gần đúng con số cũ.

Thước đo không nói dối là innerText: nó chỉ trả về thứ THẬT SỰ được vẽ. Ba con số nên ghi lại mỗi lần cắt, đo ở cùng một bề ngang và cùng một bộ dữ liệu:

TrướcSau
Dòng chữ hiện ra3325 → 21
Ký tự hiện ra701313 → 210
Chiều cao trang1030px681px → 650px

(Số thật của /ielts/leaderboard ở 1100px với ba dòng bảng. Cột thứ ba là vòng hai, SRC-912: chủ dự án xem bản vòng một rồi cắt tiếp dòng phụ mỗi hàng và dòng thứ hạng — tổng cộng chữ hiện ra giảm 70%.)

Vòng hai dạy thêm một điều: cái nên bỏ là chữ tả learner, cái nên giữ là chữ họ làm được gì đó với nó. "3 of 7 days · 68 questions" là hai cách đo khác của cùng công sức mà cột giờ đã nói; "9h 15m to go" thì nói việc còn phải làm. Và "You are #2 this week" kể lại thứ mắt đã thấy — hàng của chính mình đã tô nền, viền sáng, gắn chữ "YOU".

Xoá chữ thì xoá luôn mã chết theo nó. Bỏ "N of M days" làm spanDays() không còn ai gọi, và một ca e2e mất luôn chủ đề (nó gác lỗi "31 of 30 days"). Cả hai đều phải đi cùng: một lỗi trong chữ KHÔNG CÒN ĐƯỢC VẼ thì không xảy ra được, nên giữ lại cái cổng ấy là giữ cổng gác cửa của một căn phòng đã tháo. Số ký tự là con số nói đúng nhất cảm giác "trang này nhiều thứ quá", vì nó đo lượng chữ mắt phải đi qua — còn số dòng thì một tiêu đề và một đoạn văn dài đếm bằng nhau.

Gập lại chứ không xoá, khi nội dung có thật: luật thưởng của leaderboard vào <details>, không bị xoá, vì phần thưởng đã hiện ngay trên từng dòng của bảng rồi — cái thẻ là lần kể thứ hai. Dùng <details> của HTML chứ không dựng nút tự chế: bàn phím, aria-expanded và trạng thái mở/đóng đã có sẵn và đúng.

Class gắn lên phần tử phải có quy tắc CSS (SRC-902, 2026-09-20) ​

Lỗi. Codemod của SRC-596 đổi tiền tố n12-swim* của màn chờ sang n12-loader-sea*, nhưng đổi lây cả ba dòng của một lớp TRANG TRÍ trùng tên. Từ 26.08.2026 tới 20.09.2026 — gần một tháng — 17 file vẫn gắn n12-swim lên mascot mà không còn quy tắc nào nhận, nên con cá đứng yên khi rê chuột.

Vì sao không ai thấy. CSS thiếu quy tắc thì không báo lỗi: trang vẫn vẽ, không ngoại lệ, không log, không cổng nào đỏ. Chỉ có hiệu ứng lặng lẽ biến mất. Bản vá từng được viết ngày 10.09 nhưng kẹt trong một nhánh bỏ dở vì SRC-698 bị cấp trùng, nên nó chưa bao giờ lên production.

Cổng gác. scripts/check-orphan-classes.mjs trong npm run check:code: mọi class n12-* xuất hiện trong className mà không có quy tắc .n12-* trong bất kỳ file CSS nào của repo thì đỏ.

Hai chỗ dễ báo nhầm đã xử lý, và cách xử lý là phần đáng đọc lại:

Trường hợpCách xử lý
bg-(--n12-deep-surface)là biến CSS, không phải class — bỏ mọi chỗ đứng sau --
n12-type-exemptclass đánh dấu cho check-vi-type.mjs đọc, cố ý không có kiểu — khai trong MARKERS kèm lý do

Quét CẢ repo chứ không riêng packages/design-system: n12-prose sống ở prose.css, và quét hẹp sẽ báo nhầm nó. Chỉ đọc trong className/class chứ không quét cả file: tên lớp còn xuất hiện trong chú thích và tên biến, quét rộng thì nhiễu nhiều hơn tín hiệu.

Cổng đã được kiểm là có đỏ thật: gỡ ba quy tắc ra thì nó đỏ đúng 17 file, lắp lại thì xanh.

Dọn kèm. n12-fade trong apps/marlins/src/EventRoom.tsx là class chết — không CSS, không script nào đọc, và nó nằm trên <FadeIn> vốn đã tự làm hiệu ứng. Đã gỡ.

Điều đáng rút ra. Chú thích trong index.css đã ghi đúng bài học của lần trước ("đặt tên lớp mới thì grep tên đó trước, đừng tin là nó chưa ai dùng"), nhưng một bài học viết ra không gác được gì. Cái gác được là một phép đối chiếu bằng máy — và phép ấy phải được kiểm là có đỏ.