---
url: https://docs.nemo12.com/design-system/visual-v03-and-typography.md
description: >-
  Hướng nhìn v0.3 Finding Nemo, xưởng hình N12Art cấm emoji, nguồn dữ liệu
  school, typography, chữ to có dấu, công thức qua MathText.
---

# DS-001 · Visual v0.3, N12Art, nguồn school và typography

Một phần của [DS-001](./index.md). Hướng nhìn v0.3 Finding Nemo, xưởng hình N12Art cấm emoji, nguồn dữ liệu school, typography, chữ to có dấu, công thức qua MathText.

## 2. Visual direction v0.3 · "Finding Nemo" (SRC-067; REQ-UX-03, REQ-UX-08)

Hai nguồn tham chiếu do chủ dự án chốt 2026-08-14: **khung trình bày kiểu framer.com / screen.studio** (sạch, cực ít element, chữ display lớn, section thoáng py-24, card nổi bằng bóng mềm thay viền) + **màu sắc/hình ảnh kiểu phim Finding Nemo, dưới biển, hoạt hình 3D Pixar**:

* **Nước biển điện ảnh**: hero `n12-hero-ocean` = gradient sâu→aqua nhiều lớp + **tia sáng xuyên nước** (::before) + **bong bóng trôi** (::after, tôn trọng reduced-motion). Hero school cùng cấu trúc theo accent.
* **Chất 3D Pixar**: gloss highlight trên nút accent (gradient dọc + inset trắng), icon tròn = **bóng nước** (radial highlight góc trên trái + inset đáy), fill progress có highlight mặt nước.
* **Khung framer**: nền "nước trong" #F7FAFB (bỏ tông cát ấm), viền card gần như tan (`rgb(6 48 79 / .06)`), radius card 1.25rem / hero 2rem, bóng blur rộng 24-80px, hero full-viewport (min-h ~86vh) với đúng 1 CTA coral.
* Aqua scale mới (`--n12-aqua-500/300/100`) cho EAC/dòng biển; ocean scale đậm-bão-hòa hơn.

Mỗi màn vẫn đúng một nút coral (*One coral action*, REQ-UX-01).

```css
/* brand scales */
--n12-ocean-950:#041A2E; --n12-ocean-900:#06304F; --n12-ocean-700:#0E5A94;
--n12-ocean-500:#1E9AD6; --n12-ocean-300:#6FCBEE; --n12-ocean-100:#DFF3FB;
--n12-aqua-500:#19B8DD;  --n12-aqua-300:#63D8F0;  --n12-aqua-100:#D9F5FB;
--n12-coral-600:#E85535; --n12-coral-500:#FF6B4A; --n12-coral-100:#FFE6DE;
--n12-sun-400:#FFB347;   --n12-sun-100:#FFF3DC;   --n12-amber-700:#935F00;
--n12-seafoam-500:#2E9E6B; --n12-seafoam-200:#DFF3EC;
--n12-sand-50:#F7FAFB;   --n12-sand-100:#ECF2F5;  --n12-ink-900:#0F1E2A;
```

**Màu trạng thái học tập (REQ-UX-03 — đỏ KHÔNG dùng cho học tập):**

| Trạng thái | Token | Màu |
| --- | --- | --- |
| Vững / mastered | `--state-solid` | seafoam-500 |
| Đang học | `--state-learning` | ocean-500 |
| Cần ôn lại | `--state-review` | sun-400 (text: amber-700) |
| Chưa đo / chưa đủ evidence | `--state-unknown` | neutral #B9C4CC |
| Lỗi **hệ thống** (duy nhất được đỏ) | `--destructive` | #C0392B |

