# Vui Cười Lên - Brand Guidelines (bản cho AI agent & developer)

> **Version 1.0 · Cập nhật 2026-08-27**
> Bản người xem: https://vuicuoilen.vn/brand
> Token máy đọc: https://vuicuoilen.vn/data/brand.json
> Token CSS: https://vuicuoilen.vn/css/brand-tokens.css

---

## 0. Đọc cái này trước (dành cho AI agent)

Khi bạn được giao làm bất cứ thứ gì mang thương hiệu Vui Cười Lên (website, landing page, slide, banner, email, tài liệu, app):

1. **Cần nhắc tới dịch vụ, giá, bài viết hay đường dẫn của site thì đọc `https://vuicuoilen.vn/llms.txt` TRƯỚC. Không bịa.** Xem mục 0b.
2. Tải `css/brand-tokens.css`, import trước mọi stylesheet khác, dùng `var(--token)` thay vì hardcode hex. File này ngoài token còn **kèm sẵn** thanh cuộn hồng, màu bôi đen, chặn trượt ngang và khối `prefers-reduced-motion`, không phải chép lại.
3. Không tự sinh màu mới. Cần sắc độ khác thì lấy `rgba()` của màu brand có sẵn.
4. Font duy nhất: **Nunito**. Không thêm font thứ hai.
5. Trước khi đặt chữ lên nền màu, tra bảng tương phản ở mục 3. Hồng `#FF69B4` **không được** dùng làm màu chữ trên nền trắng.
6. Viết tiếng Việt theo mục 8. Không dùng dấu gạch dài, không hứa hẹn tuyệt đối.
7. Mỗi màn hình chỉ một nút primary.
8. Icon dùng Lucide dạng nét (stroke), không dùng emoji trong UI.
9. **Mọi khối logo bọc trong `<a href="/">`**, xem mục 2.
10. **Thanh cuộn dùng gradient hồng VCL**, không để mặc định xám. Xem mục 5b.
11. **Có trạng thái tải đầy đủ**, nút submit bắt buộc có khối `finally`. Xem mục 5c.

Nếu yêu cầu của người dùng mâu thuẫn với tài liệu này, làm theo người dùng nhưng nói rõ chỗ lệch chuẩn.

---

## 0b. Nội dung site: đọc llms.txt, đừng đoán

Tài liệu này quy định **hình thức**: màu, font, component. Nó không chứa **nội dung**: có bao nhiêu dịch vụ, giá bao nhiêu, bài viết nào đang đăng. Dữ liệu đó thay đổi liên tục và chỉ có một nguồn đúng.

| Bạn cần | Đọc ở đâu |
|---|---|
| Màu, font, component, quy tắc thiết kế | `/data/brand.json` |
| **Danh sách dịch vụ, giá, mô tả** | `/llms.txt` |
| **Bài viết blog, chuyên mục, tiêu đề** | `/llms.txt` |
| **Cấu trúc URL, biểu mẫu, chính sách** | `/llms.txt` |
| Toàn bộ URL đang có | `/sitemap.xml` |
| Chi tiết một trang cụ thể | Đọc thẳng chính trang đó |

> **Không được bịa nội dung site.** Không tự nghĩ ra tên dịch vụ, không tự đặt giá, không tự chế tiêu đề bài viết, không tự tạo đường dẫn. `llms.txt` được sinh động từ dữ liệu thật trong KV, luôn đúng tại thời điểm đọc. Bịa ra một mức giá sai là làm hại khách hàng thật.

`llms.txt` có sẵn: giới thiệu công ty, cấu trúc URL đầy đủ, danh sách dịch vụ kèm giá từng gói lấy trực tiếp từ KV, danh sách bài viết blog kèm tiêu đề và chuyên mục, biểu mẫu đang mở, chính sách, đội ngũ, thống kê, và tóm tắt bộ nhận diện.

```bash
# Luôn đọc cả hai trước khi bắt tay
curl -s https://vuicuoilen.vn/llms.txt        # NỘI DUNG: dịch vụ, bài viết, URL
curl -s https://vuicuoilen.vn/data/brand.json # HÌNH THỨC: màu, font, component

# Cần biết toàn bộ URL đang tồn tại
curl -s https://vuicuoilen.vn/sitemap.xml
```

| Sai | Đúng |
|---|---|
| Viết landing page rồi tự đặt "Gói Premium 5.000.000đ/tháng" vì nghe hợp lý, trong khi bảng giá thật không có gói đó | Đọc `llms.txt`, lấy đúng tên gói và giá đang niêm yết, rồi mới dựng trang |

---

## 1. Thương hiệu

| Trường | Giá trị |
|---|---|
| Tên thương hiệu | Vui Cười Lên |
| Viết tắt | VCL |
| Pháp nhân | CÔNG TY TNHH TRUYỀN THÔNG, CÔNG NGHỆ VÀ GIẢI TRÍ, VUI CƯỜI LÊN |
| MST | 0319374592 |
| Thành lập | 2026 |
| Domain | vuicuoilen.vn |
| Email | hi@vuicuoilen.vn |
| Hotline | 0981875139 |
| Địa chỉ | E10/211, ấp 9, Xã Tân Nhựt, Bình Chánh, TP.HCM |
| Tagline | Đồng hành cùng bạn trong kinh doanh |
| Lĩnh vực | Truyền thông, Marketing, Công nghệ, Giải trí |

**Cách viết tên:** "Vui Cười Lên" viết hoa 3 chữ cái đầu, có dấu đầy đủ. Trong văn bản kỹ thuật hoặc tên file có thể dùng `vuicuoilen` hoặc `VCL`. Không viết "VUI CƯỜI LÊN" toàn hoa trừ khi trong logo hoặc tiêu đề pháp lý.

---

## 2. Logo

### Cấu tạo
Logo là **lockup 3 chữ SVG** `V` `C` `L` xếp ngang, căn đáy, cách nhau 2px, kèm wordmark "Vui Cười Lên" bên phải.

| Asset | URL |
|---|---|
| Logo đầy đủ (PNG) | `https://raw.githubusercontent.com/trinhleminhan/img-all-site/main/vuicuoilen.com/vuicuoilen.png` |
| Favicon (PNG) | `https://raw.githubusercontent.com/trinhleminhan/img-all-site/main/vuicuoilen.com/favicon.png` |
| Chữ V (SVG) | `.../vuicuoilen.com/V-VCL.svg` |
| Chữ C (SVG) | `.../vuicuoilen.com/C-VCL.svg` |
| Chữ L (SVG) | `.../vuicuoilen.com/L-VCL.svg` |

### Markup chuẩn

```html
<a href="/" class="logo">
  <div class="logo-letters">
    <img src=".../V-VCL.svg" alt="V" class="logo-letter">
    <img src=".../C-VCL.svg" alt="C" class="logo-letter">
    <img src=".../L-VCL.svg" alt="L" class="logo-letter">
  </div>
  <span class="logo-text">Vui Cười Lên</span>
</a>
```

```css
.logo-letters { display:flex; align-items:flex-end; gap:2px; height:45px; }
.logo-letter  { height:40px; width:auto; transform-origin:bottom center;
                transition: transform .3s var(--ease-bounce); }
.logo-text    { font-size:1.5rem; font-weight:800;
                background: var(--gradient-primary);
                -webkit-background-clip:text; background-clip:text;
                -webkit-text-fill-color:transparent; }
```

