/* ============================================================
   Vinpack — home.css
   CSS RIÊNG CỦA TRANG CHỦ. Chỉ nạp khi is_front_page().

   NẠP SAU global.css VÀ layout.css. inc/enqueue.php khai vinpack-global làm
   dependency của vinpack-home, và vinpack-layout cũng phụ thuộc vinpack-global,
   nên thứ tự nguồn cố định là global → layout → home. Nhiều rule ở đây hoà độ
   ưu tiên với global.css rồi thắng bằng thứ tự nguồn; đảo file là đảo kết quả.

   VÌ SAO LÀ FILE RIÊNG, KHÔNG NHÉT VÀO layout.css: cùng lý lẽ inc/enqueue.php
   đã ghi cho home.js. layout.css là tài sản toàn site, nạp trên mọi trang; lưới
   bento, băng chạy logo và slider chứng nhận chỉ có ở trang chủ.

   BA LUẬT CỦA FILE NÀY — tests/test-css-cascade.php mục 7 canh cả ba:

   1. KHÔNG khai custom property nào. Token thuộc về theme.json và global.css.
      Khai lại `--navy` ở đây sẽ thắng theo thứ tự nguồn và âm thầm đổi màu ở
      mọi nơi khác dùng token đó — mà bộ giải cascade không mô hình hoá var()
      nên sẽ không có khẳng định nào lộ ra.

   2. KHÔNG dùng !important. Mọi thứ ở đây thắng bằng độ ưu tiên hoặc thứ tự
      nguồn, hai thứ đo được.

   3. KHÔNG khai `outline` ở bất kỳ đâu. Vòng focus của mục 10 global.css ở
      (0,1,0); một `.cert-dot:focus{outline:none}` là (0,1,1) và nuốt trọn nó.
      Đó là lỗi đắt nhất của Phase A.

   VÀ MỘT LUẬT NỮA, VỀ `hidden`:

   Slide chứng nhận, dãy chấm và thanh điều khiển báo chí đều đóng/mở bằng
   THUỘC TÍNH `hidden` do assets/js/home.js bật tắt. (Hai nút "Tạm dừng chuyển
   động" từng nằm trong danh sách này; khách yêu cầu gỡ — xem mục 10.) UA có
   sẵn `[hidden]{display:none}`. Một dòng `.press-controls{display:flex}` trông
   vô hại sẽ đè nó và hai nút chết hiện ra khi không có JS. Nên mọi rule chạm
   `display` của những thứ đó đều đi qua `:not([hidden])`, VÀ file này khai lại
   `X[hidden]{display:none}` tường minh — bộ giải cascade không mô hình hoá
   stylesheet của UA, nên không khai lại thì không có gì để đo.

   VỀ 100vw — LỖI CÓ SẴN CỦA MOCKUP, ĐÃ SỬA:
   Mockup tràn `.partner-row` và `.certs-visual-bleed` bằng `100vw` /
   `50vw` + `margin-left:calc(50% - 50vw)`. `vw` TÍNH CẢ THANH CUỘN, nên trên
   Windows với thanh cuộn cố định mỗi khối thừa ra ~17px và sinh cuộn ngang cho
   cả trang. File này KHÔNG dùng đơn vị `vw` ở đâu cả — xem chú thích tại chỗ.
   ============================================================ */

/* ============================================================
   1. Dùng chung cho các khối trang chủ
   ============================================================ */

/*
 * `.section-lead` là tên Task 2–5 đặt cho đoạn dẫn (mockup dùng style nội tuyến
 * trên một thẻ <p> đóng bằng </span>). Trong `.section-head` nó đã được
 * `.section-head p` (0,1,1) của global.css tô --text-muted.
 *
 * TRÊN NỀN --bg-alt PHẢI ĐỔI MÀU. --text-muted trên --bg-alt chỉ 4.24:1, trượt
 * AA cho chữ dưới 18px — đúng con số đã ghi ở `.vp-table tbody tr:hover td`.
 * --text trên --bg-alt là 15.25:1. Ba khối nền --bg-alt của trang chủ là
 * .stats, .press và .certs.
 *
 * (0,2,0) nên thắng `.section-head p` (0,1,1) bằng ĐỘ ƯU TIÊN, không nhờ thứ tự
 * nguồn: chuyển khối này lên đầu file vẫn ra cùng kết quả.
 */
.stats .section-lead,
.press .section-lead,
.certs .section-lead{ color:var(--text); }

/*
 * Căn giữa khối tiêu đề — mockup làm bằng style nội tuyến, đã bỏ.
 *
 * `.gallery .section-head` (STT4, feedback khách đợt 1): toàn site
 * `.section-head` căn TRÁI theo global.css — chỉ đảo ở đúng khối "Con người
 * và hệ thống" theo yêu cầu, không đụng khối nào khác. Thêm vào ĐÚNG nhóm
 * đã căn giữa sẵn (cùng bộ chọn (0,2,0) đã thắng `.section-head` (0,1,0) của
 * global.css) thay vì viết một khối riêng — một quy tắc dùng chung.
 */
.stats .section-head,
.video-sec .section-head,
.partners .section-head,
.press .section-head,
.gallery .section-head{ margin-inline:auto; text-align:center; }

/*
 * SECTION CÓ NỀN MÀU RIÊNG PHẢI TRÀN HẾT CHIỀU NGANG — SỬA LỖI CÓ SẴN CỦA MOCKUP.
 *
 * Mockup viết `<section class="press wrap">` MÀ VẪN cho nó `background:
 * var(--bg-alt)`. Hậu quả đo được trên site thật (viewport 1280, thanh cuộn
 * 15px): `.stats` cùng màu --bg-alt rộng 1265 (tràn), còn `.press` chỉ 1180 —
 * hai dải CÙNG MỘT MÀU, hai bề rộng khác nhau, và hai bên dải press hở ra nền
 * trắng của body. `.gallery` và `.partners` mắc đúng lỗi đó với nền --white.
 *
 * LUẬT: section MANG NỀN thì bản thân section rộng hết trang, chỉ NỘI DUNG bị
 * giới hạn --wrap-max. Section không nền thì `.wrap` thẳng lên section là đủ
 * (products, video-sec giữ nguyên).
 *
 * CÁCH ÁP — VÌ SAO KHÔNG THÊM MỘT `.wrap` BÊN TRONG:
 * Ba part này là `<section class="X wrap">`; thêm một `<div class="wrap">` bên
 * trong là đổi cấu trúc của ba template part và của mọi khẳng định XPath bám
 * vào chúng. Ở đây làm bằng CSS thuần: bỏ `max-width` rồi trả lại đúng phần
 * đệm đã mất bằng padding co giãn.
 *
 *   padding-inline = max(--wrap-x, (100% - --wrap-max + 2*--wrap-x) / 2)
 *
 * Ở 1265px: max(32, (1265 - 1180 + 64)/2 = 74.5) = 74.5 → hộp nội dung
 * 1265 - 149 = 1116px, ĐÚNG BẰNG 1180 - 2*32 của `.wrap` cũ. Ở 375px vế thứ
 * hai âm nên `max()` trả --wrap-x → không bao giờ hẹp hơn phần đệm tối thiểu.
 *
 * KHÔNG `100vw`, KHÔNG `50vw`: Phase C Task 7 đã bỏ chúng vì `vw` tính cả
 * thanh cuộn dọc và đẻ ra cuộn ngang trên Windows. `100%` ở đây là bề rộng
 * khối chứa (body), KHÔNG tính thanh cuộn — đó là toàn bộ lý do dùng nó.
 *
 * `.video-sec` VÀO DANH SÁCH DÙ BẢNG ĐO BÀN GIAO GHI NÓ "KHÔNG NỀN".
 * Nền của nó là `linear-gradient(160deg, var(--navy) …)`, tức nằm ở
 * `background-image`. Phép đo bàn giao đọc `background-color`, ra `transparent`,
 * và xếp nhầm nó vào nhóm không nền — trong khi dải navy vẫn chỉ rộng
 * --wrap-max, hai bên hở nền trắng, ĐÚNG lỗi đang sửa. Bài học: dò "section có
 * nền" bằng `background-color` là mù với mọi gradient. test-css-cascade.php mục
 * 7.4c đã đổi sang quét CẢ BA thuộc tính nền.
 *
 * `.partner-row`/`.gallery-row` KHÔNG phải sửa: chúng tràn bằng
 * `margin-inline:calc(var(--wrap-x) * -1)` tính theo token, không theo bề rộng
 * vỏ, nên hình học của chúng so với mép nội dung không đổi một pixel nào.
 *
 * Độ ưu tiên: `.press.wrap` (0,2,0) thắng `section.wrap` (0,1,1) của global.css
 * bằng ĐỘ ƯU TIÊN chứ không nhờ thứ tự nạp — đảo thứ tự file vẫn ra kết quả này.
 * `padding` của `section.wrap` là viết tắt; ở đây đặt hai longhand để phần
 * padding dọc không phụ thuộc vào việc rule kia có thắng hay không.
 */
/*
 * ---------------------------------------------------------------------------
 * KHÁCH LẬT LẠI LUẬT TRÊN CHO HAI KHỐI — STT 7 VÀ STT 9, FEEDBACK ĐỢT 1
 * ---------------------------------------------------------------------------
 * Luật "section mang nền thì rộng hết trang" ở trên là suy luận của chúng ta,
 * KHÔNG phải điều mockup vẽ. Đo lại chính file mockup trong trình duyệt ở
 * 1265px cho thấy mockup dựng NGƯỢC LẠI:
 *
 *     .video-sec.wrap   trái 43, rộng 1180   (dải navy là MỘT KHỐI, hai bên hở)
 *     .press.wrap       trái 43, rộng 1180   (dải xanh nhạt cũng vậy)
 *     .partners.wrap    trái 43, rộng 1180
 *     .gallery.wrap     trái 43, rộng 1180
 *     .hero/.stats/.certs/.why/.cta   trái 0, rộng 1265  (những khối này MỚI tràn)
 *
 * Khách khoanh đúng hai khối và viết tay:
 *   STT 7 (video):  "chỉnh lại như bản cũ giúp e, shape một phần chứ không tràn viền"
 *   STT 9 (báo chí): "cho vào shape giúp e, chứ hong tràn viền ạ"
 *
 * Nên `.video-sec.wrap` và `.press.wrap` RA KHỎI danh sách này, trả về
 * `section.wrap` của global.css: max-width --wrap-max, padding --sp-8/--wrap-x
 * — đúng "96px 32px" và bề rộng 1180 đo được trên mockup.
 *
 * `.gallery.wrap` Ở LẠI, có chủ ý: nền nó là --white, trùng nền trang, nên
 * tràn hay không tràn KHÔNG nhìn thấy được; khách không nhắc tới nó; và kéo nó
 * về 1180 sẽ đổi hình học của `.gallery-row` mà STT 4 vừa chốt xong. Đây là
 * sai lệch so với mockup được GHI NHẬN và cố ý chưa sửa, không phải bỏ sót.
 */
.gallery.wrap{
  max-width:none;
  padding-block:var(--sp-8);
  padding-inline:max( var(--wrap-x), calc( (100% - var(--wrap-max) + var(--wrap-x) * 2) / 2 ) );
}

