/* ============================================================
   AI ARCHITECT STUDIO — Project Catalog engine (Labs)
   Shared, themeable styles for full-screen scroll catalogs.
   Link AFTER styles.css. Theme via the :root vars below
   (default = Aura / gold / geometric caps). Maison Royale
   overrides them for terracotta / serif / mixed-case.
   ============================================================ */

:root {
  --cat-bg:        var(--aas-black);
  --cat-panel:     #0c0c0c;
  --cat-ink:       var(--aas-white);
  /* ⛔⛔ CHỮ PHỤ TỪNG CHÌM — Sếp báo 15/08 «phần chữ màu xám đang bị ẩn». Đo trên nền
     #050505 mới thấy vì sao: `--aas-fg-3` (#6f6c66) chỉ được **3,89:1**, DƯỚI ngưỡng 4,5:1
     của chữ nhỏ — mà token ấy đang cầm toàn nhãn 9–10px (`.label` · `.cfoot` · `.hint`).
     Nay catalog dùng thang riêng, đo lại: ink-2 **9,58:1** · ink-3 **5,63:1**.
     ⛔ KHÔNG sửa `--aas-fg-*` trong aas.css — đó là bảng màu nhận diện của cả studio, đổi ở
        đó là đổi mọi sản phẩm. Chỉ đổi CÁCH CATALOG ÁNH XẠ. */
  --cat-ink-2:     #b8b4ac;
  --cat-ink-3:     #8d8880;
  --cat-accent:    var(--aas-gold);
  --cat-rule:      var(--aas-line);

  --cat-display:   var(--font-display);
  --cat-title-tt:  uppercase;      /* Aura = caps; Maison = none */
  --cat-title-w:   800;
  --cat-title-ls:  0.005em;
  --cat-title-lh:  0.98;
  --cat-eyebrow-ls: 0.28em;
  --cat-pad:       clamp(28px, 4.4vw, 76px);
}

* { box-sizing: border-box; }
html, body { margin: 0; }
html { scroll-behavior: smooth; }

body.cat {
  background: var(--cat-bg);
  color: var(--cat-ink-2);
  font-family: var(--font-body);
  height: 100dvh;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  overscroll-behavior-y: none;
}
body.cat::-webkit-scrollbar { width: 0; height: 0; }

.slide {
  position: relative;
  min-height: 100dvh;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  padding: var(--cat-pad);
  overflow: hidden;
  isolation: isolate;
}
.slide-in {
  position: relative; z-index: 2;
  width: 100%; max-width: 1320px; margin: 0 auto;
  flex: 1; display: flex; flex-direction: column;
}

/* thin inner frame line */
.frame::after {
  content: ""; position: absolute; inset: 16px; z-index: 3;
  border: 1px solid rgba(255,255,255,0.10); pointer-events: none;
}

/* ---- header / footer lockups ---- */
.chdr { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; }
.lock { display: inline-flex; align-items: center; gap: 12px; }
.lock img { width: 40px; height: 40px; display: block; object-fit: contain; }
.lock .wm { display: flex; flex-direction: column; gap: 3px; line-height: 1; }

/* ============================================================
   LOGO 3D XOAY — LUẬT TỐI THƯỢNG (Sếp chốt 2026-08-15)
   Canonical: ~/AAS-Studio/LUAT-LOGO-3D.md
   ------------------------------------------------------------
   Mọi tài liệu mang nhận diện AI Architect Studio dùng LOGO 3D
   XOAY, không dùng monogram phẳng. Một tấm sprite 72 khung,
   MỘT HÀNG NGANG, chạy steps() trên background-position.

   ⛔ SPRITE PHẢI MỘT HÀNG NGANG. Lưới nhiều hàng thì
      background-position nội suy CHÉO cả hai trục, mỗi bước rơi
      vào GIỮA hai ô — logo hiện nửa khung nọ ghép nửa khung kia.
      Cổng máy không bắt được chỗ này, chỉ mắt thấy.

   ⛔⛔ KHÔNG VIẾT «infinite linear». Vị trí cuối trong khai báo
      gọn là HƯỚNG CHẠY, chỉ nhận normal/reverse/alternate. Nhét
      `linear` vào đó là SAI CÚ PHÁP và trình duyệt bỏ luôn CẢ
      khai báo animation — logo đứng im, không báo lỗi gì.
      steps(72) đã là nhịp chạy rồi.

   ⭐ VÌ SAO SPRITE CHỨ KHÔNG NHÚNG BỘ XEM 3D: catalog phải
      TỰ-CHỨA, không phụ thuộc mạng của khách. Và sprite chạy
      bằng background-position nên máy bật «Giảm chuyển động»
      VẪN xoay — đúng luật LUÔN CÓ ANIMATION.

   ⭐ 72 khung / 6s = 12 hình mỗi giây. Dưới 10 là mắt thấy giật.
   ============================================================ */