### Quy tắc link: logo luôn dẫn về trang chủ

**Bắt buộc.** Bất kỳ chỗ nào chèn khối logo để điều hướng, phần tử bao ngoài phải là thẻ `<a>` trỏ về `/`. Chỉ hai chỗ được miễn: màn hình loading và khối trưng bày logo trong chính tài liệu này. Người dùng đã quen bấm logo để về trang chủ, một logo không bấm được là lỗi trải nghiệm.

| Vị trí | Đích đến | Ghi chú |
|---|---|---|
| Header | `/` | Bắt buộc, kèm `aria-label="Vui Cười Lên, về trang chủ"` |
| Footer | `/` | Bắt buộc, dùng chung markup với header |
| Màn hình loading | không link | Ngoại lệ, loading tự biến mất |
| Khối trưng bày logo | không link | Ngoại lệ, là mẫu vật để xem. Chỉ áp dụng trong trang /brand |
| Trang lỗi 404 | `/` | Quan trọng nhất, lối thoát duy nhất của người dùng |
| Email, PDF | `https://vuicuoilen.vn` | URL tuyệt đối, không dùng đường dẫn tương đối |
| Trên chính trang chủ | `/` | Vẫn giữ link, bấm sẽ tải lại trang, đó là hành vi người dùng mong đợi |

```html
<!-- ĐÚNG: cả khối logo nằm trong một thẻ a -->
<a href="/" class="logo" aria-label="Vui Cười Lên, về trang chủ">
  <div class="logo-letters">
    <img src=".../V-VCL.svg" alt="" class="logo-letter">
    <img src=".../C-VCL.svg" alt="" class="logo-letter">
    <img src=".../L-VCL.svg" alt="" class="logo-letter">
  </div>
  <span class="logo-text">Vui Cười Lên</span>
</a>

<!-- SAI: logo trơ, không bấm được -->
<div class="logo"><img src=".../vuicuoilen.png" alt="Vui Cười Lên"></div>

<!-- SAI: mỗi chữ một link, screen reader đọc thành ba link rời -->
<a href="/"><img src=".../V-VCL.svg" alt="V"></a>
<a href="/"><img src=".../C-VCL.svg" alt="C"></a>
<a href="/"><img src=".../L-VCL.svg" alt="L"></a>
```

- **Một link duy nhất bọc cả khối**, không tách mỗi chữ một link.
- **Ảnh bên trong để `alt=""`**, đặt `aria-label` ở thẻ `a`. Ba chữ cái là hình trang trí, tên thương hiệu đã nằm ở wordmark.
- **Vùng bấm tối thiểu 44px chiều cao.** Logo footer chỉ cao 28px nên cần thêm padding dọc.
- **Con trỏ phải là `pointer`** khi rê vào.

### Kích thước
- Chiều cao chữ tối thiểu: **28px** (footer). Chuẩn header: 40px.
- Khoảng trống bảo vệ quanh logo: tối thiểu bằng chiều cao chữ `V`.

### Trên nền tối
Giữ nguyên màu 3 chữ. Wordmark chuyển sang trắng đặc `#FFFFFF`, bỏ gradient clip.

### Hiệu ứng đặc trưng: "Happy Jump"
Khi hover logo, 3 chữ nhún lên lần lượt (delay 0s / 0.08s / 0.16s), easing `cubic-bezier(0.34, 1.56, 0.64, 1)`. Đây là chữ ký chuyển động của thương hiệu, dùng lại được cho icon và card nổi bật.

```css
@keyframes logoJump {
  0%   { transform: translateY(0)     scaleY(1)    scaleX(1); }
  20%  { transform: translateY(0)     scaleY(.85)  scaleX(1.08); }
  40%  { transform: translateY(-12px) scaleY(1.1)  scaleX(.95); }
  60%  { transform: translateY(-14px) scaleY(1.05) scaleX(1); }
  75%  { transform: translateY(-4px)  scaleY(1)    scaleX(1); }
  85%  { transform: translateY(0)     scaleY(.95)  scaleX(1.03); }
  100% { transform: translateY(0)     scaleY(1)    scaleX(1); }
}
```

### Cấm
- Đổi màu logo sang màu ngoài bộ brand
- Xoay, nghiêng, bóp méo tỉ lệ
- Thêm đổ bóng, viền, outline vào logo
- Đặt logo lên ảnh nền rối mà không có lớp phủ tối
- Dùng logo dưới 28px chiều cao
- Tách rời hoặc đổi thứ tự 3 chữ V C L

### Favicon
```html
<link rel="icon" type="image/png" href=".../favicon.png">
<link rel="apple-touch-icon" href=".../favicon.png">
```
Kích thước nguồn tối thiểu 512x512. Bản hiện tại dùng nền tối để logo hồng nổi trên thanh tab sáng, giữ nguyên. Ở 16px chỉ còn nhận ra khối hồng, đó là chủ ý, không thêm chi tiết.

---

## 3. Màu sắc

### Bảng màu đầy đủ

| Token | Hex | Vai trò | Tương phản / trắng | Dùng làm chữ trên nền trắng? |
|---|---|---|---|---|
| `--primary-color` | `#FF69B4` | Màu chủ đạo: nền nút, gradient, icon, viền | 2.65 | **Không** |
| `--primary-dark` | `#E91E8C` | Hover, đáy gradient, active | 4.18 | Chỉ chữ lớn (≥24px) |
| `--primary-light` | `#FFAEC9` | Đỉnh gradient, nền mềm, `::selection` | 1.73 | **Không** |
| `--primary-deep` | `#C2185B` | **Chữ hồng, link, nút chữ nhỏ** | 5.87 | Có |
| `--secondary-color` | `#9B59B6` | Badge phụ, biểu đồ, phân nhóm | 4.67 | Chỉ chữ lớn |
| `--secondary-dark` | `#8E44AD` | Hover secondary, chữ tím | 5.87 | Có |
| `--secondary-light` | `#BB8FCE` | Nền, viền mềm | 2.61 | **Không** |
| `--accent-color` | `#FFD1DC` | Chỉ làm nền: badge, tag, khối nổi | 1.36 | **Không** |
| `--success-color` | `#2ECC71` | Thành công, đã thanh toán | 2.10 | Không, chữ dùng `#1E8449` |
| `--warning-color` | `#F39C12` | Chờ xử lý, cảnh báo | 2.19 | Không, chữ dùng `#B35C00` |
| `--danger-color` | `#E74C3C` | Lỗi, huỷ, xoá | 3.82 | Không, chữ dùng `#C0392B` |
| `--info-color` | `#3498DB` | Thông tin trung tính | 3.15 | Không |
| `--dark-color` | `#2D2D2D` | Nền footer, nền tối, tiêu đề | 13.77 | Có |
| `--light-color` | `#FFF0F5` | Nền section xen kẽ, viền input | 1.10 | Nền only |
| `--text-color` | `#333333` | Chữ body | 12.63 | Có |
| `--text-light` | `#666666` | Chữ phụ, mô tả | 5.74 | Có |
| `--text-muted` | `#999999` | Placeholder, chữ ≥ 24px | 2.85 | **Không** cho chữ thường |
| `--border-color` | `#EAEAEA` | Viền chia khối | | Nền only |