/*
 * `.partners` — STT 8, YÊU CẦU NGƯỢC HẲN HAI KHỐI TRÊN.
 *
 * Khách: "Phần logo a cho chạy tràn viền giúp e nha", kèm hai mũi tên chỉ ra
 * ngoài hai mép màn hình. Mockup cũng tràn thật: `.partner-row` đo được
 * trái -7, rộng 1280 trên vùng nhìn 1265 — nó PHÁ RA NGOÀI cả vỏ lẫn màn hình.
 *
 * Vỏ giữ `max-width:none` để băng logo với tới được mép, nhưng ĐỆM NGANG VỀ 0:
 * còn đệm thì băng không bao giờ chạm mép, và đó chính là lý do bản trước
 * trông "chưa tràn". Phần đệm chuyển xuống cho riêng `.section-head` bên dưới,
 * vì chỉ tiêu đề mới cần thẳng hàng với nội dung các khối khác.
 *
 * VẪN KHÔNG DÙNG `100vw`: mockup tràn bằng `width:100vw; margin-left:calc(50%
 * - 50vw)`, mà `vw` tính cả thanh cuộn dọc nên đẻ ra cuộn ngang trên Windows
 * (lý lẽ đầy đủ ở docblock `.partner-row`). Ở đây bề rộng đến từ chính vỏ
 * `max-width:none` — tức bề rộng KHỐI CHỨA, không tính thanh cuộn — nên tràn
 * đúng mép nhìn thấy được, không dư một pixel nào.
 */
.partners.wrap{
  max-width:none;
  padding-block:var(--sp-8);
  padding-inline:0;
}

/*
 * Tiêu đề nhận lại phần đệm mà vỏ vừa bỏ. `.section-head` có max-width 640px
 * và căn giữa, nên trên màn rộng phần đệm này không đổi gì; nó chỉ có tác dụng
 * dưới ~704px, đúng lúc cần để chữ không dính mép.
 */
.partners .section-head{ padding-inline:var(--wrap-x); }

/* ============================================================
   2. Hero
   ============================================================ */
/*
 * SỬA 07/09/2026 (khách, ảnh khoanh đỏ cả khối hero): "phía trên tone #001bb7
 * cho nó liền với header luôn, phía dưới cũng tone màu đó nhưng sắc độ sẽ nhạt
 * hơn".
 *
 * HAI THAY ĐỔI so với bản cũ `linear-gradient(160deg, --navy → --navy-dark)`:
 *   - HƯỚNG 160deg -> 180deg. Ở 160deg mép trên KHÔNG phải một màu: góc trái
 *     trên là điểm đầu (#001BB7) còn góc phải trên đã ngả sang #001291, nên
 *     đường ranh với header (nền phẳng var(--navy)) lộ ra rõ ở nửa phải. Dọc
 *     100% thì cả mép trên đúng một màu, khớp header thành một khối liền.
 *   - ĐIỂM CUỐI --navy-dark (#001291, TỐI hơn nền) -> #1E44D8 (SÁNG hơn), đúng
 *     chiều khách vừa chốt: cùng tone xanh thương hiệu, chỉ nhạt hơn.
 *
 * #1E44D8 là literal chứ không phải token vì bảng màu không có bậc "navy nhạt"
 * nào: --cobalt (#5C90F5) và --cobalt-ink (#3366CC) đều đã nhạt tới mức chữ phụ
 * --on-navy-text (#C3CDE8) chỉ còn 3.38:1 khi đặt lên — trượt AA. Với #1E44D8:
 * chữ trắng 7.3:1, chữ phụ --on-navy-text 4.6:1, cả hai còn dư chuẩn. Cùng sắc
 * 228° với --navy, chỉ nâng độ sáng.
 */
.hero{
  /*
   * HAI ĐIỂM MÀU KHÁCH GỬI THẲNG (07/09/2026, khách dán nguyên rule mong
   * muốn): `linear-gradient(180deg, #001bb7 0%, #3366cc 100%)`. Cả hai đều
   * trùng khít token có sẵn nên viết bằng token: #001bb7 = --navy (đúng nền
   * .site-header, nên mép trên liền header), #3366cc = --cobalt-ink. Giá trị
   * hiện ra không đổi một pixel so với bản khách gửi, chỉ khác cách khai —
   * luật số 1 của file này là không ghi cứng màu đã có token.
   *
   * Thay cho bản trong suốt alpha .6 thử sáng nay: nền giờ ĐẶC, đáy không còn
   * hoà vào nền trắng bên dưới nữa.
   */
  background:linear-gradient(180deg, var(--navy) 0%, var(--cobalt-ink) 100%);
  color:var(--white);
  padding:var(--sp-8) 0 0;
  position:relative;
  overflow:hidden;
}

/*
 * STT 2 (feedback khách, đợt 1): "background sẽ chèn ảnh full khung phủ lớp
 * gradient nền chính đậm dần từ trên xuống dưới từ trái qua phải". Khách CHƯA
 * gửi ảnh nền thật — dựng khung trước, chừa chỗ đổ ảnh thật sau (đã chốt với
 * khách): khi có ảnh, chỉ cần thêm `background-image` vào `.hero` (hoặc một
 * lớp `<img>` nền), lớp phủ này vẫn đứng nguyên phía trên.
 *
 * Một `linear-gradient(160deg, …)` DUY NHẤT diễn đạt được cả hai chiều khách
 * yêu cầu (trên→dưới VÀ trái→phải) vì 160deg vừa nghiêng dọc vừa nghiêng
 * ngang; chồng hai gradient (một dọc + một ngang) sẽ đục màu và không đậm dần
 * mượt như một hướng chéo duy nhất — khách chỉ nói "đậm dần", không nói phải
 * tách hai lớp.
 *
 * rgba(0,27,183,…) là --navy ở dạng có alpha — token không mang alpha nên bên
 * trong linear-gradient() phải viết tay giá trị số. Điểm đầu nhạt (alpha .35)
 * để không đục ảnh nền khi có ảnh thật; điểm cuối đậm (alpha .82) để đủ tương
 * phản cho chữ trắng. inset:0 phủ full khung; z-index:0 đặt lớp phủ NGAY TRÊN
 * nền nhưng DƯỚI nội dung (`.hero-grid` mang z-index:1 ở dưới).
 */
.hero::before{
  content:"";
  position:absolute;
  inset:0;
  /*
   * KHÔNG CÒN BACKGROUND (07/09/2026, khách bỏ dòng này trong rule gửi kèm):
   * nền hero giờ tự nó đủ, mọi lớp phủ chồng thêm chỉ làm đục dải màu vừa chốt.
   * Khung rỗng giữ nguyên để lúc khách gửi ẢNH NỀN thật (STT 2 đợt 1, xem ghi
   * chú ngay trên) chỉ cần đặt lại một `background` ở đây — không phải dựng lại
   * cả pseudo-element và tính lại z-index.
   */
  z-index:0;
}

/*
 * LỚP PHỦ TỐI `.hero::after` ĐÃ GỠ (07/09/2026).
 *
 * Nó dựng hôm 05/09 theo yêu cầu "đậm ở phía trên, nhạt dần xuống dưới" — phủ
 * #000A3C alpha .75 ở mép trên. Yêu cầu mới ngày 07/09 THAY THẾ chính chỗ đó:
 * mép trên phải đúng #001BB7 để liền với header, mà lớp phủ tối này kéo mép
 * trên sang #001240 — đậm hơn header thấy rõ, đúng đường ranh khách muốn xoá.
 * Chiều "đậm trên → nhạt dưới" vẫn còn nguyên, nay do chính nền đảm nhiệm
 * (#001BB7 → #1E44D8), không cần lớp phủ thứ hai. Xem lịch sử git nếu cần dựng
 * lại.
 */


/*
 * repeat(auto-fit, …) chứ không phải `1.1fr .9fr`: `.hero-body` có thể vắng hẳn
 * (mọi ô chữ trống, chỉ còn hình) và `.hero-visual` cũng vậy. Hai cột ghi cứng
 * để lại một cột rỗng chiếm nửa màn hình.
 *
 * position:relative + z-index:1: nội dung phải nổi TRÊN `.hero::before` — nếu
 * không, lớp phủ gradient (position:absolute, z-index:0) sẽ đè lên chữ vì nó
 * đứng sau trong DOM.
 */
.hero-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(320px, 1fr));
  gap:var(--sp-7);
  align-items:center;
  max-width:var(--wrap-max);
  margin-inline:auto;
  padding:0 var(--wrap-x) var(--sp-8);
  position:relative;
  z-index:1;
}

/*
 * RÀ SOÁT FEEDBACK LẦN 2 — STT 2 CHƯA ĐẠT TRỌN VẸN.
 *
 * `white-space:nowrap` trên `<em>` (xem `.hero h1 em`) đã khoá đúng nghĩa
 * đen của câu feedback: "toàn diện" không bao giờ bị TÁCH ĐÔI giữa hai chữ.
 * Nhưng đo lại bằng trình duyệt thật ở CẢ 1265px lẫn 1905px cho thấy tiêu đề
 * vẫn ra BA dòng ("Giải pháp bao bì" / "toàn diện" / "cho doanh nghiệp"),
 * trong khi ảnh đích của khách vẽ HAI dòng ("Giải pháp bao bì toàn diện" /
 * "cho doanh nghiệp"). Nguyên nhân: hai cột `1fr 1fr` chia đều chỗ khách cột
 * chữ chỉ còn 534px ở 1265px, mà "Giải pháp bao bì toàn diện" ở
 * 800/46px/Manrope cần tối thiểu 578px mới vừa một dòng (đo bằng
 * `getBoundingClientRect` trên một `<span>` dựng tạm cùng font, không đoán).
 *
 * Hỏi lại khách và chốt: nới cột chữ, thu hẹp cột ảnh — CHỈ ở dải đủ rộng.
 *
 *   min-width:1180px = ĐÚNG `--wrap-max`. Từ ngưỡng này, `.hero-grid` đã chạm
 *   max-width nên bề rộng nội dung khả dụng ổn định ở 1068px (1180 − 2×32 đệm
 *   − 48 gap) DÙ MÀN CÓ RỘNG HƠN NỮA — không có ngưỡng nào an toàn hơn để ép
 *   tỉ lệ cột mà không phải tính lại theo từng viewport.
 *
 *   Tỉ lệ 1.3fr:1fr tại 1068px cho cột chữ 604px — dư 26px so với 578px cần
 *   thiết, đủ biên an toàn cho sai số đo font. Cột ảnh còn 464px (từ 534px),
 *   `.hero-visual` bên dưới co theo `1fr` của grid nên KHÔNG cần sửa gì thêm.
 *
 *   DƯỚI 1180px KHÔNG ĐỤNG: `repeat(auto-fit, minmax(320px, 1fr))` ở rule
 *   gốc vẫn nguyên — hai cột 1:1 khi đủ hai lần 320px+gap, tự dồn một cột khi
 *   hẹp hơn. Vùng 752–1180px (giữa hai ngưỡng) vẫn có thể ra ba dòng như
 *   trước — đó là hành vi ĐÃ TỒN TẠI SẴN, không phải lỗi mới, và ngoài phạm
 *   vi phép đo đã xác nhận (dự án chỉ đo ở hai mốc 1265px và 375px).
 */
@media (min-width:1180px){
  .hero-grid{ grid-template-columns:1.3fr 1fr; }
}

/*
 * Nền của nhãn: --navy-dark chứ không phải rgba() của mockup — file này không
 * được khai custom property, nên một màu pha chỉ có thể là hằng số ngoài hệ
 * token. --on-navy-accent trên --navy-dark đạt 10.10:1; viền --cobalt trên
 * --navy-dark đạt 4.52:1, trên ngưỡng 3:1 của WCAG 1.4.11.
 */
.eyebrow{
  display:inline-flex;
  align-items:center;
  gap:var(--sp-2);
  background:var(--navy-dark);
  border:1px solid var(--cobalt);
  border-radius:var(--r-pill);
  color:var(--on-navy-accent);
  font-size:13px;
  font-weight:700;
  letter-spacing:.4px;
  padding:7px 14px;
  margin-bottom:var(--sp-5);
}

/* Dấu chấm trang trí, aria-hidden. Giấu nó vô hại; giấu `.eyebrow` thì mất chữ. */
.eyebrow-dot{ color:var(--cobalt); }