.logo3d {
  --l3d: 40px;                       /* cạnh ô = cạnh MỘT khung */
  width: var(--l3d); height: var(--l3d);
  display: block; flex: none;
  background-image: url(../assets/logo-turntable.png);
  background-repeat: no-repeat;
  background-size: calc(var(--l3d) * 72) var(--l3d);
  animation: xoay-logo 6s steps(72) infinite;
  filter: drop-shadow(0 2px 8px rgba(0,0,0,0.55));
}
/* ⛔ CHỈ MỘT khai báo @keyframes tên này trong cả trang — hai lần
   khai cùng tên thì bản sau ĐÈ bản trước, im lặng. Cỡ ô đổi bằng
   biến --l3d, KHÔNG đẻ thêm keyframes cho từng cỡ. */
@keyframes xoay-logo {
  from { background-position: 0 0; }
  to   { background-position: calc(var(--l3d) * -72) 0; }
}
.lock .logo3d { --l3d: 40px; }
.lock .wm b { font-family: var(--font-display); font-weight: 600; font-size: 13px;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--cat-accent); }
.lock .wm span { font-family: var(--font-display); font-weight: 500; font-size: 8px;
  letter-spacing: 0.32em; text-transform: uppercase; color: var(--cat-ink-3); }
.caseno { font-family: var(--font-display); font-weight: 600; font-size: 11px;
  letter-spacing: 0.34em; text-transform: uppercase; color: var(--cat-accent); white-space: nowrap; }

.cfoot { display: flex; justify-content: space-between; align-items: center; gap: 16px;
  padding-top: 14px; border-top: 1px solid var(--cat-rule); }
.cfoot span { font-family: var(--font-display); font-weight: 600; font-size: 9.5px;
  letter-spacing: 0.3em; text-transform: uppercase; color: var(--cat-ink-3); }
.cfoot .pow { display: inline-flex; align-items: center; gap: 7px; color: var(--cat-ink-2); }
.cfoot .pow i { width: 5px; height: 5px; border-radius: 999px; background: var(--cat-accent); font-style: normal; }

/* ---- type ---- */
.eyebrow { display: inline-flex; align-items: center; gap: 12px;
  font-family: var(--font-display); font-weight: 600; font-size: clamp(10px, 0.85vw, 12px);
  letter-spacing: var(--cat-eyebrow-ls); text-transform: uppercase; color: var(--cat-accent); }
.eyebrow .r { width: 34px; height: 1px; background: var(--cat-accent); }
/* ⛔⛔ EYEBROW MÀU ACCENT ĐẶT THẲNG LÊN ẢNH THÌ KHÔNG BAO GIỜ ĐỌC ĐƯỢC. Đo trên bìa Maison
   15/08: nền sau chữ ra L=0,075, accent #C47A50 chỉ được **2,50:1** — dưới chuẩn AA 4,5:1.
   Cùng chỗ ấy chữ trắng được 7,66:1. Tính ngược lại: muốn accent đạt 4,5:1 thì nền phải
   xuống L≤0,008, tức gần như đen tuyệt đối — trên một bức ảnh là chuyện không thể.
   ⭐ Nên chữ trên ảnh đi vào Ô KÍNH, đúng nếp Sếp đã chốt cho chú thích: kính hạ nền cục bộ
   nên accent giữ được vai màu nhấn mà vẫn đọc ra. Chỉ áp cho slide có ẢNH TOÀN MÀN — eyebrow
   trên nền đặc vốn đã đủ tương phản, bọc thêm kính là thừa. */
.eyebrow.tren-anh { background: rgba(5,5,5,0.62); border: 1px solid rgba(255,255,255,0.10);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  padding: 7px 14px; border-radius: 2px;
  /* ⛔ `.stack` là flex CỘT nên con mặc định bị GIÃN HẾT BỀ NGANG — ô kính hoá ra một thanh
     chạy suốt trang thay vì ôm lấy chữ. Bó lại bằng chính hai dòng này. */
  align-self: flex-start; width: fit-content; }
