/* Gallery Ước Mơ (SoW 7.1) — docs/acceptance/2026-09-11-gallery-4-zone-reskin.md
 * (bản đầu: docs/acceptance/2026-09-04-gallery-uoc-mo.md)
 *
 * Khuôn thiết kế: 1125 x 2436 (iPhone X @3x) ⇒ cqw = px_thiết_kế / 11,25.
 *
 * NGUỒN SỐ ĐO từ 11/09 là 12 PSD gốc của team design (4 Căn cứ × 3 màn), KHÔNG phải Figma:
 * bản Figma là bản convert từ PSD và bbox của nó cõng cả glow đã bake, nên phồng hơn mực thật
 * (ví dụ tab màn rỗng: Figma 521x259, PSD 434x173). Lệch số thì tin PSD.
 *
 * KHÁC Bảng Xếp Hạng ở một điểm kiến trúc: trang kia là MỘT màn hình nên .lb-stage khoá
 * aspect-ratio rồi đặt mọi thứ absolute theo %. Ở đây lưới dài ra theo số tác phẩm, nên
 * phần đầu (tiêu đề + tab) tính theo cqw còn lưới thì chảy theo luồng tài liệu. Nền phòng
 * tách thành lớp position:fixed riêng để không phải kéo dài theo nội dung.
 *
 * Bốn chỗ CỐ Ý lệch thiết kế, đều vì quy đổi thẳng từ Figma sẽ trượt cổng chất lượng —
 * bảng số đầy đủ ở mục 6 của file acceptance:
 *   1. Vùng chạm tab nới tối thiểu 44px (quy đổi thẳng ra 34.1px ở bề rộng 320px).
 *   2. Cỡ chữ tên tác phẩm và tên bé có SÀN 11px (Figma ra 8.8px và 6.5px ở 320px).
 *   3. BỎ nút back — quy đổi ra 27.3px, dưới sàn chạm ở CẢ BA bề rộng, và thanh nav dưới đã làm
 *      đúng việc của nó (user chốt Q2 ngày 04/09; bộ design 11/09 vẫn vẽ nút back ở màn chi tiết
 *      và màn rỗng, vẫn không dựng, cùng lý do).
 *   4. Màn rỗng GIỮ thanh tab dù design không vẽ — bỏ đi thì bé đứng ở Căn cứ rỗng không còn
 *      đường sang Căn cứ khác. Banner thì bỏ đúng như design (user chốt 11/09).
 */

/* ---------- nền phòng ---------- */

/* position:fixed nên PHẢI nằm ngoài <main> (main mang container-type ⇒ thành containing
   block cho con fixed). */
/* ⚠️ KHÔNG dùng z-index: -1. Layout đặt nền cho <body> qua ViewData["BodyBgClass"], mà phần tử
   z-index âm vẽ SAU nền của khối chứa nó ⇒ ảnh phòng biến mất hoàn toàn, trang ra một mảng tím
   phẳng. Ảnh chụp bắt được; mọi phép đo hình học vẫn xanh vì hộp vẫn đúng chỗ.
   Thay vào đó: không z-index, và <main> đứng sau trong DOM lại có position:relative nên tự vẽ đè. */
/* Ảnh nền xuất theo ĐÚNG canvas thiết kế 1125x2436 (tỉ lệ 0,462), không phải theo tỉ lệ ảnh gốc
   của Căn cứ (0,563). Điện thoại dọc cũng ~0,462 nên `cover` gần như không cắt gì ⇒ phần nhìn
   thấy đúng bằng phần designer bố cục. Bản cũ `gal-room.webp` là 1060x1883 nên `cover` cắt mất
   hai bên và để lộ phần sáng hơn ở giữa — một nửa lý do trang trông sáng hơn thiết kế.
   Nửa còn lại: hai lớp MULTIPLY của PSD, nay đã nén sẵn vào ảnh
   (tools/figma-assets/build-gallery-zone-backgrounds.py). KHÔNG thêm lớp phủ tối bằng CSS ở đây:
   hai lớp đó là vignette biến thiên theo vị trí, lớp phủ phẳng chỉ trúng giá trị trung bình. */
.gal-bg {
  position: fixed;
  inset: 0;
  background-color: #090743;   /* tránh nháy trắng trước khi ảnh nền tải xong */
  background-image: url("../images/portal/gallery/gal-bg-sieu-anh-hung.webp");
  background-size: cover;
  background-position: center top;
  pointer-events: none;
}

/* Nền theo Căn cứ. Khoá là ThemeToken của Zone — cùng quy ước với gal-tab-{token}.webp, nên thêm
   Căn cứ mới là thêm một dòng DB + thả file, không sửa code. Giá trị trần ở trên là đường lui cho
   ca `activeZone == null` (vd ?zoneId=999, GalleryService.cs:39-41 cố ý không rơi về mặc định). */
.gal-bg--sieu-anh-hung { background-image: url("../images/portal/gallery/gal-bg-sieu-anh-hung.webp"); }
.gal-bg--mong-mo       { background-image: url("../images/portal/gallery/gal-bg-mong-mo.webp"); }
.gal-bg--sang-tao      { background-image: url("../images/portal/gallery/gal-bg-sang-tao.webp"); }
.gal-bg--tham-hiem     { background-image: url("../images/portal/gallery/gal-bg-tham-hiem.webp"); }

/* Bộ design 11/09 vẽ BA khung cảnh khác nhau cho mỗi Căn cứ, không phải một: màn danh sách đứng
   trong sảnh kín, màn chi tiết và màn rỗng nhìn ra ngoài trời. Đo trên ảnh xuất từ 12 PSD gốc:
   lệch trung bình 33–39/255 giữa danh sách và chi tiết, 49–54 giữa chi tiết và rỗng — khác ẢNH,
   không phải khác độ sáng. Nên 12 file, không phải 4.

   ⚠️ ĐỔI CÓ CHỦ Ý so với mục A8 của docs/qc/2026-09-08-gallery-zone-bg-ajax-card-frame.md
   ("chi tiết dùng đúng nền của lưới cùng zone"). User chốt 11/09, xem mục 0.5-2 của
   docs/acceptance/2026-09-11-gallery-4-zone-reskin.md. QC đừng báo đây là hồi quy. */
.gal-bg--sieu-anh-hung-detail { background-image: url("../images/portal/gallery/gal-bg-detail-sieu-anh-hung.webp"); }
.gal-bg--mong-mo-detail       { background-image: url("../images/portal/gallery/gal-bg-detail-mong-mo.webp"); }
.gal-bg--sang-tao-detail      { background-image: url("../images/portal/gallery/gal-bg-detail-sang-tao.webp"); }
.gal-bg--tham-hiem-detail     { background-image: url("../images/portal/gallery/gal-bg-detail-tham-hiem.webp"); }

.gal-bg--sieu-anh-hung-empty { background-image: url("../images/portal/gallery/gal-bg-empty-sieu-anh-hung.webp"); }
.gal-bg--mong-mo-empty       { background-image: url("../images/portal/gallery/gal-bg-empty-mong-mo.webp"); }
.gal-bg--sang-tao-empty      { background-image: url("../images/portal/gallery/gal-bg-empty-sang-tao.webp"); }
.gal-bg--tham-hiem-empty     { background-image: url("../images/portal/gallery/gal-bg-empty-tham-hiem.webp"); }

/* ---------- tiêu đề ---------- */

.gal {
  position: relative;
  /* Khung viền của màn chi tiết rộng hơn màn hình — thiết kế cố ý cho nó tràn hai mép (bleed).
     Trên web thì bleed phải được CẮT, nếu không trang cuộn ngang được và mọi gate "không tràn
     ngang" đỏ. `clip` chứ không `hidden`: `hidden` biến main thành scroll container, kéo theo
     một trục cuộn dọc thứ hai và làm hỏng cả `position:fixed` của nền. */
  overflow-x: clip;
}

/* .nuvi-nav-clearance chừa chỗ cho THÂN thanh nav, nhưng các mục của nó mang .home-tap-safe —
   vùng chạm nới lên cao hơn thân thanh. Gate đo được 2 thẻ hàng cuối bị chính vùng chạm đó
   nuốt cú chạm (elementFromPoint tại tâm thẻ trả về mục nav). Cộng thêm một khoảng bằng chính
   phần nới đó; số đo lại phải cho 0 thẻ bị che ở MỌI vị trí cuộn. */
.gal > :last-child {
  margin-bottom: 8cqw;
}

.gal-title {
  position: relative;
  width: 81.333%;            /* 915 / 1125 */
  margin: 25.956cqw auto 0;  /* y = 292 / 1125 */
  container-type: inline-size;
}

.gal-title img {
  display: block;
  width: 100%;
  height: auto;
}

/* `.gal-title__text` ĐÃ GỠ 2026-09-11. Bộ asset cũ giao khung RỖNG nên chữ phải dựng bằng CSS;
   bộ mới bake sẵn "GALLERY ƯỚC MƠ" vào từng banner theo Căn cứ, kèm nét viền và quầng sáng mà
   text-shadow không tái lập được. Nhãn này cố định (không đổi theo dữ liệu) nên dùng thẳng ảnh
   là bản khớp tuyệt đối — đúng luật "chữ bake sẵn trong asset" của skill figma-clone-verify.
   Chữ cho trình đọc màn hình nay nằm ở thuộc tính alt của chính ảnh, xem Index.cshtml. */

/* ---------- thanh 4 tab Căn cứ ---------- */

/* CUỘN NGANG là bắt buộc, không phải lựa chọn thẩm mỹ: Figma trải 4 nút trên 1356px trong
   khung 1125px, nút thứ tư nằm hoàn toàn ngoài khung. */