/*
 * h1 của global.css đã là 46px/800/--navy. Trên nền navy phải đổi màu chữ; cỡ
 * và font giữ nguyên để thang tiêu đề chỉ có một nguồn.
 */
.hero h1{ color:var(--white); margin-bottom:var(--sp-4); }

/*
 * STT 2 (feedback khách, đợt 1): chữ "toàn diện" bị tách 2 dòng — đo trên
 * trình duyệt thật thấy KHÔNG có <br> nào trong nguồn (hero_title_em chỉ là
 * text thô, xem inc/template-tags.php::vinpack_home_hero()); nguyên nhân là
 * bề rộng khối chữ làm nó tự xuống dòng giữa hai tiếng, tái hiện được ở CẢ
 * 1280px (toàn: top 265 / diện: top 318) LẪN 375px (toàn: top 186 / diện: top
 * 223 — đo bằng Range trên text node). `white-space:nowrap` chỉ khoá riêng
 * cụm trong `<em>` — câu vẫn tự do xuống dòng ở chỗ khác khi cần, không ép
 * cứng cả h1 như một `max-width` sẽ làm.
 */
/*
 * STT bổ sung (feedback khách, sau đợt 1): "toàn diện" trong <em> nhấn màu
 * --on-navy-accent (#A9C4FF) — một quyết định thiết kế có chủ đích, không
 * phải lỗi. Nhưng khách khoanh đỏ đúng chữ này và nói "sửa cho đồng bộ": trên
 * nền navy, cạnh chữ trắng thuần xung quanh, #A9C4FF (11.41:1 → 6.53:1, vẫn
 * đạt AA nhưng thấp hơn hẳn) nhìn MỜ đi chứ không đọc ra là "nhấn mạnh" — dễ
 * bị hiểu nhầm là lỗi màu hơn là chủ ý thẩm mỹ.
 *
 * Đổi về --white, ĐỒNG MÀU với phần còn lại của h1. Thẻ <em> vẫn giữ —
 * `font-style:normal` (không nghiêng) và `white-space:nowrap` (khoá không
 * tách dòng giữa "toàn" và "diện", xem docblock ngay trên) vẫn còn tác dụng;
 * chỉ bỏ đúng phần màu gây lệch tông.
 */
.hero h1 em{ font-style:normal; color:var(--white); white-space:nowrap; }

/*
 * `.hero-lead` là tên MỚI. Mockup chọn bằng `.hero p`, mà bộ chọn đó sẽ vơ luôn
 * mọi đoạn khác lọt vào hero sau này.
 * --on-navy-text trên --navy: 7.19:1.
 */
.hero-lead{
  font-size:16.5px;
  color:var(--on-navy-text);
  max-width:480px;
  margin-bottom:var(--sp-6);
}

/* Có thể vắng hoàn toàn, hoặc chỉ chứa MỘT nút. flex-wrap cho cả hai ca. */
.hero-actions{ display:flex; flex-wrap:wrap; gap:var(--gap-inline); }

/*
 * Nút chính trên nền navy: nền --navy của `.btn-primary` chỉ 1.23:1 so với nền
 * hero, tức nút TÀNG HÌNH vì viền `.btn` vốn trong suốt. Đây đúng khiếm khuyết
 * mà `.on-navy .btn-primary` của global.css sinh ra để chặn — nhưng `.hero`
 * KHÔNG mang class `.on-navy`, nên rule đó không với tới.
 *
 * Chữ trắng trên --cobalt-ink: 5.37:1. Viền --cobalt so với nền hero: 3.67:1 —
 * viền không phải chữ nên --cobalt dùng được, và nó mới là thứ vẽ ra đường bao.
 * Cả hai con số lấy nguyên từ lý lẽ đã ghi ở mục 6 global.css.
 *
 * Phải viết RÕ cả :hover. `.btn-primary:hover` của global.css là (0,2,0); bản
 * không-hover ở đây cũng (0,2,0) và chỉ thắng nhờ thứ tự nguồn, nên bỏ trống
 * :hover là để nền rơi về --navy-dark lúc trỏ vào — 1.23:1, nút biến mất.
 * `.hero .btn-primary:hover` ở (0,3,0) thắng bằng độ ưu tiên.
 */
/*
 * ĐÍCH ĐỔI 03/09/2026 (khách gửi ảnh chụp nút chìm vào nền navy ở khối
 * "Vẫn còn nhiều dự án khác", /du-an-tieu-bieu/, "nền xanh nút trắng"):
 * --cobalt-ink 5.37:1 với CHỮ nhưng chỉ 2.13:1 với chính NỀN HERO — đỡ hơn
 * nút tàng hình 1.23:1 nói trên nhưng vẫn mờ trên nền navy đồng màu.
 *
 * Đổi sang trắng/navy — RULE NÀY LÀ NGUỒN mà `.cta-box`, `.nl-cta-box`,
 * `.contact-cta-box`, `.news-cta-box` đều trích dẫn làm chuẩn ("cùng lý lẽ
 * đúng từng chữ như `.hero .btn-primary`"). Đã đổi hết bốn nơi đó sang
 * trắng/navy trước — sửa Ở ĐÂY cho khớp lại đúng cái mà chúng tự nhận là
 * đang theo.
 */
.hero .btn-primary,
.hero .btn-primary:hover{ background:var(--white); color:var(--navy); border-color:var(--white); }

/*
 * `.hero-visual` chứa HOẶC `<img.hero-img>` HOẶC `<svg.hero-fallback>`, không
 * bao giờ cả hai. Nền --navy-dark để khung không bao giờ là một lỗ trống.
 */
.hero-visual{
  position:relative;
  border-radius:var(--r-lg);
  overflow:hidden;
  aspect-ratio:4 / 3.1;
  background:var(--navy-dark);
  display:flex;
  align-items:center;
  justify-content:center;
}
.hero-img{ width:100%; height:100%; object-fit:cover; }

/*
 * Mã màu bên trong SVG dự phòng là màu CỨNG cố ý — xem docblock hero.php. Ở đây
 * chỉ định cỡ, KHÔNG đụng màu.
 */
.hero-fallback{ width:100%; height:100%; max-width:100%; }

/* ============================================================
   3. Dải số liệu
   ============================================================ */
.stats{ background:var(--bg-alt); padding-block:var(--sp-8); }

/*
 * `.stats-grid` là <ul>, con là <li class="stat">. global.css có
 * `:where(ul, ol){list-style:none}` ở (0,0,0) — khai lại tường minh ở đây để
 * lưới không phụ thuộc vào việc reset ở file khác còn nguyên.
 *
 * VỀ `padding-left:0` MÀ DOCBLOCK stats.php YÊU CẦU: CỐ Ý KHÔNG viết.
 * `padding-inline` bên dưới đã đặt CẢ hai bên tường minh, nên thụt lề mặc định
 * của <ul> không thể lọt qua. Thêm `padding-left:0` SAU `padding-inline` thì nó
 * đè mất đệm TRÁI và lưới lệch 32px so với mọi khối khác; thêm TRƯỚC thì nó là
 * một khai báo chết. Cả hai đều tệ hơn việc không viết.
 * tests/test-css-cascade.php mục 7.8 khoá đúng chuyện này: nó khẳng định KHÔNG
 * rule nào đặt `padding-left` cho `.stats-grid`.
 *
 * SỐ MỤC LÀ 1..n. Mockup ghi cứng `repeat(6,1fr)`; auto-fit chịu được cả một
 * mục lẫn mười.
 */
.stats-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));
  gap:var(--sp-6) var(--sp-4);
  max-width:var(--wrap-max);
  margin-inline:auto;
  margin-block:0;
  padding-inline:var(--wrap-x);
  text-align:center;
  list-style:none;
}

/*
 * --cobalt-ink, KHÔNG phải --cobalt của mockup. `.stat-num` nằm trên --bg-alt,
 * nơi --cobalt chỉ đạt 2.72:1 — trượt cả ngưỡng 3:1 của chữ lớn. --cobalt-ink
 * đạt 4.70:1, và `.stat-unit` 15px bên trong nó là chữ NHỎ nên 4.5:1 mới là
 * ngưỡng đúng, không phải 3:1.
 */
.stat-num{
  font-family:var(--font-head);
  font-weight:800;
  font-size:30px;
  color:var(--cobalt-ink);
  line-height:1.15;
}

/*
 * KHOẢNG CÁCH ĐƠN VỊ — QUYẾT ĐỊNH, xem docblock stats.php.
 * Mockup viết `10.000m²` liền và `30.000 tấn/tháng` hở. Ở đây LUÔN hở
 * --sp-1 (4px). Lý do: SI và quy tắc chính tả tiếng Việt đều yêu cầu một
 * khoảng trắng giữa số và ký hiệu đơn vị, kể cả `m²`. Bản gốc liền nhau là lỗi
 * chính tả của mockup, không phải ý đồ thiết kế.
 */
.stat-unit{ font-size:15px; font-weight:700; margin-left:var(--sp-1); letter-spacing:.2px; }

/* Dấu `+` dính liền con số: `30.000+`, không phải `30.000 +`. */
.stat-suffix{ font-size:inherit; }

/*
 * --text chứ không phải --text-muted. Xem lý lẽ ở mục 1: --text-muted trên
 * --bg-alt là 4.24:1, trượt AA ở cỡ 12.5px.
 *
 * KHÔNG chép `.stats-grid.counting .stat-num{opacity:1}` của mockup. Đó là rule
 * CHẾT: không JS nào gắn class `.counting`, và không có luật nào đặt opacity
 * ban đầu để nó khôi phục.
 */
.stat-label{ font-size:12.5px; color:var(--text); font-weight:600; margin-top:6px; }

/* ============================================================
   4. Lưới bento sản phẩm
   ============================================================ */

/*
 * SỐ Ô LÀ 1..n — KHÔNG ghi cứng sáu, và KHÔNG dùng grid-template-areas.
 *
 * Bản đồ của mockup đặt từng ô vào một ô lưới cụ thể (`.bt-2{grid-column:3}`…).
 * Bản đồ đó vỡ theo cả hai hướng: thiếu ô thì để lỗ, ô thứ bảy thì rơi ra ngoài.
 *
 * SỬA Ở TASK 6 (feedback STT 6) — CHỈ CÒN HAI BỀ RỘNG, KHÔNG CÒN THEO VỊ TRÍ.
 * Trước đây products.php gán `.bt-split` (rộng 2 cột, ảnh nửa trái/chữ nửa
 * phải) cho vị trí 2, 5, 6 và mọi ô từ thứ bảy — đúng bản đồ sáu ô của
 * mockup. Với 5 danh mục thật, bản đồ đó xếp ra ba hàng lệch (hai ô ngang
 * chen giữa các ô vuông) và bỏ trống hai ô ở góc dưới-phải — không phải khối
 * "1 thẻ lớn + 4 thẻ nhỏ lưới 2×2" mà khách yêu cầu. products.php nay CHỈ
 * gán hai class: `.bt-1` cho ô đầu, `.bt-tall` cho MỌI ô — kể cả ô đầu.
 *
 * Ở đây vẫn chỉ khai BỀ RỘNG theo class, rồi để auto-placement lo vị trí:
 *   - `.bt-1` rộng 2 cột và cao 2 hàng (ô lớn, luôn đứng đầu);
 *   - `.bt-tall` không có `.bt-1` đi kèm thì rộng 1 cột, cao 1 hàng — MỌI ô
 *     còn lại, không phân biệt vị trí thứ mấy.
 * Với đúng 5 ô, kết quả là một hình chữ nhật đặc 4×2: ô lớn chiếm góc trên-
 * trái (2 cột × 2 hàng), bốn ô nhỏ lấp đúng phần còn lại (2 cột × 2 hàng) —
 * đúng ảnh khách gửi. Với 1, 4 hay 9 ô nó vẫn là một lưới đặc, không lỗ.
 *
 * `.bt-split` (rộng 2 cột, ảnh nửa trái/chữ nửa phải) VẪN còn định nghĩa bên
 * dưới — test-css-cascade.php kiểm nó như một class CSS độc lập — nhưng
 * KHÔNG còn ô nào trong products.php mang class này. Nếu một thiết kế sau
 * này cần lại một ô ngang thì đó là quyết định MỚI của người xem, không phải
 * khôi phục ngầm bản đồ cũ mà feedback STT 6 đã bác bỏ.
 *
 * `grid-auto-flow:dense` để ô 1 cột lấp lại chỗ trống mà ô 2 cột bỏ qua.
 */