.sec-idx { font-family: var(--cat-display); font-weight: var(--cat-title-w); color: var(--cat-accent);
  font-size: clamp(13px, 1vw, 15px); letter-spacing: 0.1em; }
.title {
  margin: 0; font-family: var(--cat-display); font-weight: var(--cat-title-w);
  text-transform: var(--cat-title-tt); letter-spacing: var(--cat-title-ls);
  line-height: var(--cat-title-lh); color: var(--cat-ink); text-wrap: balance;
}
.t-hero { font-size: clamp(52px, 8.5vw, 128px); }
.t-1    { font-size: clamp(30px, 4.4vw, 62px); }
.t-2    { font-size: clamp(24px, 3vw, 40px); }
.gold   { color: var(--cat-accent); }
/* ⛔⛔ `margin: 0` — cổng đăng đối bắt ra 15/08: thẻ `<p>` mang MARGIN MẶC ĐỊNH của trình
   duyệt (~1em), nên trong lưới hai cột phần ấy tụt xuống 15px so với `<h2>` bên cạnh (`.title`
   đã có `margin:0`). Lệch 15px thì mắt chỉ thấy «hơi cấn» chứ không chỉ ra được — sống sót
   qua nhiều vòng duyệt. Trong hệ này mọi khoảng cách do `gap` của `.stack` lo, margin thẻ
   là thừa và là nguồn lệch. */
.lead { font-size: clamp(15px, 1.25vw, 19px); line-height: 1.62; color: var(--cat-ink-2);
  max-width: 60ch; margin: 0; }
.body { font-size: clamp(13px, 1.05vw, 16px); line-height: 1.62; color: var(--cat-ink-2);
  max-width: 62ch; margin: 0; }
/* ⭐ Ô TRONG SUỐT — Sếp chốt 15/08: chú thích đi trong ô kính giống nút «Xem catalog»,
   để cả trang nói một thứ tiếng. Viền mảnh accent + nền tối mờ + kính lỏng. */
.o-kinh { border: 1px solid rgba(var(--cat-accent-rgb, 230,178,97), 0.30);
  background: rgba(5,5,5,0.30); -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); }
/* ⛔ Ô KÍNH PHẢI BẰNG BỀ RỘNG CHỮ DƯỚI PHẦN ẤY — Sếp bắt 15/08. `width:fit-content` làm ô ôm
   sát chữ nhãn, nên ô «THỂ LOẠI» hẹp hơn dòng «Căn hộ cao tầng» ngay dưới: mỗi cụm một
   kiểu, mất đăng đối. Trong một ô thông tin, ô kính trải bằng cả cột. */
.spec .label, .stack > .label { align-self: stretch; width: auto; }
.label { font-family: var(--font-display); font-weight: 600; font-size: 10px;
  letter-spacing: 0.3em; text-transform: uppercase; color: var(--cat-ink-3);
  /* ⛔ `inline-block` KHÔNG đủ: trong một `.stack` (flex cột) phần tử bị KÉO CĂNG hết
     chiều ngang cột, nên ô kính dài thượt thay vì ôm chữ. Phải chặn cả hai đường. */
  display: inline-block; width: fit-content; align-self: flex-start; padding: 6px 12px;
  border: 1px solid rgba(var(--cat-accent-rgb, 230,178,97), 0.26);
  background: rgba(255,255,255,0.035);
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); }
.cap { font-family: var(--font-display); font-weight: 600; font-size: 10px;
  letter-spacing: 0.3em; text-transform: uppercase; color: var(--cat-ink); }

/* ---- imagery ---- */
.fill { position: absolute; inset: 0; z-index: 0; }
/* ⛔⛔⛔ ẢNH TRANG PHẢI HIỆN TRỌN KHUNG — Sếp chốt 15/08 khi nhìn trang chia của Maison:
   *«các ảnh page đang bị không đủ hình nên nhìn không rõ và không thể hiện được nội dung»*.
   `object-fit: cover` lấp kín khung bằng cách CẮT hai bên: ảnh 2000×813 vào khung 1440×900
   bị xén mất 35% bề rộng — cái cổng sắt mất một nửa, bức ảnh không còn kể được gì.
   ⭐ Nay ảnh chạy `contain`: **trọn khung, không xén một milimet**. Phần thừa của khung
   không để đen chết — lót một bản CHÍNH TẤM ẤY phóng to, làm mờ và hạ sáng, nên mép ảnh
   tan vào nền thay vì cắt cụp thành hai vệt đen.
   ⛔ Đổi chỗ này thì phải sửa `KIEU_O` trong build_catalog.py — phép đo độ nét tính khác
   hẳn giữa `cover` và `contain`. */