.gal-tabs {
  display: flex;
  align-items: flex-end;
  gap: 3.262cqw;                 /* 36,7 / 1125 — đo từ PSD: 412-(40+335)=37, 753-(412+304)=37,
                                    1093-(753+304)=36 */
  padding: 0 3.556cqw;           /* tab đầu x = 40 / 1125 */
  margin-top: 0;                 /* PSD: tab y=568, banner kết thúc y=572 — chồng 4px */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.gal-tabs::-webkit-scrollbar {
  display: none;
}

/* Vùng chạm nới bằng min-height RIÊNG cho từng trạng thái, KHÔNG bằng ::after và KHÔNG bằng
 * MỘT min-height dùng chung cho cả hai trạng thái.
 *
 * Lý do bỏ ::after: .gal-tabs phải cuộn ngang, mà `overflow-x: auto` kéo theo `overflow-y: auto`
 * (CSS không cho một trục visible còn trục kia auto) ⇒ phần ::after thò ra ngoài chiều cao thanh
 * bị CẮT khỏi hit-test. Gate đo được đúng 38.50px thay vì 44px ở bề rộng 320.
 *
 * Lý do KHÔNG dùng một min-height chung (11/09, P0-04): min-height dùng chung là một CLAMP về
 * cùng một số — nó chỉ nâng bản KHÔNG chọn (cao tự nhiên < 44px ở phần lớn bề rộng) lên đúng
 * 44px, còn bản ĐANG CHỌN thường đã cao hơn 44px nên không bị nâng. Kết quả đo
 * `getBoundingClientRect`: không chọn dừng cứng ở 44.00, đang chọn chỉ nhỉnh hơn một chút
 * (46.09) — tỉ lệ "cao hơn" tụt còn ~4,8%, dưới sàn 7,7% của tiêu chí A4 (đích 10,7%).
 *
 * Đo lại chiều cao TỰ NHIÊN (không sàn) của hai asset thật: tỉ lệ ổn định **~9,0%** ở mọi bề rộng
 * 320-448 — đã nằm trong ngưỡng A4 (7,7%-13,7%). Muốn giữ sàn chạm 44px cho bản KHÔNG chọn (natural
 * < 44px ở 320-390) MÀ KHÔNG kéo tỉ lệ xuống dưới sàn, hai trạng thái cần HAI mức sàn riêng, cách
 * nhau đúng theo tỉ lệ đích 10,7% (44 × 1,107 ≈ 49px) — không phải cùng một số 44px. */
.gal-tab {
  position: relative;
  display: flex;
  align-items: flex-end;
  flex: 0 0 auto;
  width: 27.111cqw;              /* 305 / 1125 — bản KHÔNG được chọn */
}

.gal-tabs:not(.gal-tabs--lg) .gal-tab {
  min-height: 44px;
}

.gal-tabs:not(.gal-tabs--lg) .gal-tab.is-on {
  min-height: 49px;              /* 44 × 1,107 — giữ tỉ lệ A4 khi cả hai đụng sàn (bề rộng ≤390) */
}

.gal-tab.is-on {
  width: 29.778cqw;              /* 335 / 1125 — bản đang chọn, to hơn 1.098 lần */
}

/* Thanh tab của MÀN RỖNG to hơn hẳn: PSD vẽ tab Căn cứ ở 434x173 thay vì 335x134, tức ×1,2955.
   Đó là chủ ý thiết kế (màn rỗng không có banner nên tab gánh vai trò tiêu đề), không phải lệch.
   Nhân đều mọi bề ngang để tỉ lệ 2,50 của asset giữ nguyên — xem mục 0.5-5 của acceptance. */
.gal-tabs--lg {
  gap: 4.225cqw;                 /* 36,7 × 1,2955 */
  /* Lề trái/phải = (100 - 38,578) / 2 = 30,711cqw, KHÔNG phải lề 40px port từ màn danh sách.
     Lý do: `centerActiveTab()` (Index.cshtml) canh giữa tab đang chọn bằng cách set scrollLeft,
     nhưng scrollLeft bị kẹp trong [0, scrollWidth-clientWidth]. Tab ở HAI ĐẦU hàng (Siêu Anh Hùng
     đứng đầu, Thám Hiểm đứng cuối — xem TabOrder) cần scrollLeft âm hoặc vượt max để canh giữa,
     nên bị kẹp về biên và lệch tâm hẳn 26cqw (QC 11/09, P0-03). Lề đủ rộng để offsetLeft của tab
     đầu/cuối luôn nằm trong biên [0, max] là đúng công thức (khung nhìn - bề rộng tab chọn) / 2. */
  padding: 0 30.711cqw;
  margin-top: 36.267cqw;         /* tab y = 408 / 1125 — chỗ banner để lại khi màn rỗng bỏ nó */
}

.gal-tabs--lg .gal-tab {
  width: 35.122cqw;              /* 305 × 1,2955 / 11,25 */
}

.gal-tabs--lg .gal-tab.is-on {
  width: 38.578cqw;              /* 434 / 1125 */
}

.gal-tab img {
  display: block;
  width: 100%;
  height: auto;
}

/* Trạng thái sáng/tối nay là HAI ASSET THẬT: gal-tab-{token}-on.webp / -off.webp, xuất từ 12 PSD
   gốc của team design (11/09). Khối filter brightness/saturate/drop-shadow dựng tạm hồi 04/09 đã
   gỡ hẳn — nó tồn tại chỉ vì bộ cũ thiếu 4 trạng thái, và không tái lập được quầng sáng riêng của
   từng Căn cứ. Đổi trạng thái giờ là đổi `src`, không đụng CSS. */

/* Quy đổi thẳng từ Figma cho tab cao 34.1px ở bề rộng 320px — dưới sàn chạm 44px của Apple HIG
   và của gate tools/ui-check. Sàn nằm ở .gal-tab phía trên. */

/* ---------- lưới tác phẩm ---------- */

.gal-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  column-gap: 3.911cqw;        /* 44 / 1125 */
  row-gap: 3.556cqw;           /* 40 / 1125 */
  width: 95.289%;              /* 1072 / 1125 */
  margin: 3.2cqw auto 0;       /* lưới y=737, tab kết thúc y=701 */
}

/* container-type ở CHÍNH thẻ để mọi cqw bên trong tính theo bề rộng THẺ, không theo bề rộng
   trang. Đặt nhầm chỗ này là bug đã trả giá ở trang Chat (viền dày hơn 76%) —
   docs/CONVENTIONS.md:171. */
.gal-card {
  position: relative;
  display: block;
  aspect-ratio: 514 / 734;
  container-type: inline-size;
  text-decoration: none;
}

/* Ruột thẻ: bát giác vát góc trên, khớp lỗ đã khoét trong gal-card-frame.webp.
   Toạ độ theo hộp THẺ; hai trục quy đổi bằng hai mẫu số khác nhau (514 ngang, 734 dọc).

   BỐN SỐ NÀY ĐO TỪ KÊNH ALPHA CỦA ASSET, không suy từ thiết kế và không lấy trung bình hai bên.
   Bản trước dùng 2,335% cho CẢ hai mép ngang — trung bình của một lỗ vốn KHÔNG đối xứng — nên mép
   phải hụt 0,135% bề rộng thẻ và để lọt nền phòng vào trong khung. Đổi asset là phải đo lại:
       ~/.local/share/nuvi-psd/bin/python3 tools/figma-assets/extract-gallery-card-from-psd.py
   rồi quét lỗ ở gate G10 của tools/ui-check/regression/gallery.mjs. */
.gal-card__art {
  position: absolute;
  /* ĐO LẠI 11/09 trên bộ khung mới, đúng cách gate G10 đo (alpha < 8, lỗ = đoạn trong suốt DÀI
     NHẤT trên cột giữa). Bốn Căn cứ trùng nhau ở ba mép; riêng mép dưới lệch: Siêu Anh Hùng
     24,387% còn ba Căn cứ kia 24,659%.
     Lấy 24,387% — số NHỎ nhất — chứ không lấy trung bình: ảnh phủ quá mép lỗ thì phần thừa chui
     xuống dưới bảng tên (đục, che kín), còn phủ hụt thì hở một dải trong suốt và lộ nền trang.
     Hụt là lỗi nhìn thấy được, thừa thì không. */
  left: 3.891%;                /* 20 / 514 */
  right: 3.696%;               /* 19 / 514 — KHÁC mép trái, đúng như lỗ thật */
  top: 2.452%;                 /* 18 / 734 */
  bottom: 24.387%;             /* mép trên bảng tên, bản hẹp nhất trong 4 Căn cứ */
  display: block;
  overflow: hidden;
  background: #0b1740;
  /* Vát góc trên. Chỉ cần nằm giữa BIÊN LỖ và BIÊN NGOÀI của khung: hẹp hơn biên lỗ thì hở nền,
     rộng hơn biên ngoài thì ảnh thò ra khỏi thẻ. Băng an toàn đo được là 13–31px bên trái và
     16–92px bên phải, nên lấy đường giữa băng. Bên phải gần như không cần vát (biên ngoài đã ở
     x=494/514 ngay từ hàng đầu) — không đối xứng, và thiết kế cũng không đối xứng. */
  clip-path: polygon(8.91% 0, 99.8% 0, 100% 0.6%, 100% 100%, 0 100%, 0 10.07%);
}

/* position:absolute là LOAD-BEARING, không phải trang trí.
   Chữ dự phòng .gal-card__artmsg cũng absolute và đứng TRƯỚC trong DOM. Nếu ảnh nằm trong luồng
   thì mọi phần tử đã định vị đều vẽ ĐÈ LÊN nó, nên chữ "Chưa xem được tác phẩm này" hiện chồng
   lên chính tấm ảnh vừa tải xong. Ảnh chụp bắt được lỗi này; 44 phép đo số thì không, vì cả hai
   phần tử đều nằm đúng hộp của chúng. */
.gal-card__img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Chuột phải / nhấn giữ / kéo rơi xuống link .gal-card thay vì ảnh: menu chỉ còn mục của link
     (Mở tab mới…), không còn "Lưu ảnh" — /thumb là ảnh TRẦN tới 1440px, bản có khung chỉ lấy qua
     nút Tải về ở /Result (2026-10-08, cùng lô chặn ở /Result và /Gallery/{token}). */
  pointer-events: none;
}

.gal-card__artmsg {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 6cqw;
  color: #cbd8ff;
  font-family: var(--font-sans);
  font-size: max(11px, 5.2cqw);
  line-height: 1.3;
  text-align: center;
}

/* ---------- nhãn loại hình trên thẻ (lô 2026-09-20) ----------
   Acceptance docs/acceptance/2026-09-20-gallery-video-audio.md — QĐ-2, A1, A4, A7, A8.

   CHIP CHỮ chứ không phải nút phát: cùng một cái nhãn, cùng một góc cho cả Nhạc lẫn Video, để
   hai loại hình thành MỘT hệ nhãn. Đã dựng thử cả ba phương án rồi chụp ảnh đối chiếu ở
   ~/Desktop/gallery-av-ux-review — nút phát giữa ô che đúng mặt nhân vật, còn huy hiệu góc thì
   chìm hẳn vào nền khi tranh nhiều chi tiết. Ảnh KHÔNG có chip: loại thường gặp nhất là loại
   không cần nói tên. */
.gal-card__type {
  position: absolute;
  /* ⚠️ HAI SỐ NÀY BỊ RÀNG BUỘC BỞI VÁT GÓC của `.gal-card__art`, không phải chọn cho đẹp.
     Vát góc trên-trái cắt theo đường từ (8,91% bề ngang ô, 0) tới (0, 10,07% bề cao ô). Quy ra
     cqw của THẺ: ô rộng 92,413cqw và cao 104,5cqw ⇒ đường cắt đi từ x=8,23cqw tới y=10,52cqw.
     Góc trên-trái của chip phải nằm NGOÀI đường đó: x/8,23 + y/10,52 > 1.
       7,0/8,23 + 5,5/10,52 = 1,374  ✓
     Bản prototype dùng 4/4 (= 0,87) và mép trái chip bị vát ăn mất một góc — thấy trên ảnh chụp,
     không thấy khi đọc code. Đổi vát góc là phải tính lại hai số này; gate G17 đo thật. */
  left: 7cqw;
  top: 5.5cqw;
  display: inline-flex;
  align-items: center;
  gap: 1.4cqw;
  padding: 1.2cqw 3cqw;
  border-radius: 99px;
  /* Nền ĐỦ ĐỤC để đạt contrast ≥ 4,5:1 trên MỌI tấm tranh — chip nằm trên ảnh của bé, không
     trên một màu phẳng nào biết trước. rgba(10,16,44,.82) cho chữ #e8efff tỉ số 13,8:1 ngay cả
     khi nền dưới là trắng tinh. backdrop-filter chỉ là trang trí, không được tính vào contrast:
     Firefox trên Android từng không hỗ trợ và nền sẽ mỏng hơn. */
  background: rgba(10, 16, 44, 0.82);
  backdrop-filter: blur(4px);
  border: 1px solid rgba(160, 190, 255, 0.32);
  color: #e8efff;
  font-family: var(--font-sans);
  /* Sàn 11px là luật chung của pack (G5) — ở 320px thì 3,9cqw chỉ còn 5,4px. */
  font-size: max(11px, 3.9cqw);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.2px;
  white-space: nowrap;
  /* Chạm vào chip phải rơi xuống thẻ <a> — A8. Cùng lý do và cùng cách với `.gal-card__frame`. */
  pointer-events: none;
}

/* Dải sóng trang trí của thẻ Nhạc đã BỊ GỠ (user chốt 20/09, đảo P1-1 của acceptance
   2026-09-20). Chip `.gal-card__type` ở góc trái đã nói đủ loại hình; dải sóng là lớp trang trí
   thứ hai cho cùng một thông tin và nó phủ 22cqw đáy ô, che mất phần dưới poster Căn cứ.
   Cùng lý do, thẻ Video không được thêm lớp nào tương đương — hai loại hình về cùng khuôn
   ảnh + chip. Gate G17 của gallery.mjs nay assert SỰ VẮNG MẶT của `.gal-card__wave`. */

/* Khối chờ: gỡ ở CẢ onload lẫn onerror. Gỡ thiếu một nhánh là khối chờ quay mãi trên ô có
   ảnh chết — đúng lỗi trang Kết quả đã dính. */
.gal-card__art.is-loading::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, #12205a 30%, #1e3486 50%, #12205a 70%);
  background-size: 300% 100%;
  animation: gal-shimmer 1.2s linear infinite;
}

