/* ============================================================
   home.css — Chỉ dùng cho index.html
   Section: banner · m-product · m-search · m-news · m-project
   Nạp SAU common.css.
   Card (.tile-card / .news-card / .project-card) nằm trong
   common.css §15.6 — ở đây chỉ ghi đè phần riêng của trang chủ.
   ============================================================ */

/* ===== 1 · BANNER ===== */
/* Đáy của cụm chữ + nút CTA (trái) và cụm điều khiển (phải) lấy chung
   --banner-base khai ở :root của common.css — cùng một mốc với banner trang con
   và hero bộ sưu tập. Trang chủ KHÔNG khai lại token ở đây: khai lại là mở
   đường cho ba trang trôi ra ba con số khác nhau, đúng thứ vừa gỡ bỏ. */
.banner { position: relative; }
/* TRÀN TOÀN MÀN HÌNH. Phản hồi MONA R1 tr.4: "Khu vực video đầu trang chủ cho
   full màn hình, hiện tại vẫn bị dư một khoảng trắng phía dưới, phần này tối ưu
   cho mọi độ phân giải màn hình giúp mình" — và tr.12 lặp lại cho bản điện thoại.

   Bản cũ là `min(92vh, 860px)`. Chính TRẦN 860px là chỗ "dư khoảng trắng":
   trên màn cao 1440px thì banner dừng ở 860px, chừa lại gần 600px trống trước
   khi tới section kế — mà `92vh` cũng đã cố tình hụt 8% khung nhìn.

   `100dvh` thay vì `100vh` cho phần "tối ưu mọi độ phân giải": trên trình duyệt
   điện thoại, `vh` luôn tính theo khung nhìn LỚN NHẤT (lúc thanh địa chỉ đã thu),
   nên `100vh` làm banner cao hơn màn hình thật và đẩy chữ + nút CTA xuống dưới
   mép — đúng ảnh tr.12 khách chụp. `dvh` bám theo khung nhìn động nên khớp.
   Dòng `100vh` PHẢI đứng trước làm dự phòng cho Safari <15.4 / Chrome <108;
   trình duyệt không hiểu `dvh` sẽ bỏ qua dòng sau và giữ dòng trước.

   Header là `position: fixed` và `body.has-banner .main { padding-top: 0 }`
   (common.css §14), nên banner đã bắt đầu từ y=0 — không phải trừ chiều cao
   header ở đây. */
.banner__swiper, .banner .swiper-slide {
  height: 100vh;
  height: 100dvh;
}
.banner .swiper-slide { position: relative; }

.banner__img { width: 100%; height: 100%; object-fit: cover; }

/* ---- Zoom chậm (Ken Burns) ----
   @keyframes tl-ken ĐÃ DỜI sang common.css §15.3 từ lúc banner chi tiết sản phẩm
   dùng chung hiệu ứng này. Toàn bộ lý do "vì sao chạy vô hạn trên MỌI ảnh chứ
   không gắn theo .swiper-slide-active" nằm ở đó — đọc trước khi đổi bất kỳ số
   nào bên dưới. Đừng khai lại @keyframes ở đây: trùng tên thì file nạp sau đè
   file nạp trước, và hai bản số liệu sẽ âm thầm trôi khỏi nhau. */
.banner__img { animation: tl-ken 16s linear infinite alternate; }
/* Lớp phủ tối phía dưới-trái để chữ trắng luôn đủ tương phản,
   kể cả trên slide Travertine gần như trắng hoàn toàn. */
.banner .swiper-slide::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(105deg, rgba(0,0,0,.5) 0%, rgba(0,0,0,.22) 42%, rgba(0,0,0,0) 72%);
}

.banner__body {
  position: absolute; left: 0; right: 0; bottom: var(--banner-base); z-index: 2;
  color: #fff;
}
/* Client: "text mô tả ở banner trang chủ bị nhỏ" (30/07/2026). Dòng này đứng
   ngay trên tiêu đề 86px nên ở cỡ cũ (tối đa 17px) nó biến thành chú thích chứ
   không còn là câu dẫn. Nâng cỡ + độ đậm để đọc ra là một cặp với tiêu đề. */
.banner__eyebrow {
  font-size: clamp(17px, 1.8vw, 22px); font-weight: var(--fw-medium);
  letter-spacing: .01em; margin-bottom: 10px;
}
/* CỐ Ý nhẹ hơn --fw-title (700) — bù quang học, không phải ngoại lệ tuỳ hứng.
   Độ đậm cảm nhận được đi theo BỀ DÀY NÉT tính bằng pixel, mà nét ở 86px dày gấp
   ~4 lần cùng font-weight ở 20px. Chữ 700 ở cỡ này đọc ra là "béo" chứ không phải
   "mạnh", nhất là khi nằm trên ảnh: nét dày nuốt mất khoảng hở trong chữ (ô của
   ê, ô, ơ trong "Bộ sưu tập") và tiêu đề bết lại thành một mảng trắng.
   600 giữ nguyên độ tương phản với nền mà chữ vẫn thoáng.
   Từ 31/07/2026 .page-banner__title cũng về 600 theo cùng lý do (client góp ý
   "giảm font-weight" ở banner trang con) — hai loại banner nay CÙNG độ đậm. */