.fill img.nen-mo { position: absolute; inset: -6%; width: 112%; height: 112%;
  object-fit: cover; filter: blur(38px) saturate(.72) brightness(.42); z-index: 0; }
.fill img.tron-khung { position: relative; width: 100%; height: 100%;
  object-fit: contain; display: block; z-index: 1; }
.scrim { position: absolute; inset: 0; z-index: 1; }
/* Độ phủ là BIẾN, không gõ cứng — mặc định giữ NGUYÊN giá trị cũ để bộ đã khoá dựng lại
   vẫn trùng byte. Bộ nào theo luật «ảnh là chính» thì khai giá trị nhẹ hơn trong spec.
   Xem ~/AAS-Studio/LUAT-CATALOG-ANH-LA-CHINH.md */
.scrim.b { background: linear-gradient(180deg, rgba(5,5,5,0) var(--cat-scrim-b-0, 40%), rgba(5,5,5,var(--cat-scrim-b-1, 0.86)) 100%); }
.scrim.l { background: linear-gradient(90deg, rgba(5,5,5,var(--cat-scrim-l-0, 0.92)) 0%, rgba(5,5,5,var(--cat-scrim-l-1, 0.55)) 42%, rgba(5,5,5,var(--cat-scrim-l-2, 0.1)) 100%); }
.scrim.tb { background: linear-gradient(180deg, rgba(5,5,5,var(--cat-scrim-tb-0, 0.6)) 0%, rgba(5,5,5,0) 30%, rgba(5,5,5,0) 62%, rgba(5,5,5,var(--cat-scrim-tb-1, 0.72)) 100%); }
.scrim.dim { background: rgba(5,5,5,var(--cat-scrim-dim, 0.5)); }

.figure { position: relative; overflow: hidden; background: var(--cat-panel);
  border: 1px solid var(--cat-rule); }
.figure img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* ⛔ CHÚ THÍCH ẢNH DÙNG CHUNG NGÔN NGỮ Ô KÍNH VỚI NHÃN VÀ NÚT — 15/08.
   Trước đây ô này pha riêng (nền đen .6, viền vàng gõ cứng 230,178,97) nên trên cùng một
   trang có HAI KIỂU ô kính: chú thích ảnh một kiểu, nhãn một kiểu. Viền còn gõ cứng màu
   vàng nên catalog accent terracotta vẫn ra viền vàng — sai bảng màu của chính bộ ảnh. */
.figure .fcap { position: absolute; left: 14px; bottom: 12px; z-index: 2;
  background: rgba(5,5,5,0.42); border: 1px solid rgba(var(--cat-accent-rgb, 230,178,97), 0.26);
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); padding: 6px 12px; }
.figure .fnum { position: absolute; left: 16px; top: 14px; z-index: 2; }
/* ⭐ CHÚ THÍCH BẤM ĐƯỢC — dấu mũi tên nhỏ báo cho khách biết đây là cửa vào, không phải
   nhãn suông. Ô kính giữ nguyên ngôn ngữ chung, chỉ thêm phản hồi khi rê chuột. */
.figure .kg-di { display: inline-flex; align-items: center; gap: 8px; text-decoration: none;
  cursor: pointer; transition: background .2s, border-color .2s; }
.figure .kg-di i { width: 13px; height: 1px; background: var(--cat-accent);
  position: relative; display: block; }
.figure .kg-di i::after { content: ""; position: absolute; right: 0; top: -2.5px;
  width: 5px; height: 5px; border-top: 1px solid var(--cat-accent);
  border-right: 1px solid var(--cat-accent); transform: rotate(45deg); }
.figure .kg-di:hover { background: rgba(5,5,5,.72);
  border-color: rgba(var(--cat-accent-rgb, 230,178,97), .62); }

/* ⭐⭐ CHỮ CÁI ĐẦU — Sếp chốt 15/08. Chữ mở đầu khối chữ lớn được nhấc lên, phóng to và
   đặt trong ô trong suốt: mắt có chỗ bám trước khi đọc, và đó là nếp của trang in
   editorial. Ô dùng chung ngôn ngữ với nút và nhãn.
   ⛔ Dùng `float` chứ không dùng `::first-letter` — pseudo đó không nhận được khung, nền
      và kính lỏng trên mọi trình duyệt, đặt vào là mất ô mà chẳng báo gì. */