@keyframes gal-shimmer {
  to { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .gal-card__art.is-loading::before { animation: none; }
}

.gal-card__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* ---------- bảng tên trên thẻ ---------- */

/* CĂN GIỮA DỌC bằng flex, KHÔNG bằng `top` tính tay — cùng khuôn với `.lb-card__name`
   (leaderboard.css:415) và `.profile-info-row__text` (profile.css:295): một box cố định kích
   thước bọc ngoài, `display:flex; flex-direction:column; justify-content:center` để trình duyệt
   TỰ ĐO chiều cao thật của các dòng con rồi chia đều khoảng trống — không tính tay.
   Bản đầu dùng top:7.2%/37.5% tính sẵn từ line-height hai dòng (dồn khối lên sát mép trên, xem
   docs/acceptance/2026-09-08-gallery-card-plate-vertical-center.md mục 0) — SAI vì `.gal-card__kid`
   là hàng icon+chữ, và icon 26×29px cao hơn dòng chữ ~25%, nên hộp hàng đó cao hơn line-height
   chữ mà phép tính tay giả định (code review 08/09 bắt bằng Playwright). Đúng lý do hai bên kia
   đã chọn flex thay vì tự tính `top`. */
/* ĐO LẠI 11/09 trên bảng tên của bộ khung mới. Toạ độ lấy từ bbox layer trong PSB của thẻ —
   bbox của layer pixel ở đó CHÍNH LÀ mực, nên so được thẳng với mực đo trên trang:
       bảng tên      x  2,75%.. 96,67%   y 75,34%..
       tên tác phẩm  x 13,53%            mực y 81,51%..86,58%
       icon bé       x 13,53%..18,63%        y 88,36%..92,33%
       tên bé        x 21,57%            mực y 88,90%..92,74%
   Bản trước đặt chữ ở 8,82% — lệch trái 4,7 điểm phần trăm so với thiết kế; chiều DỌC thì đã
   đúng sẵn (lệch 0,19–0,31) nên phần flex căn giữa bên dưới giữ nguyên. */
.gal-card__plate {
  position: absolute;
  left: 2.75%;                 /* 14,1 / 514 */
  right: 3.33%;                /* 514 - 496,9 */
  top: 75.75%;                 /* 556 / 734 */
  bottom: 1.09%;
  display: flex;
  flex-direction: column;
  justify-content: center;     /* căn giữa dọc theo chiều cao THẬT của 2 dòng con */
  gap: 1.744cqw;                /* giữ khoảng cách 2 dòng như bản gốc: 8.96 / 514 */
  pointer-events: none;
}

/* Cắt bằng ellipsis và KHOÁ một dòng, nên tên dài không kéo cao bảng tên. `margin:0 4%` (thay
   left/right:4% cũ) — vẫn tính theo bề rộng CHÍNH plate vì containing block của margin% trên một
   flex item luôn là content box của flex container chứa nó, giống hệt cơ sở % cũ. `flex:none` —
   không co giãn theo chiều dọc (main axis của cột); bề rộng đến từ `align-items:stretch` mặc định
   của plate trừ margin. */
.gal-card__name,
.gal-card__kid {
  flex: none;
  /* margin% của một flex item tính theo BỀ RỘNG CONTENT BOX của flex container (bảng tên), không
     theo thẻ. Bảng tên rộng 93,92% thẻ ⇒ 11,48% × 93,92% = 10,78% thẻ ⇒ chữ bắt đầu ở
     2,75 + 10,78 = 13,53% thẻ, đúng thiết kế.
     Lề phải để 10% (hẹp hơn lề trái): thiết kế cho chữ mẫu chạy tới 86,67% thẻ, lấy 11,48% cả hai
     bên thì mép phải rơi vào 85,89% và chuỗi dài bị cắt sớm hơn thiết kế một chút. */
  margin: 0 10% 0 11.48%;
  color: #ffffff;
  font-family: var(--font-tektur);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: left;
}

.gal-card__name {
  font-size: max(11px, 7.198cqw);   /* 37 / 514 */
  line-height: 1.15;
  font-weight: 700;
}

.gal-card__kid {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 2.94cqw;                     /* 21,57% - 18,63% = khe icon → chữ của thiết kế */
  font-size: max(11px, 5.447cqw);   /* 28 / 514 */
  line-height: 1.15;
}

.gal-card__kid img {
  flex: 0 0 auto;
  width: max(9px, 5.10cqw);    /* 26,2 / 514 — bộ cũ để 7,004cqw, rộng hơn thiết kế 1,9 điểm */
  height: auto;
}

.gal-card__kid span {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- trạng thái Căn cứ rỗng ---------- */

/* PSD màn rỗng: khối y=568, thanh tab kết thúc y=581 (tab 408 + cao 173) ⇒ khối bắt đầu TRƯỚC
   khi tab kết thúc 13px. Chồng lấn đó là vùng trong suốt của cả hai asset (mực khối rỗng chỉ bắt
   đầu ở y=568+38=606), không phải lỗi bố cục — giữ đúng số. */
.gal-empty {
  width: 95.2%;                /* 1071 / 1125 */
  margin: -1.156cqw auto 0;    /* (568 - 581) / 1125 */
}

.gal-empty__art {
  display: block;
  width: 100%;
  height: auto;
}

/* Thiết kế đặt nút đè lên mép dưới khối rỗng 64px — giữ nguyên chồng lấn đó.
   PSD: khối 26,568 1071x1403 (kết thúc y=1971), nút 292,1907 542x164. */
.gal-empty__update {
  position: relative;
  display: block;
  width: 50.61%;               /* 542 / 1071 */
  margin: -5.976cqw auto 0;    /* (1907 - 1971) / 1071 của bề rộng khối */
  min-height: 44px;
}

.gal-empty__update img {
  display: block;
  width: 100%;
  height: auto;
}

/* ---------- phân trang ---------- */

/* Figma không vẽ nút phân trang nào (user chốt Q4 giữ cơ chế ?page= đã có), nên khối này
   KHÔNG có số đo thiết kế để bám. Dùng token màu của theme và sàn chạm 44px. */
.gal-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3.556cqw;
  margin: 4.5cqw auto 0;
  width: 95.289%;
  font-family: var(--font-tektur);
}

.gal-pager__btn {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 4cqw;
  border: 1px solid #5b8cff;
  border-radius: 6px;
  background: rgba(16, 30, 88, 0.85);
  color: #dce7ff;
  font-size: max(12px, 3.2cqw);
  text-decoration: none;
}

.gal-pager__at {
  color: #b9c9f5;
  font-size: max(11px, 3cqw);
}

/* ==========================================================================
   TRANG CHI TIẾT — Figma 338:749
   ========================================================================== */

/* Trang chi tiết dùng CHUNG bộ nền theo Căn cứ với trang lưới (`gal-bg--{token}`), nên không còn
   biến thể riêng. `gal-room-detail.webp` cũ đã xoá: đo ra nó là CÙNG một ảnh với `gal-room.webp`
   ở độ phân giải khác (lệch 1,1/255), tức 377 KB cho một tấm ảnh. */

/* ══════════════════════════════════════════════════════════════════════════════════════
   Khung VUÔNG tràn viền, PORT nguyên từ `.result2-stage` của /Result (Figma 432:173,
   `Views/Result/Index.cshtml` + `Styles/portal.css:2568+`) — acceptance 2026-09-08 QĐ-1.
   Mọi hằng số cqw dưới đây COPY Y HỆT bản portal.css, không tính lại: đơn vị là % BỀ RỘNG
   khối chứa `.gd-stage` (container-type: inline-size), kể cả toạ độ DỌC — docs/CONVENTIONS.md
   mục "Quy đổi Figma → cqw". Lệch một số ở đây là logo/tên/tuổi rơi lệch chỗ so với /Result
   dù cùng vẽ cho cùng một tác phẩm.
   ══════════════════════════════════════════════════════════════════════════════════════ */
/* Bảng màu poster — MẶC ĐỊNH là Sáng Tạo, cũng là đường lui khi không tra được Căn cứ.
   Ba Căn cứ còn lại đè bằng class `result2-stage--{token}` khai trong portal.css (layout load
   cả hai file), nên bảng hex chỉ tồn tại ở MỘT chỗ cho phần khác biệt.
   Nguồn: `NuVi.Core.Helper.ZonePosterPalette`. */
/* `:where()` để độ đặc hiệu = 0 — BẮT BUỘC, không phải cho đẹp: bảng override
   `.result2-stage--{token}` nằm trong portal.css, cùng độ đặc hiệu (0,1,0) với `.gd-stage`, mà
   gallery.css lại load SAU layout. Khai thẳng `.gd-stage` thì mặc định luôn thắng override và
   mọi tác phẩm ra màu Sáng Tạo dù class Căn cứ đã gắn đúng — đo trên trang thật: class đúng,
   asset đúng, nhưng 9/12 phép đo màu vẫn tím. */
:where(.gd-stage) {
  --r2-type: #410677;
  --r2-title: #a130b9;
  --r2-idea: #48024e;
  --r2-idea-name: #48024e;
  --r2-sketch-shadow: #b481c1;
  --r2-sketch-shadow-a: 0.54;
  /* ZonePosterPalette.Fallback.IdeaGlowBlurEm/IdeaGlowOpacity — copy y hệt `.result2-stage`
     bên portal.css (2026-09-10). Ba Căn cứ còn lại đè bằng class `result2-stage--{token}`
     như mọi biến `--r2-*` khác ở trên; chỉ đường lui Sáng Tạo cần chép riêng vì `.gd-stage`
     không bao giờ mang class trần `result2-stage`. */
  --r2-idea-glow-blur: 0.5417em;
  --r2-idea-glow-a: 0.58;
  --r2-logo-shadow: #6d7cbb;
  --r2-logo-shadow-a: 1;
  --r2-logo-shadow-spread: 0.3704cqw;
  --r2-logo-shadow-blur: 1.1111cqw;
  /* Box mô tả khung Video — bản Sáng Tạo (ZonePosterPalette.Fallback.DescFill/DescEdge), chép y
     hệt `.result2-stage` bên portal.css. Thiếu hai biến này thì Căn cứ Sáng Tạo (CssClass rỗng,
     không class override nào phủ) ra `rgb(var(--rv-desc-fill) / .88)` không hợp lệ ⇒ box trần. */
  --rv-desc-fill: 92 70 160;
  --rv-desc-edge: #b587e1;
}

/* ── Khung viền + badge Căn cứ quanh ô tác phẩm (bộ design 11/09) ─────────────────────────
 *
 * Số đo PSD "GALLERY Chi tiết tác phẩm", canvas 1125:
 *     khung  'shape gallery chi tiết'  -11, 411  1146x1409
 *     ô ảnh  'ZONE <CĂN CỨ>'            59, 561  1011x1005
 *     badge  'cc <căn cứ>'             371, 452   383x152
 *
 * Khung là ảnh RỘNG HƠN màn hình (1146 > 1125) — tràn hai mép là CỐ Ý. Mực thật của nó chỉ
 * 1014x1021 (đo alpha>128), tức viền ôm sát mép ô ảnh; phần còn lại là quầng sáng trong suốt.
 * Vì thế khung phải nằm NGOÀI .gd-stage: .gd-stage có `overflow:hidden` để cắt ảnh tác phẩm,
 * để khung bên trong là cắt mất quầng sáng.
 *
 * ⚠️ `img { max-width: 100% }` của Tailwind preflight sẽ kẹp khung về đúng bề ngang sân khấu và
 * mọi thứ bên trong vẫn đúng chỗ nên rất khó nhận ra — `max-width: none` bên dưới là bắt buộc. */
/* ⚠️ MỌI toạ độ `.gd-*` bên trong ô tác phẩm đều là `.result2-*` của portal.css ĐI QUA MỘT PHÉP
   BIẾN ĐỔI, không phải số đo lại bằng tay. Bộ design 11/09 KHÔNG đặt poster full-bleed: nó thu
   poster nhỏ lại rồi đặt lệch vào trong ô, chừa chỗ cho viền khung vẽ đè lên mép ô.

   Đo bằng đăng ký ảnh (so composite PSD với `NUVI OUTPUT SAH KHÔNG HÌNH.png`, quét s/ox/oy trên
   4 vùng độc lập — cả 4 hội tụ về cùng một bộ số):

       template(1080×1080)  →  ô tác phẩm(1010×1005):   scale 0.898,  đặt tại (15, 22)

   ⇒ với một giá trị cqw `v` của `.result2-*`:
       kích thước :  v × 1080 × 0,898 / 1010            = v × 0,96024
       toạ độ x   :  v × 0,96024 + 15/1010×100          = v × 0,96024 + 1,4851
       toạ độ y   :  v × 0,96024 + 22/1010×100          = v × 0,96024 + 2,1782
                     (trục dọc cũng chia cho BỀ RỘNG — quy ước cqw của NuVi)

   Bản trước 11/09 chỉ chỉnh tay `left` của `.gd-type` và `.gd-logo` nên mỗi khối lệch một kiểu:
   chữ "Tác Phẩm Của:" cách mép trong khung 4,6px (design 7,0), logo nhô lên cao hơn design 7,3px,
   và tên bé ở cuối câu ý tưởng THÒ RA NGOÀI mép dưới khung 4,0px — bị viền cắt ngang.
   Đổi một số ở đây thì đổi cả bộ, đừng chỉnh lẻ. */
.gd-frame {
  position: relative;
  /* 1011 / 1125 — ô tác phẩm KHÔNG chiếm hết bề ngang màn, khác bản 08/09 ("khung vuông tràn
     viền"). Bộ design 11/09 thu nó lại để chừa chỗ cho khung viền: khung rộng 1146 tức 101,9%
     bề ngang màn, tràn mỗi mép ~10px thiết kế. Để ô ảnh rộng 100% thì khung thành 113% màn và
     trang tràn ngang 24px — đã đo được đúng con số đó trước khi sửa. */
  width: 89.7778%;               /* 1010 / 1125 — ô tác phẩm 'ZONE <CĂN CỨ>' của PSD */
  max-width: 646px;              /* 720 × 0,897778 */
  /* .nuvi-topbar là `position: fixed; height: 68px` — KHÔNG chiếm chỗ trong dòng, nên khối phải
     tự chừa clearance, cùng công thức `calc(68px + …cqw)` đang dùng ở profile.css/Chat/Index.
     Phần cqw = (452-294)/11,25 + 109/11,25: badge Căn cứ nhô LÊN TRÊN mép ô ảnh 109px thiết kế,
     nên clearance phải tính từ badge chứ không từ ô ảnh — giữ đúng khoảng hở badge↔topbar của
     thiết kế (158px ⇒ 52,7px CSS ở khổ 375). Đặt margin ở ĐÂY chứ không ở .gd-stage: wrapper
     không có padding/border nên margin của con sẽ collapse xuyên qua, khó đọc và dễ vỡ. */
  margin: calc(68px + 24.0cqw) auto 0;   /* (564 - 294) / 11,25 — mép trên ô ảnh thật */
}

.gd-frame__img {
  position: absolute;
  left: -7.1287%;               /* (-11 - 61) / 1010 */
  top: -15.2239%;               /* (411 - 564) / 1005 */
  width: 113.4653%;             /* 1146 / 1010 */
  max-width: none;
  height: auto;
  pointer-events: none;         /* cú chạm rơi xuống ô ảnh bên dưới */
}

.gd-zone-badge {
  position: absolute;
  left: 30.6931%;               /* (371 - 61) / 1010 */
  top: -11.1443%;               /* (452 - 564) / 1005 */
  width: 37.9208%;              /* 383 / 1010 */
  max-width: none;
  height: auto;
  pointer-events: none;
}

/* Nút Back — PSD `BACK ICON` (60, 323) 96×96. Neo vào `.gd-frame` chứ không vào luồng: cùng
   cách `.gd-zone-badge` đang nhô lên trên mép ô ảnh, nên không đụng tới margin-top đã canh theo
   top bar, và không có margin nào collapse xuyên qua `<main>`.
   `top` âm tính theo CHIỀU CAO khối chứa (= chiều cao ô ảnh, 1005), `left` theo bề rộng (1010). */
.gd-back {
  position: absolute;
  left: 0.2475%;                /* (60 - 57,5) / 1010 — mép trái `.gd-frame` ở 57,5 trong 1125 */
  top: -23.9801%;               /* (323 - 564) / 1005 */
  width: 9.5050%;               /* 96 / 1010 */
  aspect-ratio: 1;
  display: block;
  z-index: 2;
}
.gd-back img {
  width: 100%;
  height: 100%;
  display: block;
}
/* Phản hồi cú chạm — chép `.result-media__expand:active span` của portal.css, không chế kiểu mới. */
.gd-back:active img { transform: scale(.9); }
/* Vùng CHẠM tách khỏi kích thước NHÌN THẤY (32×32 ở khổ 375) — docs/CONVENTIONS.md mục
   "Tỉ lệ Figma vs sàn 44px". Không cha nào của `.gd-back` có `overflow:hidden` nên vùng này không
   bị cắt; `.gd-stage` có, nhưng nó là anh em chứ không phải cha.
   48px chứ không đúng 44px: hộp hit-test kết thúc TRỪ 1px cuối (nửa mở, giống mọi hộp CSS khác)
   — QC 11/09 quét đúng bán kính 22px (= 44/2) quanh tâm và trúng cạnh phải/dưới đúng lúc hộp hết,
   nên rơi ra ngoài (P0-01). Nới thêm 4px dư mỗi phía để 44px luôn nằm THỰC SỰ bên trong, không
   nằm đúng biên. */
.gd-back::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 48px;
  height: 48px;
  transform: translate(-50%, -50%);
}