.banner__title {
  color: #fff; font-size: clamp(44px, 7.2vw, 86px); font-weight: 600;
  letter-spacing: -.02em; line-height: 1.05; margin-bottom: clamp(18px, 2.4vw, 28px);
}

/* Intro chữ banner — chạy lại mỗi lần đổi slide mà KHÔNG cần JS.
   Banner dùng effect:'fade' nên mọi slide đều nằm lại trong DOM, Swiper chỉ
   chuyển class. Slide vừa nhận .swiper-slide-active mới bắt đầu khớp rule này,
   nên trình duyệt tự khởi động lại animation — kể cả khi rewind quay lại slide cũ.
   Delay gốc 120ms để chữ vào sau khi ảnh đã bắt đầu hiện (speed fade 900ms). */
@keyframes tl-slide-in { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }
.banner .swiper-slide-active .banner__eyebrow,
.banner .swiper-slide-active .banner__title,
.banner .swiper-slide-active .banner__body .btn {
  animation: tl-slide-in var(--reveal-dur) var(--reveal-ease) both;
}
.banner .swiper-slide-active .banner__eyebrow    { animation-delay: .12s; }
.banner .swiper-slide-active .banner__title      { animation-delay: calc(.12s + var(--reveal-stagger)); }
.banner .swiper-slide-active .banner__body .btn  { animation-delay: calc(.12s + var(--reveal-stagger) * 2); }

/* Cụm điều khiển banner: lùi · tiếp · tạm dừng — MỘT HÀNG Ở GÓC PHẢI DƯỚI
   (phản hồi client 31/07/2026, đúng file thiết kế trang 3).

   Bản 30/07 tách lùi/tiếp ra hai mép trái–phải còn nút tạm dừng neo riêng bằng
   `position:absolute`; nay cả ba đi chung một hàng flex nên nút tạm dừng chỉ cần
   `position: relative` và tự xếp cuối hàng — xem .banner__play ngay dưới.

   `padding-right` dùng đúng giá trị của .page-banner__nav (common.css §15.3) và
   .cd-hero__nav (collection.css §1): đủ để cả cụm né dải nút liên hệ nổi
   (.group-contact) đang `position: fixed` ở góc phải dưới VIEWPORT — mà góc đó
   trùng ngay với góc phải dưới của banner cao ~90vh. */
.banner__ctrl {
  position: absolute; inset: auto 0 0 auto; z-index: 3;
  display: flex; align-items: center; gap: 14px;
  padding-right: clamp(62px, 5.4vw, 86px);
  /* Cụm neo `bottom: 0` rồi đẩy lên bằng padding chứ không phải `bottom` —
     giữ nguyên cách cũ, chỉ thay số bằng token dùng chung. Đáy cụm vì thế
     chính là đáy nút tạm dừng, khớp đáy nút CTA bên trái. */
  padding-bottom: var(--banner-base);
  pointer-events: none;
}
.banner__ctrl > * { pointer-events: auto; }
/* Nút lùi/tiếp mượn .nav-btn của common.css nhưng đứng trên ảnh nên phải đảo
   sang kiểu viền trắng trong suốt, giống .page-banner__nav ở §15.3. */
.banner__ctrl .nav-btn {
  background: transparent; border-color: rgba(255,255,255,.7); color: #fff;
}
.banner__ctrl .nav-btn:hover {
  background: #fff; border-color: #fff; color: var(--c-ink);
}

/* `relative` chứ không `absolute`: nút chảy thành phần tử cuối của hàng
   .banner__ctrl. Vẫn PHẢI là positioned element vì vòng đếm tiến độ
   (.banner__play-ring) do banner.js chèn vào trong nút và định vị bằng
   `position:absolute; inset:0` — đổi sang `static` là vòng văng ra .banner. */