### Quy tắc 60/30/10
- **60%** nền trung tính: trắng `#FFFFFF` + hồng phớt `#FFF0F5`
- **30%** chữ và cấu trúc: `#333333`, `#2D2D2D`
- **10%** hồng thương hiệu

Hồng là **điểm nhấn**, không phải nền chính. Một trang phủ hồng toàn bộ là sai chuẩn.

### Gradient

```css
--gradient-primary: linear-gradient(180deg, #FFAEC9 0%, #FF69B4 100%); /* mặc định: nút, badge */
--gradient-warm:    linear-gradient(135deg, #FF69B4 0%, #FFAEC9 100%); /* hero, khối lớn */
--gradient-purple:  linear-gradient(135deg, #9B59B6 0%, #FF69B4 100%); /* nhấn đặc biệt, hiếm dùng */
```
Chữ trên `gradient-primary` phải là **trắng, weight ≥ 600, size ≥ 16px**. Không đặt chữ nhỏ hoặc chữ mảnh lên gradient.

### Màu trạng thái, cách dùng đúng

| Trạng thái | Nền badge | Màu chữ | Icon Lucide |
|---|---|---|---|
| Thành công | `rgba(46,204,113,.12)` | `#1E8449` | `check-circle` |
| Chờ xử lý | `rgba(243,156,18,.12)` | `#B35C00` | `clock` |
| Lỗi / huỷ | `rgba(231,76,60,.12)` | `#C0392B` | `x-circle` |
| Thông tin | `rgba(52,152,219,.12)` | `#1F6391` | `info` |

Không bao giờ truyền tải trạng thái **chỉ bằng màu**, luôn kèm icon hoặc chữ.

---

## 4. Typography

**Font duy nhất: Nunito.** Bo tròn, thân thiện, hỗ trợ đầy đủ dấu tiếng Việt, khớp tính cách vui vẻ của thương hiệu.

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap" rel="stylesheet">
```

Fallback: `'Segoe UI', Tahoma, Geneva, Verdana, sans-serif`

### Thang chữ

| Cấp | Desktop | Mobile | Weight | Line-height |
|---|---|---|---|---|
| H1 | 3rem | 2rem | 800 | 1.2 |
| H2 / section-title | 2.5rem | 1.75rem | 800 | 1.3 |
| H3 | 1.5rem | 1.25rem | 700 | 1.4 |
| H4 | 1.15rem | 1.05rem | 700 | 1.4 |
| Body | 1rem | 1rem | 400 | **1.8** |
| Small | 0.9rem | 0.9rem | 400 | 1.7 |
| Caption / tag | 0.85rem | 0.85rem | 600 | 1.5 |

### Quy tắc tiếng Việt
- Đoạn văn dùng `line-height: 1.8`, không phải 1.5. Dấu thanh tiếng Việt chiếm chiều cao, chữ sát nhau sẽ chạm dấu.
- Bật `text-wrap: pretty`, tắt `hyphens`. Tiếng Việt không gạch nối từ.
- Không viết HOA TOÀN BỘ quá 4 từ. Chữ hoa có dấu đọc rất khó.
- Độ dài dòng lý tưởng 60-75 ký tự, đặt `max-width: 65ch` cho khối văn bản dài.

```css
p, li { line-height: 1.8; text-wrap: pretty; hyphens: none; }
h1, h2, h3, h4 { text-wrap: balance; }
```

---

## 5. Layout & spacing

| Token | Giá trị |
|---|---|
| Container max-width | 1200px |
| Container padding ngang | 20px |
| Section padding dọc | 80px desktop / 50px mobile |
| Grid gap | 30px |
| Thang spacing | 4, 8, 12, 15, 20, 25, 30, 40, 60, 80 (px) |

**Breakpoints:** mobile ≤ 768px · tablet 769-1024px · desktop ≥ 1025px

### Bo góc
| Token | Giá trị | Dùng cho |
|---|---|---|
| `--border-radius` | 12px | Nút, input, card nhỏ |
| `--border-radius-lg` | 20px | Card lớn, khối nổi, ảnh |
| pill | 999px | Badge, tag |
| circle | 50% | Nút icon, avatar |

### Đổ bóng
| Token | Giá trị | Dùng cho |
|---|---|---|
| `--shadow-sm` | `0 2px 4px rgba(0,0,0,.1)` | Nút, header |
| `--shadow-md` | `0 4px 12px rgba(0,0,0,.15)` | Card, hover nút |
| `--shadow-lg` | `0 8px 24px rgba(0,0,0,.2)` | Card hover, dropdown |
| `--shadow-xl` | `0 12px 48px rgba(0,0,0,.25)` | Modal |

Không dùng bóng màu hồng. Bóng luôn là đen trong suốt.

---

## 5b. Thanh cuộn & vùng bôi đen

Thanh cuộn mặc định của trình duyệt màu xám, đặt cạnh giao diện hồng ấm trông rất lạc quẻ. VCL thay bằng thanh cuộn gradient hồng chuyển sang tím.

> **Không cần chép đoạn CSS bên dưới.** `brand-tokens.css` đã kèm sẵn thanh cuộn, màu bôi đen, chặn trượt ngang và khối `prefers-reduced-motion`. Nạp file token là có đủ. Đoạn CSS dưới đây chỉ để bạn biết bên trong nó là gì.

| Thành phần | Giá trị | Ghi chú |
|---|---|---|
| Bề rộng | `12px` | Đủ dày để kéo bằng chuột |
| Rãnh (track) | `#FFF0F5` | Hồng phớt, bo góc 10px |
| Con trượt (thumb) | gradient hồng nhạt → hồng → tím | Bo 10px, viền 2px màu rãnh |
| Thumb hover | gradient đậm hơn một bậc | Viền đổi sang trắng |
| Thumb active | `#E91E8C` đặc | Bỏ gradient khi đang kéo |
| Firefox | `scrollbar-width: thin` | Firefox không hỗ trợ gradient, dùng màu đặc |
| Bôi đen chữ | nền `#FFAEC9`, chữ `#C2185B` | Tỉ lệ 3.4:1 |

```css
/* ===== Thanh cuộn chuẩn VCL ===== */

/* Chrome, Safari, Edge */
::-webkit-scrollbar { width: 12px; height: 12px; }

::-webkit-scrollbar-track {
  background: var(--light-color);
  border-radius: 10px;
}

::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg,
    var(--primary-light) 0%,
    var(--primary-color) 50%,
    var(--secondary-color) 100%);
  border-radius: 10px;
  border: 2px solid var(--light-color);   /* viền cùng màu rãnh, tạo cảm giác nổi */
  transition: all .3s ease;
}

::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(180deg,
    var(--primary-color) 0%,
    var(--primary-dark) 50%,
    var(--secondary-dark) 100%);
  border-color: var(--white);
}

::-webkit-scrollbar-thumb:active { background: var(--primary-dark); }
::-webkit-scrollbar-corner       { background: var(--light-color); }

/* Firefox: không hỗ trợ gradient, dùng màu đặc */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--primary-color) var(--light-color);
}

/* ===== Màu bôi đen chữ ===== */
::selection {
  background: var(--primary-light);
  color: var(--primary-dark);
}
```