**Logo (SRC-049):** cá hề cam `NemoLogo` — SVG procedural (thân #FF7A2F, viền #E85535, 3 sọc trắng), zero-asset; favicon.svg cùng hình. Dùng ở header mọi portal + trang public thay emoji 🐟/🐠.

School themes (accent + deep + soft + ink; gradient hero = `linear-gradient(135deg, deep, accent)`):

| Theme | accent | deep | soft | ink (text trên soft) |
| --- | --- | --- | --- | --- |
| turtle | #2E9E6B | #1D7A4F | #E2F5EB | #14603C |
| shark | #E4572E | #B33C1B | #FCE7DE | #96371A |
| octopus | #7C5CBF | #5B3F99 | #EFE9FA | #55398F |
| squid | #1F7AB8 | #14578A | #E1F0FA | #175E8E |
| ray | #C99700 | #9A7400 | #FBF2D3 | #7C5E00 |
| whale | #145DA0 | #0E4578 | #E0EBF7 | #114C82 |
| marlins | #0E7C86 | #095E66 | #DFF2F4 | #0A5B63 |
| coral (staff) | #E85535 | #B33C1B | #FFE6DE | #96371A |

(Palette v0.3 theo chỉ đạo SRC-067, supersede Q-107/Q-056. Dark mode chưa bật — Q-109.)

## 2c. Xưởng hình ảnh N12Art (SRC-068) — CẤM emoji cho icon chính

Một ngôn ngữ hình duy nhất, SVG procedural zero-asset trong `N12Art.tsx` (copy per-app như NemoLogo):

* **Mascot** `<Mascot school="turtle|shark|octopus|squid|ray|whale|dolphin|marlin" />`: DNA Pixar-cute chung — mắt to trắng + đồng tử lệch + glint, thân gradient dọc 2 stop theo hue của school, highlight bóng nước góc trên trái, miệng cười nhỏ. Dùng ở school card, School Home hero, header badge (`em` mode = 1em).
* **Icon** `<N12Icon name="…" />` (28 icon): ẩn dụ đại dương thay đồ vật văn phòng — lab=sứa phát sáng, exam=cuộn hải đồ, goal=hải đăng, map=bản đồ san hô, compass=la bàn, portrait=khung tranh có cá hề, family=cá bố+cá con, treasure=rương ngọc, review=dòng hải lưu, progress=cá vượt sóng, practice=cá qua vòng san hô, shell=vỏ ốc xoắn (tư duy sâu), crab=cua (dự án/robotics), puzzle, anchor=mỏ neo (công cụ), speak=bong bóng thoại, quill=bút mực, grad=mũ tốt nghiệp, world=địa cầu sóng, foundation=rạn san hô tầng, submarine=tàu ngầm vàng (tự học), bottle=thư trong chai (lời mời), starfish=sao biển, trophy=cúp, flag=cờ đích, heart=tim san hô, books=sách sóng, learner=cá hề con. Đặt trong khung `n12-icon`/`n12-icon-lg`.
* **`<ArtIcon e="🧪" />`**: map emoji cũ → icon N12Art (EMOJI_MAP), fallback hiện emoji nếu chưa map — dùng khi icon đến từ DATA (schoolsInfo.method, DOMAIN_ICON…); code mới thì dùng thẳng `N12Icon`.
* **Scene** `<Seafloor />`: silhouette đáy biển (2 lớp cồn cát + san hô + rong + sao biển) đặt absolute đáy hero (`n12-hero*` đã là relative).
* **Kích thước** (SRC-069): mascot trên school card/hero = `h-36` trần trụi (không bó vòng tròn nhỏ); khung `n12-icon` 3.5rem, `n12-icon-lg` 7rem, `n12-icon-xl` 10rem — icon phải TO, dễ nhìn. Logo = bản 3D (radial gradient + rim light); Turtle = dáng đang bơi; Shark = dáng lao tới.
* \~~**Màu school trên card + hover** (SRC-071): `.n12-card-school` = wash accent 9%→4%~~ → **bỏ ở v0.4 (SRC-160)**: `.n12-card-school` không còn nhuộm nền, chỉ còn vạch `--school-mark` 2px ở mép trên. `.n12-swim` (mascot bơi lên 8px + scale 1.06 khi hover) giữ nguyên. Icon phải là **vật thể biển** — không dùng đồ vật văn phòng/đất liền.
* Quy tắc: icon chính/card/hero **không dùng emoji** (emoji chỉ còn cho phần chưa vẽ và chi tiết chữ nhỏ); icon mới → thêm vào N12Art theo đúng DNA, không chế tại chỗ.
* **Mọi hình minh hoạ trong bài học vẽ bằng CODE, không bitmap** (SRC-255): trục số, thanh phân số, hình học đều là SVG procedural. Bitmap không đổi theo số liệu của đề, không co giãn theo màn, và mỗi hình là một file phải nuôi; hình vẽ bằng code thì một bộ dựng hình phục vụ được hàng trăm câu — bộ hiện tại gắn **131 hình cho 9 node** chỉ bằng luật đọc số từ chính đề bài, câu nào không đọc ra số thì không có hình còn hơn có hình sai.
* Hai luật đọc số đi kèm, vì cả hai đều đã sinh ra hình sai: bỏ **dấu chấm phân nhóm hàng nghìn** trước khi bắt số (`50.618` là MỘT số, không phải hai); hai nhãn nằm gần nhau trên trục thì **so le trên/dưới** thay vì đè lên nhau.
* **Icon Thoát là dấu nhân**, không phải mũi tên hay chữ (SRC-255) — cùng lý do §2·0 cấm ký tự mũi tên/tam giác trong khung giao diện.

## 2d. Nguồn dữ liệu school duy nhất (SRC-090)

`apps/learn/src/schoolsInfo.ts` là **nguồn chuẩn** cho mọi surface (web, learn, marlins, coral): code/emoji/name/label/theme/tagline/goal/forWho/method/outcomes/areas + `SCHOOL_SUBJECTS`. Các app khác giữ **bản copy y hệt** (không import chéo app, giống N12Art/NemoLogo) — sửa ở learn rồi copy sang, KHÔNG tự viết danh sách school riêng. Tagline/theme/mascot vì thế luôn khớp giữa nemo12.com, learn và marlins.

## 3. Typography (REQ-UX-04) — kế thừa ràng buộc CÓ CHỦ ĐÍCH

**Arial** toàn hệ (chỉ đạo 2026-08-16), **không webfont** (Q-110), **sàn 14px**, body 16px, line-height 1.6.

Một token duy nhất chi phối cả hệ: `--font-sans` trong `packages/design-system/tokens/base.css`. Mọi app đều `@import "@nemo12/design-system/index.css"` nên đổi ở đây là đổi cả learn · web · marlins · coral · admin · mentors; pearl bắc cầu qua `--vp-font-family-base`. Hai site VitePress (pearl và docs) dùng chung một bridge `packages/design-system/vitepress.css` (REQ-DOC-10, SRC-495) — cùng token, không fork theme.

Chuỗi dự phòng là `Arial, Helvetica, "Liberation Sans", "Nimbus Sans", sans-serif`: Helvetica cho macOS đời cũ; **Liberation Sans** và **Nimbus Sans** là hai bản thay thế **đúng số đo** của Arial trên Linux — thiếu chúng thì máy Linux (gồm cả máy chạy CI và chụp ảnh kiểm giao diện) rơi về một font sans bất kỳ và chữ xô lệch so với bản đã duyệt.

**Hai màn cấp cao nhất của marlins, và một cái bẫy của luật remap (SRC-858).** `<main>` màn "Đang tải"
(`App.tsx`) và `ErrorBoundary` nằm NGOÀI `<Dashboard>`, tức ngoài mọi `od-root`, nên chúng render ở thế giới
SÁNG (`bg-sand`) giữa một cổng biển sâu — còn `contrast-check.mjs` lại khai cả marlins là
`.theme-marlins.od-root`, nghĩa là cổng đo một thế giới, người dùng nhìn một thế giới khác. Chưa sai kết luận
chỉ vì auto-scan chỉ ghép `bg-*` với `text-*` khi chúng đứng CÙNG một `className`, mà ở hai màn này nền nằm
trên `<main>` còn chữ nằm ở thẻ con: không cặp nào được tạo nên không có phán quyết nào để sai. Nay hai màn
mang `theme-marlins od-root`.

Cái bẫy đi kèm, đo được chứ không suy: luật remap là `.theme-marlins.od-root .text-muted-foreground` —
selector **hậu duệ**. Viết `text-muted-foreground` ngay trên CHÍNH thẻ mang hai lớp ấy thì không khớp, và màu
rơi về bản của thế giới sáng (`rgb(82,100,115)`, xám đậm trên nền biển). Chữ phải nằm ở thẻ CON
(`rgb(169,198,230)`, đúng `--n12-deep-ink-muted`). Hai cách viết trông như nhau khi đọc mã; chỉ mở trình
duyệt đo mới thấy.

**Màn thông báo ra mắt của learn (SRC-857).** `LaunchNotice` là chỗ cuối cùng trong learn còn mang
`n12-hero-ocean`, nay về `n12-deep` như cả app. Nó đáng sửa dù chỉ sống tới 08:00 20.09: đây là màn ĐẦU
TIÊN mọi tài khoản thường gặp sau khi đăng nhập, nên ấn tượng đầu tiên về sản phẩm lại là một sắc xanh
không có ở bất kỳ màn nào sau đó.

**Thứ tự hai dòng `backdrop-filter` (SRC-852).** Viết bản chuẩn trước rồi `-webkit-` sau thì trình rút gọn
(lightningcss) bỏ mất bản chuẩn: CSS lên production chỉ còn `-webkit-backdrop-filter`, nên Chrome và Safari
vẫn mờ còn **Firefox thì không**. Phát hiện khi đo CSS thật trên `marlins.nemo12.com`, không phải khi đọc mã.
Luật: **`-webkit-` đứng trước, bản chuẩn đứng sau** — áp cho cả `.n12-panel` lẫn quy tắc thẻ
`.theme-marlins.od-root [data-slot="card"]` trong DS, vì cả hai đều đang viết ngược. Kiểm bằng cách grep bản
build (`apps/*/dist/assets/*.css`), không bằng cách đọc file nguồn: lỗi này chỉ hiện ra sau khi rút gọn.

### 3b. Luật chữ to có dấu (SRC-160)

Chủ dự án nêu hai quan sát trong cùng một ngày: *"Arial mà font chữ nhỏ dưới 15, thì tôi thấy vô cùng đẹp"* và *"các ký tự tiếng Việt, có dấu, và font to tầm 20 trở lên, thì thường bị lỗi hoặc nhìn xấu xấu"*. Hai câu đó không mâu thuẫn — chúng mô tả đúng lý do ngành chữ có khái niệm **optical size**: một mặt chữ đẹp ở 13px cần nét dày và dấu tách xa thân chữ; phóng lên 34px thì nét hoá thô và dấu thanh chồng sát mũ (ế ộ ữ ặ). Arial chỉ có **một** bản vẽ cho mọi cỡ nên được đầu này mất đầu kia.

Đã dựng bản đối chiếu Arial ↔ Inter (font có trục optical size) ở 13/15/20/28/40px; chủ dự án xem rồi **chốt giữ Arial**. Vậy không đổi được mặt chữ thì chặn khuyết điểm bằng bốn luật, khoá sẵn ở thẻ tiêu đề trong `index.css` để không phải nhớ lại ở từng trang:

1. `line-height` ≥ **1.22** cho h1/h2/h3 — dấu hai tầng cần chỗ, chật là chạm dòng trên.
2. **Không** `letter-spacing` âm ở cỡ lớn — Arial bóp lại là dấu chữ này đụng chữ bên cạnh (v0.3 từng đặt `tracking-tight`, đó là một phần nguyên nhân).
3. Đậm tối đa **700** và chỉ ở h1; Arial Bold cỡ lớn làm dấu bết thành một khối.
4. Trần cỡ hiển thị **32px**. Trên ngưỡng đó khuyết điểm của Arial lộ rõ, và không có cách CSS nào cứu được.

#### Vì sao luật 3 là luật đắt nhất, và vì sao đã cần một cổng (SRC-795)

Bốn luật trên bị phá suốt nhiều tuần trên production mà không ai thấy, vì khối khoá ở
`packages/design-system/index.css` nằm trong `@layer base` — mà trong Tailwind v4 `utilities`
**luôn** thắng `base`. Một chữ `tracking-tight` viết thẳng lên thẻ `<h1>` là luật im lặng biến mất.
Đã có **114 chỗ** như vậy trên cả 7 app React khi đi đếm.

Chủ dự án phát hiện ngày 2026-09-17 khi mở nemo12.com **trên Windows**. Cơ chế, đã tái hiện được
trên macOS bằng cách ép font:

* `font-extrabold` là weight **800**. Arial không có bản vẽ 800. Trên macOS, Chrome giữ nguyên
  Arial Bold (đo được: ascent 0,896em, bề rộng y hệt weight 700).
* Trên Windows, Arial và **Arial Black** nằm cùng một họ theo mô hình font của hệ, nên weight 800
  rơi sang Arial Black — một mặt chữ KHÁC, ascent **1,10em**.
* Nét gạch `.n12-swash` bản cũ neo bằng `background-position: 0 0.78em`, tức đo từ mép TRÊN của hộp
  nội dung. Ascent đổi 0,90em → 1,10em thì nét trôi lên ~10px ở cỡ 48px: từ chỗ đè bụng chữ thành
  **xuyên giữa chữ**, trông đúng như chữ bị gạch bỏ.
* Cùng lúc đó Arial Black ở 48px làm dấu hai tầng bết lại — đúng triệu chứng thứ hai chủ dự án tả.

Một nguyên nhân, hai triệu chứng, và macOS che cả hai. Rút ra ba điều, đã áp:

1. `.n12-swash` neo bằng `text-underline-offset` (đo từ **đường chân chữ**), không bằng
   `background-position` (đo từ mép hộp). Chỗ nào cần nét trang trí bám chữ thì theo cách này.
2. Luật 3 không phải chuyện thẩm mỹ. Weight vượt 700 trên một font chỉ có hai bản vẽ là **đổi mặt
   chữ**, và mỗi hệ điều hành đổi sang một thứ khác nhau.
3. Luật nào chỉ sống trong `@layer base` thì không phải luật. `scripts/check-vi-type.mjs` (QG-014c,
   trong `npm run check:code`) kiểm mọi `.tsx` của `apps/` và `packages/`: cỡ từ **20px** trở lên
   (ngưỡng lấy từ chính lời chủ dự án ở SRC-160) mà có `leading` < 1.22, `tracking` âm, weight trên
   700, hoặc vượt trần cỡ, là đỏ.

Lối ra hợp lệ của cổng là ba lớp đánh dấu `n12-type-exempt-num` · `-emoji` · `-decor`, chỉ dùng cho
nội dung **không có dấu tiếng Việt** (số liệu, emoji, cờ, chữ cái trang trí). Tên lớp tự nó là lý do;
cổng cố ý không nhận một cờ trống.

Cũng bỏ `-webkit-font-smoothing: antialiased` (đổi sang `subpixel-antialiased`): nó bào mỏng nét trên macOS, và thứ mỏng đi trước tiên chính là dấu tiếng Việt hai tầng.

Type scale 5 bậc qua utility Tailwind:

| Bậc | Dùng cho | Spec |
| --- | --- | --- |
| Display | Hero school, chào learner | ≤32px, `font-bold`, **không** tracking âm (§3b) |
| Title | Tiêu đề màn | `text-2xl font-bold` |
| Section | `n12-section-title` | 17px font-bold + màu ocean-900 |
| Body | mặc định | 16px/1.6 |
| Caption | phụ chú, meta | 14px (KHÔNG nhỏ hơn), `text-muted-foreground` |

### 3c. Công thức toán luôn đi qua `<MathText>` (SRC-249)

**Mọi chuỗi đến từ dữ liệu bài học đều phải render qua `<MathText>`**, không riêng màn làm bài: đề bài, từng phương án, đáp án, và **cả màn xem lại cuối phiên**. Chuỗi `$\frac{5}{10}$` lọt ra nguyên dạng thì learner đọc được cú pháp LaTeX chứ không đọc được phân số — và chỗ nó lọt ra thường là màn phụ mà không ai mở lại khi kiểm.

Vì luật này chỉ hỏng ở chỗ người ta quên, nó được khoá bằng **test đếm**: mọi lần chèn `{it.prompt}` hoặc `{opt}` vào JSX đều phải nằm trong `<MathText>`. Thêm một màn mới hiện nội dung bài học thì test bắt ngay, không chờ ảnh chụp màn hình của chủ dự án.

Kèm theo là luật nội dung: **phương án nhiễu phải có người chọn được**. Bài rút gọn phân số sinh nhiễu bằng cách trừ đi `k` nên khi tử bằng 1 ra `$\frac{0}{5}$` — không ai chọn, và câu bốn phương án thực chất chỉ còn ba.