/* ⛔ KHỐI CHỮ CÓ CHỮ CÁI ĐẦU PHẢI TẮT `text-wrap: balance`. Balance cố chia các dòng
   đều nhau, float lại chiếm mất một góc — hai thứ chỏi nhau, chữ ra thụt vào thò ra như
   răng cưa. Bắt bằng mắt 15/08 trên slide lời tựa. */
.co-chu-dau { text-wrap: initial; }

/* ⭐⭐ CHỮ CÁI ĐẦU — Sếp chốt lại 15/08 (lần ba): **BỎ Ô KÍNH**, chỉ cần chữ TO và
   NGHỆ THUẬT để tự nối vào khối chữ.
   Hai bản trước đều sai một kiểu: bản 1 bọc chữ trong ô kính, bản 2 thêm số `00` cho ô ấy —
   đều là ĐẮP THÊM MỘT VẬT rồi mong vật đó nối hộ. Sếp bác cả hai. Chữ cái đầu nối được là
   nhờ phần ấy CÙNG MỘT CHẤT với câu chữ quanh phần ấy, không nhờ cái khung đeo quanh.
   ⭐ Nghiêng cùng chiều với câu (`serif-i`), cao đúng hai dòng, màu accent — mắt đọc liền
     mạch từ chữ cái sang câu, không phải nhảy qua một cái hộp. */
/* ⛔⛔ CHỮ CÁI ĐẦU — ba bản mới ra, ghi cả ba để đừng quay lại bản đã bị bác:
     · bản 1 — CHÌM 2,72em thả nổi: dòng chính một dòng thì không có dòng thứ hai cho chân
       chữ đặt, nên chữ NHẢY LÊN tách hẳn khỏi câu. Sếp bác.
     · bản 2 — CÙNG CỠ chỉ khác màu: đứng đúng hàng nhưng nhạt, đọc ra như GÕ NHẦM một chữ
       hoa màu lạ chứ không ra cái nhấn. Sếp bác.
     · bản 3 (đang dùng) — TO HƠN MÀ ĐỨNG ĐÚNG HÀNG: chung chân chữ với dòng, cỡ 1,52em,
       nghiêng cho khác nét. Ấn tượng mà không tách rời.
   ⛔ `line-height` của chữ nhấn phải NHỎ hơn nhịp dòng của tiêu đề, nếu không dòng đầu bị
      thổi cao hơn các dòng sau — hai dòng của cùng một câu mà thưa nhặt khác nhau.
   ⛔ Phần chữ nhô lên trên nhịp dòng phải được trả chỗ bằng `padding-top`, nếu không thẻ ấy
      leo lên đè dòng mắt phía trên. */
.chu-dau { color: var(--cat-accent); font-style: italic; font-size: 1.52em;
  line-height: 0.6; }
/* ⛔ Trị số này TÍNH TỪ SỐ ĐO, không ướm. Cổng `kiem_chu_nhan.py` đo đỉnh chữ vượt bao
   nhiêu pixel lên trên mép khối: ở cỡ 62px vượt 13,9px trong khi đệm cũ 0,34em = 21,1px
   -> cần 35,0px = 0,565em; ở cỡ 40px cần 0,575em. Lấy 0,58em phủ cả hai thang. */
.title.co-chu-dau { padding-top: 0.58em; }
/* Khối văn DÀI (lời tựa) thì mới đủ chỗ cho chữ chìm hẳn hai dòng. */
.chu-dau.tha { float: left; font-family: var(--cat-display); font-weight: var(--cat-title-w);
  font-size: 2.72em; line-height: 0.76; margin: 0.04em 0.10em 0 0; }
.title.co-chu-dau:has(.tha) { padding-top: 0; }

/* ⛔⛔⛔ LUẬT TUYỆT ĐỐI — MỘT KHỐI CHỮ CHỈ CÓ MỘT ĐIỂM NHẤN MÀU.
   Canonical: ~/AAS-Studio/LUAT-MOT-DIEM-NHAN.md — Sếp chốt 15/08.
   Chữ cái đầu đã là accent thì phần còn lại của khối phải TRẮNG. Trước đây tiêu đề vừa có
   chữ cái đầu accent vừa có một cụm `.gold` accent ở giữa: hai vệt cam so le, mắt không
   biết bám vào đâu. Engine tự lột `.gold` khỏi khối đã có chữ cái đầu — xem `_chu_dau()`. */
.title .chu-dau ~ .gold, .title .gold { color: inherit; }
.title.co-chu-dau .gold { color: inherit; }