**Thanh cuộn ngang.** Bảng rộng, khối code, lưới thẻ ngang đều phải cuộn **bên trong khung của nó**, không được đẩy cả trang trượt ngang. Trang bị trượt ngang trên điện thoại là lỗi nghiêm trọng.

```css
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
body { overflow-x: hidden; }
```

**Không nên:** ẩn hẳn thanh cuộn bằng `::-webkit-scrollbar { display:none }` (người dùng mất mốc vị trí) · làm thanh cuộn mảnh dưới 8px (khó trúng khi kéo) · dùng thư viện cuộn giả lập (chậm và hỏng trên bàn phím) · để trang trượt ngang trên điện thoại.

---

## 5c. Trạng thái tải

Người dùng chờ mà không thấy gì sẽ tưởng trang hỏng. VCL có **ba mức**, chọn theo thời gian chờ dự kiến, không dùng lẫn lộn.

| Mức | Khi nào | Hiển thị | Thời lượng |
|---|---|---|---|
| **1. Màn hình tải** | Vào trang lần đầu | Toàn màn hình, ba chữ V C L nhún nhảy trên nền gradient hồng | Tối đa 1.5 giây |
| **2. Khung xương** | Gọi dữ liệu về, chưa có nội dung | Khối hồng phớt nhấp nháy đúng hình dạng nội dung sắp hiện | Tới khi có dữ liệu |
| **3. Nút đang xử lý** | Gửi form, thêm giỏ hàng | Vòng xoay nhỏ trong nút, nút bị khoá | Tới khi có phản hồi |

### Mức 1: màn hình tải toàn trang

Chỉ dùng khi **vào trang lần đầu**. Đây là phiên bản phóng to của hiệu ứng Happy Jump ở logo.

| Thành phần | Thông số |
|---|---|
| Nền | `var(--gradient-primary)`, phủ kín, `z-index: 9999` |
| Chiều cao chữ | 120px desktop, 80px dưới 480px |
| Nhịp nhảy | `happyJump .8s cubic-bezier(.28,.84,.42,1) infinite`, delay 0s / 0.15s / 0.3s |
| Bóng dưới chân | Ellipse mờ, co còn 50% khi chữ ở đỉnh nhảy |
| Đốm sáng | 5 chấm trắng 8px bay lên rồi mờ dần, chu kỳ 2s |
| Chữ | "Đang tải niềm vui...", trắng, weight 600, nhấp nháy 2s |
| Cách tắt | Thêm class `.hidden`, mờ dần 0.5s |

> **Trần 1.5 giây, không hơn.** Màn hình tải là để che khoảnh khắc trang chưa vẽ xong, không phải để khoe animation. Nếu trang tải xong sớm hơn, tắt ngay chứ đừng chờ hết thời gian.

```html
<div class="loading-screen">
  <div class="loading-logo-container">
    <div class="letter-wrapper">
      <img src=".../V-VCL.svg" alt="" class="loading-letter letter-v">
      <div class="letter-shadow"></div>
    </div>
    <div class="letter-wrapper">
      <img src=".../C-VCL.svg" alt="" class="loading-letter letter-c">
      <div class="letter-shadow"></div>
    </div>
    <div class="letter-wrapper">
      <img src=".../L-VCL.svg" alt="" class="loading-letter letter-l">
      <div class="letter-shadow"></div>
    </div>
    <div class="sparkles">
      <span class="sparkle"></span><span class="sparkle"></span>
      <span class="sparkle"></span><span class="sparkle"></span>
      <span class="sparkle"></span>
    </div>
  </div>
  <p class="loading-text">Đang tải niềm vui...</p>
</div>
```

```css
.loading-screen {
  position: fixed; inset: 0;
  background: var(--gradient-primary);
  display: flex; flex-direction: column;
  justify-content: center; align-items: center;
  z-index: 9999; overflow: hidden;
  transition: opacity .5s ease, visibility .5s ease;
}
.loading-screen.hidden { opacity: 0; visibility: hidden; }

.loading-logo-container {
  display: flex; align-items: flex-end; justify-content: center;
  gap: 8px; margin-bottom: 40px; position: relative; height: 180px;
}
.letter-wrapper { display: flex; flex-direction: column; align-items: center; }

.loading-letter {
  height: 120px; width: auto; transform-origin: bottom center;
  filter: drop-shadow(0 8px 25px rgba(255,255,255,.4));
}
.letter-v { animation: happyJump .8s cubic-bezier(.28,.84,.42,1) infinite;      }
.letter-c { animation: happyJump .8s cubic-bezier(.28,.84,.42,1) .15s infinite; }
.letter-l { animation: happyJump .8s cubic-bezier(.28,.84,.42,1) .3s  infinite; }

@keyframes happyJump {
  0%, 100% { transform: translateY(0)     scaleY(1)    scaleX(1);    }
  10%      { transform: translateY(0)     scaleY(.9)   scaleX(1.05); }
  30%      { transform: translateY(-50px) scaleY(1.1)  scaleX(.95);  }
  50%      { transform: translateY(-55px) scaleY(1.05) scaleX(1);    }
  70%      { transform: translateY(-20px) scaleY(1)    scaleX(1);    }
  80%      { transform: translateY(0)     scaleY(.92)  scaleX(1.04); }
  90%      { transform: translateY(-8px)  scaleY(1.02) scaleX(.98);  }
}

/* Bóng dưới chân, co lại khi chữ lên đỉnh */
.letter-shadow {
  width: 60px; height: 12px; margin-top: 8px; border-radius: 50%;
  background: radial-gradient(ellipse, rgba(0,0,0,.25) 0%, transparent 70%);
  animation: shadowPulse .8s cubic-bezier(.28,.84,.42,1) infinite;
}
.letter-wrapper:nth-child(2) .letter-shadow { animation-delay: .15s; }
.letter-wrapper:nth-child(3) .letter-shadow { animation-delay: .3s;  }

@keyframes shadowPulse {
  0%, 100%  { transform: scaleX(1);   opacity: .6; }
  30%, 50%  { transform: scaleX(.5);  opacity: .2; }
  80%       { transform: scaleX(1.1); opacity: .7; }
}

.loading-text {
  color: #fff; font-size: 1.1rem; font-weight: 600;
  animation: textPulse 2s ease-in-out infinite;
}
@keyframes textPulse { 0%,100% { opacity:.7 } 50% { opacity:1 } }

@media (max-width: 480px) {
  .loading-letter { height: 80px; }
  .loading-logo-container { gap: 5px; height: 130px; }
  .letter-shadow { width: 40px; height: 8px; }
}
```

```js
function initLoading() {
  const screen = document.querySelector('.loading-screen');
  if (!screen) return;

  const hide = () => {
    screen.classList.add('hidden');
    document.body.style.overflow = 'auto';
  };

  // Tắt ngay khi trang sẵn sàng, KHÔNG chờ hết 1.5 giây
  if (document.readyState === 'complete') hide();
  else window.addEventListener('load', hide, { once: true });

  // Chốt chặn: dù có gì kẹt cũng không giữ quá 1.5 giây
  setTimeout(hide, 1500);
}
document.addEventListener('DOMContentLoaded', initLoading);
```