.products-bento{
  display:grid;
  grid-template-columns:repeat(4, 1fr);
  grid-auto-rows:206px;
  grid-auto-flow:dense;
  gap:var(--sp-4);
}

.bt-split{ grid-column:span 2; }

/*
 * ĐỨNG SAU `.bt-split` là CỐ Ý. Hai rule cùng (0,1,0); hôm nay ô đầu luôn là
 * `.bt-tall` nên chúng không bao giờ cùng khớp, nhưng nếu bản đồ trong
 * products.php đổi thì thứ tự này là thứ quyết định — và nó quyết định về phía
 * đúng: ô dẫn đầu phải cao 2 hàng.
 *
 * CHIỀU CAO ĐẾN TỪ LƯỚI (`span 2` trên grid-auto-rows), KHÔNG phải `height`
 * cố định: nội dung ô lớn dài ngắn tuỳ chữ nhân viên gõ.
 */
.bt-1{ grid-column:span 2; grid-row:span 2; }

/*
 * KHÔNG `overflow:hidden` trên chính `.bento-tile`. Cả ô là một <a>, vòng focus
 * của global.css có `outline-offset:2px` nên nó nằm NGOÀI hộp — overflow:hidden
 * ở đây sẽ cắt cụt nó. Mockup đặt overflow:hidden lên tile; ở đây nó chuyển
 * xuống `.bt-media`, đúng chỗ cần cắt (ảnh phóng to lúc hover).
 */
.bento-tile{
  position:relative;
  border-radius:var(--r-lg);
  background:var(--navy);
  text-align:left;
  transition:box-shadow var(--dur-state) var(--ease),
             transform var(--dur-state) var(--ease);
}
.bento-tile:hover{ transform:translateY(-3px); box-shadow:var(--sh-3); }

/*
 * `.bt-media-fallback` PHẢI THẤY ĐƯỢC — hôm nay TẤT CẢ các ô đang dùng nó, chưa
 * danh mục nào có ảnh. Nền --navy-dark trên `.bt-media` là thứ vẽ ra khối đó;
 * biểu tượng --cobalt trên --navy-dark đạt 4.52:1 (trang trí, aria-hidden).
 */
/*
 * `border-radius:inherit` LA VE CON LAI CUA QUYET DINH TREN.
 *
 * Chuyen `overflow:hidden` xuong day cuu duoc vong focus, nhung dong thoi lam
 * o het clip anh theo ban kinh cua no: anh ra goc VUONG trong mot o BO TRON.
 * Khach gui anh chup dung cho nay.
 *
 * KHONG tra `overflow:hidden` len `.bento-tile` de sua - lam vay la cat cut
 * lai vong focus (`outline-offset:2px` ve NGOAI hop). Hop cat va hop bo goc
 * phai la CUNG MOT hop: `.bt-media` da la hop cat, nen chinh no mang ban kinh.
 * `inherit` lay dung gia tri da tinh cua `.bento-tile`, khong nhan ban token -
 * doi `--r-lg` mot cho la ca hai di theo.
 *
 * `.bento-tile` van KHONG co overflow. Ca hai ve deu duoc giai.
 */
.bt-media{
  display:block;
  position:absolute;
  inset:0;
  overflow:hidden;
  border-radius:inherit;
  background:var(--navy-dark);
}
.bt-media-img{
  width:100%;
  height:100%;
  max-width:100%;
  object-fit:cover;
  transition:transform .6s var(--ease);
}
.bento-tile:hover .bt-media-img{ transform:scale(1.05); }

.bt-media-fallback{
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  justify-content:center;
  color:var(--cobalt);
}

/* Ô CAO: ảnh phủ toàn ô, chữ nằm trên lớp phủ đáy. */
.bt-tall{ display:flex; align-items:flex-end; }
.bt-tall .bt-media{ background:var(--navy-dark); }

/*
 * Lớp phủ đáy bằng token thay cho ba nút rgba() của mockup — file này không
 * được khai custom property nên một màu pha sẽ là hằng số ngoài hệ token.
 */
.bt-tall .bt-media::after{
  content:"";
  position:absolute;
  inset:0;
  background:linear-gradient(to top, var(--navy-dark) 0%, transparent 100%);
}

/* Ô NGANG: chia đôi ảnh | chữ, tránh cắt ảnh quá mức. */
.bt-split{ display:grid; grid-template-columns:1fr 1fr; }
/*
 * O NGANG o kho rong: `.bt-media` chi chiem NUA TRAI, `.bt-body` nua phai. Mot
 * `inherit` o day se bo tron ca hai goc PHAI cua anh, tao hai khe ho giua anh
 * va khoi chu. Nen moi ben bo dung hai goc ngoai cua minh. Do la ly do rule
 * nay ton tai thay vi de `inherit` chay tiep.
 */
.bt-split .bt-media{
  position:relative;
  inset:auto;
  height:100%;
  border-radius:var(--r-lg) 0 0 var(--r-lg);
}
.bt-split .bt-body{
  display:flex;
  flex-direction:column;
  justify-content:center;
  padding:var(--sp-5);
  border-radius:0 var(--r-lg) var(--r-lg) 0;
  background:linear-gradient(150deg, var(--navy) 0%, var(--navy-dark) 100%);
}

.bt-body{ display:block; position:relative; z-index:2; width:100%; padding:var(--sp-5); }

.bt-name{
  display:block;
  font-family:var(--font-head);
  font-weight:800;
  font-size:18px;
  color:var(--white);
  line-height:1.3;
  margin-bottom:6px;
}

/* --on-navy-text trên --navy: 7.19:1 — đủ AA cho chữ 12.5px. */
.bt-spec{ display:block; font-size:12.5px; color:var(--on-navy-text); line-height:1.5; }

/*
 * `.bt-desc` và `.bt-cta` CHỈ có ở `.bt-1`. Không viết luật nào giả định chúng
 * có mặt ở mọi ô — và không cần `.bt-desc{display:none}` như mockup, vì markup
 * đơn giản là không in chúng ra ở ô thường.
 * --on-navy-text-2 trên --navy: 7.54:1.
 */
.bt-desc{
  display:block;
  color:var(--on-navy-text-2);
  font-size:14px;
  line-height:1.65;
  max-width:390px;
  margin-top:10px;
}

.bt-cta{
  display:inline-flex;
  align-items:center;
  gap:7px;
  margin-top:var(--sp-3);
  font-size:13.5px;
  font-weight:700;
  color:var(--white);
  border-bottom:2px solid var(--cobalt);
  padding-bottom:3px;
  transition:gap var(--dur-micro) var(--ease);
}
.bento-tile:hover .bt-cta{ gap:12px; }

/* Ô lớn dẫn đầu — (0,2,0), thắng `.bt-body`/`.bt-name` (0,1,0) bằng độ ưu tiên. */
.bt-1 .bt-body{ padding:var(--sp-6); }
.bt-1 .bt-name{ font-size:26px; margin-bottom:9px; }
.bt-1 .bt-spec{ font-size:13.5px; }

/*
 * ĐÃ XOÁ — nhãn "Dòng chủ lực" (`.bt-badge`). Feedback khách (21/08/2026,
 * ảnh khối "Sản phẩm" trang chủ): "Xóa button 'dòng chủ lực'". Khác với
 * `.bt-split` (mục 3, giữ lại như một biến thể độc lập vì có thể cần lại) —
 * ở đây khách muốn bỏ HẲN, nên gỡ luôn rule thay vì để một class định nghĩa
 * mà không nơi nào gán. template-parts/home/products.php không còn in
 * `<span class="bt-badge">` nào nữa; xem docblock "Ô ĐẦU LÀ Ô LỚN" ở đó.
 */

/* ============================================================
   5. Video quy trình
   ============================================================ */
.video-sec{ background:linear-gradient(160deg, var(--navy) 0%, var(--navy-dark) 100%); }
.video-sec .section-head h2{ color:var(--white); }

/* (0,2,0) thắng `.section-head p` (0,1,1). --on-navy-text-2 trên --navy: 7.54:1. */
.video-sec .section-lead{ color:var(--on-navy-text-2); }

/*
 * 28px VÀ 22px CỐ Ý KHÔNG THÀNH TOKEN — QUYẾT ĐỊNH, KHÔNG PHẢI BỎ SÓT.
 *
 * Vòng viền này là hình trang trí (`::before`, pointer-events:none), và bán
 * kính của nó KHÔNG phải một bậc của thang bo góc bề mặt: nó là bán kính của
 * khối bên trong CỘNG khoảng lệch `inset`. Desktop: 8 + 20 = 28. Mobile lệch
 * 14 nên 8 + 14 = 22. Đưa hai con số dẫn xuất này vào bộ token sẽ tạo hai bậc
 * radius mà không bề mặt nào dùng, và bậc đó sẽ bị chọn nhầm cho một thẻ nào
 * đó về sau. Ràng buộc thật ở đây là "bằng inset cộng --r-btn", không phải
 * "bằng một bậc trong thang".
 */
.video-shell{ position:relative; max-width:860px; margin-inline:auto; }
.video-shell::before{
  content:"";
  position:absolute;
  inset:-20px;
  z-index:0;
  border:1px solid var(--cobalt);
  border-radius:28px;
  pointer-events:none;
  transition:inset .45s var(--ease), border-color .45s var(--ease);
}
.video-shell:hover::before{ inset:-30px; }

/* Bốn dấu góc — trang trí thuần, aria-hidden. */
.corner{
  position:absolute;
  width:11px;
  height:11px;
  z-index:1;
  pointer-events:none;
  border-color:var(--on-navy-accent);
  border-style:solid;
  border-width:0;
  transition:top .45s var(--ease), left .45s var(--ease),
             right .45s var(--ease), bottom .45s var(--ease);
}
.c-tl{ top:-20px; left:-20px; border-top-width:2px; border-left-width:2px; border-top-left-radius:4px; }
.c-tr{ top:-20px; right:-20px; border-top-width:2px; border-right-width:2px; border-top-right-radius:4px; }
.c-bl{ bottom:-20px; left:-20px; border-bottom-width:2px; border-left-width:2px; border-bottom-left-radius:4px; }
.c-br{ bottom:-20px; right:-20px; border-bottom-width:2px; border-right-width:2px; border-bottom-right-radius:4px; }
.video-shell:hover .c-tl{ top:-30px; left:-30px; }
.video-shell:hover .c-tr{ top:-30px; right:-30px; }
.video-shell:hover .c-bl{ bottom:-30px; left:-30px; }
.video-shell:hover .c-br{ bottom:-30px; right:-30px; }

/*
 * TỈ LỆ bằng aspect-ratio, KHÔNG bằng chiều cao cố định: iframe do JS chèn lấp
 * đúng khung này, và nó chỉ có kích thước khi khung có kích thước.
 */
.video-frame{
  position:relative;
  z-index:2;
  width:100%;
  aspect-ratio:16 / 9;
  border-radius:var(--r-lg);
  overflow:hidden;
  background:var(--video-bg);
  box-shadow:var(--sh-4);
}