.toc-the { border: 1px solid rgba(var(--cat-accent-rgb, 230,178,97), 0.24);
  background: rgba(255,255,255,0.035);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  padding: clamp(20px, 2.3vw, 36px); }
/* ⛔ `baseline` KHÔNG phải «cùng một hàng thẳng» — số cỡ 46px và nhãn cỡ 10px chung chân
   chữ thì nhãn bị đẩy tụt hẳn xuống đáy số, mắt đọc ra hai hàng. Sếp bắt 15/08.
   Căn theo TÂM mới ra một hàng ngang thật. */
.toc-dau { display: flex; align-items: center; gap: 14px;
  padding-bottom: 16px; margin-bottom: 4px; }
.toc-dau b { font-family: var(--cat-display); font-weight: var(--cat-title-w);
  font-size: clamp(30px, 3.2vw, 46px); line-height: 1; color: var(--cat-accent); }
/* ⛔ Nhãn nằm TRONG tấm kính thì bỏ ô kính riêng của phần ấy — ô trong ô là hai lớp viền chồng
   nhau, đọc ra rối chứ không ra sang. */
.toc-dau .label { border: 0; background: none; padding: 0;
  -webkit-backdrop-filter: none; backdrop-filter: none; }

/* ⭐⭐ CON TRỎ RADAR — Sếp chốt 15/08. Chấm accent bám chuột, ba vòng sóng lan ra lệch pha.
   ⛔ `pointer-events:none` — thiếu dòng này thì chính con trỏ chắn mất nút bên dưới.
   ⛔ Máy CẢM ỨNG không có chuột: trả con trỏ hệ thống lại, ẩn hẳn khối này. Đây là dò
      THIẾT BỊ, không phải khối giảm-chuyển-động — §5c vẫn nguyên.
   ⛔ Vòng sóng chạy bằng `transform`+`opacity`, KHÔNG đụng layout, nên không ăn khung hình. */
.chuot { position: fixed; left: 0; top: 0; z-index: 90; pointer-events: none;
  width: 10px; height: 10px; margin: -5px 0 0 -5px; border-radius: 50%;
  background: var(--cat-accent); box-shadow: 0 0 16px rgba(var(--cat-accent-rgb, 230,178,97), 0.85);
  transform: translate3d(-100px, -100px, 0); }
/* ⛔⛔ SẾP BÁO 15/08 «chuột chưa có animation radar». Đo lại thì animation CÓ chạy —
   hỏng ở chỗ THẤY: viền 1px, đục nhất mới 0,8 và tắt hẳn từ 62% chu kỳ, nằm trên ảnh nội
   thất nhiều chi tiết thì gần như vô hình. «Chạy đúng» và «nhìn thấy» là hai việc khác nhau;
   cổng đo được việc thứ nhất, việc thứ hai chỉ mắt.
   Nay: viền dày hơn, đục hẳn ở đầu sóng, giữ sáng lâu hơn, thêm quầng đổ bóng để vòng bám
   được cả trên nền sáng, và nhịp chậm lại cho mắt kịp bắt. */
.chuot i { position: absolute; left: 50%; top: 50%; display: block;
  width: 58px; height: 58px; margin: -29px 0 0 -29px; border-radius: 50%;
  border: 1.5px solid var(--cat-accent);
  box-shadow: 0 0 10px rgba(var(--cat-accent-rgb, 230,178,97), 0.45),
              inset 0 0 10px rgba(var(--cat-accent-rgb, 230,178,97), 0.22);
  animation: chuot-radar 2.8s cubic-bezier(.16,.7,.3,1) infinite; }
.chuot i:nth-child(2) { animation-delay: 0.93s; }
.chuot i:nth-child(3) { animation-delay: 1.86s; }
@keyframes chuot-radar { 0% { transform: scale(0.16); opacity: 0; }
  12% { opacity: 1; } 72% { opacity: 0.42; }
  100% { transform: scale(1); opacity: 0; } }
/* trên thứ bấm được thì vòng phình to — «nút radar bắt được mục tiêu» */
.chuot.bam { width: 14px; height: 14px; margin: -7px 0 0 -7px; }
.chuot.bam i { width: 84px; height: 84px; margin: -42px 0 0 -42px; border-width: 2px; }
body.cat { cursor: none; }
@media (hover: none) { .chuot { display: none; } body.cat { cursor: auto; } }