### Mức 2: khung xương

Khung xương phải **đúng hình dạng nội dung sắp hiện**, nếu không trang sẽ nhảy khi dữ liệu về.

```css
.skeleton {
  background: linear-gradient(90deg,
    var(--light-color) 25%,
    #FFE4EF 50%,
    var(--light-color) 75%);
  background-size: 200% 100%;
  border-radius: 8px;
  animation: skelShimmer 1.4s ease-in-out infinite;
}
@keyframes skelShimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
```

- Dùng tông hồng phớt `#FFF0F5` đến `#FFE4EF`, **không dùng xám trung tính**, để khung xương vẫn thuộc về thương hiệu
- Số khối bằng số nội dung thật hoặc ít hơn. Hiện 9 khung xương rồi chỉ về 2 kết quả là làm người dùng hụt hẫng
- Không lồng khung xương quá 3 cấp

### Mức 3: nút đang xử lý

Nút phải bị **khoá ngay lập tức** để người dùng không bấm hai lần tạo hai đơn hàng.

```css
.btn-spinner {
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.35);
  border-top-color: #fff;
  animation: btnSpin .7s linear infinite;
}
@keyframes btnSpin { to { transform: rotate(360deg); } }
```

```js
async function submitForm(btn, payload) {
  const original = btn.innerHTML;
  btn.disabled = true;                       // khoá NGAY, chống bấm hai lần
  btn.innerHTML = '<span class="btn-spinner"></span>Đang gửi...';

  try {
    const res = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
    if (!res.ok) throw new Error('Gửi thất bại');
    showToast('Đã gửi, chúng tôi sẽ liên hệ trong 24 giờ');
  } catch (err) {
    showToast('Không gửi được, vui lòng thử lại hoặc gọi 0981875139');
  } finally {
    btn.disabled = false;                    // LUÔN mở khoá, kể cả khi lỗi
    btn.innerHTML = original;
  }
}
```

> **Lỗi hay gặp nhất:** quên khối `finally`. Khi API lỗi, nút kẹt ở trạng thái "Đang gửi..." vĩnh viễn, người dùng không gửi lại được và bỏ đi.

### Quy tắc chung

- **Dưới 300ms thì đừng hiện gì.** Chớp nháy một cái loading rồi tắt ngay còn khó chịu hơn chờ im lặng
- **300ms đến 1 giây:** khung xương hoặc vòng xoay trong nút
- **Trên 3 giây:** phải nói rõ đang làm gì, không để vòng xoay trống trơn
- **Luôn có đường thoát khi lỗi**, kèm cách khắc phục và hotline

```css
@media (prefers-reduced-motion: reduce) {
  .loading-letter, .letter-shadow,
  .sparkle, .skeleton, .btn-spinner { animation: none; }
  .loading-letter { transform: none; }
}
```

```html
<!-- Báo cho trình đọc màn hình biết đang tải -->
<div class="loading-screen" role="status" aria-live="polite">
  <p class="loading-text">Đang tải niềm vui...</p>
</div>
```

---

## 6. Components

### 6.1 Nút

```css
.btn {
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  padding:12px 24px; font-size:1rem; font-weight:600;
  border-radius: var(--border-radius); transition: var(--transition);
  min-height:44px; cursor:pointer; border:none;
}
.btn-primary   { background: var(--gradient-primary); color:#fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }

.btn-secondary { background:#fff; color: var(--primary-deep); border:2px solid var(--primary-color); }
.btn-secondary:hover { background: var(--primary-color); color:#fff; }

.btn-ghost     { background:transparent; color: var(--text-light); }
.btn-ghost:hover { color: var(--primary-deep); }

.btn-danger    { background: var(--danger-color); color:#fff; }

.btn-icon      { width:45px; height:45px; border-radius:50%; padding:0; }

.btn:disabled  { opacity:.5; cursor:not-allowed; transform:none; box-shadow:none; }
```

| Loại | Khi nào dùng | Số lượng cho phép |
|---|---|---|
| primary | Hành động chính: Liên hệ ngay, Đặt hàng, Gửi form | **1 mỗi màn hình** |
| secondary | Hành động phụ: Xem bảng giá, Tìm hiểu thêm | 1-2 |
| ghost | Hành động nhẹ: Huỷ, Quay lại | không giới hạn |
| danger | Xoá, huỷ đơn | chỉ khi thật sự phá huỷ dữ liệu |

**Nhãn nút:** động từ + tân ngữ, 2-4 từ. Viết "Nhận báo giá" thay vì "Gửi", viết "Đặt lịch tư vấn" thay vì "Submit".

### 6.2 Card

```css
.card {
  background:#fff; border-radius: var(--border-radius-lg); padding:35px;
  box-shadow: var(--shadow-sm); transition: var(--transition);
  position:relative; overflow:hidden;
}
.card::before {
  content:''; position:absolute; top:0; left:0; right:0; height:4px;
  background: var(--gradient-primary); transform: scaleX(0);
  transform-origin:left; transition: var(--transition);
}
.card:hover { transform: translateY(-8px); box-shadow: var(--shadow-lg); }
.card:hover::before { transform: scaleX(1); }
```

### 6.2b Spotlight Card, quầng sáng đi theo con trỏ

Hiệu ứng chữ ký thứ hai của VCL, dùng cho lưới "Đội ngũ chuyên gia" (`.teams-grid > .team-card`) và lưới kênh dịch vụ (`.channel-grid > .channel-card`). Con trỏ đi tới đâu quầng sáng theo tới đó, và **cả cụm card cùng sáng** chứ không chỉ card đang bị hover.

**Hai lớp chồng nhau:**

| Lớp | Vai trò | Bán kính | Alpha |
|---|---|---|---|
| `::before` | Viền phát sáng, chỉ chừa vòng 2px quanh mép | 400px | 0.5 - 0.6 |
| `::after` | Quầng sáng nền, nằm dưới nội dung (`z-index: -1`) | 300px | 0.08 |

**Bốn kỹ thuật:**

1. **Toạ độ con trỏ qua biến CSS.** JS ghi `--mouse-x` / `--mouse-y` lên từng card, CSS đọc vào tâm `radial-gradient`. Mặc định `-999px` đẩy quầng sáng ra ngoài màn hình khi chưa có chuột.
2. **Mẹo mask để chỉ chừa viền.** `padding: 2px` cộng hai lớp mask với `mask-composite: exclude` khoét rỗng phần ruột, chỉ còn vòng viền. Đây là cách duy nhất tạo viền gradient theo `border-radius`, `border-image` không làm được.
3. **Proximity, cả cụm cùng sáng.** Điều kiện là `.teams-grid:hover`, tức con trỏ ở bất kỳ đâu trong lưới, **không phải** `.team-card:hover`. Nhờ vậy card bên cạnh sáng dần khi con trỏ mới tới gần. Đây là chi tiết quan trọng nhất, đừng đổi.
4. **Fade vào nhanh, ra chậm.** Hiện `.15s ease-in`, tắt `.4s ease-out`. Bất đối xứng có chủ đích: vào nhanh cho cảm giác nhạy, ra chậm để không giật khi con trỏ lướt qua mép lưới.