.banner__play {
  position: relative;
  width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,.7); color: #fff;
  display: flex; align-items: center; justify-content: center;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.banner__play:hover { background: #fff; color: var(--c-ink); }

/* ---- Vòng đếm tiến độ autoplay ----
   SVG do banner.js chèn, index.html không đổi.

   Rãnh và vòng chạy là HAI <circle> CÙNG BÁN KÍNH trong cùng một SVG, thay vì
   lấy `border` của nút làm rãnh. Bản đầu làm kiểu đó thì nét vẽ (r=20) và đường
   viền (tâm nét ở r=21.5) lệch nhau ~1.5px — nhìn ra hai vòng tròn lồng nhau.
   Vẽ chung một hệ toạ độ thì đồng tâm là chuyện đương nhiên, không phải canh tay.

   Viền CSS vẫn giữ nguyên làm phương án dự phòng khi JS/Swiper không chạy;
   dựng được vòng rồi thì banner.js gắn .has-ring để làm nó trong suốt — bỏ
   `border-color` chứ không bỏ `border`, để không đổi kích thước hộp. */
.banner__play.has-ring { border-color: transparent; }

/* `inset: 0` + width/height 100% = phủ đúng CONTENT/PADDING BOX của nút (42×42
   sau khi trừ viền 1px), và hộp đó vốn nằm chính giữa border-box nên vòng tự
   đồng tâm với nút.

   KHÔNG dùng `inset: -1px` để với ra border-box: <svg> là replaced element,
   `width: auto` được tính từ tỷ lệ nội tại và thắng `right/bottom`, nên hộp chỉ
   bị DỊCH lên-trái 1px chứ không nở ra — đo được lệch tâm đúng 1px mỗi chiều. */
.banner__play-ring {
  position: absolute; inset: 0; width: 100%; height: 100%;
  transform: rotate(-90deg);         /* mốc 0% ở 12 giờ thay vì 3 giờ */
  pointer-events: none;
}
/* cx/cy/r đặt bằng ATTRIBUTE trong banner.js chứ không phải CSS: thuộc tính hình
   học của SVG mới được CSS hoá gần đây, attribute thì chạy ở mọi nơi. */
.banner__play-ring circle { fill: none; stroke: currentColor; stroke-width: 2; }
/* Rãnh: cùng currentColor để khi hover (nút đảo sang nền trắng, chữ đen) nó vẫn
   tương phản, thay vì trắng-trên-trắng như khi hard-code rgba(255,255,255,…). */
.banner__play-ring .is-track { opacity: .4; }
.banner__play-ring .is-bar {
  stroke-linecap: round;
  stroke-dasharray: var(--len);
  /* --p do banner.js ghi mỗi frame (1 → 0). CỐ Ý không đặt transition:
     Swiper cập nhật gần như mỗi frame, thêm transition là vòng chạy trễ và giật. */
  stroke-dashoffset: calc(var(--len) * var(--p, 1));
}

/* ===== 2 · SẢN PHẨM ===== */
.m-product__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; margin-bottom: clamp(24px, 3vw, 34px);
}
/* Wrapper riêng đã bỏ: cụm tab dùng .pill-group của common.css §6 */
.m-product__nav { margin-top: clamp(24px, 3vw, 34px); }

/* ===== 3 · BỘ LỌC NHANH ===== */
.m-search { padding-bottom: var(--sp-section); }
.m-search__box {
  display: grid; grid-template-columns: 1fr 1.3fr; gap: clamp(28px, 4vw, 60px);
  align-items: center;
  background: var(--c-grey-light); border-radius: var(--r-lg);
  padding: clamp(28px, 4vw, 56px);
}
.m-search__form h2 {
  font-size: var(--fs-h2); font-weight: var(--fw-title); letter-spacing: -.01em;
  margin-bottom: 10px;
}
.m-search__form > p { font-size: var(--fs-sm); color: var(--c-muted); margin-bottom: 22px; }

/* Bộ lọc xếp dọc trong cột hẹp — đúng bố cục trang 3 file thiết kế */
.m-search__fields { display: grid; gap: 12px; max-width: 300px; }
.m-search__submit { margin-top: 20px; width: 100%; max-width: 300px; }

/* Thiết kế trang 3: ảnh chiếm ~54% bề rộng khung và VUÔNG, cột form chỉ ~40%. */
.m-search__media { border-radius: var(--r-md); overflow: hidden; aspect-ratio: 1; }
.m-search__media img { width: 100%; height: 100%; object-fit: cover; }

/* ===== 4 · TIN TỨC — dải card "một to, hai nhỏ" (trang 3 file thiết kế) =====

   Bản vẽ KHÔNG phải ba card bằng nhau rồi làm mờ hai bên: card giữa to, hai
   card cạnh THẬT SỰ nhỏ hơn (~47,5%) và canh giữa theo chiều dọc với ẢNH của
   card giữa — không phải theo cả thẻ. Thẻ còn thẻ chuyên mục + tiêu đề + ngày
   ở dưới; canh theo cả thẻ thì hai ảnh nhỏ tụt xuống thấp hơn hẳn bản vẽ.

   Ba số điều khiển toàn bộ bố cục, đo lại từ trang 3:
     --news-c  bề ngang card giữa / bề ngang cột nội dung   (233pt / 475,5pt)
     --news-s  tỉ lệ thu của hai card cạnh                  (110pt / 233pt)
     --news-k  tâm card cạnh, tính theo BỀ NGANG CARD GIỮA
   --news-k KHÔNG đặt tuỳ ý được, nó suy ra từ hai số trên:
       d = 1/2 − s·c/2      tâm card cạnh, tính theo bề ngang cột nội dung
       k = d / c            đổi mốc sang bề ngang card giữa
   d = 1/2 − s·c/2 chính là điều kiện "mép ngoài hai ảnh nhỏ chạm đúng mép cột
   nội dung" của bản vẽ. Đổi --news-c hay --news-s mà quên tính lại --news-k thì
   hai ảnh nhỏ lệch khỏi mép — ba breakpoint bên dưới đều đã tính sẵn.

   RÀNG BUỘC bắt buộc kiểm khi đổi số:  s·c < (1 − c)/2
   Cả hàng phải vừa đúng bề ngang cột nội dung: c + 2·s·c + 2·khe = 1, nên khe
   giữa hai ảnh = (1 − c)/2 − s·c. Vượt ràng buộc là khe ÂM và hai ảnh nhỏ
   chồng đè lên card giữa — đã dính đúng lỗi này ở hai breakpoint hẹp, khe ra
   −43px. Card giữa càng to thì hai ảnh cạnh bắt buộc càng phải nhỏ.

   Vì sao thu bằng transform trên .news-card__media chứ không đổi `width` slide:
   Swiper đo bề ngang slide MỘT LẦN lúc dựng (slidesPerView:'auto'), đổi width
   theo class .swiper-slide-active sẽ lệch hẳn với bản đồ vị trí nó đang giữ.
   transform không đụng tới layout nên Swiper vẫn tính đúng.
   Và đặt trên .news-card__media chứ không trên .swiper-slide vì §14e của
   common.css đã giữ chỗ transform của slide cho hiệu ứng .is-shifting — hai
   bên ghi chung một thuộc tính là đá nhau.

   spaceBetween ĐẶT 0 trong news.js: khoảng hở giữa ba ảnh do phần dịch ngang ở
   đây tạo ra, để Swiper chèn thêm nữa là cộng hai lần. */