/* ============================================================
   ⛔⛔ LUẬT TUYỆT ĐỐI — ĐĂNG ĐỐI (Sếp chốt 2026-08-15)
   Canonical: ~/AAS-Studio/LUAT-DANG-DOI.md
   ------------------------------------------------------------
   Nguyên văn: «Tuyệt đối đăng đối trong tất cả các tài liệu của
   chúng ta. Luật tuyệt đối.»
   Hai khối đứng cạnh nhau thì MÉP TRÊN thẳng nhau, MÉP DƯỚI
   thẳng nhau, bề rộng cân nhau. Không có khối nào cao lêu nghêu
   cạnh một khối lửng lơ.
   ⛔ `align-items: center` trên lưới hai cột là nguồn lệch mép
      thường gặp nhất: mỗi cột tự cao theo ruột của phần ấy rồi thả
      giữa, nên hai mép lệch nhau ở cả trên lẫn dưới.
   ⭐ Cưỡng hành bằng máy: python3 ~/AAS-Studio/tools/kiem_dang_doi.py
   ============================================================ */
.dang-doi { align-items: stretch !important; }
/* ⛔⛔⛔ CHỈ ÁP CHO KHỐI BỌC (`div`), TUYỆT ĐỐI KHÔNG ÁP CHO THẺ CHỮ.
   Đây là gốc của lỗi «chữ N nhảy lên» — ba lượt chỉnh cỡ chữ và line-height đều trượt,
   vì lỗi KHÔNG nằm ở chữ nhấn mà nằm ở chính luật đăng đối này.
   `display:flex; flex-direction:column` đặt lên một `<h2>` thì mọi thứ trong lòng thẻ ấy thành
   FLEX ITEM RIÊNG — kể cả chữ trần (anonymous item). Chữ cái đầu nằm trong `<span>` nên
   thành một item, phần chữ còn lại thành item thứ hai, hai item XẾP CHỒNG DỌC.
   Nhìn ra thì đúng y như «chữ N nhảy lên một dòng riêng».
   ⭐ Bài học rộng hơn: một luật bố cục quét bằng `> *` sẽ tóm cả những thẻ thẻ ấy không định
     tóm. Ghi rõ loại thẻ, đừng quét sạch. */
.dang-doi > div { display: flex; flex-direction: column; justify-content: center; }
.dang-doi.cuoi > div { justify-content: flex-end; }
/* ⭐ ĐĂNG ĐỐI THẬT chứ không đăng đối trên giấy: khối chữ ít nội dung hơn thì TRẢI ĐỀU ra
   hết chiều cao, để mép trên và mép dưới của CHỮ thẳng với tấm thẻ bên kia. Trước đây em
   căn giữa rồi bọc thêm một tầng cho tấm thẻ ôm ruột — cổng xanh mà mắt vẫn thấy lệch
   165px. Bọc thêm lớp là giấu vật đi, không phải xếp cho thẳng. */
.dang-doi > .trai-deu { justify-content: space-between; }
/* ⛔ `max-width` trên chữ làm CỘT PHẢI HẸP HƠN CỘT TRÁI — đo được 17% ở slide Feature.
   Trong lưới đăng đối, bề rộng do LƯỚI chia, không do ruột tự bó. */
/* ⛔ HẬU DUỆ, không phải chỉ CON TRỰC TIẾP. Bọc chữ vào một `div` là `> .body` hết khớp,
   `max-width: 62ch` sống lại và cột phải hẹp đi 17% — cổng đăng đối bắt ngay sau khi bọc.
   Sửa cấu trúc thì phải soi lại mọi bộ chọn neo vào cấu trúc cũ. */
.dang-doi .body, .dang-doi .lead { max-width: none; }

/* ============================================================
   CỬA VÀO TỪNG KHÔNG GIAN — Sếp chốt 15/08
   Nút to, có ảnh nhận diện, để khách BIẾT LÀ BẤM ĐƯỢC.
   ⛔ Chú thích nhỏ trên ảnh không đủ làm cửa — quá kín.
   ============================================================ */
.kg-luoi { display: grid; grid-template-columns: repeat(3, 1fr);
  gap: clamp(12px, 1.4vw, 22px); }
.kg-o { display: flex; align-items: center; gap: 14px; text-decoration: none;
  padding: 12px 16px 12px 12px; position: relative;
  border: 1px solid rgba(var(--cat-accent-rgb, 230,178,97), 0.26);
  background: rgba(255,255,255,0.035);
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
  transition: background .2s, border-color .2s; }