```css
.team-card {
  background:#fff; border-radius: var(--border-radius-lg); padding:30px;
  box-shadow: var(--shadow-sm); position:relative; overflow:hidden;
  border:1px solid rgba(255,255,255,.1);
  transition: transform .3s ease, box-shadow .3s ease;

  /* Tối ưu hiệu năng */
  contain: layout style paint;
  will-change: transform;
  transform: translate3d(0,0,0);
}

/* Lớp 1: viền phát sáng, chỉ chừa lại vòng 2px */
.team-card::before {
  content:''; position:absolute; inset:0; border-radius:inherit;
  padding:2px;
  background: radial-gradient(
    400px circle at var(--mouse-x, -999px) var(--mouse-y, -999px),
    var(--card-glow-color, rgba(255,107,107,.5)),
    transparent 40%
  );
  /* Mẹo mask: phủ kín rồi khoét rỗng ruột */
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
          mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity:0; transition: opacity .4s ease-out; pointer-events:none;
}

/* Lớp 2: quầng sáng nền, nằm dưới nội dung */
.team-card::after {
  content:''; position:absolute; inset:0; border-radius:inherit;
  background: radial-gradient(
    300px circle at var(--mouse-x, -999px) var(--mouse-y, -999px),
    var(--card-glow-color, rgba(255,107,107,.08)),
    transparent 40%
  );
  opacity:0; transition: opacity .4s ease-out; pointer-events:none;
  z-index:-1;
}

/* Proximity: con trỏ ở BẤT KỲ ĐÂU trong lưới thì CẢ CỤM cùng sáng */
.teams-grid:hover .team-card::before,
.teams-grid:hover .team-card::after {
  opacity:1;
  transition: opacity .15s ease-in;   /* vào nhanh, ra chậm */
}

.team-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); }

/* Màu quầng sáng riêng từng nhóm */
.team-card:has(.team-icon.marketing) { --card-glow-color: rgba(255,107,107,.6); }
.team-card:has(.team-icon.legal)     { --card-glow-color: rgba(74,144,164,.6); }
.team-card:has(.team-icon.business)  { --card-glow-color: rgba(102,126,234,.6); }
.team-card:has(.team-icon.dev)       { --card-glow-color: rgba(17,153,142,.6); }
.team-card:has(.team-icon.hr)        { --card-glow-color: rgba(255,105,180,.6); }
.team-card:has(.team-icon.it)        { --card-glow-color: rgba(52,152,219,.6); }
```

**JavaScript theo dõi con trỏ.** Ba nguyên tắc bắt buộc: một listener trên lưới chứ không phải mỗi card một cái, gộp cập nhật vào một khung hình bằng `requestAnimationFrame`, và `passive: true`.

```js
function initCardGlowEffect() {
  const grids = document.querySelectorAll('.teams-grid, .channel-grid');

  grids.forEach(grid => {
    if (grid.dataset.glowInit) return;   // chống gắn listener hai lần
    grid.dataset.glowInit = 'true';

    const cards = Array.from(grid.querySelectorAll('.team-card, .channel-card'));
    if (!cards.length) return;

    let rafId = null;
    let mouseX = 0, mouseY = 0;

    // MỘT listener cho cả lưới
    grid.addEventListener('mousemove', (e) => {
      mouseX = e.clientX;
      mouseY = e.clientY;

      // Bỏ qua nếu đã có khung hình đang chờ.
      // mousemove bắn ~120 lần/giây, màn hình chỉ vẽ 60 lần/giây.
      if (rafId) return;

      rafId = requestAnimationFrame(() => {
        for (let i = 0; i < cards.length; i++) {
          const rect = cards[i].getBoundingClientRect();
          cards[i].style.setProperty('--mouse-x', (mouseX - rect.left) + 'px');
          cards[i].style.setProperty('--mouse-y', (mouseY - rect.top)  + 'px');
        }
        rafId = null;
      });
    }, { passive: true });
  });
}

document.addEventListener('DOMContentLoaded', initCardGlowEffect);
setTimeout(initCardGlowEffect, 1000);  // chạy lại cho card render bằng JS
```

**Vì sao mượt, bốn tối ưu hiệu năng:**

| Kỹ thuật | Tác dụng |
|---|---|
| `requestAnimationFrame` | `mousemove` bắn ~120 lần/giây, màn hình vẽ 60 lần. Gộp còn một lần mỗi khung hình, cắt một nửa việc thừa. |
| `{ passive: true }` | Báo trước handler không gọi `preventDefault`, việc cuộn trang không bị chặn chờ. |
| `contain: layout style paint` | Khoá vùng ảnh hưởng của card, trình duyệt không tính lại layout cả trang. |
| `translate3d(0,0,0)` + `will-change` | Đẩy card lên lớp GPU, quầng sáng vẽ bằng card đồ hoạ thay vì CPU. |

Toàn bộ chuyển động nằm ở `opacity` và `background-position`, hai thuộc tính không gây reflow.

> **Cảnh báo:** `will-change` tốn bộ nhớ video. Chỉ đặt cho card thật sự có hiệu ứng. Đặt hàng loạt sẽ làm chậm ngược lại.

**Màu quầng sáng đến từ đâu.** Biến `--card-glow-color` quyết định màu quầng sáng của từng card. Có **hai cách** cấp giá trị, chọn theo việc card dùng icon hay dùng ảnh.

| Cách | Khi nào dùng | Nguồn màu |
|---|---|---|
| **A. Gán cứng** | Card dùng **icon** Lucide trên nền gradient | Bảng gán sẵn theo nhóm |
| **B. Trích từ ảnh** | Card dùng **ảnh đại diện**: avatar nhân sự, logo đối tác, logo kênh | JS đọc màu chủ đạo của chính tấm ảnh đó |

#### Cách A: gán cứng theo nhóm, dùng cho card icon

Đây là **ngoại lệ có kiểm soát** của bộ màu thương hiệu. Sáu màu này chỉ dùng cho icon và quầng sáng của card đội ngũ. Không mang đi dùng cho nút, chữ hay nền ở nơi khác.

| Nhóm | Màu quầng sáng | Gradient icon |
|---|---|---|
| Marketing | `rgba(255,107,107,.6)` | `#FF6B6B → #FF8E53` |
| Pháp lý & Kế toán | `rgba(74,144,164,.6)` | `#4A90A4 → #50C9C3` |
| Hỗ trợ Kinh doanh | `rgba(102,126,234,.6)` | `#667eea → #764ba2` |
| Lập trình | `rgba(17,153,142,.6)` | `#11998e → #38ef7d` |
| Nhân sự | `rgba(255,105,180,.6)` | `#FF69B4 → #FFAEC9` |
| IT Helpdesk | `rgba(52,152,219,.6)` | `#3498db → #2980b9` |

#### Cách B: trích màu chủ đạo từ ảnh đại diện

Khi card dùng ảnh thay cho icon, **không gán màu cứng nữa**. JS đọc màu chủ đạo của chính tấm ảnh rồi ghi vào `--card-glow-color`. Viền phát sáng nhờ vậy tự khớp với ảnh, thêm thành viên mới hay đối tác mới không phải sửa CSS.