/*
 * `.video-embed` là iframe do assets/js/home.js chèn — nó CHƯA TỒN TẠI lúc file
 * này được viết. Thiếu bốn dòng dưới thì bấm phát ra video 300×150, kích thước
 * mặc định của <iframe>.
 *
 * global.css có `:where(img, svg, video, canvas, iframe){height:auto}` ở
 * (0,0,0); rule này (0,1,0) thắng bằng độ ưu tiên, không nhờ thứ tự nguồn.
 */
.video-embed{ position:absolute; inset:0; width:100%; height:100%; border:0; }

.video-poster{ position:absolute; inset:0; width:100%; height:100%; max-width:100%; object-fit:cover; }

/* Hôm nay chưa có ảnh poster nào, nên đây là thứ mọi người đang nhìn. */
.video-poster-fallback{
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  justify-content:center;
  background:var(--video-bg);
  color:var(--cobalt);
}

/*
 * Vùng bấm 74px, trên ngưỡng 44px. Nền --white ĐẶC là có chủ đích: poster là
 * ảnh nhân viên tải lên, sáng tối không đoán được, nên vòng focus của
 * global.css (--focus-ring, offset 2px) cần một mặt tựa xác định — --focus-ring
 * trên --white đạt 3.61:1, trên --video-bg đạt 4.90:1. Không có nền đặc thì
 * tương phản của vòng focus phụ thuộc vào tấm ảnh khách tải lên.
 * Mũi tên --navy trên --white: 11.40:1.
 */
.video-play{
  position:absolute;
  top:50%;
  left:50%;
  transform:translate(-50%, -50%);
  width:74px;
  height:74px;
  border-radius:var(--r-full);
  z-index:3;
  background:var(--white);
  color:var(--navy);
  border:0;
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  box-shadow:var(--sh-3);
  transition:transform var(--dur-state) var(--ease);
}
.video-play:hover{ transform:translate(-50%, -50%) scale(1.08); }

/* ============================================================
   6. Bảng ảnh nhà xưởng và băng chạy logo đối tác
   ============================================================ */
.gallery{ background:var(--white); }

.gallery-row{ overflow:hidden; position:relative; margin-bottom:var(--sp-4); }
.gallery-row::before,
.gallery-row::after{ content:''; position:absolute; top:0; bottom:0; width:60px; z-index:2; pointer-events:none; }
.gallery-row::before{ left:0; background:linear-gradient(90deg, var(--white), transparent); }
.gallery-row::after{ right:0; background:linear-gradient(270deg, var(--white), transparent); }

.gallery-track{ display:flex; gap:var(--sp-4); width:max-content; }
.gallery-track.left{ animation:vpScrollLeft 18s linear infinite; }
.gallery-track.right{ animation:vpScrollRight 18s linear infinite; }
.gallery-row:hover .gallery-track{ animation-play-state:paused; }

.gallery-item{
  flex:0 0 260px;
  height:168px;
  border-radius:var(--r-card);
  overflow:hidden;
  box-shadow:var(--sh-2);
  position:relative;
  background:var(--img-bg);
}

/* Mockup đặt <svg> chỗ ảnh; markup thật là <img class="gallery-img">. */
.gallery-img{ width:100%; height:100%; max-width:100%; object-fit:cover; }

/* --white trên --navy: 11.40:1. */
.gallery-tag{
  position:absolute;
  bottom:10px;
  left:10px;
  background:var(--navy);
  color:var(--white);
  font-size:11px;
  font-weight:600;
  padding:4px 10px;
  border-radius:var(--r-pill);
}

.partners{ background:var(--white); }

/*
 * TRÀN NGANG — SỬA LỖI CÓ SẴN CỦA MOCKUP.
 *
 * Mockup: `width:100vw; margin-left:calc(50% - 50vw); margin-right:calc(50% - 50vw)`.
 * `100vw` bao gồm cả bề rộng thanh cuộn dọc, nên trên Windows với thanh cuộn cố
 * định khối này rộng hơn vùng nhìn thấy ~17px và ĐẨY RA một thanh cuộn ngang cho
 * cả trang. Trên macOS thanh cuộn nổi nên lỗi không bao giờ xuất hiện — đúng
 * loại lỗi chỉ một nửa số người thấy.
 *
 * `.partners` là `section.wrap`, tức nó đã có --wrap-x đệm hai bên. Ở đây băng
 * chạy tràn ĐÚNG phần đệm đó bằng margin âm theo token, KHÔNG dùng `vw`. Đổi
 * lại: ở màn rất rộng băng dừng ở mép --wrap-max thay vì mép màn hình. Đó là
 * một nhượng bộ thị giác CÓ CHỦ Ý, đổi lấy việc không bao giờ có cuộn ngang.
 */
.partner-row{
  overflow:hidden;
  position:relative;
  margin-bottom:var(--sp-4);
  /*
   * KHÔNG còn margin âm — STT 8. Trước đây là
   * `margin-inline:calc(var(--wrap-x) * -1)` để ăn ngược phần đệm của vỏ; nay
   * vỏ `.partners.wrap` đã có `padding-inline:0` nên băng vốn đã chạm mép, và
   * giữ margin âm sẽ đẩy nó ra NGOÀI mép — đúng loại tràn ngang mà cả dự án
   * đang tránh. Để 0 tường minh chứ không xoá dòng: xoá thì người sửa sau
   * không biết chỗ này từng có margin âm và vì sao nó biến mất.
   */
  margin-inline:0;
}
.partner-row::before,
.partner-row::after{ content:''; position:absolute; top:0; bottom:0; width:120px; z-index:2; pointer-events:none; }
.partner-row::before{ left:0; background:linear-gradient(90deg, var(--white), transparent); }
.partner-row::after{ right:0; background:linear-gradient(270deg, var(--white), transparent); }

/*
 * VÁ LỖ HỞ GIỮA CHU KỲ — khách khoanh đỏ khoảng trắng lộ ra ở mép phải mỗi
 * hàng logo trước khi băng lặp lại ("cho section này infinity đi nó hở
 * quá").
 *
 * NGUYÊN NHÂN GỐC: `.partner-track` dịch chuyển bằng `translateX(-50%)` (xem
 * `@keyframes vpScrollLeft/Right` bên dưới) — công thức NÀY chỉ liền mạch khi
 * NỬA TRACK (tổng bề rộng từ giữa nội dung trở về sau) đủ dài để lấp kín
 * khung nhìn `.partner-row` tại MỌI thời điểm giữa chu kỳ. STT 8 (ngay trên)
 * đã bỏ margin âm khiến `.partner-row` giờ chạm ĐÚNG mép `.partners.wrap` —
 * rộng hơn hẳn lúc track được tính hai lượt (1 gốc + 1 sao). Track không lớn
 * theo, nên nửa track hoá ra HẸP HƠN khung nhìn: đo thật ở viewport 1280px
 * cho `.partner-row` rộng 1265px trong khi nửa-track chỉ 918px (hàng trái) /
 * 1053.5px (hàng phải) — thiếu đúng bằng khoảng trắng khách thấy.
 *
 * FIX: partners.php lặp SÁU lượt thay vì hai (`$vp_pn_so_luot`, xem docblock
 * ở đó) — nửa track (ba lượt) giờ đủ dài cho màn hình tới khoảng 2700px. CSS
 * ở đây KHÔNG cần đổi gì: `translateX(-50%)` vẫn đúng với N lượt bất kỳ miễn
 * N chẵn, vì nửa sau luôn giống hệt nửa đầu về nội dung.
 *
 * GIỚI HẠN CÒN LẠI, không giấu: đây vẫn là một ngưỡng cứng, không phải vô hạn
 * thật. Màn hình rộng hơn ~2700px (ultra-wide, nhiều màn ghép trình duyệt
 * full-screen) lý thuyết vẫn có thể lộ lại đúng lỗ hở này. Vá triệt để cần
 * JS đo `.partner-row` lúc runtime và sinh đủ số lượt lặp — ngoài phạm vi
 * sửa CSS/PHP thuần của đợt này.
 */
.partner-track{ display:flex; gap:var(--sp-4); width:max-content; padding:6px 0; }
.partner-track.left{ animation:vpScrollLeft 26s linear infinite; }
.partner-track.right{ animation:vpScrollRight 26s linear infinite; }
.partner-row:hover .partner-track{ animation-play-state:paused; }

/*
 * Viền --border trên --white chỉ 1.23:1. RỦI RO ĐÃ CÂN NHẮC, cùng lý lẽ đã ghi
 * cho ô nhập ở mục 7 global.css: khối này được nhận ra bằng CHÍNH LOGO bên
 * trong chứ không bằng đường viền, và viền là trang trí chứ không mang thông
 * tin. Đổi --border sang màu đậm hơn kéo theo mọi đường kẻ của cả hệ thiết kế.
 */
.partner-badge{
  flex:0 0 auto;
  height:88px;
  padding:0 var(--sp-6);
  border-radius:var(--r-card);
  background:var(--white);
  border:1px solid var(--border);
  box-shadow:var(--sh-1);
  display:flex;
  align-items:center;
  justify-content:center;
  transition:box-shadow var(--dur-micro) var(--ease);
}
.partner-badge:hover{ box-shadow:var(--sh-2); }

/*
 * KHÔNG `width:100%`. Logo là ảnh bên thứ ba, tỉ lệ không đoán được: ép bề rộng
 * sẽ kéo giãn logo vuông và bóp logo dài. Ghim CHIỀU CAO, để bề rộng tự do, và
 * `object-fit:contain` để không bao giờ cắt.
 */
.partner-logo{ height:46px; width:auto; max-width:170px; object-fit:contain; }

/*
 * NÚT DỪNG BĂNG CHẠY ĐÃ BỊ GỠ — KHÁCH YÊU CẦU.
 *
 * Trước đây ở đây có `[data-vp-marquee-paused="true"] [data-vp-marquee-track]
 * {animation-play-state:paused}`, cặp với `button.vp-marquee-toggle` và module
 * initMarqueeToggle() của home.js. Cả ba đã gỡ cùng lúc: giữ lại rule CSS khi
 * không còn gì đặt thuộc tính đó là để lại một rule KHÔNG BAO GIỜ THẮNG, đúng
 * loại nợ dự án này đã ba lần vấp.
 *
 * CÒN LẠI HAI CƠ CHẾ DỪNG, cả hai đều KHÔNG do nút:
 *   - `prefers-reduced-motion: reduce` → `animation-name:none` ở mục 12;
 *   - `:hover` trên hàng → `animation-play-state:paused` ngay dưới đây.
 *
 * MẤT GÌ: WCAG 2.2.2 đòi một cơ chế dừng TRONG TRANG cho nội dung tự chuyển
 * động quá 5 giây. Băng chạy không còn cơ chế đó cho người dùng bàn phím và
 * cảm ứng (hover không đến được với họ). Đây là một RỦI RO ĐÃ BIẾT do khách
 * quyết, không phải một thiếu sót.
 */

@keyframes vpScrollLeft{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }
@keyframes vpScrollRight{ from{ transform:translateX(-50%); } to{ transform:translateX(0); } }

/* ============================================================
   7. Báo chí
   ============================================================ */
.press{ background:var(--bg-alt); }
.press-carousel{ position:relative; }

/*
 * BẮT BUỘC. Không có `overflow-x:auto` thì KHÔNG JS là một danh sách bị cắt
 * cụt, và CÓ JS thì `scrollBy()` của home.js không có gì để cuộn.
 *
 * `.press-viewport` mang `tabindex="0"` nên NÓ nhận tiêu điểm bàn phím. Vòng
 * focus đến từ `:where(…, [tabindex]):focus-visible` của global.css; file này
 * không khai `outline` ở đâu cả nên không có gì nuốt nó.
 */
.press-viewport{
  overflow-x:auto;
  overflow-y:hidden;
  scroll-snap-type:x mandatory;
  scroll-behavior:smooth;
  scrollbar-width:none;
  -ms-overflow-style:none;
}
.press-viewport::-webkit-scrollbar{ display:none; }