.gd-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 1010 / 1005;    /* ô ảnh đúng số PSD, không làm tròn về 1/1 */
  container-type: inline-size;
  overflow: hidden;
  /* iOS: nhấn giữ không bật menu "Lưu ảnh" — cùng lý do `.result2-stage` (2026-10-07). */
  -webkit-touch-callout: none;
  /* Vùng cắt ảnh = đúng `Rectangle 1082` của PSD (layer ảnh clip vào nó), KHÔNG phải cả ô
     'ZONE <CĂN CỨ>' 61..1071: hình chữ nhật 63..1058 × 564..1569, bo 88 ở cả 4 góc.
     Bản cũ `border-radius: 16px` trên cả ô 1010 để lộ ảnh ra ngoài khung viền ở 2 chỗ (đo PSD
     `shape gallery chi tiết`, alpha>128): (1) mực viền bên phải chỉ tới x=1068 mà ô ảnh tới 1071;
     (2) phần vát góc của viền bắt đầu cách góc ô 20–26 theo đường chéo, bo 16px (≈48 thiết kế)
     chỉ lùi 14 ⇒ 4 góc ảnh thò ra (user báo 15/09). Bo 88 lùi 25,8 và nằm gọn dưới mực ở cả 4
     góc — đã dò từng hàng biên dạng góc TL/BR.
     inset: phải (1071-1058)/1010, trái (63-61)/1010. Bán kính % trong `round` quy theo hộp của
     chính phần tử nên tách 2 trục: 88/1010 ngang, 88/1005 dọc. Cùng số cho cả 4 PSD Căn cứ. */
  clip-path: inset(0 1.2871% 0 0.1980% round 8.7129% / 8.7562%);
}

/* ── KHUNG 9:16 CHO VIDEO (acceptance 2026-09-21 QĐ-10, D1–D4) ─────────────────────────────
   Chỉ Video CÓ file. Ảnh và Nhạc giữ nguyên khung vuông 1010/1005 ở trên (D7).

   Khung viền: `gal-art-frame-{token}-video.webp` — khung vuông KÉO DÀI dọc bởi
   tools/figma-assets/build-gallery-video-frames.py: góc vát, khấc và cụm 3 chấm giữ nguyên pixel,
   chỉ hai dải cạnh trơn được kéo ⇒ hoạ tiết không méo (D2). Ảnh 1146×2200, ô 9:16 nằm ở
   x 72..1082 (như bản vuông), y 153..1948,6 ⇒ left/width GIỮ số cũ, chỉ `top` quy lại theo chiều
   cao mới: 153 / 1795,6.

   Bề rộng: như bản vuông, nhưng co lại khi màn thấp để CẢ khung nằm giữa top bar và nav đáy (D3)
   — sàn 70% viewport cho màn rất thấp (được cuộn). Ô cách top bar 12cqw thay vì 24cqw: bản vuông
   chừa 24cqw cho badge + nút Back, ở đây badge vẫn nhô 11,09% bề rộng khung như cũ (vẫn dưới top
   bar) còn nút Back GIỮ NGUYÊN vị trí trên màn (68px + 2,578cqw) — nó nằm lệch trái, không đụng
   badge. `cqw` ở đây là của `<main>` (container), `.gd-frame` không khai container. */
.gd-frame--video {
  width: max(70%, min(89.7778%, calc((100svh - 68px - 12cqw - var(--nuvi-nav-clearance, 0px) - 4px) * 0.5625)));
  margin-top: calc(68px + 12cqw);
}
.gd-frame--video .gd-frame__img {
  top: -8.5208%;                 /* −153 / 1795,6 */
}
.gd-frame--video .gd-zone-badge {
  top: -6.2375%;                 /* −112 / 1795,6 — cùng khoảng nhô như bản vuông (−112/1005) */
}
.gd-frame--video .gd-back {
  top: -9.4220cqw;               /* (2,578 − 12) cqw: giữ đúng chỗ trên màn như bản vuông */
}
.gd-stage--video {
  aspect-ratio: 9 / 16;
  /* Cùng vùng cắt với bản vuông; bán kính dọc quy theo chiều cao mới: 88 / 1795,6. */
  clip-path: inset(0 1.2871% 0 0.1980% round 8.7129% / 4.9009%);
}
.gd-stage--video[data-fit="contain"] .gd-bg__media {
  object-fit: contain;           /* video ngang cũ (E10): hiện trọn, không lớp chữ */
}

/* ── Nền: ảnh/video tác phẩm tràn viền ──────────────────────────────────────────────── */
.gd-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Nền lót khi ảnh chưa tải xong, hỏng, hoặc không có (Nhạc) — không để lộ mảng trắng. */
  background: linear-gradient(180deg, #4A2A63 0%, #37206A 45%, #46296F 100%);
  display: flex;
  align-items: center;
  justify-content: center;
}
.gd-bg__media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;             /* giữ tỉ lệ, cắt giữa — kho có cả ảnh 1:1, 9:16 và 3:4 */
  object-position: center;
}
/* `.gd-bg__audio-label` ("Tác phẩm âm thanh" trên nền tím) ĐÃ GỠ 2026-09-18 — tác phẩm Nhạc nay
   có nền hoạt cảnh của Căn cứ + lớp chữ `.ra-*` dùng chung với /Result (acceptance 2026-09-18). */

/* ══ TRÌNH PHÁT (lô 2026-09-20) ═══════════════════════════════════════════════════════════
   Acceptance docs/acceptance/2026-09-20-gallery-video-audio.md — QĐ-1, B1–B6, C1–C3.

   ⚠️ GIẢ ĐỊNH CỦA DEV, y như `.result2-play` bên portal.css: bộ design vẽ trang này như một
   TẤM POSTER TĨNH, không có nút phát nào. Cỡ nút lấy nguyên từ asset designer đã vẽ
   (result-btn-play.png) và từ khuôn `.result2-play`; vị trí thì do dev đặt và cần design duyệt.
   Ba phương án vị trí đã dựng thử và chụp ảnh đối chiếu ở ~/Desktop/gallery-av-ux-review. */

.gd-play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* 22cqw chứ không phải 16cqw của `.result2-play`: ô ở đây nhỏ hơn ô bên /Result (khung viền
     ăn mất mép), nên cùng một số cqw ra một nút nhỏ hơn về mặt px thật. 22 là số cho ra đường
     kính NHÌN THẤY xấp xỉ bằng nhau ở hai trang. Sàn 44px là luật chạm (B1) — ở 320px thì
     22cqw chỉ còn ~63px nên sàn chưa chạm tới, nhưng nó phải có mặt cho khổ hẹp hơn nữa. */
  width: 22cqw;
  min-width: 44px;
  aspect-ratio: 1;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  /* Trên khung viền (`.gd-frame__img`, không có z-index nhưng đứng SAU trong DOM nên vẽ đè).
     Nút phải nằm trên nó, nếu không quầng sáng của viền phủ lên nút. */
  z-index: 3;
  transition: opacity 0.22s ease;
}
.gd-play img {
  width: 100%;
  height: 100%;
  display: block;
}
/* Phản hồi cú chạm — chép `.gd-back:active img`, không chế kiểu mới. */
.gd-play:active img { transform: scale(0.92); }

/* Đang phát thì nút BIẾN MẤT khỏi cả mắt lẫn tay (B3). `pointer-events:none` là phần bắt buộc:
   chỉ để opacity 0 thì còn một nút vô hình 22cqw nằm giữa tác phẩm, nuốt mọi cú chạm vào đó. */