Thuật toán: vẽ ảnh xuống canvas 32x32 rồi gom pixel vào các ô màu. Điểm mấu chốt là **lọc bỏ pixel vô nghĩa trước khi đếm**. Ảnh đại diện thường có nền trắng và viền tối, nếu đếm tất thì màu chủ đạo luôn ra trắng hoặc xám.

- Bỏ pixel trong suốt (`alpha < 200`), logo PNG có nền trong suốt rất nhiều
- Bỏ pixel quá tối (`max < 45`) và quá sáng (`min > 220`), đây là nền và viền
- Bỏ pixel xám (`max - min < 25`), xám không tạo được quầng sáng có ý nghĩa
- Gom theo ô 16 giá trị (dịch bit `>> 4`) rồi lấy trung bình thật của ô đông nhất

```js
/**
 * Đọc màu chủ đạo của một ảnh đã tải xong. Trả về [r,g,b] hoặc null.
 * Ảnh BẮT BUỘC có crossorigin="anonymous" và server phải trả header
 * Access-Control-Allow-Origin, nếu không canvas bị "nhiễm bẩn" và
 * getImageData ném lỗi bảo mật.
 */
function getDominantColor(img) {
  const S = 32;                       // 32x32 là đủ, nhanh hơn ảnh gốc hàng trăm lần
  const canvas = document.createElement('canvas');
  canvas.width = canvas.height = S;
  const ctx = canvas.getContext('2d', { willReadFrequently: true });
  ctx.drawImage(img, 0, 0, S, S);

  let data;
  try { data = ctx.getImageData(0, 0, S, S).data; }
  catch (e) { return null; }          // thiếu CORS, giữ màu mặc định

  const buckets = new Map();
  for (let i = 0; i < data.length; i += 4) {
    const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
    if (a < 200) continue;                       // bỏ pixel trong suốt
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    if (max < 45 || min > 220) continue;         // bỏ quá tối và quá sáng
    if (max - min < 25) continue;                // bỏ pixel xám

    const key = (r >> 4) + '|' + (g >> 4) + '|' + (b >> 4);
    const acc = buckets.get(key) || { n: 0, r: 0, g: 0, b: 0 };
    acc.n++; acc.r += r; acc.g += g; acc.b += b;
    buckets.set(key, acc);
  }

  let best = null;
  buckets.forEach(v => { if (!best || v.n > best.n) best = v; });
  if (!best) return null;             // ảnh toàn xám hoặc toàn trắng

  return [
    Math.round(best.r / best.n),
    Math.round(best.g / best.n),
    Math.round(best.b / best.n)
  ];
}

/** Gán màu quầng sáng cho card theo ảnh đại diện bên trong nó. */
function applyAvatarGlow(card) {
  const img = card.querySelector('.team-avatar');
  if (!img) return;                   // card dùng icon, giữ màu gán cứng

  const run = () => {
    const rgb = getDominantColor(img);
    if (!rgb) return;                 // đọc lỗi thì giữ màu mặc định
    card.style.setProperty('--card-glow-color', `rgba(${rgb.join(',')},.6)`);
  };

  if (img.complete && img.naturalWidth) run();   // ảnh có thể đã nằm sẵn trong cache
  else img.addEventListener('load', run, { once: true });
}

document.querySelectorAll('.team-card, .channel-card').forEach(applyAvatarGlow);
```

**Ba điều dễ sai:**

1. Thiếu `crossorigin="anonymous"` trên thẻ `img` thì `getImageData` ném lỗi bảo mật, **dù server có gửi header CORS**.
2. Ảnh từ CDN ngoài phải trả `Access-Control-Allow-Origin`. CDN ảnh của VCL đã có sẵn.
3. Luôn giữ giá trị mặc định trong CSS: `var(--card-glow-color, rgba(255,107,107,.5))`, để card vẫn đẹp khi đọc màu thất bại.

**Quy chuẩn ảnh đại diện:**

- Kích thước hiển thị 55x55px, file nguồn tối thiểu 160x160px cho retina
- Bo góc 12px giống ô icon, để card icon và card ảnh đứng chung lưới vẫn đồng bộ
- Ảnh chân dung nền trắng hoặc `#FFF0F5`. Logo đối tác giữ nền trong suốt
- Ảnh chân dung cắt vuông, mặt ở một phần ba trên, `object-fit: cover`
- Luôn có `width` và `height` trên thẻ `img` để trang không nhảy layout
- Ảnh quá nhạt sẽ trả `null` và card rơi về màu hồng mặc định, đó là hành vi đúng

**Khi nào dùng:** lưới giới thiệu đội ngũ, năng lực, đối tác · danh sách kênh dịch vụ · bảng giá gói · lưới từ 3 card trở lên.

**Khi nào không dùng:** card đơn lẻ (không có cụm thì không có gì để lan sáng) · danh sách trên 12 card (mỗi lần di chuột phải đọc `getBoundingClientRect` của tất cả) · bảng dữ liệu và form · trang đã có nhiều animation khác.

**Hai điều bắt buộc kèm theo:**

- **Thiết bị cảm ứng.** Điện thoại không có `mousemove`, quầng sáng không hiện. Card vẫn phải đọc được đầy đủ khi không có hiệu ứng, đây là lớp trang trí chứ không mang thông tin.
- **Tôn trọng `prefers-reduced-motion`:**

```css
@media (prefers-reduced-motion: reduce) {
  .team-card::before,
  .team-card::after { display: none; }
  .team-card:hover  { transform: none; }
}
```

### 6.3 Section tag (nhãn phía trên tiêu đề)

```css
.section-tag {
  display:inline-block; background: var(--gradient-primary); color:#fff;
  padding:8px 20px; border-radius:20px;
  font-size:.85rem; font-weight:600;
  text-transform:uppercase; letter-spacing:1px;
}
```

### 6.4 Form

```css
.form-group label { display:block; font-weight:600; color: var(--text-color); margin-bottom:8px; }
.form-group input, .form-group textarea, .form-group select {
  width:100%; padding:15px;
  border:2px solid var(--light-color); border-radius: var(--border-radius);
  font-family:inherit; font-size:1rem; transition: var(--transition);
}
.form-group input:focus {
  border-color: var(--primary-color);
  box-shadow: 0 0 0 3px rgba(255,105,180,.15);
  outline: 2px solid transparent; /* giữ outline cho chế độ tương phản cao */
}
.form-group .error { color:#C0392B; font-size:.9rem; margin-top:6px; }
```

Label luôn hiển thị, không dùng placeholder thay label. Lỗi hiển thị dưới field kèm icon `alert-circle`.

### 6.5 Header

Cố định trên cùng, nền trắng, `shadow-sm`, `z-index: 1000`. Nav link hover: gạch chân gradient chạy từ 0 tới 100% width, dày 3px, bo 2px.

### 6.6 Footer

Nền `--dark-color` `#2D2D2D`, chữ trắng, padding `60px 0 0`. Logo footer thu nhỏ còn 28px chữ, wordmark trắng đặc.

---

## 7. Icon, ảnh, chuyển động