.press-track{ display:flex; gap:var(--sp-7); }

/* Số thẻ là 1..n; một thẻ thì không có gì để cuộn và luật này vẫn đúng. */
.press-item{
  flex:0 0 calc((100% - 2 * var(--sp-7)) / 3);
  min-width:0;
  scroll-snap-align:start;
  display:flex;
  flex-direction:column;
  padding-top:var(--sp-5);
}
.press-item::before{
  content:"";
  display:block;
  width:30px;
  height:2px;
  background:var(--cobalt);
  margin-bottom:var(--sp-4);
  transition:width var(--dur-state) var(--ease);
}
.press-item:hover::before{ width:56px; }

/* --text trên --bg-alt: 15.25:1. Xem mục 1 về vì sao KHÔNG dùng --text-muted. */
.press-source{
  display:block;
  font-size:11px;
  font-weight:700;
  color:var(--text);
  letter-spacing:1.8px;
  text-transform:uppercase;
  margin-bottom:var(--sp-3);
}
.press-text{
  display:block;
  font-family:var(--font-head);
  font-size:19px;
  line-height:1.5;
  color:var(--text);
  font-weight:700;
  flex-grow:1;
  transition:color var(--dur-state) var(--ease);
}
/* --navy trên --bg-alt: 10.00:1. */
.press-item:hover .press-text{ color:var(--navy); }

.press-date{
  margin-top:var(--sp-5);
  font-size:12.5px;
  color:var(--text);
  display:flex;
  align-items:center;
  gap:10px;
  letter-spacing:.3px;
}
.press-more{
  color:var(--navy);
  font-weight:700;
  opacity:0;
  transform:translateX(-6px);
  transition:opacity var(--dur-state) var(--ease), transform var(--dur-state) var(--ease);
}
.press-item:hover .press-more{ opacity:1; transform:translateX(0); }

/*
 * `.press-controls` do home.js bỏ `hidden` khi nó chạy được. `display:flex` VÔ
 * ĐIỀU KIỆN sẽ ĐÈ `display:none` của UA và hai nút chết hiện ra khi không có
 * JS. Cặp rule dưới đây là cách CSS không gỡ `hidden` của JS.
 *
 * Khai lại `[hidden]{display:none}` là CẦN THIẾT chứ không thừa: bộ giải
 * cascade của tests/test-css-cascade.php chỉ thấy CSS của theme, không thấy
 * stylesheet của UA — không khai lại thì không có gì để ĐO.
 */
.press-controls:not([hidden]){ display:flex; }
.press-controls[hidden]{ display:none; }
.press-controls{ align-items:center; gap:var(--sp-5); margin-top:var(--sp-7); }

/*
 * Con được JS đặt `width` và `left` dạng phần trăm; cha PHẢI position:relative
 * và con PHẢI position:absolute, không thì `left` không có tác dụng nào.
 * Thanh chạy --navy trên rãnh --decor: 8.63:1; trên nền --bg-alt: 10.00:1.
 */
.press-progress{ flex:1; height:1px; background:var(--decor); position:relative; }
.press-progress > span{
  position:absolute;
  left:0;
  top:-1px;
  height:3px;
  background:var(--navy);
  transition:left .45s var(--ease), width .45s var(--ease);
}

/*
 * Vùng bấm 44px. Viền --navy trên --bg-alt: 10.00:1.
 *
 * VIẾT LONGHAND, KHÔNG viết `border:1px solid var(--navy)`. Trạng thái tắt ngay
 * bên dưới chỉ đổi `border-color`; với dạng viết tắt thì màu viền lúc BẬT không
 * còn là một khai báo `border-color` nào cả, nên không có gì để đo — và một
 * `border-color` viết nhầm chỗ sẽ lặng lẽ không có đối chứng. Đây đúng thứ
 * tests/test-css-cascade.php mục 7.8 đã bắt được ở lượt chạy đầu.
 */
.press-nav{
  width:44px;
  height:44px;
  border-radius:var(--r-full);
  flex-shrink:0;
  border-style:solid;
  border-width:1px;
  border-color:var(--navy);
  background:transparent;
  display:flex;
  align-items:center;
  justify-content:center;
  color:var(--navy);
  cursor:pointer;
  transition:background var(--dur-state) var(--ease),
             color var(--dur-state) var(--ease),
             border-color var(--dur-state) var(--ease);
}
.press-nav:hover:not(:disabled){ background:var(--navy); color:var(--white); }

/*
 * TRẠNG THÁI BAN ĐẦU CỦA CẢ HAI NÚT, không phải một ca hiếm.
 *
 * KHÔNG dùng `opacity:.28` như mockup: nó kéo mọi thứ về phía nền một cách
 * không đo được và làm mũi tên gần như biến mất.
 *
 * Ở đây tắt bằng HAI tín hiệu độc lập và đo được: viền tụt từ --navy (10.00:1
 * so với --bg-alt) xuống --disabled (1.39:1 — gần như tan vào nền, đó là ý đồ:
 * đường bao biến mất), còn mũi tên tụt từ --navy xuống --text-muted (4.24:1) —
 * vẫn trên ngưỡng 3:1 của WCAG 1.4.11 cho thành phần đồ hoạ, tức vẫn ĐỌC ĐƯỢC.
 * WCAG 1.4.3 miễn trừ điều khiển đang tắt; con số 4.24 là dư dả so với mức đó.
 */
.press-nav:disabled{
  border-color:var(--disabled);
  color:var(--text-muted);
  background:transparent;
  cursor:default;
}

/* ============================================================
   8. Cam kết ESG
   ============================================================ */
.why{ background:var(--white); }

/*
 * Cột trái có thể vắng HẲN (mọi ô chữ trống, chỉ còn hình) hoặc cột phải là thứ
 * duy nhất còn lại. auto-fit thay cho `1fr 1fr` ghi cứng.
 */
.why-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(320px, 1fr));
  gap:var(--sp-7);
  align-items:center;
}

.why-kicker{ display:flex; align-items:center; gap:var(--sp-4); margin-bottom:var(--sp-4); }
.why-kicker::before{ content:""; width:44px; height:2px; background:var(--cobalt); flex-shrink:0; }
/* --navy trên --white: 11.40:1. */
.why-kicker span{ font-size:12px; font-weight:700; color:var(--navy); letter-spacing:2.2px; text-transform:uppercase; }

.why-copy h2{ margin-bottom:var(--sp-4); }
/* --text-muted trên --white: 4.83:1 — nền TRẮNG, khác hẳn ba khối nền --bg-alt. */
.why-copy .section-lead{ color:var(--text-muted); max-width:470px; }
.why-goal{ color:var(--text); margin-top:var(--sp-4); max-width:470px; }

.why-list{ display:flex; flex-direction:column; gap:var(--sp-5); margin-top:var(--sp-6); }
.why-item{ display:flex; gap:var(--sp-4); }
/* Viền --cobalt trên --white: 3.11:1, đạt ngưỡng 3:1. Chữ --navy: 11.40:1. */
.why-num{
  font-family:var(--font-head);
  font-weight:800;
  font-size:13px;
  color:var(--navy);
  border:1px solid var(--cobalt);
  border-radius:var(--r-full);
  background:var(--white);
  width:32px;
  height:32px;
  flex-shrink:0;
  display:flex;
  align-items:center;
  justify-content:center;
}
.why-item h4{ margin-bottom:4px; }
.why-item p{ color:var(--text-muted); font-size:14px; line-height:1.6; }

/*
 * SỬA (27/08/2026, khách yêu cầu "đồng bộ lại tất cả nút") — bỏ `.why-btn`
 * riêng (viền --navy 1px, padding 14px 30px, font 14px), đổi markup
 * (why.php) sang `.btn.btn-secondary` chung — CÙNG vai trò "nút viền đơn
 * trên nền sáng" đã có ở `.certs-link` (template-parts/home/certs.php,
 * CÙNG TRANG). Chỉ giữ lại margin-top riêng của layout khối này (khoảng
 * cách với đoạn văn phía trên) — không phải phần thị giác của nút, không
 * thuộc về `.btn-secondary` dùng chung.
 */
.why-copy .btn{ margin-top:var(--sp-6); }

/*
 * BA THẺ ẢNH — thay vòng ESG (feedback khách STT 10, đợt 1).
 *
 * `svg.esg-wheel`/`.esg-seg` KHÔNG còn markup nào sinh ra chúng nữa (xem
 * template-parts/home/why.php) — rule của chúng bị XOÁ khỏi đây thay vì để
 * lại chết, và tests/test-css-cascade.php mục ESG đã đổi thành khẳng định
 * PHỦ ĐỊNH để ai lỡ viết lại `.esg-wheel` (CSS hoặc HTML) thì bộ test báo đỏ.
 *
 * Bố cục: một thẻ CAO bên trái (`.why-card--tall`), hai thẻ xếp bên phải.
 * Grid 2 cột × 2 hàng, thẻ đầu chiếm cả hai hàng của cột 1 — không dùng
 * `float`/`position:absolute` để dàn vị trí, đúng ràng buộc của Task 7.
 */
.why-cards{
  display:grid;
  grid-template-columns:1fr 1fr;
  grid-template-rows:repeat(2, minmax(160px, 1fr));
  gap:var(--sp-4);
  height:100%;
  min-height:420px;
}
.why-card--tall{ grid-row:1 / 3; }

/* Mỗi thẻ là khung định vị cho ảnh phủ kín + lớp gradient + tiêu đề. */
.why-card{
  position:relative;
  overflow:hidden;
  border-radius:var(--r-lg);
  background:var(--navy);
  isolation:isolate;
  display:flex;
  flex-direction:column;
  justify-content:flex-end;
}
.why-card-img{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:cover;
  display:block;
}

/*
 * LỚP PHỦ GRADIENT LÀ BẢO ĐẢM TƯƠNG PHẢN, KHÔNG PHẢI TRANG TRÍ.
 *
 * `.why-card-title` là chữ trắng (`--white`) đặt trực tiếp lên dải gradient
 * tối nhất ở đáy thẻ (rgba(11,18,32,.92) — gần đen, khác --navy vì ảnh nền
 * không đoán trước được và cần một mốc tối ổn định hơn token thương hiệu).
 * Tại điểm chữ ngồi (dải 0%–46% tính từ đáy đặc gần như hoàn toàn), độ phủ
 * đo được xấp xỉ ~90%; tương phản trắng/rgba(11,18,32,.92) hoà với BẤT KỲ
 * ảnh nền nào (kể cả ảnh trắng) vẫn ≥ 4.5:1 — ngưỡng WCAG cho chữ thường —
 * vì lớp phủ gần như che kín ảnh gốc tại đúng vùng chữ nằm. Càng lên cao
 * gradient càng trong suốt (chủ động để phần ảnh phía trên thẻ vẫn nhìn
 * thấy được), nhưng không có chữ nào đặt ở vùng đó.
 */
.why-card-overlay{
  position:absolute;
  inset:0;
  background:linear-gradient(to top, rgba(11,18,32,.92) 0%, rgba(11,18,32,.62) 30%, rgba(11,18,32,0) 65%);
  pointer-events:none;
}
.why-card-title{
  position:relative;
  z-index:1;
  margin:0;
  padding:var(--sp-4);
  align-self:end;
  color:var(--white);
  font-size:15px;
  line-height:1.4;
  font-weight:700;
}

@media (max-width: 640px){
  .why-cards{
    grid-template-columns:1fr;
    grid-template-rows:none;
    min-height:0;
  }
  .why-card--tall{ grid-row:auto; }
  .why-card{ aspect-ratio:4 / 3; }
}

/* ============================================================
   9. Chứng nhận
   ============================================================ */