.gd-stage[data-playing="true"] .gd-play {
  opacity: 0;
  pointer-events: none;
}

/* Lớp chữ mờ đi khi đang phát — B3/C2. Dùng chung cho `.gd-type` (Ảnh/Video) và `.ra-overlay`
   (Nhạc): hai khối khác nhau, cùng một luật, nên một class chứ không hai.
   `visibility` KHÔNG đổi theo: chữ vẫn là nội dung thật của trang, trình đọc màn hình phải
   đọc được nó kể cả lúc mắt không thấy. */
.gd-fade {
  transition: opacity 0.5s ease;
}
.gd-stage[data-playing="true"] .gd-fade {
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .gd-play, .gd-fade, .gd-bar { transition: none; }
}

/* Thanh tiến trình. Video: neo ĐÁY ô, trong mép trong của khung viền. Nhạc: `.gd-bar--below`.
   Màu phần đã phát = token Căn cứ (B1). Hex dự phòng = đúng giá trị @theme của Styles/portal.css:
   Tailwind chỉ xuất biến `--color-zone-*` khi có class dùng nó, và file này không được quét. */
.gd-bar {
  --gd-bar-zone: var(--color-zone-sieu-anh-hung, #2F6BFF);
  position: absolute;
  left: 8cqw;
  right: 8cqw;
  bottom: 5cqw;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 2.6cqw;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.gd-bar--mong-mo { --gd-bar-zone: var(--color-zone-mong-mo, #E5308A); }
.gd-bar--tham-hiem { --gd-bar-zone: var(--color-zone-tham-hiem, #21C17A); }
.gd-bar--sang-tao { --gd-bar-zone: var(--color-zone-sang-tao, #8B5CF6); }
/* Nhạc (QĐ-4, 2026-09-22): thanh ra NGOÀI khung vuông, ngay dưới — lớp chữ hiện suốt bài chiếm
   dải đáy khung (khối ý tưởng 74–97%), để thanh trong khung là đè chữ. Nằm SAU `.gd-frame` trong
   luồng chứ không trong nó: `top:%` của `.gd-frame__img`/badge/Back quy theo CHIỀU CAO
   `.gd-frame`, thêm một hàng vào đó là cả khung viền trượt. Cùng bề rộng/căn giữa với `.gd-frame`.
   Chỗ đứng giữ sẵn cả khi thanh còn ẩn (opacity) — hiện ra không xô dải "Ý tưởng khác". */
.gd-bar--below {
  position: relative;
  left: auto;
  right: auto;
  bottom: auto;
  width: 89.7778%;
  max-width: 646px;
  height: max(7cqw, 44px);
  margin: max(10px, 2.6cqw) auto 0;
}
/* Hiện từ cú chạm phát đầu tiên và ở lại cho tới khi bài về đầu — xem getter `started`. */
.gd-bar[data-shown="true"] {
  opacity: 1;
  pointer-events: auto;
}
/* Bố cục B của Video nằm SÁT MÉP khung (QĐ-5 mới, 21/09) — riêng trang này có hai thứ chiếm đúng
   hai mép đó mà /Result và video.mp4 không có, nên Gallery lệch khỏi bộ số chung ở ĐÚNG hai chỗ
   (user chốt 21/09, D5b):
   · mép dưới: `.gd-bar` — cột box neo đáy ngay TRÊN thanh. Chiều cao thanh = nút dừng
     max(7cqw, 44px); khe = DescGap 2.2222cqw. `height:auto` trả quyền cho cặp top/bottom.
   · mép trên: `.gd-zone-badge` thò vào khung 43/1080 — header hạ thêm 30px (2.7778cqw) so với
     PersistentHeaderShift −41.5741cqw, logo về y 70 thay vì 40. */
.gd-stage .rv-descs {
  height: auto;
  bottom: calc(5cqw + max(7cqw, 44px) + 2.2222cqw);
}
.gd-stage .rv-head {
  transform: translateY(-38.7963cqw);
}
.gd-bar__toggle {
  flex: none;
  width: 7cqw;
  min-width: 44px;
  aspect-ratio: 1;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  /* Sàn chạm 44px làm nút to hơn hẳn phần nhìn thấy — dồn về giữa bằng padding trong chứ không
     phóng to icon. Cùng cách `.gd-back` tách vùng chạm khỏi kích thước nhìn thấy. */
  display: flex;
  align-items: center;
  justify-content: center;
}
.gd-bar__toggle svg {
  width: 7cqw;
  min-width: 18px;
  height: auto;
  aspect-ratio: 1;
  display: block;
  fill: #ffffff;
  /* Icon trắng nằm trên chính tác phẩm — nền có thể trắng. Bóng đổ là thứ giữ nó đọc được,
     KHÔNG phải màu (không có màu nào an toàn trên mọi tấm tranh). */
  filter: drop-shadow(0 0.3cqw 0.8cqw rgba(0, 0, 0, 0.75));
}
/* ── Thanh KẸO (acceptance 2026-09-22 QĐ-5, concept C user chốt; mockup
   ~/Desktop/audio-overlay-progress-bar-review/concepts/zoom-C.png) ─────────────────────────
   Ống bo tròn viền trắng 2px, lòng trắng mờ; phần đã phát đổ màu Căn cứ có sọc chéo (chỉ trôi
   khi đang phát); viên tròn trắng ở mép phải phần đã phát.

   `.gd-bar__track` là VÙNG CHẠM của thanh trượt (S4: cao ≥ 44px, `role=slider`), `.gd-bar__rail`
   là phần NHÌN THẤY. Tách hai lớp vì ống mảnh 14px mà làm vùng chạm là bé chạm hụt. Padding
   ngang 12px để viên kẹo ở hai đầu vẫn có đủ 44px vùng chạm quanh tâm (cộng khe `gap`).
   `touch-action:none` (S3): kéo ngang trên thanh không cuộn trang; vuốt ngoài thanh vẫn cuộn.
   Vị trí chạm quy theo hộp của `.gd-bar__rail` (xem seekFracAt) — fill và viên kẹo cùng tính
   % theo padding box của rail nên mép phải fill trùng tâm viên kẹo. */
.gd-bar__track {
  flex: 1;
  min-width: 0;
  position: relative;
  align-self: stretch;
  min-height: 44px;
  display: flex;
  align-items: center;
  padding: 0 12px;
  touch-action: none;
  cursor: pointer;
  outline: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}
.gd-bar__rail {
  position: relative;
  flex: 1;
  height: max(14px, 3.6cqw);
  border: 2px solid #ffffff;
  border-radius: 99px;
  /* Lòng TỐI, không phải trắng 25% như mockup C — user chốt 22/09 sau khi prototype đo ra lòng
     trắng chỉ đạt 1,05–2,3:1 với phần đã phát (B5 đòi ≥ 3:1) và gần như vô hình trên tranh Video
     sáng. Mực NuVi 85%: đạt ≥ 3:1 với cả 4 màu Căn cứ trên nền trắng tuyệt đối; 72% đã rớt. */
  background: rgba(11, 6, 32, 0.85);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}
.gd-bar__track:focus-visible .gd-bar__rail {
  box-shadow: 0 0 0 3px var(--gd-bar-zone), 0 1px 4px rgba(0, 0, 0, 0.45);
}
.gd-bar__fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0;
  border-radius: 99px;
  background:
    repeating-linear-gradient(-45deg, rgba(255, 255, 255, 0.28) 0 5px, transparent 5px 10px),
    var(--gd-bar-zone);
  background-size: 14px 14px, auto;
}
.gd-bar[data-playing="true"] .gd-bar__fill {
  animation: gd-bar-stripes 1s linear infinite;
}
@keyframes gd-bar-stripes {
  to { background-position: 14px 0, 0 0; }
}
.gd-bar__knob {
  position: absolute;
  top: 50%;
  left: 0;
  width: max(18px, 4.8cqw);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ffffff 0 40%, #f1ecff 70%);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5), 0 0 0 2px var(--gd-bar-zone);
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .gd-bar__fill { animation: none; }
}
/* Chip tối sau đồng hồ (user chốt 22/09): chữ trắng + bóng đổ chỉ đạt 1,1:1 trên tranh Video
   nền trắng — B5 đòi ≥ 4,5:1 trên pixel nền thật, và nền thật của chữ giờ là chip. */
.gd-bar__time {
  flex: none;
  min-width: 11cqw;
  white-space: nowrap;
  padding: 2px 7px;
  border-radius: 99px;
  background: rgba(11, 6, 32, 0.62);
  text-align: right;
  color: #ffffff;
  font-family: var(--font-sans);
  font-size: max(11px, 3.2cqw);
  font-variant-numeric: tabular-nums;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}

/* Khung NHẠC trong Gallery — CÙNG phép co mà mọi khối `.gd-*` dưới đây dùng (× 0,96024, dịch
   1,4851 / 2,1782cqw) để template nằm lọt trong viền trang trí `.gd-frame__img`. Không co thì
   logo NuVi góc phải trên bị viền cắt mất một phần chữ (chụp thấy 18/09).

   Làm bằng cách biến `.ra-overlay` thành CONTAINER rộng đúng 96,024% và đặt đúng chỗ, chứ không
   viết lại bộ số: mọi `cqw` của `.ra-*` bên trong tự tính theo hộp này, nên số trong portal.css
   (= hằng `SharePosterLayoutAudio`) vẫn là nguồn duy nhất — viết bộ thứ hai là thêm một bản chép
   tay phải giữ khớp, đúng thứ partial `_AudioFrameText` sinh ra để tránh. */
.gd-stage .ra-overlay {
  inset: auto;
  left: 1.4851cqw;
  top: 2.1782cqw;
  width: 96.024cqw;
  aspect-ratio: 1 / 1;
  container-type: inline-size;
}

/* ── Khối TYPE: chữ viền trắng, góc trái trên — PORT `.result2-type` ────────────────── */
/* Lề trái/phải của khối chữ và logo LỆCH KHỎI `.result2-*` của /Result từ 11/09, có chủ ý.
   Bản port dùng lề của /Result — trang đó không có khung viền nên chữ sát mép vẫn đọc được. Ở
   đây khung viền của bộ design mới đè lên mép ô ảnh: đo trên ảnh chụp thật, chữ "T" của
   "Tác Phẩm Của:" bị viền ăn mất nửa và logo "NuVi" mất hẳn chữ "i" cuối.
   Số mới đo thẳng trên poster thiết kế (node 498:42, 1011x1005): chữ bắt đầu ở 3,57% bề rộng,
   logo nằm trong 78,71%–94,29%. Khi /Result nhận bộ design có khung viền thì port ngược lại. */
.gd-type {
  position: absolute;
  left: 4.3496cqw;              /* 2.9831 × 0,96024 + 1,4851 */
  top: 5.6033cqw;               /* 3.5669 × 0,96024 + 2,1782 */
  width: 35.1448cqw;            /* 36.6 × 0,96024 — lô 2026-09-16, cột trái ≤ 40% */
  height: 24.7498cqw;           /* 25.7746 × 0,96024 */
  /* Quầng TRẮNG — PORT nguyên portal.css `.result2-type` (2026-09-10), xem chú thích ở đó. */
  filter: drop-shadow(0 0 0.9335cqw rgba(255, 255, 255, 0.79));
  container-type: inline-size;
  pointer-events: none;
}
.gd-type > * {
  position: absolute;
  margin: 0;
  color: var(--r2-type);
  font-family: var(--font-tektur);
  letter-spacing: -0.05em;
  white-space: nowrap;
}
.gd-type__byline,
.gd-type__name,
.gd-type__title {
  -webkit-text-stroke-width: 1.5179cqw;   /* = .result2-type__* — 6px trên TYPE 36.6% (16/09) */
  -webkit-text-stroke-color: #ffffff;
  paint-order: stroke fill;
}
.gd-type__age span {
  -webkit-text-stroke-width: 4.4659cqw;
  -webkit-text-stroke-color: #ffffff;
  paint-order: stroke fill;
}
.gd-type__byline {
  left: 0.7281cqw;
  top: 0cqw;
  font-weight: 500;
  font-size: 7.7711cqw;
  line-height: 7.7711cqw;
}
.gd-type__name {
  left: 0.6959cqw;
  top: 9.6260cqw;
  font-weight: 700;
  /* `--name-fit` do SERVER tính, cùng con số với `.result2-type__name` và ảnh poster (lô
     2026-09-16, B4) — thay bậc `--s1..s4` theo số ký tự.
     KHÔNG có sàn 11px (khác mục 2 ở đầu file) — user chốt 16/09 "giữ đủ tên, không bị cắt" thắng
     sàn đọc được: có sàn thì ở khổ ~390 tên ≥ ~20 ký tự chạm sàn rồi bị "…". Trùng /Result, vốn
     không có sàn. `gallery.mjs` G5 miễn trừ node này. */
  font-size: calc(13.4559cqw * var(--name-fit, 1));
  line-height: calc(13.4559cqw * var(--name-fit, 1));
  max-width: 48.5498cqw;         /* chặn bởi mép trái chip tuổi, không phải mép khung */
  overflow: hidden;
  text-overflow: ellipsis;
}
.gd-type__title {
  left: 0cqw;
  top: 23.0885cqw;
  width: calc(100cqw - 0.05em);   /* = .result2-type__title — xem chú thích ở portal.css (16/09) */
  font-weight: 700;
  /* IN HOA — user chốt 2026-09-09. `text-transform` chứ không ToUpper ở server: dữ liệu gốc
     trong Creation.Title phải giữ nguyên chữ bé đặt (A11-4), đây chỉ là cách hiển thị.
     Bản poster (SharePosterComposer.V2) in hoa bằng ToUpperInvariant để ra đúng cùng chuỗi. */
  text-transform: uppercase;
  /* CO CHO VỪA 2 DÒNG — hệ số do `fitTitleText()` đo trên trình duyệt rồi ghi vào
     `--title-fit`, y hệt `.result2-type__title` bên /Result (QC 09/09 P0-01). */
  font-size: calc(15.18cqw * var(--title-fit, 1));     /* = .result2-type__title, lô 2026-09-16 */
  line-height: calc(18.216cqw * var(--title-fit, 1));
  color: var(--r2-title);
  white-space: normal;
  /* CHẶN TRÀN NGANG cho chuỗi liền không khoảng trắng — `.result2-type__title` có dòng này từ
     đầu, bản Gallery thì thiếu. Thiếu nó thì một từ dài không ngắt được: khối chỉ cao 1 dòng
     nên phép đo "vừa 2 dòng" nói VỪA, không co gì cả, và chữ tràn ngang khỏi 100.1728cqw. */
  overflow-wrap: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* LƯỚI DỰ PHÒNG khi JS chưa đo xong — bậc thô theo độ dài, đặt `--title-fit` chứ không đặt
   `font-size` để hệ số đo thật (inline style) vẫn thắng. Xem chú thích dài ở
   `.result2-type__title--s*` trong NuVi.Portal/Styles/portal.css. */
.gd-type__title--s1 { --title-fit: 0.86; }   /* ≥ 18 ký tự */
.gd-type__title--s2 { --title-fit: 0.74; }   /* ≥ 24 ký tự */
.gd-type__title--s3 { --title-fit: 0.62; }   /* ≥ 30 ký tự */
.gd-type__age {
  left: 52.2457cqw;
  top: 13.3306cqw;
  width: 33.9882cqw;
  height: 9.4903cqw;
  container-type: inline-size;
}
.gd-type__age img {
  position: absolute;
  left: 0;
  top: 0;
  width: 22.8615cqw;
  aspect-ratio: 1;
  display: block;
}
.gd-type__age span {
  position: absolute;
  left: 26.2939cqw;
  top: 0;
  font-family: var(--font-tektur);
  font-weight: 600;
  font-size: max(11px, 25.1557cqw);   /* sàn 11px — xem chú thích ở .gd-type__name */
  line-height: max(11px, 25.1557cqw);
  letter-spacing: -0.05em;
  color: var(--r2-type);
  white-space: nowrap;
}

/* Tuổi xuống hàng riêng / không có tuổi — PORT nguyên `.result2-type--age-row` (lô 2026-09-16). Đặt SAU
   các luật gốc để đọc theo thứ tự cascade. */
.gd-type--age-row .gd-type__name,
.gd-type--no-age .gd-type__name { max-width: 99.3041cqw; }
.gd-type--age-row .gd-type__age { left: 0.6959cqw; top: 23.5cqw; }
.gd-type--age-row .gd-type__title { top: 33.9903cqw; }

/* ── Logo NuVi — PORT `.result2-logo` ────────────────────────────────────────────────── */
.gd-logo {
  position: absolute;
  left: 78.7487cqw;             /* 80.4630 × 0,96024 + 1,4851 */
  top: 4.8456cqw;               /* 2.7778 × 0,96024 + 2,1782 */
  width: 15.8262cqw;            /* 16.4815 × 0,96024 */
  display: block;
  pointer-events: none;
}
/* Bóng logo — PORT `.result2-logo-shadow` (portal.css, đọc chú thích ở đó), mọi độ dài bóng nhân
   cùng hệ số 0,96024 với logo. `.gd-stage` có clip-path + container-type ⇒ là stacking context
   CHỨA `.gd-bg`, nên multiply thấy ảnh nền. top = 4,8456 + 0,7407 × 0,96024 = 5,5569cqw;
   cao = 5,3704 × 0,96024 = 5,1569cqw. */
.gd-logo-shadow {
  position: absolute;
  left: calc(78.7487cqw - var(--r2-logo-shadow-spread) * 0.96024);
  top: calc(5.5569cqw - var(--r2-logo-shadow-spread) * 0.96024);
  width: calc(15.8262cqw + 2 * var(--r2-logo-shadow-spread) * 0.96024);
  height: calc(5.1569cqw + 2 * var(--r2-logo-shadow-spread) * 0.96024);
  filter: blur(calc(var(--r2-logo-shadow-blur) * 0.96024));
  opacity: var(--r2-logo-shadow-a);
  mix-blend-mode: multiply;
  pointer-events: none;
}
/* Ruột (`.logo-shadow::before`) nằm ở portal.css — layout nạp cả hai file. Ở đây chỉ khai độ
   giãn đã nhân hệ số của khung Gallery. */
.gd-logo-shadow { --logo-shadow-s: calc(var(--r2-logo-shadow-spread) * 0.96024); }

/* ── Chữ ý tưởng: ruột tím, viền TRẮNG — PORT nguyên portal.css ─────────────────────── */
.gd-idea__text,
.gd-info__text,
.gd-idea__name,
.gd-info__name {
  font-family: var(--font-tektur);
  font-weight: 500;
  letter-spacing: -0.05em;
  color: var(--r2-idea);
  -webkit-text-stroke-color: #ffffff;
  paint-order: stroke fill;
  /* Quầng TRẮNG (PSD `OuterGlow`), không phải bóng đổ có màu — PORT nguyên portal.css
     2026-09-10, xem chú thích ở đó. */
  text-shadow: 0 0 var(--r2-idea-glow-blur) rgb(255 255 255 / var(--r2-idea-glow-a));
  margin: 0;
}
.gd-idea__text,
.gd-idea__name,
.gd-info__name { color: var(--r2-idea-name); }
.gd-idea__text,
.gd-idea__name { -webkit-text-stroke-width: 0.9218cqw; }
.gd-info__text,
.gd-info__name { -webkit-text-stroke-width: 1.3183cqw; }
.gd-idea__name,
.gd-info__name {
  font-weight: 700;
  display: block;
  overflow-wrap: break-word;     /* cùng lý do CHẶN TRÀN như .gd-idea__text/.gd-info__text */
}
.gd-idea__text,
.gd-info__text {
  display: -webkit-box;
  /* 3 -> 7, khớp SharePosterLayoutV2.IdeaTextMaxLines. Từ 2026-09-09 clamp chỉ là LƯỚI CUỐI,
     việc trình bày do fitIdeaText() lo — xem chú thích cùng tên trong portal.css. */
  -webkit-line-clamp: var(--idea-lines, 7);
  -webkit-box-orient: vertical;
  overflow: hidden;                /* co được trong flex column của __body — xem chú thích ở đó */
  /* QC 2026-09-11 P0-01: clamp chạm sàn --idea-fit vẫn có thể cắt giữa từ (câu quá dài để
     fitIdeaText() nhét vừa ngân sách) — không có dấu hiệu còn chữ, đọc như lỗi hiển thị. */
  text-overflow: ellipsis;
  /* ⚠️ THIẾU cho tới 2026-09-18 — `.result2-idea__text`/`.result2-info__text` của portal.css có
     dòng này từ lô 09/09, bản port sang Gallery thì rơi mất. Hệ quả đo được 18/09 trên prototype:
     một chuỗi liền không khoảng trắng (dán nhầm URL, hoặc từ ghép dài) tràn NGANG khỏi cột chữ
     tới 35px ở vw 320 — chữ trắng đè lên tranh, đúng lớp lỗi mà `.gd-idea__pill span` đã phải vá
     hôm 10/09. `-webkit-line-clamp` không cứu được vì khối chỉ cao 1 dòng nên phép đo "vừa
     ngân sách" nói VỪA và vòng co chữ không chạy. */
  overflow-wrap: break-word;
}

/* ── Không có ảnh gốc: khối ý tưởng RỘNG — PORT `.result2-idea` ─────────────────────── */
.gd-idea {
  position: absolute;
  left: 4.6859cqw;              /* 3.3333 × 0,96024 + 1,4851 */
  /* ── NEO ĐÁY, CÓ SÀN (2026-09-15) — PORT `.result2-idea` ──
     ⚠️ ĐỪNG viết ngoặc nhọn đóng trong chú thích của file này: bộ tách khối CSS của
     SharePosterLayoutGuardTests cắt luật tại ngoặc đóng ĐẦU TIÊN, nên một cặp ngoặc nằm trong
     comment làm nó đọc nhầm sang nửa khối và test đỏ với thông báo rất khó lần ra.

     Bản 11/09 dùng `top` + `height` CỨNG, cộng neo đáy 0 và dồn-về-cuối trên khối con `__body`.
     Cơ chế đó chặn được tràn thật, nhưng trả giá ở hai chỗ, đo được 15/09: cùng câu 150 ký tự nó chỉ hiện 2/7 dòng ở
     vw 320 và 3/6 ở 375–402 (NUỐT mất câu), và với câu ngắn thì chữ tựa đáy hộp cứng nên hở tới
     7,28cqw dưới pill. Nay khối cao theo nội dung, tựa đáy, mọc LÊN khi dài — vừa không tràn vừa
     không nuốt, và chữ bám ngay dưới pill ở mọi độ dài.

     Đáy giữ nguyên 94.7345cqw. ⚠️ `.gd-stage` khai tỉ lệ khung 1010/1005 nên mép đáy sân
     khấu ở 100 × 1005/1010 = 99.5050cqw, KHÔNG phải 100 — xem `.gd-stack`. */
  bottom: 4.7706cqw;            /* 99.5050 − 94.7345 */
  min-height: 21.0719cqw;       /* 94.7345 − 73.6625 — giữ đỉnh ở 73.6625cqw khi câu ngắn */
  /* NGÂN SÁCH, không phải cơ chế xén — `fitIdeaText()` đọc biến này để tính SỐ DÒNG nhét vừa,
     rồi giao cho `-webkit-line-clamp`. Khai bằng biến CSS chứ KHÔNG bằng max-height là có lý do:
     max-height chặn chiều cao HỘP trong khi chữ vẫn vẽ ra ngoài, tức hộp báo một đằng mực nằm một
     nẻo — và mọi gate đo theo rect của hộp sẽ mù đúng chỗ đó. Không có max-height thì hộp luôn cao
     đúng bằng nội dung, rect không bao giờ nói dối.
     Lưới cuối khi JS không chạy là `-webkit-line-clamp` mặc định 7, cắt theo DÒNG. */
  --idea-cap: 57.6216cqw;
  /* 60.2757 -> 44.0 (2026-09-09): mọi thông tin về NỬA TRÁI khung — team Cloud Ace giữ nửa
     phải. Trang này PORT template của /Result nên phải đi cùng bộ số với `.result2-idea`
     trong portal.css và SharePosterLayoutV2.IdeaWidth; lệch một bên là cùng một tác phẩm
     hiện khác nhau ở hai trang. */
  width: 34.8567cqw;            /* 36.3 × 0,96024 — lô 2026-09-16 (trước: 44.0) */
  container-type: inline-size;
  pointer-events: none;
}
.gd-idea__pill {
  display: block;
  position: relative;
  width: 52.5364cqw;
  aspect-ratio: 342 / 57;
  container-type: inline-size;
}
.gd-idea__pill img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.gd-idea__pill span {
  position: absolute;
  left: 7.7245cqw;
  /* `right` + `overflow` THÊM 2026-09-10: bản cũ chỉ có `left`, nên span (absolute, nowrap) là
     hộp shrink-to-fit và chữ bị sàn 10px giữ lại đã TRÀN 25,2px ra ngoài ảnh pill, đè chữ trắng
     lên tranh — đo trên 8/8 token dev. `/Result` giấu được lỗi này vì có sẵn hai thứ đó nên nó
     CẮT thay vì tràn; cùng một gốc rễ, hai triệu chứng. Nay soi gương `.result2-idea__pill span`. */
  right: 7.7245cqw;
  top: 3.5909cqw;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-tektur);
  font-weight: 700;
  /* Sàn 10px đã gỡ — xem chú thích ở `.result2-idea__pill span` trong Styles/portal.css. */
  font-size: calc(7.6830cqw * var(--pill-fit, 1));
  line-height: calc(7.6830cqw * var(--pill-fit, 1));
  letter-spacing: -0.05em;
  text-transform: uppercase;
  color: #ffffff;
  white-space: nowrap;
}
.gd-idea__body {
  /* `absolute` + `bottom: 0` + `flex-end` (bản 11/09) -> trong luồng, top-align (15/09): khối cha
     nay cao theo nội dung nên con absolute không còn đóng góp chiều cao, và `flex-end` chính là
     thứ tạo lỗ 7,28cqw dưới pill khi câu ngắn. Xem chú thích ở `.gd-idea`.
     `margin-top` = 12.3168 − 8.7561 (chiều cao pill) = 3.5607, giữ nguyên toạ độ Figma. */
  position: relative;
  margin-left: 0.0473cqw;
  margin-top: 3.5607cqw;
  width: 99.7368cqw;            /* = .result2-idea__body, lô 2026-09-16 */
}
.gd-idea__text,
.gd-idea__name {
  /* --idea-fit: hệ số co do fitIdeaText() đặt trên khối cha, mặc định 1.
     ⚠️ 2026-09-18 — `var(--idea-fit)` chuyển RA NGOÀI `max()`. Bản cũ để nó bên trong
     (`max(11px, calc(5.2635cqw * fit))`), và điều đó làm vòng co chữ VÔ HIỆU trên mọi khổ điện
     thoại: `.gd-idea` chỉ rộng 34.8567cqw của sân khấu và tự khai `container-type`, nên
     5.2635cqw của con ra ~6,2px ở khổ 375 — sàn 11px luôn thắng, dò `--idea-fit` từ 1.0 xuống
     0.55 không đổi nổi một pixel. Đo 18/09 trên prototype: font = 11,00px ở CẢ 9 bề rộng
     320–500, với MỌI giá trị `--idea-fit`. Tức cơ chế co chữ chưa từng chạy, và câu dài chỉ còn
     đường bị `-webkit-line-clamp` cắt — đúng "…" mà team Cloud Ace báo trên iPhone SE.
     Nay sàn 11px là cỡ GỐC (trước khi co), hệ số co nhân lên cả nó, và SÀN TUYỆT ĐỐI là 8px.
     8px không phải số tròn cho đẹp. Ca chật nhất là Template 1 ở vw 320 — cỡ chữ LỚN NHẤT mà câu
     ở ĐÚNG trần 150 ký tự (LiveSessionLimits.PromptSummaryMaxLength) còn nằm trọn ngân sách
     `--idea-cap` đo được ở đó là 9,08px với câu thường và 8,34px với câu VIẾT HOA toàn bộ (Tektur
     hoa rộng hơn thường ~9%). Sàn phải nằm dưới con số nhỏ hơn, nên 8 chứ không phải 9.
     Sàn này CHỈ cắn ở ca biên: đo 18/09, câu 150 ký tự thường vẫn ra 11,00px ở Template 2 và
     10,64px ở Template 1 tại khổ 375 — vòng co chỉ hạ khi thật sự cần.
     User chốt 18/09: đủ nội dung thắng sàn đọc được, cùng hướng với `.gd-type__name` đã bỏ sàn
     11px hôm 16/09. */
  font-size: max(8px, calc(max(11px, 5.2635cqw) * var(--idea-fit, 1)));
  line-height: max(9.6px, calc(max(13.2px, 6.3162cqw) * var(--idea-fit, 1)));
}
/* "Xem thêm" nằm NGANG HÀNG pill, bên phải nó — dải trống duy nhất của khối này
   (pill rộng 52.5364cqw trong khối 100cqw). Xuống dòng là nhãn cao gấp đôi, đè dòng đầu. */
.gd-idea,
.gd-info {
  pointer-events: none;
}

/* ── Có ảnh gốc: ô phác thảo (pill) + khối ý tưởng HẸP — PORT `.result2-sketch`/`.result2-info` */
/* Cụm neo ĐÁY — port `.result2-stack` (2026-09-15). Trước đó `.gd-sketch` đứng cứng ở 38.6672cqw
   còn chữ ý tưởng đã tựa đáy sẵn, nên khoảng giữa hai khối phình/co theo độ dài câu.
   ⚠️ CỐ Ý KHÔNG đặt `container-type`: `cqw` của con phải tiếp tục quy theo khung `.gd-stage`. */
.gd-stack {
  position: absolute;
  left: 0;
  width: 100cqw;
  /* ⚠️ KHÔNG phải `100 − 94.7345`. `.gd-stage` khai `aspect-ratio: 1010 / 1005` (:611) — nó
     KHÔNG vuông như `.result2-stage`, nên mép đáy sân khấu nằm ở `100 × 1005/1010 = 99.5050cqw`,
     không phải 100cqw. Trừ từ 100 đẩy cả cụm lên 0,495cqw ≈ 2px so với đáy của `.gd-idea`, và
     lệch ĐỀU ở mọi bề rộng nên nhìn bằng mắt không ra. Đo trên prototype 15/09, 6 bề rộng.
     99.5050 − 94.7345 (đáy `.gd-idea`, = top 73.6625 + height 21.0719) = 4.7706. */
  bottom: 4.7706cqw;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.9205cqw;               /* SharePosterLayoutV2.SketchInfoGap 2.0 × 0,96024 */
  pointer-events: none;
}
.gd-sketch {
  position: relative;
  /* XẾP DỌC với .gd-info từ 2026-09-09 — port đúng thay đổi của `.result2-sketch`.
     ⚠️ 2026-09-15: 4.6859 -> 1.7718 — port việc căn lề theo MỰC thay vì theo hộp, xem chú thích
     dài ở `.result2-sketch` trong portal.css. */
  margin-left: 1.7718cqw;       /* SharePosterLayoutV2.SketchLeft 0.2986 × 0,96024 + 1,4851 */
  width: 24.9662cqw;            /* 26.0 × 0,96024 */
  aspect-ratio: 1;
  container-type: inline-size;
  pointer-events: none;
}
.gd-sketch__photo {
  position: absolute;
  left: 11.5753cqw;
  top: 14.1956cqw;      /* = SketchPillTop + SketchPillHeight — ảnh đứng DƯỚI pill, không đè */
  width: 84.2707cqw;
  height: 80.2579cqw;   /* đáy giữ nguyên 94.4535cqw như trước khi tách pill */
  object-fit: contain;
  display: block;
  background-color: #ffffff;  /* nền trắng lộ ra quanh ảnh khi contain hụt khung — theo design gốc */
  /* Màu và độ mờ theo Căn cứ — ba Căn cứ mới dùng mực đậm nên phải mờ hơn cùng một
     hình dạng bóng, xem `ZonePosterPalette.SketchShadowOpacity`. */
  box-shadow: 0 8px 21.5px color-mix(in srgb, var(--r2-sketch-shadow) calc(var(--r2-sketch-shadow-a) * 100%), transparent);
}
.gd-sketch__pill {
  display: block;
  position: absolute;
  left: 11.6719cqw;
  top: 0.3155cqw;
  width: 84.2271cqw;
  aspect-ratio: 267 / 44;
  container-type: inline-size;
}
.gd-sketch__pill img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.gd-sketch__pill span {
  position: absolute;
  left: 7.4963cqw;
  right: 7.4963cqw;             /* CHẶN TRÀN — cùng lý do như `.gd-idea__pill span` (tràn 29,3px) */
  top: 3.5086cqw;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-tektur);
  font-weight: 700;
  font-size: calc(7.6985cqw * var(--pill-fit, 1));
  line-height: calc(7.6985cqw * var(--pill-fit, 1));
  letter-spacing: -0.05em;
  text-transform: uppercase;
  color: #ffffff;
  white-space: nowrap;
}
.gd-info {
  position: relative;
  /* 2026-09-09: xuống DƯỚI ô phác thảo và về cột trái — port `.result2-info`.
     2026-09-15: mép đáy chuyển lên `.gd-stack` (cả cụm cùng treo từ đáy), nên khối này thôi khai
     `top`/`height` cố định và cao theo nội dung như `.result2-info`.
     max-height chặn khối trèo lên đội ô phác thảo vào khối TYPE. */
  margin-left: 4.6859cqw;       /* 3.3333 × 0,96024 + 1,4851 */
  width: 34.8567cqw;            /* 36.3 × 0,96024 — lô 2026-09-16 (trước: 44.0) */
  /* NGÂN SÁCH, không phải cơ chế xén — `fitIdeaText()` đọc biến này để tính SỐ DÒNG nhét vừa,
     rồi giao cho `-webkit-line-clamp`. Khai bằng biến CSS chứ KHÔNG bằng max-height là có lý do:
     max-height chặn chiều cao HỘP trong khi chữ vẫn vẽ ra ngoài, tức hộp báo một đằng mực nằm một
     nẻo — và mọi gate đo theo rect của hộp sẽ mù đúng chỗ đó. Không có max-height thì hộp luôn cao
     đúng bằng nội dung, rect không bao giờ nói dối.
     Lưới cuối khi JS không chạy là `-webkit-line-clamp` mặc định 7, cắt theo DÒNG. */
  --idea-cap: 30.2764cqw;
  /* ⚠️ GIỮ BỐN DÒNG DƯỚI. Chúng là thứ khiến phần chữ dôi dư tràn LÊN rồi bị xén, thay vì đẩy
     dòng tên bé XUỐNG dưới đáy khung. Bộ số cũ có được điều đó "miễn phí" nhờ hộp cao CỐ ĐỊNH:
     thẻ chữ là flex item trong hộp định chiều nên bị co, `-webkit-line-clamp` nuốt phần thừa, và
     tên luôn ghim ở đáy. Lô 15/09 đổi hộp sang cao-theo-nội-dung để cả cụm tựa đáy được — mất
     luôn cơ chế đó, và đo được trên prototype: ở vw 320 tên bé tụt 6,49cqw xuống dưới đáy thiết
     kế rồi bị `overflow: hidden` của sân khấu cắt (bản Gallery tràn thật 5px). Đó chính là P1-01
     của QC 07/09 tái phát. `justify-content: flex-end` + `overflow: hidden` trả lại cơ chế cũ mà
     không phải bỏ cụm neo đáy. Ở bề rộng ≥ 390 nội dung vào lọt trần nên bốn dòng này không làm
     gì cả — chúng chỉ là lưới cho dải hẹp. */
  /* ⚠️ Cắt theo DÒNG, không cắt theo PIXEL — xem chú thích cùng tên ở `.result2-info` trong
     portal.css. Tóm tắt: overflow ẩn cắt theo pixel nên nó chặt ngang thân chữ ở vw 320 (còn
     4,15px trên hộp em 11,8px), mà phép kiểm tràn khung không bắt được. Nay `fitIdeaText()` tính
     số dòng vừa khung rồi đặt `--idea-lines`, clamp đọc biến đó, nên mọi phép cắt rơi đúng ranh
     giới dòng. Mục G16 của gallery.mjs canh đúng điều này. */
  container-type: inline-size;
  pointer-events: none;
}
.gd-info__body {
  position: relative;
  width: 99.7367cqw;            /* = .result2-info__body, lô 2026-09-16 */
  /* `min-height: 0` + flex column: để THẺ CHỮ co được trong hộp đã chạm trần, cho `line-clamp`
     cắt gọn theo dòng thay vì hộp xén ngang thân chữ. Không có nó thì flex item giữ nguyên
     chiều cao nội dung và phép co ở trên thành vô nghĩa. */
}
.gd-info__text,
.gd-info__name {
  /* Xem chú thích --idea-fit (và sàn 8px của lô 18/09) ở .gd-idea__text. */
  font-size: max(8px, calc(max(11px, 5.2660cqw) * var(--idea-fit, 1)));
  line-height: max(9.6px, calc(max(13.2px, 6.3191cqw) * var(--idea-fit, 1)));
}
/* Nhãn "Xem thêm" và vùng chạm của nó đã GỠ 2026-09-09 cùng bản /Result — câu ý tưởng nay co
   nhỏ cho vừa khung thay vì bị cắt (fitIdeaText trong Views/Gallery/Detail.cshtml). */
/* ── Lớp đọc toàn văn ý tưởng — PORT `.result-idea-modal` nguyên khối (portal.css:1931+).
   Fixed theo viewport, px tuyệt đối — không phụ thuộc cqw của .gd-stage nên copy thẳng,
   không quy đổi lại. */
.gd-idea-modal {
  position: fixed;
  inset: 0;
  z-index: 95;
  background: rgba(6, 2, 20, .94);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 6vmin;
}
.gd-idea-modal[data-open="true"] { display: flex; }
.gd-idea-modal__panel {
  position: relative;
  width: min(92vw, 560px);
  max-height: 82vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 24px 20px 22px;
  border-radius: 18px;
  background: #2A1364;
  border: 2px solid #8C4BED;
  box-shadow: 0 0 24px rgba(140, 75, 237, .45);
}
.gd-idea-modal__title {
  display: block;
  margin-bottom: 12px;
  padding-right: 44px;
  color: #C9A6FF;
  font-family: var(--font-tektur);
  font-weight: 600;
  font-size: 16px;
  line-height: 1.2;
}
.gd-idea-modal__text {
  margin: 0;
  color: #faf3fd;
  font-family: var(--font-tektur);
  font-weight: 700;
  font-size: 17px;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
.gd-idea-modal__close {
  position: absolute;
  top: 10px;
  right: 10px;
  min-width: 44px;
  min-height: 44px;
  border: 0;
  border-radius: 999px;
  background: rgba(255,255,255,.14);
  color: #fff;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

/* Nút Chia sẻ ĐÃ GỠ 2026-09-11 (user chốt): bộ design mới không vẽ nó ở trang chi tiết.
   Đường đi tới trang chia sẻ vẫn còn — /Result/{token} — chỉ là không vào từ đây nữa.
   Kéo theo: mục E6 của docs/qc/2026-09-08-gallery-detail-result-style.md và phần đo nút này
   trong case G4 của tools/ui-check/regression/gallery.mjs đều phải sửa, không phải để đỏ. */

/* ---------- ý tưởng khác cùng zone ---------- */

.gd-related {
  width: 91.6444%;             /* 1031 / 1125 — nhóm 'box phụ' của PSD, không phải 1065 */
  /* Hở từ mép dưới ô ảnh tới chữ tiêu đề: PSD cho mực viền dưới kết thúc ở y=1574 và mực chữ bắt
     đầu ở y=1647 ⇒ 73px thiết kế = 24,3px CSS ở khổ 375. Bản trước để 1.4cqw ⇒ đo được 3,9px,
     tiêu đề gần như dính vào khung. */
  margin: 6.4178cqw auto 0;    /* (1641,2 - 1569) / 11,25 */
}

/* Figma cho tiêu đề cao 38px ⇒ 10.8px ở bề rộng 320px — dưới sàn 11px. */
.gd-related__heading {
  /* (1718 - 1690,8) / 11,25 = 2.4178cqw ra đúng khoảng cách HỘP DÒNG ↔ mép trên 3 ô, nhưng QC
     11/09 đo bằng MỰC (không phải hộp) thấy hở dư ~3px ở cả 4 viewport: line-height:1.2 nhỏ hơn
     leading tự nhiên của font Tektur nên mực chữ tràn XUỐNG dưới mép hộp dòng thay vì thụt vào,
     cộng với 13/444 lề trong sẵn có của khung 3 ô — hai khoản này không có trong số Figma. Trừ
     lùi đúng phần dư đo được (~3px @375) để mực-tới-mực khớp tiêu chí C3, không phải hộp-tới-hộp. */
  margin: 0 0 1.6231cqw;       /* 2.4178cqw − phần dư đo bằng mực, xem docs/qc/2026-09-11-gallery-detail-spacing-back-folder.md P0-02 */
  padding-left: 1.4549%;       /* (62 - 47) / 1031 */
  color: #ffffff;
  font-family: var(--font-tektur);
  font-size: max(11px, 3.378cqw);   /* 38 / 1125 */
  line-height: 1.2;
  font-weight: 700;
}

.gd-related__row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.0970%;                /* 1 / 1031 — PSD xếp ba ô cách nhau đúng 1px thiết kế */
}

.gd-rel {
  position: relative;
  display: block;
  aspect-ratio: 343 / 444;
  container-type: inline-size;
  text-decoration: none;
}

/* Khung ô liên quan có RUỘT ĐẶC màu (giống khung ảnh tác phẩm, khác khung thẻ ở trang lưới vốn
   đã được khoét trong suốt). Nên ảnh phải nằm TRÊN khung, không nằm dưới — để khung ở z-index
   cao hơn là nó bịt kín ảnh, và ba ô hiện ra ba khung xanh rỗng. Ảnh chụp bắt được. */
.gd-rel__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
}

/* Ảnh = `Rectangle 1080` của PSD (layer ảnh clip vào nó), trong khung 343x444 ở gốc (43, 1718):
   path 87,35..343,65 × 1827,26..2122,55 ⇒ 44,35..300,65 × 109,26..404,55, bo 11, stroke 3px
   CĂN GIỮA path, tô gradient dọc 3 điểm màu theo Căn cứ.
   Bản trước (15/09) đặt ảnh sát mép trong viền sáng của asset (22..323 × 90..427) — to hơn thiết
   kế và đè lên phần vát 2 góc đáy khung, nên 2 góc dưới ảnh thò ra ngoài khung (user báo 15/09).
   Thiết kế chừa một dải ruột khung màu đặc quanh ảnh.

   Hộp này là MÉP NGOÀI của stroke (path ± 1,5): 42,85..302,15 × 107,76..406,05. Stroke vẽ bằng
   `border` trong suốt + nền gradient `border-box`: con absolute `inset:0` bám padding box (bên
   trong border) và `overflow:hidden` cắt theo bán kính TRONG = 12,5 − 3 = 9,5, nên ảnh tự rơi
   đúng vào mép trong stroke — không cần phần tử phụ. */
.gd-rel__media {
  position: absolute;
  left: 12.4927%;              /* 42,85 / 343 */
  right: 11.9096%;             /* (343 - 302,15) / 343 */
  top: 24.2703%;               /* 107,76 / 444 */
  bottom: 8.5473%;             /* (444 - 406,05) / 444 */
  z-index: 1;
  display: block;
  overflow: hidden;
  border: 0.8746cqw solid transparent;   /* 3 / 343 */
  border-radius: 3.6443cqw;              /* (11 + 1,5) / 343 */
  background:
    linear-gradient(#0b1740, #0b1740) padding-box,
    linear-gradient(to top, var(--gd-rel-stroke-a), var(--gd-rel-stroke-b) 50%, var(--gd-rel-stroke-c)) border-box;
}

/* Màu stroke ô liên quan — `strokeStyleContent` của `Rectangle 1080` trong từng PSD "Chi tiết tác
   phẩm" (Angl 90 ⇒ điểm 0 ở ĐÁY). Mặc định = Siêu Anh Hùng, cùng luật rơi về Căn cứ mặc định
   của `themeToken` trong Detail.cshtml. */
.gd-related {
  --gd-rel-stroke-a: #63a3ff;
  --gd-rel-stroke-b: #b6dcff;
  --gd-rel-stroke-c: #72beff;
}
.gd-related--mong-mo {
  --gd-rel-stroke-a: #ff63bb;
  --gd-rel-stroke-b: #ffc6ea;
  --gd-rel-stroke-c: #ff63bb;
}
.gd-related--sang-tao {
  --gd-rel-stroke-a: #b763ff;
  --gd-rel-stroke-b: #ead3ff;
  --gd-rel-stroke-c: #b763ff;
}
.gd-related--tham-hiem {
  --gd-rel-stroke-a: #7dff63;
  --gd-rel-stroke-b: #d3ffda;
  --gd-rel-stroke-c: #7dff63;
}

/* absolute vì cùng lý do với .gd-art__img: chữ dự phòng đứng trước trong DOM và đã định vị, nên
   ảnh nằm trong luồng sẽ bị chính nó vẽ đè. */
/* Chip loại hình của ô liên quan (lô 2026-09-20, acceptance D2). Cùng ngôn ngữ với
   `.gal-card__type` của lưới, nhưng KHÔNG dùng chung khai báo: ô này có `border-radius` riêng
   (không có vát góc như thẻ lưới) nên hai số neo góc khác hẳn, và cqw ở đây quy theo hộp
   `.gd-rel` 343 chứ không theo thẻ 514. Gộp lại thành một class là ép hai hộp khác nhau dùng
   chung một bộ toạ độ — đúng cái bẫy mà `.gd-type` đã phải tách khỏi `.result2-type`. */
.gd-rel__type {
  position: absolute;
  left: 4.4cqw;
  top: 3.8cqw;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  padding: 0.8cqw 2.2cqw;
  border-radius: 99px;
  background: rgba(10, 16, 44, 0.82);
  border: 1px solid rgba(160, 190, 255, 0.32);
  color: #e8efff;
  font-family: var(--font-sans);
  /* Sàn 11px của G5. Ô liên quan hẹp hơn thẻ lưới nhiều nên sàn là thứ quyết định ở mọi khổ
     máy thật — con số cqw chỉ có tác dụng trên màn rất rộng. */
  font-size: max(11px, 3.2cqw);
  font-weight: 600;
  line-height: 1.25;
  white-space: nowrap;
  pointer-events: none;
}

.gd-rel__media img,
.gd-rel__media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.gd-rel__msg {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 6cqw;
  color: #cbd8ff;
  font-family: var(--font-sans);
  font-size: max(11px, 6cqw);
  line-height: 1.3;
  text-align: center;
}

/* Figma cho chữ cao 23px ⇒ 6.5px ở bề rộng 320px — dưới sàn ở CẢ BA bề rộng đo. */
/* Icon folder + tên xếp thành MỘT HÀNG NEO TRÁI, không canh giữa. PSD (ô 343 rộng): icon ở x=50,
   chữ ở x=92 — ba ô của bộ design có tên dài 205/186/205px mà icon vẫn đứng nguyên ở 50/50/53,
   tức khối này neo trái chứ không canh giữa (canh giữa thì ô giữa phải lệch sang phải ~10px). */
.gd-rel__title {
  position: absolute;
  left: 14.5773%;              /* 50 / 343 */
  right: 5.83%;                /* (343 - 323) / 343 — mép trong khung, như .gd-rel__media */
  top: 11%;                    /* 61.5 / 444 */
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 2.6239cqw;              /* 9 / 343 — chữ rơi đúng 14.5773 + 9.6210 + 2.6239 = 26.8222% */
  color: #ffffff;
  font-family: var(--font-tektur);
  font-size: max(11px, 6.706cqw);   /* 23 / 343 */
  line-height: 1.2;
  overflow: hidden;
}
/* `aspect-ratio` chứ không phải `height`: tên dài làm co cột chữ, icon PHẢI giữ nguyên tỉ lệ
   33:25 chứ không bị bóp — `flex: 0 0 auto` chặn cả việc flexbox rút bề rộng của nó. */
.gd-rel__folder {
  flex: 0 0 auto;
  width: 9.6210cqw;            /* 33 / 343 */
  aspect-ratio: 33 / 25;
  display: block;
}
/* `min-width: 0` là bắt buộc: mặc định `min-width:auto` của flex item khiến chữ nowrap ĐẨY RỘNG ô
   thay vì bị cắt, và ellipsis không bao giờ hiện. */
.gd-rel__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
