DS-001 · Visual v0.3, N12Art, nguồn school và typography
Một phần của DS-001. 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).
/* 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 (emmode = 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 khungn12-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ẳngN12Icon.- 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-36trần trụi (không bó vòng tròn nhỏ); khungn12-icon3.5rem,n12-icon-lg7rem,n12-icon-xl10rem — 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):→ bỏ ở v0.4 (SRC-160):.n12-card-school= wash accent 9%→4%.n12-card-schoolkhông còn nhuộm nền, chỉ còn vạch--school-mark2px ở 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.618là 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:
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.- 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 đặttracking-tight, đó là một phần nguyên nhân). - Đậ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.
- 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 utilitiesluô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-extraboldlà 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-swashbản cũ neo bằngbackground-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:
.n12-swashneo bằngtext-underline-offset(đo từ đường chân chữ), không bằngbackground-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.- 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.
- Luật nào chỉ sống trong
@layer basethì không phải luật.scripts/check-vi-type.mjs(QG-014c, trongnpm run check:code) kiểm mọi.tsxcủaapps/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.