/*
 * `padding-block:0` thắng `:where(section){padding-block:var(--sp-8)}` của
 * global.css — cái đó ở (0,0,0) vì :where() cộng 0, còn `.certs` ở (0,1,0).
 * Đệm dọc thuộc về `.certs-text-container`, không thuộc về vỏ: cột hình bên
 * trái phải chạm mép trên dưới.
 */
.certs{ background:var(--bg-alt); padding-block:0; }

/*
 * KHÔNG `50vw` + `margin-left:calc(50% - 50vw)` như mockup — xem lý lẽ đầy đủ ở
 * `.partner-row`. `.certs` KHÔNG phải `.wrap`, tức vỏ đã rộng hết trang, nên
 * `flex:0 0 50%` cho ra ĐÚNG hiệu ứng tràn nửa trang mà không cần `vw` nào.
 */
.certs-v2-grid{ display:flex; align-items:stretch; min-height:560px; }
.certs-visual-bleed{
  flex:0 0 50%;
  background:linear-gradient(155deg, var(--navy) 0%, var(--navy-dark) 100%);
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  position:relative;
  overflow:hidden;
  padding:var(--sp-8) var(--sp-7);
  gap:var(--sp-5);
}
.certs-text-container{
  flex:1;
  min-width:0;
  display:flex;
  flex-direction:column;
  justify-content:center;
  padding:var(--sp-8) var(--sp-7);
  max-width:620px;
}

/*
 * KHUNG ẢNH TO THÊM MỘT PHẦN BA — KHÁCH YÊU CẦU (STT 5).
 *
 * Ghi tay trên ảnh minh họa: "Tăng size khung ảnh thêm 1/3 nữa".
 *
 *   desktop   270 × 340  →  360 × 453   (×4/3, làm tròn xuống)
 *   ≤900px    230 × 290  →  307 × 387   (cùng hệ số, xem khối media bên dưới)
 *
 * ĐÃ ĐO CHỖ CHỨA TRƯỚC KHI PHÓNG, không nhân bừa: `.certs-visual-bleed` rộng
 * 633px với đệm 48px hai bên → lòng 537px, nên 360px vừa thoải mái. Chiều cao
 * lòng là 440px nên khung 453px sẽ đẩy khối cao thêm ~13px; đó là hệ quả
 * chấp nhận được của việc phóng to, không phải tràn.
 */
.cert-frame{
  position:relative;
  width:360px;
  height:453px;
  border-radius:var(--r-card-lg);
  background:var(--white);
  border:1px solid var(--border);
  box-shadow:var(--sh-4);
  padding:10px;
  overflow:hidden;
}

/*
 * SLIDE ĐÓNG/MỞ BẰNG `hidden`, KHÔNG bằng `.active` + opacity như mockup.
 *
 * Mockup cho `.cert-slide{position:absolute; opacity:0}` vô điều kiện. Nếu chép
 * nguyên và thêm `display:flex` vô điều kiện thì `display:none` của `[hidden]`
 * bị ĐÈ và cả năm slide chồng lên nhau. Cặp rule dưới là cách CSS không gỡ
 * `hidden` của home.js. Hai rule đều (0,2,0) và không bao giờ cùng khớp.
 *
 * Hiệu ứng mờ chồng của mockup đã BỎ: nó chỉ hoạt động khi mọi slide cùng nằm
 * trong luồng, tức đúng cái mà `hidden` ngăn. Giữ lại là một transition không
 * bao giờ chạy.
 */
.cert-slide:not([hidden]){ display:flex; }
.cert-slide[hidden]{ display:none; }
.cert-slide{
  position:absolute;
  inset:10px;
  border-radius:var(--r-sm);
  background:var(--white);
  border:1px solid var(--border);
  flex-direction:column;
  align-items:center;
  padding:var(--sp-5) var(--sp-4) var(--sp-4);
}

.cert-logo{ display:flex; align-items:center; justify-content:center; margin-bottom:var(--sp-4); }
/* Logo là ảnh bên thứ ba, tỉ lệ không đoán được. */
.cert-logo-img{ max-width:100%; height:auto; object-fit:contain; }

/* Viền --navy trên --white: 11.40:1. */
.cert-seal{
  width:56px;
  height:56px;
  border-radius:var(--r-full);
  border:2px solid var(--navy);
  color:var(--navy);
  display:flex;
  align-items:center;
  justify-content:center;
  margin-bottom:var(--sp-4);
  flex-shrink:0;
}
/* --navy trên --white: 11.40:1. */
.cert-title{ display:block; font-size:15px; font-weight:700; color:var(--navy); text-align:center; margin-bottom:6px; font-family:var(--font-head); }
/* --text-muted trên --white: 4.83:1. */
.cert-sub{ display:block; font-size:11.5px; color:var(--text-muted); text-align:center; }

/* Cùng cái bẫy `hidden` như `.press-controls`. Số chấm là 1..n. */
.cert-dots:not([hidden]){ display:flex; }
.cert-dots[hidden]{ display:none; }
/*
 * Gap RIENG cho khoi nay (KHONG dung chung --sp-1 toan site): chấm active
 * co transform:scale(1.35) tren nen 24x24 -- phong to 4.2px MOI ben VE
 * HINH HOC (transform khong chiem them layout), lan vao dung khong gian
 * gap voi chấm ke ben. Voi gap 4px (--sp-1) cu, Lighthouse do duoc vung
 * bam an toan con 23.8px -- duoi nguong 24px cua WCAG 2.5.8 (2.5.8 la
 * nguong CHUNG cho vung bam, khac 24x24 rieng cua .cert-dot o duoi). Tang
 * len 10px du bu phan lan do, khong dung toi phai doi kich thuoc/ty le
 * scale dang co (van giu hieu ung phong to ro rang cho chấm active).
 */
.cert-dots{ gap:10px; justify-content:center; flex-wrap:wrap; }

/*
 * VÙNG BẤM 24px (WCAG 2.5.8). Chấm 8px của mockup KHÔNG đạt.
 *
 * Cách làm: hộp 24×24 với 8px đệm TRONG SUỐT, còn màu chỉ tô vào hộp nội dung
 * nhờ `background-clip:content-box`. Chấm nhìn thấy vẫn 8px, vùng bấm 24px, và
 * không cần một phần tử giả nào.
 *
 * --cobalt trên nền --navy của cột hình: 3.67:1 (thành phần đồ hoạ, ngưỡng 3:1).
 * Chấm đang chọn đổi sang --white: 11.40:1 so với nền, và 3.11:1 so với chấm
 * thường — hai trạng thái phân biệt được bằng chính tương phản, không chỉ bằng
 * kích thước.
 */
.cert-dot{
  width:24px;
  height:24px;
  padding:8px;
  border:0;
  border-radius:var(--r-full);
  background-color:var(--cobalt);
  background-clip:content-box;
  cursor:pointer;
  flex:0 0 auto;
  transition:background-color var(--dur-state) var(--ease), transform var(--dur-state) var(--ease);
}
.cert-dot[aria-current="true"]{ background-color:var(--white); transform:scale(1.35); }

.certs-text-container h2{ margin-bottom:var(--sp-4); }
.certs-list{ display:flex; flex-direction:column; gap:var(--sp-3); margin:var(--sp-5) 0 var(--sp-6); list-style:none; padding-left:0; }
.certs-item{ display:flex; align-items:center; gap:var(--gap-inline); }
/* Nền --navy, biểu tượng --white: 11.40:1. */
.certs-check{
  width:24px;
  height:24px;
  border-radius:var(--r-full);
  background:var(--navy);
  color:var(--white);
  display:flex;
  align-items:center;
  justify-content:center;
  flex-shrink:0;
}
/* --text trên --bg-alt: 15.25:1. */
.certs-item-name{ font-size:15px; font-weight:600; color:var(--text); }
/* --navy trên --bg-alt: 10.00:1. */
.certs-link{ color:var(--navy); font-weight:700; }

/* ============================================================
   10. (TRỐNG) — hai nút "Tạm dừng chuyển động" đã bị gỡ
   ============================================================ */

/*
 * Mục này từng chứa style của `.vp-slider-toggle` (slider chứng nhận) và
 * `.vp-marquee-toggle` (thư viện ảnh, đối tác). Khách yêu cầu bỏ cả ba nút;
 * markup, module JS và style đã gỡ CÙNG MỘT LƯỢT, vì style của một phần tử
 * không còn được in ra là rule không bao giờ thắng.
 *
 * Số mục giữ nguyên là 10 chứ không đánh lại: mọi chú thích trong ba file CSS
 * và trong tests/ đều trỏ nhau bằng "mục N", đánh lại số là làm hỏng im lặng
 * hàng chục con trỏ chéo mà không có khẳng định nào bắt được.
 *
 * CƠ CHẾ DỪNG CÒN LẠI: xem chú thích ở mục 6 (băng chạy) và mục 12
 * (prefers-reduced-motion). Slider chứng nhận vẫn ngưng khi chuột hoặc tiêu
 * điểm vào vỏ, và không khởi động dưới prefers-reduced-motion.
 */

/* ============================================================
   11. Khối kêu gọi hành động
   ============================================================ */
.cta{ background:var(--bg-alt); }
/*
 * CĂN GIỮA (03/09/2026, khách: "cta cho center hết xem" — ảnh chụp
 * /du-an-tieu-bieu/ khoanh đỏ khối "Vẫn còn nhiều dự án khác"). Trước đây
 * `space-between` đẩy chữ sát trái, nút sát phải — khối rộng full-wrap thì
 * lệch hẳn về hai đầu, không có tâm.
 *
 * `.cta-box` là MỘT rule lặp lại y hệt ở BA file (home.css, page-dichvu.css,
 * page-duan.css) — không import lẫn nhau, phải sửa cả ba cùng lúc. Đã grep
 * xác nhận cả ba giống nhau tuyệt đối trước khi sửa.
 */
/*
 * KHUÔN CTA CHUẨN — chốt 07/09/2026 (khách: "mấy cái call to action đồng bộ
 * với nhau và title nó đừng cho xuống hàng trên desktop").
 *
 * ĐO SÁU KHỐI TRÊN TRANG THẬT TRƯỚC KHI SỬA (1280px): `.cta-box` (chủ, dịch
 * vụ, dự án, tuyển dụng) bo 20 đệm 48 chữ mô tả 14.5; `.news-cta-box` bo 24
 * đệm 52/56 mô tả 16; `.nl-cta-box` bo 20 đệm 52/56 h2 30; `.au-cta-box` bo 24
 * đệm 52/56 h2 30 mô tả 15 và HỘP chỉ 820px; `.contact-cta-box` bo 24 đệm
 * 64/40 gradient 160deg hộp 1080px. Sáu khối cùng một vai trò mà không giá trị
 * nào trùng nhau đủ bộ.
 *
 * CHUẨN LẤY THEO ĐA SỐ, trừ hai chỗ có lý do đo được: bo góc lấy var(--r-xl)
 * (test-css-cascade đang khoá đúng token này cho .cta-box, đổi sang 24px sẽ
 * làm đỏ test và bỏ token lấy số cứng), đệm lấy var(--sp-7) = 48px của
 * `.cta-box` — khối phổ biến nhất, có mặt ở 4/8 trang.
 *
 * `max-width` của HỘP bỏ hẳn (au 820, contact 1080): hộp giờ lấp đúng bề ngang
 * `.wrap` như bốn khối kia, nên mọi trang có cùng một mép.
 */