.m-news__stage {
  position: relative;

  /* SÂN KHẤU RỘNG ĐÚNG BẰNG CỘT NỘI DUNG — đây là bản sửa lỗi màn 4K.

     Phản hồi MONA R1 tr.8: "Khi xem bằng màn 4K thì bị lỗi hiển thị trang chủ
     mục tin tức mới nhất ạ", kèm ảnh khoanh đỏ HAI ảnh nhỏ lạc hẳn ra ngoài cột
     nội dung, một bên trái một bên phải.

     Nguyên nhân: .m-news__stage là ANH EM của .container (cố ý tràn viền) nhưng
     trước đây KHÔNG có max-width nào, nên khung nhìn của Swiper rộng bằng cả màn
     hình. Bề ngang mỗi slide lại bị chốt ở .49 × 1360 ≈ 666px, nên ở 3840px có
     tận 5 slide lọt vào khung — trong khi CHỈ prev/active/next được gán
     translateX kéo về giữa (§ bên dưới). Slide thứ 4 và thứ 5 rơi vào rule mặc
     định `scale(.475)` KHÔNG kèm translate, nên đứng nguyên tại vị trí Swiper
     xếp: hai vệt ảnh nhỏ lơ lửng cách cột nội dung cả nghìn pixel. Đúng hai
     khung đỏ trong ảnh.

     Chặn bề ngang sân khấu về đúng cột nội dung là sửa TẬN GỐC chứ không phải
     che: cả bố cục ba card vốn được thiết kế để vừa khít cột nội dung
     (c + 2·s·c + 2·khe = 1, xem ràng buộc ở trên), nên slide thứ 4 trở đi luôn
     rơi ngoài khung và bị `overflow: hidden` sẵn có của .swiper cắt sạch. Không
     cần thêm rule ẩn slide — làm vậy còn sinh lỗi nháy khi kéo trên điện thoại.

     Ở MỌI khổ ≤1440px, con số ra y hệt bản cũ nên bố cục không đổi một pixel:
     sân khấu = min(100% − 2·--pad, 1360px), đúng bằng biểu thức mà --news-w
     đang dùng; và vì sân khấu căn giữa nên tâm bố cục vẫn là tâm màn hình. */
  max-width: min(100% - var(--pad) * 2, var(--maxw) - 80px);
  margin-inline: auto;

  --news-c: .49;
  --news-s: .475;
  --news-k: .783;    /* = (.5 − .475 × .49 / 2) / .49 */

  /* Sau khi sân khấu đã bị chặn ở trên, `100%` CHÍNH LÀ bề ngang cột nội dung —
     nên bề ngang slide rút gọn còn `c × 100%`, không phải lồng lại min() nữa.
     Đây cũng là lý do phép chặn bên trên phải dùng ĐÚNG biểu thức cũ: giữ
     nguyên min(100% − 2·--pad, --maxw − 80px) thì --news-w ra cùng giá trị.
     80px = 2 × --pad lúc chạm --maxw: clamp() của --pad chốt ở 40px từ 1000px.

     Bản --news-wv dùng 100vw chỉ để đặt `top` cho nút lùi/tiếp — `top` quy %
     theo CHIỀU CAO khối cha nên không xài được `100%`. Lệch tối đa đúng một
     bề rộng thanh cuộn, và chỉ ảnh hưởng vị trí hai cái nút. */
  --news-w:  calc(var(--news-c) * 100%);
  --news-wv: calc(var(--news-c) * min(100vw - var(--pad) * 2, var(--maxw) - 80px));
}
.m-news__swiper { padding-bottom: 6px; }
.m-news .swiper-slide { width: var(--news-w); }

/* scale(1) chứ không phải `none` cho slide giữa: rule hover bên dưới ghép
   var(--news-tf) với translateY(-5px), mà `none translateY(...)` là giá trị
   hỏng — cả khai báo bị bỏ và card giữa mất luôn cú nhấc khi rê chuột. */
.m-news .news-card__media { transform: var(--news-tf, scale(1)); }
.m-news .swiper-slide        { --news-tf: scale(var(--news-s)); }
.m-news .swiper-slide-active { --news-tf: scale(1); }
.m-news .swiper-slide-prev {
  --news-tf: translateX(calc((1 - var(--news-k)) * 100%)) scale(var(--news-s));
}
.m-news .swiper-slide-next {
  --news-tf: translateX(calc((var(--news-k) - 1) * 100%)) scale(var(--news-s));
}
/* Ghép lại cú nhấc của common.css §15.6 thay vì để nó đè mất phần thu nhỏ.
   translateY nằm SAU scale nên -5px được nhân theo tỉ lệ card — ảnh nhỏ nhấc
   nhẹ hơn ảnh to, đúng cảm giác xa/gần. */