### Icon
- Bộ duy nhất: **Lucide** `https://unpkg.com/lucide@0.460.0`
- Chỉ dùng dạng nét, `stroke-width: 2`, không dùng icon tô đặc
- Kích thước: 16px inline · 18px trong nút · 24-32px feature
- Màu: kế thừa `currentColor`, hoặc `var(--primary-color)` khi làm điểm nhấn
- Không trộn nhiều bộ icon trong một trang. Không dùng emoji thay icon trong UI.

### Ảnh
- Phong cách: người thật, ánh sáng ấm, có nụ cười, bối cảnh Việt Nam
- Tránh: stock "doanh nhân bắt tay", ảnh AI lỗi tay, tông xám lạnh
- Xử lý: bo góc 20px. Ảnh có chữ đè phải phủ `linear-gradient(rgba(45,45,45,.55), rgba(45,45,45,.55))`
- Kỹ thuật: WebP, khai báo `width`/`height`, `loading="lazy"` trừ ảnh hero
- OG image: 1200x630px, logo góc trái, chữ tối đa 8 từ

### Chuyển động
- Mặc định: `transition: all .3s ease`
- Chữ ký thương hiệu: `cubic-bezier(0.34, 1.56, 0.64, 1)` cho hiệu ứng nhún
- Hover nút: `translateY(-2px)`. Hover card: `translateY(-8px)`
- Luôn tôn trọng:

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration:.01ms !important; animation-iteration-count:1 !important;
    transition-duration:.01ms !important; scroll-behavior:auto !important;
  }
}
```

---

## 8. Giọng nói & cách viết

**Tính cách:** vui vẻ, gần gũi, chuyên nghiệp, rõ ràng, thực tế.

**Xưng hô:** "chúng tôi" với "bạn" (B2C, marketing), "chúng tôi" với "quý khách" (B2B, hợp đồng, hoá đơn).

### Nên
- Nói lợi ích trước, tính năng sau
- Dùng số cụ thể: "từ 2 triệu/tháng" thay vì "giá tốt"
- Câu ngắn, thể chủ động
- Kết bằng lời mời hành động rõ ràng
- Emoji tối đa 1 cái mỗi đoạn, chỉ ở nội dung marketing

### Không nên
- **Không dùng dấu gạch dài (em dash)**, thay bằng dấu phẩy hoặc hai chấm
- Không hứa hẹn tuyệt đối: "cam kết top 1", "đảm bảo x10 doanh thu"
- Không dùng từ sáo rỗng: "giải pháp toàn diện", "đột phá", "tiên phong"
- Không viết HOA TOÀN BỘ quá 4 từ
- Không emoji trong hợp đồng, hoá đơn, chính sách

### Ví dụ

| Sai | Đúng |
|---|---|
| "Giải pháp marketing toàn diện, đột phá doanh thu!" | "Quản trị fanpage trọn gói từ 2 triệu/tháng. Có báo cáo hằng tuần." |
| "SUBMIT" | "Gửi yêu cầu tư vấn" |
| "Chúng tôi cam kết đưa bạn lên TOP 1 Google" | "Chúng tôi tối ưu SEO theo bộ từ khoá bạn chọn, báo cáo thứ hạng mỗi tháng." |
| "Liên hệ ngay hôm nay 🔥🔥🔥" | "Liên hệ ngay để nhận tư vấn miễn phí 🎉" |

---

## 9. Accessibility (bắt buộc, mục tiêu WCAG 2.1 AA)

- Tương phản chữ thường ≥ **4.5:1**, chữ ≥ 24px hoặc ≥ 19px bold ≥ **3:1**
- Không dùng `#FF69B4` hay `#FFAEC9` làm màu chữ trên nền trắng
- Vùng chạm tối thiểu **44x44px**
- Focus ring luôn nhìn thấy: `outline: 2px solid var(--primary-deep); outline-offset: 2px`
- Không truyền tải thông tin chỉ bằng màu
- Mọi ảnh có `alt` mô tả, ảnh trang trí dùng `alt=""`
- Thứ tự heading không nhảy cấp, mỗi trang một `h1`

**Cảnh báo kỹ thuật đã biết:** `.btn-primary` hiện đặt chữ trắng trên `#FF69B4` (tỉ lệ 2.65:1, không đạt AA). Với chữ ≥ 19px bold thì chấp nhận được ở mức 3:1 vẫn còn thiếu. Khi làm dự án mới, dùng nền `var(--primary-deep)` `#C2185B` cho nút có chữ nhỏ, hoặc giữ gradient nhưng ép `font-size ≥ 18px; font-weight: 700`.

---

## 10. Quy ước web (áp dụng cho vuicuoilen.vn và các dự án con)

- **Clean URLs:** link nội bộ, `canonical` và sitemap **không có đuôi `.html`**
- `<html lang="vi">` mặc định, i18n bằng cặp class `.lang-vi` / `.lang-en`
- Favicon khai báo cả `rel="icon"` và `rel="apple-touch-icon"`
- CSS nạp theo thứ tự: `brand-tokens.css` → `style.css` → CSS trang

---

## 11. Checklist trước khi bàn giao

- [ ] Đã import `brand-tokens.css`, không còn hex hardcode ngoài file token
- [ ] Font Nunito đã load, không có font thứ hai
- [ ] Mọi cặp chữ/nền đạt tương phản ≥ 4.5:1 (hoặc 3:1 với chữ lớn)
- [ ] Mỗi màn hình đúng một nút primary
- [ ] Icon toàn bộ là Lucide stroke, không lẫn emoji trong UI
- [ ] Logo ≥ 28px, có khoảng trống bảo vệ, không bị bóp méo
- [ ] Mọi khối logo bọc trong `<a href="/">`, bấm được về trang chủ
- [ ] Thanh cuộn dùng gradient hồng VCL, không để mặc định xám
- [ ] Có trạng thái tải: khung xương khi gọi dữ liệu, spinner trong nút khi gửi form
- [ ] Nút submit có khối `finally` để luôn mở khoá kể cả khi API lỗi
- [ ] Nội dung dịch vụ, giá, bài viết lấy từ `llms.txt`, không bịa
- [ ] Đoạn văn tiếng Việt `line-height: 1.8`
- [ ] Có `prefers-reduced-motion`
- [ ] Vùng chạm ≥ 44px, focus ring nhìn thấy
- [ ] Nội dung không dùng em dash, không hứa hẹn tuyệt đối
- [ ] Link nội bộ không có `.html`
- [ ] OG image 1200x630, favicon khai báo đủ

---

## 12. Snippet khởi tạo dự án mới

```html
<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tên trang | Vui Cười Lên</title>

  <link rel="icon" type="image/png" href="https://raw.githubusercontent.com/trinhleminhan/img-all-site/main/vuicuoilen.com/favicon.png">
  <link rel="apple-touch-icon" href="https://raw.githubusercontent.com/trinhleminhan/img-all-site/main/vuicuoilen.com/favicon.png">

  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap" rel="stylesheet">

  <script src="https://unpkg.com/lucide@0.460.0"></script>
  <link rel="stylesheet" href="https://vuicuoilen.vn/css/brand-tokens.css">
  <link rel="stylesheet" href="css/app.css">
</head>
<body>
  <!-- nội dung -->
  <script>lucide.createIcons();</script>
</body>
</html>
```

---

*Có gì chưa rõ hoặc cần bổ sung, liên hệ hi@vuicuoilen.vn.*