.cta-box{
  background:linear-gradient(135deg, var(--navy) 0%, var(--navy-dark) 100%);
  border-radius:var(--r-xl);
  padding:var(--sp-7);
  text-align:center;
  color:var(--white);
  position:relative;
  overflow:hidden;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:var(--sp-5);
}
/* Vòng sáng góc trái dưới — phần khuôn chung, trước đây chỉ 3/6 khối có. */
.cta-box::after{
  content:"";
  position:absolute;
  left:-60px;
  bottom:-60px;
  width:260px;
  height:260px;
  border-radius:var(--r-full);
  background:radial-gradient(circle, rgba(92,144,245,.25), transparent 70%);
}
.cta-box > *{ position:relative; z-index:1; }
.cta-copy{ min-width:0; }
/*
 * BỎ `max-width:420px` — ĐÂY LÀ THỨ ÉP TIÊU ĐỀ XUỐNG HÀNG. Đo tại 1280px:
 * hộp rộng 1116, trừ đệm còn 1020px cho chữ, nhưng tiêu đề bị bó trong 420px
 * nên "Vẫn còn nhiều dự án khác đang chờ bạn khám phá" (và cả ba tiêu đề CTA
 * còn lại) đều gãy làm hai dòng — cao 80px thay vì 40px. Bỏ giới hạn thì
 * tiêu đề dài nhất trong bốn trang vẫn gọn một dòng trên desktop.
 *
 * `text-wrap:balance` thay chỗ nó: khi bề ngang hẹp lại (điện thoại) buộc
 * phải xuống dòng thì hai dòng chia đều chữ, không còn dòng dưới trơ một từ.
 */
.cta-box h2{ color:var(--white); text-wrap:balance; }
/* --on-navy-text trên --navy: 7.19:1. (0,2,0) thắng `.section-head p` (0,1,1). */
.cta-box .section-lead{ color:var(--on-navy-text); margin-top:10px; font-size:16px; line-height:1.7; max-width:600px; margin-inline:auto; }
/* Có thể có 0, 1 hoặc 2 nút. */
.cta-actions{ display:flex; flex-wrap:wrap; justify-content:center; gap:var(--gap-inline); }

/*
 * Cùng lý lẽ đúng từng chữ như `.hero .btn-primary` ở mục 2: `.cta-box` KHÔNG
 * mang `.on-navy` nên rule của global.css không với tới, và nền --navy của
 * `.btn-primary` chỉ 1.23:1 so với hộp navy — nút tàng hình.
 * `.cta-box .btn-primary:hover` ở (0,3,0) thắng `.btn-primary:hover` (0,2,0)
 * bằng độ ưu tiên, không nhờ thứ tự nguồn.
 * `.btn-outline-light` đã có sẵn ở mục 6 global.css — KHÔNG viết lại ở đây.
 */
/*
 * TÁCH BẠCH nút chính khỏi nền hộp (cùng đợt 03/09/2026, ảnh chụp khoanh đỏ
 * nút "Yêu cầu báo giá dự án" — nút gần như biến mất vào nền navy).
 *
 * Đo trên trang thật trước khi sửa: nền hộp #001BB7 (điểm đầu gradient),
 * nút cũ #3366CC (--cobalt-ink) — độ tách bạch giữa hai màu chỉ 2.13:1.
 * Đổi nút chính sang NỀN TRẮNG/CHỮ NAVY (cùng khuôn nút "Yêu cầu báo giá"
 * trên header, .vp-header-cta) — đo lại đạt 11.41:1.
 */
.cta-box .btn-primary,
.cta-box .btn-primary:hover{ background:var(--white); color:var(--navy); border-color:var(--white); }

/* ============================================================
   12. Responsive
   ============================================================ */
@media (max-width:1024px){
  .products-bento{ grid-template-columns:repeat(2, 1fr); grid-auto-rows:200px; }
  /* Ô ngang trở lại dạng ảnh phủ vì khung đã gần vuông. */
  .bt-split{ display:flex; align-items:flex-end; }
  /*
   * Anh tro lai phu toan o, nen bo tron LAI CA BON goc. Cung (0,2,0) voi ban o
   * muc 4, thang bang THU TU NGUON - media query khong cong do uu tien.
   */
  .bt-split .bt-media{ position:absolute; inset:0; border-radius:inherit; }
  .bt-split .bt-media::after{
    content:"";
    position:absolute;
    inset:0;
    background:linear-gradient(to top, var(--navy-dark) 0%, transparent 100%);
  }
  .bt-split .bt-body{ background:none; padding:var(--sp-5); border-radius:0; }
  .bt-1 .bt-name{ font-size:23px; }
}

@media (max-width:960px){
  .press-track{ gap:var(--sp-7); }
  .press-item{ flex:0 0 calc((100% - var(--sp-7)) / 2); }
  .press-text{ font-size:17.5px; }
}

@media (max-width:900px){
  .stats{ padding-block:var(--sp-8); }
  .hero-visual{ aspect-ratio:16 / 9; }
  .stats-grid{ grid-template-columns:repeat(auto-fit, minmax(120px, 1fr)); gap:var(--sp-4) 10px; }
  .certs-v2-grid{ flex-direction:column; min-height:0; }
  .certs-visual-bleed{ flex:0 0 auto; padding:var(--sp-7) var(--sp-5); }
  .certs-text-container{ max-width:none; padding:var(--sp-7) var(--sp-5); }
  /*
   * STT 5 — cùng hệ số 4/3 như bản desktop: 230×290 → 307×387.
   *
   * `min(307px, 100%)` chứ không phải 307px trần: ở 360px lòng của
   * `.certs-visual-bleed` chỉ còn 312px (360 − 2×24 đệm), tức 307 vừa khít
   * và không còn biên an toàn. Màn hẹp hơn nữa — 320px vẫn còn trên máy cũ —
   * sẽ tràn ngang. `min()` cho khung tự co lại đúng bằng lòng thay vì đẩy ra
   * một thanh cuộn ngang cho cả trang.
   */
  .cert-frame{ width:min(307px, 100%); height:387px; }
  /* THUA tu 03/09/2026: .cta-box goc (muc CTA/wrap) da flex-direction:column + text-align:center o MOI be rong. Giu dong duoi de doi padding, khong xoa ca rule vi tuong lam mat can giua tren desktop. */
  .cta-box{ padding:var(--sp-6) var(--sp-5); }

  /*
   * STT 13 — CĂN GIỮA CỤM NÚT CTA TRÊN MOBILE.
   *
   * Chữ ĐÃ căn giữa sẵn từ trước (`text-align:center` ngay dòng trên), nên thứ
   * khách thấy lệch là HAI CÁI NÚT chứ không phải cả khối. Đo ở 360px:
   * `.cta-actions` rộng 248, mỗi nút ~156 nên hai nút không đủ chỗ nằm cạnh
   * nhau và `flex-wrap` đẩy chúng xuống hai dòng — cả hai dính mép TRÁI, chừa
   * 92px trống bên phải. Đặt cạnh tiêu đề đã căn giữa thì trông như lỗi.
   *
   * `justify-content` chứ không phải `margin-inline:auto` trên từng nút: đây là
   * flex container có `flex-wrap:wrap`, `justify-content` căn giữa TỪNG DÒNG
   * sau khi xuống dòng — đúng thứ cần. margin auto chỉ căn được khi mỗi dòng
   * có một phần tử, và sẽ hỏng ở bề rộng mà hai nút vừa đủ nằm chung một dòng.
   *
   * Đặt ở ĐÚNG breakpoint 900px này chứ không phải một khối mới: đây là chỗ
   * `.cta-box` chuyển sang cột và căn giữa, tức chữ và nút cùng đổi thế một
   * lượt. Tách ra hai breakpoint khác nhau là mở đường cho trạng thái nửa vời
   * (chữ giữa, nút trái) ở dải bề rộng ở giữa.
   */
  .cta-actions{ justify-content:center; }
  .video-shell::before{ inset:-14px; border-radius:22px; }
  .video-shell:hover::before{ inset:-14px; }
  .c-tl{ top:-14px; left:-14px; }
  .c-tr{ top:-14px; right:-14px; }
  .c-bl{ bottom:-14px; left:-14px; }
  .c-br{ bottom:-14px; right:-14px; }
  .video-shell:hover .c-tl{ top:-14px; left:-14px; }
  .video-shell:hover .c-tr{ top:-14px; right:-14px; }
  .video-shell:hover .c-bl{ bottom:-14px; left:-14px; }
  .video-shell:hover .c-br{ bottom:-14px; right:-14px; }
  .video-play{ width:58px; height:58px; }
}

@media (max-width:620px){
  .press-track{ gap:var(--sp-6); }
  .press-item{ flex:0 0 100%; }
  .press-text{ font-size:17px; }
}

@media (max-width:560px){
  /*
   * Một cột. Hai rule này cùng (0,1,0) với bản ở mục 4 và thắng bằng THỨ TỰ
   * NGUỒN — media query KHÔNG cộng độ ưu tiên. Chuyển khối này lên trên mục 4
   * là làm nó thành rule chết, đúng loại lỗi mà mục 11 global.css đã ghi lại.
   */
  .products-bento{ grid-template-columns:1fr; grid-auto-rows:190px; }
  .bt-split{ grid-column:span 1; }
  .bt-1{ grid-column:span 1; grid-row:span 2; }
  .bt-1 .bt-body{ padding:var(--sp-5); }
  .bt-1 .bt-name{ font-size:21px; }
  .gallery-item{ flex:0 0 200px; height:130px; }
}

/* ============================================================
   13. Tôn trọng lựa chọn của người dùng
   ============================================================ */

/*
 * BẮT BUỘC — mockup dòng 503 và 534.
 *
 * `animation-name:none`, KHÔNG phải `animation:none` viết tắt. Hai lý do, cả
 * hai đo được:
 *
 * 1. Mục 12 global.css đã có `*{animation-duration:.01ms !important;
 *    animation-iteration-count:1 !important}`. Một shorthand `animation:none` ở
 *    đây KHÔNG thắng được `!important` cho hai longhand đó, nên băng chạy sẽ
 *    vẫn "chạy" một lượt 0.01ms rồi giật về vị trí đầu — trong khi file đọc như
 *    thể đã tắt hẳn. Đặt thẳng `animation-name:none` thì KHÔNG CÓ hoạt ảnh nào
 *    để chạy, bất kể duration bằng bao nhiêu.
 *
 * 2. Bộ giải cascade chỉ mở rộng đúng MỘT shorthand là `outline`. Viết
 *    `animation:none` là viết một thứ không có khẳng định nào đo được — đúng
 *    loại rule mà tests/test-css-cascade.php sinh ra để chống.
 *
 * Rule của mục 6 (`animation:… linear infinite`) cũng là shorthand nên bộ giải
 * KHÔNG thấy nó đặt animation-name. Vì thế ở hồ sơ reduced-motion, rule dưới
 * đây là rule DUY NHẤT nói về animation-name, và nó thắng tuyệt đối.
 */
@media (prefers-reduced-motion: reduce){
  .gallery-track.left,
  .gallery-track.right,
  .partner-track.left,
  .partner-track.right{ animation-name:none; }

  .press-viewport{ scroll-behavior:auto; }
}
/*
 * KHUÔN CTA CHUẨN TRÊN ĐIỆN THOẠI (07/09/2026) — khai cuối file để chắc chắn
 * đứng SAU rule chính của hộp (bài học từ page-tacgia.css: rule điện thoại nằm
 * TRƯỚC rule chính thì thua thứ tự nguồn, khai đúng mà không hiệu lực nào —
 * hộp đó đo ra đệm 36px trong khi năm hộp kia 32/24).
 *
 * NÚT FULL-WIDTH: đo tại 375px, nút co theo độ dài chữ nên hai nút trong cùng
 * một khối lệch nhau (dự án 187px và 155px, năng lực 199px và 171px) và mỗi
 * trang một bề ngang. Kéo bằng bề ngang hộp thì cột nút thẳng nhau ở mọi
 * trang, và vùng chạm cũng rộng hơn hẳn.
 */
@media (max-width:600px){
  .cta-box .btn{ width:100%; }
}