.m-news .news-card:hover .news-card__media {
  transform: var(--news-tf, scale(1)) translateY(-5px);
}

/* Trang chủ căn giữa nội dung card (các trang khác căn trái theo mặc định) */
.m-news .news-card { text-align: center; }
.m-news .news-card__tag { margin: 18px auto 12px; }
.m-news .news-card h3 { max-width: 34ch; margin: 0 auto; font-size: clamp(21px, 2.2vw, 26px); }

/* Phần chữ chỉ hiện ở slide đang active — các slide cạnh chỉ còn ảnh */
.m-news .news-card__tag,
.m-news .news-card h3,
.m-news .news-card time { opacity: 0; transition: opacity var(--dur) var(--ease); }
.m-news .swiper-slide-active .news-card__tag,
.m-news .swiper-slide-active .news-card h3,
.m-news .swiper-slide-active .news-card time { opacity: 1; }

/* Nút điều hướng: bản vẽ đặt chúng ĐÚNG TÂM NGANG hai ảnh nhỏ, không phải dí
   vào mép màn hình — nên toạ độ ngang lấy thẳng --news-k.

   Canh giữa bằng MARGIN ÂM chứ không bằng `transform: translate(-50%)`.
   `.nav-btn:hover` dùng thuộc tính `scale` rời, mà chuỗi biến hình được ghép
   theo thứ tự translate → rotate → scale → transform: scale nằm NGOÀI transform
   nên nó nhân luôn cả -22px thành -23.32px, nút trôi 1.3px mỗi lần rê chuột.
   Margin không nằm trong chuỗi biến hình nên miễn nhiễm.
   -22px = nửa kích thước cố định 44px của .nav-btn (common.css §6).
   `margin-left/right` quy % theo BỀ NGANG khối cha nên dùng được --news-w. */
.m-news__prev, .m-news__next {
  position: absolute; z-index: 3;
  /* Tâm nút nằm dưới đáy ảnh nhỏ 0,147 × bề ngang card giữa (đo trang 3).
     .75 vì .news-card__media dùng aspect-ratio 4/3 → cao bằng 3/4 bề ngang. */
  top: calc(.75 * var(--news-wv) * (1 + var(--news-s)) / 2 + .147 * var(--news-wv));
  margin-top: -22px;
}
.m-news__prev { left:  50%; margin-left:  calc(-1 * var(--news-k) * var(--news-w) - 22px); }
.m-news__next { right: 50%; margin-right: calc(-1 * var(--news-k) * var(--news-w) - 22px); }

/* ===== 5 · DỰ ÁN ===== */
.m-project { background: var(--c-grey-light); }
.m-project__nav { margin-top: clamp(24px, 3vw, 34px); }
/* NÚT TRÊN DẢI XÁM — DẠNG VIỀN, NỀN TRONG SUỐT.

   Phản hồi MONA R1 tr.9: "Các button ở khu vực có nền màu tối đổi lại màu cho
   đồng bộ với nền, giống như hình bên phải" — ảnh tham chiếu khoanh đúng ba chỗ
   ở dải này: nút "Xem tất cả" và cặp mũi tên tròn, cả ba đang tô trắng đặc.

   Việc này ĐẢO LẠI quyết định 31/07/2026 (giữ nền trắng vì #eaeaea sáng, nút
   trắng vẫn đọc ra dạng viền). Client nhìn ra "đĩa trắng dán lên nền xám", nên
   giờ CẢ BA dải xám dùng CHUNG một công thức: dải này (--c-grey-light #eaeaea),
   .p-gallery__nav (project-page.css) và .a-gallery (news-page.css) — hai chỗ sau
   trên --c-grey #c9c9c9. Viền rgba(0,0,0,.45) cho ~3.2:1 với #eaeaea và ~2.8:1
   với #c9c9c9, đều đọc được ở cả hai nền, nên một công thức là đủ.

   ĐỪNG lùi về `background: #fff`, và cũng đừng đổi viền sang --c-line-strong
   (#c4c4c4) — trên #eaeaea nó chỉ còn ~1.2:1, gần như mất hút; đó là bản đầu đã
   bị trả lại ở các dải xám kia. */
.m-project .nav-btn,
.m-project .btn-outline {
  background: transparent; border-color: rgba(0, 0, 0, .45); color: var(--c-ink);
}
/* Bắt buộc phải khai lại ĐỦ BA thuộc tính khi hover: `.m-project .nav-btn` và
   `.nav-btn:hover` của common.css cùng độ đặc hiệu (0,2,0), mà home.css nạp sau
   → mọi thuộc tính khai ở đây đều thắng, kể cả trong trạng thái hover. Thiếu
   `color` là mũi tên giữ nguyên màu mực trên nền xanh, gần như không đọc được;
   thiếu `background` là hover không tô xanh nữa. Đúng lỗi này từng xảy ra ở
   .p-gallery__nav, ghi lại ở project-page.css §3. */