.kg-o:hover { background: rgba(var(--cat-accent-rgb, 230,178,97), 0.10);
  border-color: rgba(var(--cat-accent-rgb, 230,178,97), 0.60); }
.kg-o-anh { flex: 0 0 auto; width: 66px; height: 50px; overflow: hidden; display: block;
  border: 1px solid var(--cat-rule); }
.kg-o-anh img { width: 100%; height: 100%; object-fit: cover; display: block; }
.kg-o-chu { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.kg-o-chu b { font-family: var(--cat-display); font-weight: var(--cat-title-w);
  font-size: clamp(14px, 1.15vw, 18px); color: var(--cat-ink); line-height: 1.1; }
.kg-o-so { font-family: var(--font-display); font-weight: 600; font-size: 9.5px;
  letter-spacing: .26em; text-transform: uppercase; color: var(--cat-ink-3); }
.kg-o-mui { position: absolute; right: 14px; top: 50%; width: 7px; height: 7px;
  margin-top: -4px; border-top: 1px solid var(--cat-accent);
  border-right: 1px solid var(--cat-accent); transform: rotate(45deg); }
@media (max-width: 760px) { .kg-luoi { grid-template-columns: 1fr; } }

/* ---- layout helpers ---- */
.center { justify-content: center; }
.mt-auto { margin-top: auto; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(14px, 1.6vw, 26px); }
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(12px, 1.4vw, 22px); }
/* ⭐ DẢI CHI TIẾT — Sếp chốt 15/08: «phải dùng TOÀN BỘ ảnh của dự án đã có trước, chỉ phần
   cần thiết mới dùng crop». Ảnh cận cảnh và panel hẹp của trang lưới không đủ pixel nuôi ô
   422×556 của `.grid3`; nhét vào đó là mềm. Ô của chúng phải NHỎ HƠN, không phải ảnh phải
   to lên — pixel không có thì phóng lên cũng không sinh thêm nét.
   ⛔ Số cột và chiều cao ở đây quyết định `O_CSS['chi_tiet']` trong build_catalog.py.
      Đổi ở đây thì phải ĐO LẠI rồi sửa bảng ấy, không thì bảng nói dối. */
.grid4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(10px, 1.1vw, 18px); }
/* Hai hàng: bỏ ghim chiều cao, để tỷ lệ ô quyết định — ghim 38vh cho 8 ô là bẹp mất một nửa. */
.grid4.hai-hang .figure { aspect-ratio: 4 / 3; }
.stack { display: flex; flex-direction: column; }
.hairline { height: 1px; background: var(--cat-rule); border: 0; width: 100%; }
.gline { height: 2px; width: 64px; background: var(--cat-accent); border: 0; }

/* stat block */
.stat .n { font-family: var(--cat-display); font-weight: var(--cat-title-w);
  font-size: clamp(34px, 4.4vw, 62px); line-height: 1; color: var(--cat-ink); }
.stat .n em { font-style: normal; color: var(--cat-accent); font-size: 0.42em; }
.stat .k { margin-top: 8px; }

/* materials + palette */
.swatch { display: flex; align-items: center; gap: 14px; }
.swatch i { width: 30px; height: 30px; border-radius: 2px; border: 1px solid rgba(255,255,255,0.12); display: block; }

/* ---- fixed chrome ---- */
.cat-nav { position: fixed; right: 22px; bottom: 20px; z-index: 40;
  display: flex; align-items: center; gap: 10px; }
.cat-nav button { width: 40px; height: 40px; border-radius: 999px; cursor: pointer;
  background: rgba(5,5,5,0.55); border: 1px solid var(--cat-accent); color: var(--cat-accent);
  font-size: 15px; display: inline-flex; align-items: center; justify-content: center;
  backdrop-filter: blur(6px); transition: background .2s, color .2s; }
.cat-nav button:hover { background: var(--cat-accent); color: var(--aas-black); }
.cat-count { position: fixed; left: 22px; bottom: 24px; z-index: 40;
  font-family: var(--font-display); font-weight: 600; font-size: 12px; letter-spacing: 0.2em;
  color: var(--cat-ink); }
.cat-count b { color: var(--cat-accent); font-weight: 700; }
.cat-count s { text-decoration: none; color: var(--cat-ink-3); }
.cat-prog { position: fixed; top: 0; left: 0; height: 3px; z-index: 41;
  background: var(--cat-accent); width: 0; transition: width .25s var(--ease-out); }

a { color: var(--cat-accent); }