.m-project .nav-btn:hover,
.m-project .btn-outline:hover {
  background: var(--c-primary); border-color: var(--c-primary); color: #fff;
}

/* ===== 6 · RESPONSIVE (1024 / 720 / 640) ===== */
@media (max-width: 1024px) {
  .m-search__box { grid-template-columns: 1fr; }
  .m-search__fields, .m-search__submit { max-width: none; }
  .m-search__media { order: -1; aspect-ratio: 16 / 9; }
  /* Cột hẹp lại thì card giữa phải chiếm phần lớn hơn, nếu không tiêu đề tin
     vỡ thành 4-5 dòng — và theo ràng buộc ở §4, card giữa to lên thì hai ảnh
     cạnh phải nhỏ đi tương ứng. .34 × .56 = .1904 < (1 − .56)/2 = .22 ✔ */
  .m-news__stage { --news-c: .56; --news-s: .34; --news-k: .7229; }
}
@media (max-width: 720px) {
  /* Trần riêng cho điện thoại (`min(78vh, 620px)`) ĐÃ GỠ — banner dùng chung
     `100dvh` khai ở §1. Phản hồi MONA R1 tr.12 khoanh đỏ đúng khoảng trắng mà
     trần 620px chừa lại dưới banner trên máy Android. `dvh` đã xử lý phần thanh
     địa chỉ co/giãn, thứ mà con số 78vh trước đây là cách né thủ công. */

  /* ---- Khổ điện thoại BỎ HẲN cơ chế thu nhỏ hai card cạnh ----
     Bộ ba --news-c/-s/-k dựng cho bản vẽ desktop, nơi hai ảnh nhỏ hai bên là
     một hiệu ứng chiều sâu có chủ ý. Ở 375px thì --news-s: .19 biến chúng thành
     hai vệt cao chưa tới 40px — không đọc ra là "tin tiếp theo" mà đọc ra là
     lỗi hiển thị.
     Nay ba card CÙNG CỠ, card giữa chiếm 84% khung, hai bên ló rìa đều: cùng
     một ngôn ngữ với các slider khác trên trang. Ràng buộc s·c < (1−c)/2 ở §4
     chỉ để hai ảnh nhỏ chạm đúng mép cột nội dung — không còn ảnh nhỏ thì cũng
     không còn ràng buộc đó. */
  /* Khổ này TRẢ SÂN KHẤU VỀ TRÀN VIỀN (`max-width: none`), ngược với bản chặn ở
     §4 dựng riêng cho lỗi màn 4K. Lý do: ở đây hai card cạnh không thu nhỏ nữa,
     phần "ló rìa" của chúng chính là dải ảnh chạy ra SÁT MÉP MÀN HÌNH — chặn sân
     khấu lại thì rìa bị cắt sớm --pad mỗi bên và tín hiệu "còn tin nữa" yếu hẳn.
     Màn 4K không bao giờ rơi vào nhánh ≤720px nên không mất phần sửa lỗi.

     Đổi lại, `--news-w` phải tự trừ --pad: ở §4 nó là `c × 100%` vì sân khấu đã
     bằng đúng cột nội dung, còn ở đây `100%` lại là cả bề ngang màn hình. Không
     trừ thì card giữa rộng .84 × 375 = 315px thay vì .84 × 343 = 288px.

     `width: 100%` KHÔNG ĐƯỢC BỎ — nó phá vòng lặp kích thước, không phải khai
     thừa cho đẹp. Chuỗi phụ thuộc: bề ngang slide = `c × (100% − 2·pad)` mà
     `100%` đây tính theo chính sân khấu; còn sân khấu, sau khi `max-width: none`
     gỡ mất cái chặn của §4, lại là một grid item KHÔNG có bề ngang xác định nên
     trình duyệt quay sang đo theo NỘI DUNG — tức là theo bề ngang các slide.
     Vòng tròn khép kín, và Chrome giải nó bằng cỡ nội tại: đo được sân khấu
     phình ra 883px trên khung nhìn 390px, kéo `document.scrollWidth` lên 911 →
     TRANG CHỦ CUỘN NGANG ĐƯỢC trên điện thoại (rà responsive R2 bắt được, xuất
     hiện ở mọi khổ ≤720px).

     Ghim `width: 100%` làm bề ngang sân khấu thành XÁC ĐỊNH (= bề rộng grid
     track), nên `100%` bên trong có mốc thật để quy chiếu và vòng lặp biến mất.
     Bản trước R1 không dính lỗi này vì `--news-w` khi đó dùng `100vw` — đơn vị
     khung nhìn không phụ thuộc phần tử cha nên không tạo vòng lặp. KHÔNG quay
     lại `100vw`: nó tính cả bề rộng thanh cuộn dọc, và khi trang có thanh cuộn
     thì slide rộng hơn khung thật đúng bằng chừng đó. */
  .m-news__stage {
    max-width: none;
    width: 100%;
    --news-c: .84;
    --news-w: calc(var(--news-c) * (100% - var(--pad) * 2));
  }
  .m-news .swiper-slide,
  .m-news .swiper-slide-prev,
  .m-news .swiper-slide-next,
  .m-news .swiper-slide-active { --news-tf: scale(1); }
  /* spaceBetween để 0 trong news.js (khe vốn do phần dịch ngang tạo ra), mà giờ
     không còn dịch ngang nữa — nên khe phải lấy từ padding của chính slide. */
  .m-news .swiper-slide { padding-inline: 6px; }

  /* ===== Banner: cụm chữ trái–dưới · vòng đếm phải–dưới, cùng một đáy =====
     Nút lùi/tiếp bỏ đi (cùng lý do với mọi slider khác — người dùng vuốt), nên
     hàng dưới cùng của banner chỉ còn hai vật thể và chúng tự nhiên tạo thành
     một hàng: chữ bám lề trái, vòng đếm bám lề phải.

     Hai khối dùng CHUNG một giá trị bottom để đáy nút "Khám phá ngay" (46px) và
     đáy vòng đếm (44px) nằm trên cùng một đường — lệch 2px, mắt không thấy.
     Đây là lý do không dùng `bottom` riêng cho từng khối. */
  .banner__ctrl .nav-btn { display: none; }
  /* Lùi/tiếp đã ẩn nên hàng chỉ còn nút tạm dừng; đưa cụm về đúng vị trí mà
     .banner__play vẫn đứng trước pass 31/07 (right: var(--pad)) — khổ điện thoại
     KHÔNG đổi một pixel nào. Lề phải hẹp hơn desktop vì ở đây dải nút liên hệ
     nổi đã tự thu lại theo §16 của common.css. */
  /* 20–32px thay cho 48px của desktop: chữ tụt hẳn xuống sát đáy banner
     như bản thiết kế mobile, thay vì lửng lơ giữa khung. Một dòng này kéo theo
     cả cụm điều khiển vì hai bên đã dùng chung token. */
  .banner { --banner-base: clamp(20px, 4vh, 32px); }
  .banner__ctrl {
    padding-right: var(--pad);
  }
  .banner__body {
    /* Chừa chỗ cho vòng đếm 44px + khoảng thở, nếu không tiêu đề dài sẽ chạy
       xuống dưới nó. Ghi đè padding-right của .container-sec. */
    padding-right: calc(var(--pad) + 60px);
  }
  /* Vị trí vòng đếm nay do padding của .banner__ctrl ngay trên lo — `right`/
     `bottom` riêng cho .banner__play đã gỡ vì nút không còn `position:absolute`.
     Kết quả canh lề không đổi: mép phải vòng đếm vẫn cách banner đúng --pad,
     tức thẳng hàng với lề trái của .banner__body (.container-sec cũng dùng --pad). */

  /* ===== Nút "Xem tất cả" xuống dưới slider =====
     Nút đang là CHÁU của .container (nằm trong .sec-head/.m-product__head) còn
     slider là CON — hai cấp DOM khác nhau thì không `order` với nhau được.
     `display: contents` xoá hộp của phần đầu mục nhưng giữ nguyên các con, kéo
     chúng lên cùng cấp với slider; từ đó sắp lại bằng `order`.

     Đánh đổi đã cân nhắc: phần tử display:contents không sinh hộp nên hiệu ứng
     [data-aos] trên CHÍNH .sec-head/.m-product__head mất tác dụng ở khổ này
     (nội dung hiện ngay thay vì trượt vào — không có gì bị ẩn). Đổi lại không
     phải sửa cùng một đoạn markup ở 23 file HTML. data-aos của card bên trong
     slider không bị ảnh hưởng.

     Dùng GRID `minmax(0, 1fr)` chứ KHÔNG dùng flex column. Với flex, item mặc
     định là `min-width: auto` — không co được xuống dưới bề rộng nội dung — và
     Swiper thì đặt bề rộng slide bằng px theo bề rộng đo được của khung. Hai
     thứ đó tạo thành vòng lặp tự nuôi: khung rộng ra → Swiper ghi slide rộng
     hơn → item không co được nên khung rộng thêm → ResizeObserver đo lại…
     Đo thực tế khi còn dùng flex: .m-product__swiper phình tới 31.541.168px.
     `minmax(0, …)` chặn cứng track ở bề rộng khung nên vòng lặp không khởi
     động được.

     `minmax(0, …)` ghìm TRACK ở 343px, nhưng CHỪNG ẤY CHƯA ĐỦ: bản thân item
     vẫn nở theo nội dung do Swiper ghi bề rộng slide bằng px (đo được .swiper
     = 3478px trong track 343px). `min-width: 0` cũng không cứu được vì đây
     không phải kích thước tối thiểu tự động mà là kích thước NỘI DUNG.
     `max-width: 100%` mới là cái chặn thật — 100% của track = 343px. Nhưng chỉ
     đặt cho SLIDER: cụm tab cố tình rộng hơn track (margin âm để tràn sát mép
     phải), chặn nó ở 100% là mất luôn phần tràn đó. */
  .m-product .container,
  .m-project .container { display: grid; grid-template-columns: minmax(0, 1fr); }
  .m-product .container > *,
  .m-project .container > *,
  .m-product__head > *,
  .m-project .sec-head > * { min-width: 0; }
  /* Slider Sản phẩm kéo sát mép PHẢI màn hình: phần thẻ kế tiếp ló ra bị cắt
     ngang đúng ở viền máy, đọc ra ngay là "còn nữa". Chừa lề 16px như cũ thì
     rìa thẻ dừng lửng lơ giữa khoảng trắng, trông như một thẻ hỏng chứ không
     phải tín hiệu. Mép TRÁI vẫn thẳng cột với cụm tab bên trên.
     max-width phải nới đúng bằng phần margin âm, nếu không nó chặn lại ngay.

     Dự án KHÔNG dùng cách này: nó đã chuyển sang canh giữa (project.js), rìa ló
     đều hai bên nên phải tràn cả hai mép mới cân. */
  .m-product__swiper {
    margin-right: calc(var(--pad) * -1);
    max-width: calc(100% + var(--pad));
  }
  .m-project__swiper {
    margin-inline: calc(var(--pad) * -1);
    max-width: calc(100% + var(--pad) * 2);
  }
  .m-product__head,
  .m-project .sec-head { display: contents; }

  /* margin-bottom vốn nằm trên .m-product__head/.sec-head — hộp đó không còn
     nên phải chuyển xuống chính phần tử đứng đầu. */
  .m-product .pill-group,
  .m-project .sec-head h2 { order: 1; margin-bottom: clamp(20px, 3vw, 28px); }
  .m-product__swiper,
  .m-project__swiper { order: 2; }
  /* Dãy chấm do main.js chèn ngay SAU thẻ .swiper — nhưng khung ở đây là grid có
     `order`, mà phần tử mới sinh không mang order nên mặc định là 0 và nhảy lên
     trước cả cụm tab. Cho cùng order với slider: các item cùng order giữ nguyên
     thứ tự DOM, nên chấm nằm đúng ngay dưới slider. */
  .m-product .container > .swiper-dots,
  .m-project .container > .swiper-dots { order: 2; }
  /* Nút ôm sát chữ và canh giữa, KHÔNG kéo full ngang: nó là lối đi phụ ("xem
     thêm nếu muốn"), còn slider ngay trên mới là nội dung chính. Một nút trải
     hết bề ngang mang sức nặng thị giác của hành động chính — sai thứ bậc.
     `justify-self` chứ không phải `margin-inline: auto`, vì đây là grid item. */
  .m-product__head .btn,
  .m-project .sec-head .btn {
    order: 3; width: fit-content; justify-self: center; margin-top: 22px;
  }

  /* Tin tức xếp khác hai mục trên: .m-news__stage là ANH EM của .container
     (nó tràn viền, cố ý nằm ngoài khung), nên trục flex phải đặt ở <section>
     và .container cũng phải display:contents để nhấc .sec-head lên đủ hai cấp.
     Bù lại padding ngang mà .container không còn cấp cho hai phần tử con. */
  .m-news { display: grid; grid-template-columns: minmax(0, 1fr); }
  .m-news > *, .m-news .container > *, .m-news .sec-head > * { min-width: 0; }
  .m-news .container,
  .m-news .sec-head { display: contents; }
  .m-news .sec-head h2 { order: 1; padding-inline: var(--pad); margin-bottom: clamp(20px, 3vw, 28px); }
  .m-news__stage { order: 2; }
  /* Slider tin tức là khối DUY NHẤT có chữ nằm trong lòng card (thẻ · tiêu đề ·
     ngày), nên chân card đã là chữ rồi — cộng thêm 22px như hai mục kia thì nút
     trôi hẳn ra xa, đọc thành một khối rời chứ không còn thuộc về mục tin tức.
     Bỏ luôn 6px padding đáy của swiper (khe đó dựng cho bóng đổ card ở desktop,
     ở đây card không đổ bóng). */
  .m-news__swiper { padding-bottom: 0; }
  .m-news .sec-head .btn {
    order: 3; width: fit-content; justify-self: center; margin: 8px var(--pad) 0;
  }

  /* ===== Khối lọc nhanh: canh giữa ===== */
  .m-search__form { text-align: center; }
}
/* Khối `*` của common.css ép animation-duration về .01ms — với `both` fill thì
   tl-ken nhảy thẳng tới khung cuối và ảnh banner ĐỨNG VĨNH VIỄN ở scale(1.07),
   tức là vẫn bị phóng to dù người dùng đã tắt hiệu ứng. Phải tắt hẳn animation. */
@media (prefers-reduced-motion: reduce) {
  /* Selector phải bám theo §1: animation nay ở .banner__img chứ không còn ở
     .swiper-slide-active. Bắt buộc tắt hẳn — khối `*` của common.css chỉ ép
     duration về .01ms, ảnh sẽ đứng vĩnh viễn ở scale(1.08). */
  .banner__img { animation: none; transform: none; }
}

@media (max-width: 640px) {
  .banner .swiper-slide::after {
    background: linear-gradient(180deg, rgba(0,0,0,.28) 0%, rgba(0,0,0,.15) 45%, rgba(0,0,0,.62) 100%);
  }
  /* Cụm tab: quy tắc nằm ở common.css §16/720 và áp cho MỌI .pill-group trên
     site — không còn bản cục bộ của trang chủ, để chỉ có một nguồn sự thật.
     (Cơ chế cuộn ngang từng ở đó đã gỡ theo phản hồi MONA R1 tr.15; giờ là các
     nút rời tự xuống dòng.) */
}
