/* =========================================================================
   qucannh — 3D Artist
   Hướng thị giác: phòng trưng bày. Toàn bộ chữ dùng Helvetica, nền đen hoặc
   trắng tuyệt đối, hai màu nhấn tím và lime dùng rất tiết chế.
   ========================================================================= */

/* Thuộc tính hidden phải thắng mọi thứ. Trình duyệt có sẵn [hidden]{display:none}
   nhưng đó là stylesheet mặc định của NÓ, mà bất kỳ quy tắc nào của mình cũng
   thắng stylesheet mặc định. Nên .credits{display:grid} ở dưới file đã âm thầm
   vô hiệu hoá el.hidden = true trên khối Credits: JS đặt hidden, DevTools xác
   nhận hidden=true, mà phần tử vẫn cao 21px và không ai hiểu tại sao.
   Một dòng !important ở đây bịt luôn cả những chỗ sau này chưa viết. */
[hidden]{display:none!important}

/* ---------- Helvetica tự nhúng ----------
   Font thương mại, file nằm trong fonts/.
   Đang dùng: Helvetica bản gốc (họ "HV").
   Muốn đổi sang Helvetica Neue: đổi --display và --sans từ "HV" thành "HN"
   rồi bỏ chú thích khối @font-face của HN bên dưới. */
@font-face{
  font-family:"HV";src:url("../fonts/Helvetica-Light.woff2") format("woff2"),
      url("../fonts/Helvetica-Light.ttf") format("truetype");
  font-weight:300;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"HV";src:url("../fonts/Helvetica-Regular.woff2") format("woff2"),
      url("../fonts/Helvetica-Regular.ttf") format("truetype");
  font-weight:400;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"HV";src:url("../fonts/Helvetica-Bold.woff2") format("woff2"),
      url("../fonts/Helvetica-Bold.ttf") format("truetype");
  font-weight:700;font-style:normal;font-display:swap;
}

/* PP Pangram Sans Rounded: file vẫn nằm trong fonts/PPPangramSansRounded-Bold
   (.otf và .woff2) nhưng đang TẮT — cả trang quay lại dùng Helvetica.
   Muốn bật lại cho tên ở hero thì thêm một @font-face họ "PSR" trỏ vào file
   woff2 đó, rồi thêm vào .hero h1:  font-family:"PSR",var(--display);
   Lưu ý bộ này chỉ có đúng một độ đậm 700, giấy phép ghi "Personal Use Only".

   ĐỪNG để lại url(...) trong chú thích: build.js quét url() bằng biểu thức
   chính quy, không phân biệt chú thích, nên sẽ nhúng font vào bản đóng gói dù
   font không hề được dùng. */

/* Đổi sang Helvetica Neue: file HelveticaNeue-Regular / -Medium / -Bold đã có
   sẵn trong fonts/. Chỉ cần đổi ba dòng src ở trên trỏ sang các file đó
   (300 → Regular, 400 → Regular, 700 → Bold, thêm 500 → Medium nếu muốn). */

/* =========================================================================
   BẢNG MÀU — bốn màu, không thêm màu nào khác:
     đen #000000 · tím #7E3BED · trắng #FFFFFF · lime #C6FF34

   Cách chia việc cho hai màu nhấn (giữ nguyên ở cả bản tối lẫn bản sáng):
     TÍM  = màu của nét và chữ — gạch nhãn mục, gạch chân, chữ nhấn, viền.
     LIME = màu của mảng đặc — viên lọc đang chọn, chấm, nền nút khi rê chuột,
            vệt bôi khi quét chữ.

   Ba biến màu nhấn, đừng dùng lẫn lộn:
     --violet      tím gốc, chỉ cho mảng đặc và gradient
     --violet-ink  tím dùng làm nét và chữ (nền đen thì nâng sáng lên)
     --lime        lime gốc, chỉ cho mảng đặc
     --lime-ink    lime dùng làm chữ (nền trắng thì hạ tối xuống)
   Lime gốc đặt làm chữ trên nền trắng chỉ đạt 1.2:1, không đọc nổi — cần chữ
   màu xanh lá trên nền trắng thì luôn dùng --lime-ink.

   Giao diện KHÔNG chạy theo cài đặt sáng/tối của máy. MẶC ĐỊNH LÀ BẢN TRẮNG —
   nằm ngay trong :root nên đúng màu ngay từ khung hình đầu tiên, không loé
   một nhịp sai màu trong lúc chờ JS. Bấm nút mặt trời để sang bản đen.
   ========================================================================= */
/* BẢN TRẮNG — mặc định. Cách làm cho nền trắng trông hiện đại chứ không
   "trống trơn": nền và thẻ đều trắng tinh, tách nhau bằng đường kẻ tóc mảnh
   và bóng đổ rất nhẹ chứ không tô thẻ thành mảng xám — xám trên trắng là thứ
   làm trang trông cũ. Sắc trung tính ngả nhẹ về tím cho ăn với màu nhấn. */
:root{
  --ground:#FFFFFF;
  --raised:#FFFFFF;
  --raised-2:#F3F2F7;
  --hair:rgba(17,14,34,.10);
  --hair-2:rgba(17,14,34,.19);
  --text:#0A0910;
  --muted:#5B5867;
  --violet:#7E3BED;
  --violet-ink:#7E3BED;                  /* trên nền trắng đạt 5.6:1, dùng thẳng */
  --violet-soft:rgba(126,59,237,.12);
  --lime:#C6FF34;
  --lime-soft:rgba(198,255,52,.34);
  --lime-ink:#5C8A00;                    /* lime hạ tối, trên nền trắng đạt 4.6:1 */
  --on-lime:#000000;
  --amb-a:rgba(126,59,237,.16);
  --amb-b:rgba(198,255,52,.28);
  --shadow:0 24px 52px -30px rgba(17,14,44,.26);
  --type-sh:rgba(17,14,34,.13);
  --type-sh2:rgba(17,14,34,.06);
  --ex-near:#0A0910;                    /* = --text, xem chú thích ở khối trên */
  --ex-far:#CFC9DE;
  --ex-em-near:#7E3BED;
  --ex-em-far:#CFC9DE;
  /* Bốn biến dưới đây không đổi theo giao diện, chỉ khai báo một lần ở :root. */
  --display:"HV",Helvetica,"Helvetica Neue",Arial,sans-serif;
  --sans:"HV",Helvetica,"Helvetica Neue",Arial,sans-serif;
  --mono:"HV",Helvetica,"Helvetica Neue",Arial,sans-serif;   /* nhãn cũng dùng Helvetica */
  --ease:cubic-bezier(.16,.84,.32,1);
  color-scheme:light;
}
/* BẢN ĐEN — chỉ bật khi người xem tự bấm nút mặt trời. */
:root[data-theme="dark"]{
  --ground:#000000;
  --raised:#0B0B0C;
  --raised-2:#141417;
  --hair:rgba(255,255,255,.13);
  --hair-2:rgba(255,255,255,.24);
  --text:#FFFFFF;
  --muted:#8C8A94;
  --violet:#7E3BED;                      /* tím gốc — mảng đặc, gradient */
  /* Tím gốc trên nền đen chỉ đạt 3.7:1, chữ nhỏ đọc mệt. Nét và chữ dùng bản
     nâng sáng này; mảng đặc vẫn giữ đúng #7E3BED. */
  --violet-ink:#A57BFF;
  --violet-soft:rgba(126,59,237,.22);
  --lime:#C6FF34;
  --lime-soft:rgba(198,255,52,.16);
  /* Lime dùng làm chữ: trên nền đen thì lime gốc thoải mái, trên nền trắng
     phải hạ tối lại nếu không sẽ chìm nghỉm (lime gốc chỉ 1.2:1). */
  --lime-ink:#C6FF34;
  --on-lime:#000000;                     /* chữ đặt trên mảng lime */
  --amb-a:rgba(126,59,237,.19);          /* quầng tím góc trên phải */
  --amb-b:rgba(198,255,52,.08);          /* quầng lime góc dưới trái */
  --shadow:0 30px 70px -40px rgba(0,0,0,1);
  --type-sh:rgba(0,0,0,.50);
  --type-sh2:rgba(0,0,0,.32);
  /* Mười lớp đùn khối nằm đè lên mặt chữ, nên --ex-near CHÍNH LÀ màu chữ
     nhìn thấy — phải bằng --text, nếu không tên trang sẽ ra xám xỉn.
     --ex-far là màu lớp xa nhất, cho tan dần vào nền. */
  --ex-near:#FFFFFF;
  --ex-far:#0D0A18;
  --ex-em-near:#C6A8FF;
  --ex-em-far:#1B0B38;
  color-scheme:dark;
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{
  margin:0;background:var(--ground);color:var(--text);
  font-family:var(--sans);font-size:16px;font-weight:400;line-height:1.6;
  -webkit-font-smoothing:antialiased;overflow-x:hidden;
}
body.locked{overflow:hidden}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
:focus-visible{outline:1.5px solid var(--violet-ink);outline-offset:4px}
::selection{background:var(--lime);color:var(--on-lime)}

.wrap{width:min(1420px,100% - 56px);margin-inline:auto}
.label{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--muted);
}
.eyebrow{display:flex;align-items:center;gap:12px}
.eyebrow::before{content:"";width:26px;height:1px;background:var(--violet-ink)}
/* Helvetica thật khi máy có (macOS/iOS), Arial khi không — Arial là bản
   tương thích số đo của Helvetica nên dáng chữ gần như trùng khít.
   Archivo là lưới an toàn cuối cho máy thiếu cả hai. */
.big{
  font-family:var(--display);font-weight:700;
  letter-spacing:-.04em;line-height:1.02;text-wrap:balance;
}
.big em{font-style:normal;color:var(--violet-ink)}

/* =========================================================================
   MÀN MỞ ĐẦU
   Trình tự: tên hiện lên → số đếm chạy tới 100 → vạch tiến độ đầy →
   cả tấm rèm cắt dần lên trên để lộ hero, hero mới bắt đầu chuyển động
   của nó. Thứ tự này quan trọng: nếu hero chạy cùng lúc thì người xem
   không kịp thấy gì, mà rèm kéo xong mới chạy thì lại hụt một nhịp.

   Rèm kéo bằng clip-path chứ không phải translateY: translateY sẽ kéo cả
   nội dung bên trong đi theo, còn clip-path thì chữ đứng yên và bị xén dần
   — giống tấm màn sân khấu kéo lên hơn.

   AN TOÀN: có một animation dự phòng tự ẩn rèm sau 5 giây. Nếu JS lỗi giữa
   chừng thì trang vẫn vào được, không bị khoá sau tấm rèm. */
.intro{
  position:fixed;inset:0;z-index:200;
  background:var(--ground);color:var(--text);
  display:flex;align-items:flex-end;
  padding:0 0 clamp(30px,5vh,64px);
  clip-path:inset(0 0 0 0);
  transition:clip-path .85s cubic-bezier(.76,0,.24,1) .26s;
  animation:introSafety 0s linear 5s forwards;
}
@keyframes introSafety{to{opacity:0;visibility:hidden;pointer-events:none}}
/* Cùng bề rộng và cùng lề với .wrap, để tên trong màn mở đầu thẳng hàng với
   tên ở hero. Rèm kéo lên thì tên như đứng yên tại chỗ, chỉ có nền đổi — mắt
   đọc ra một chuyển tiếp liền mạch chứ không phải hai màn rời nhau. */
.intro-in{width:min(1420px,100% - 56px);margin-inline:auto}
.intro-mark{
  display:block;font-family:var(--display);font-weight:700;
  font-size:clamp(44px,9vw,132px);letter-spacing:-.04em;line-height:1;
  clip-path:inset(0 0 110% 0);
  transform:translateY(.14em);
  animation:introMark .95s cubic-bezier(.16,.84,.32,1) .12s forwards;
}
@keyframes introMark{to{clip-path:inset(-.25em 0 -.35em 0);transform:none}}
.intro-meta{
  display:flex;align-items:baseline;justify-content:space-between;gap:20px;
  margin-top:clamp(14px,2vh,26px);
  opacity:0;animation:introFade .7s ease .55s forwards;
}
@keyframes introFade{to{opacity:1}}
.intro-role{
  font-family:var(--mono);font-size:11px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--muted);
}
/* Bản trước: bold 700, cỡ 20–34px. Đứng cạnh nhãn mono 11px thì nó vừa nhỏ
   vừa nặng nề, mà so với chữ qucannh. cỡ 132px bên trên lại chẳng có sức
   nặng nào — rơi vào khoảng lửng không ra nhấn cũng không ra phụ.
   Giờ: to hẳn lên và mỏng đi. Chữ số lớn nét mảnh là cách các trang biên tập
   hay dùng, nó cân được với dòng tên mà không tranh chỗ. */
.intro-num{
  display:flex;align-items:baseline;gap:.1em;
  font-family:var(--display);font-weight:300;
  font-size:clamp(30px,4.4vw,58px);letter-spacing:-.035em;line-height:.9;
  font-variant-numeric:tabular-nums;   /* số không nhảy ngang khi đếm */
}
/* Số 0 đệm phía trước: giữ chỗ để bề ngang cố định, nhưng làm nhạt hẳn đi để
   mắt đọc thẳng vào con số thật. */
.intro-num .z{color:var(--hair-2)}
.intro-num .d{color:var(--text)}
/* Dấu % nhỏ, nâng lên ngang vai số, dùng đúng cỡ và giãn cách của nhãn mono
   bên trái — để hai đầu dòng nói cùng một thứ tiếng. */
.intro-num .pc{
  font-family:var(--mono);font-weight:400;font-size:11px;
  letter-spacing:.2em;color:var(--muted);
  align-self:flex-start;margin-left:.18em;
}
.intro-bar{
  display:block;height:2px;margin-top:clamp(12px,1.6vh,20px);
  background:var(--hair);overflow:hidden;
}
.intro-bar i{
  display:block;height:100%;width:100%;
  transform:scaleX(0);transform-origin:left center;
  background:linear-gradient(90deg,var(--violet),var(--lime));
}
/* Bản sáng: rèm KHÔNG dùng nền trắng của trang. Cả màn hình trắng loá ngay
   khi mở trang thì chói mắt, nhất là buổi tối. Hạ xuống một tông xám ngả tím
   cho dịu, chữ vẫn đen nên vẫn đọc rõ (14:1). Bản tối vốn đã đen, giữ nguyên. */
:root:not([data-theme="dark"]) .intro{background:#E4E1EC}
:root:not([data-theme="dark"]) .intro-role{color:#4A4757}
:root:not([data-theme="dark"]) .intro-bar{background:rgba(17,14,34,.16)}
/* Rèm kéo lên, nội dung bên trong trôi lên NHANH HƠN rèm. Chênh lệch tốc độ
   đó là thứ tạo cảm giác có chiều sâu — hai lớp ở hai khoảng cách khác nhau.
   Cùng tốc độ thì nhìn phẳng như một tờ giấy bị kéo. */
.intro.done{clip-path:inset(0 0 100% 0)}
.intro.done .intro-in{
  opacity:0;transform:translateY(-64px);
  transition:opacity .5s ease,transform .8s cubic-bezier(.7,0,.2,1);
}
/* Hero nở nhẹ về đúng cỡ trong lúc rèm kéo lên — như ống kính lùi ra lấy nét,
   thay vì trang đứng sẵn đó chờ. */
.hero{transform:scale(1.05);transition:transform 1.3s cubic-bezier(.16,.84,.32,1)}
.ready .hero{transform:none}
/* Máy bật chế độ giảm chuyển động: bỏ hẳn màn mở đầu, vào thẳng trang,
   hero cũng không nở. */
@media (prefers-reduced-motion:reduce){
  .intro{display:none}
  .hero{transform:none;transition:none}
}
/* ---------- lớp nền ---------- */
.amb{
  position:fixed;inset:0;z-index:0;pointer-events:none;
  contain:strict;   /* lớp nền tĩnh, đừng để nó dính vào việc tính bố cục */
  background:
    radial-gradient(62vw 52vh at 76% 6%,var(--amb-a),transparent 62%),
    radial-gradient(56vw 50vh at 4% 72%,var(--amb-b),transparent 66%);
}
/* Lớp hạt phủ toàn màn hình, nhấp nháy liên tục. Ba chỗ phải cẩn thận:
   - inset:-40% nghĩa là vẽ một vùng rộng gấp ~3.2 lần màn hình. Chuyển động
     chỉ dịch tối đa 3% nên -12% là thừa sức, mà diện tích phải vẽ giảm hơn
     một nửa.
   - will-change:transform đẩy nó thành một lớp riêng, mỗi khung hình chỉ
     dịch lớp đó thay vì bắt trình duyệt vẽ lại cả trang bên dưới.
   - contain:strict chặn nó ảnh hưởng bố cục của bất cứ thứ gì. */
.grain{
  position:fixed;inset:-12%;z-index:70;pointer-events:none;opacity:.05;
  will-change:transform;contain:strict;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/></filter><rect width='120' height='120' filter='url(%23n)'/></svg>");
  animation:grain 800ms steps(3) infinite;
}
.grain{opacity:.028}                             /* nền trắng chỉ cần một chút hạt, nhiều là đục */
:root[data-theme="dark"] .grain{opacity:.05}
@keyframes grain{0%{transform:translate(0,0)}33%{transform:translate(-2%,3%)}66%{transform:translate(3%,-2%)}100%{transform:translate(0,0)}}
/* ĐỪNG cho lime vào gradient này. js/app.js kéo thanh bằng scaleX chứ không
   phải width, nghĩa là cả dải màu bị nén vào phần đang hiện — đầu mút bên
   phải LÚC NÀO cũng là màu cuối của gradient, ở mọi vị trí cuộn. Để lime ở
   đó thì thành một cục xanh lá bám dính lấy thanh đầu trang, trôi theo con
   lăn. Giữ nguyên một tông tím, chỉ nhạt dần ở đuôi trái cho đỡ cụt. */
.progress{position:fixed;top:0;left:0;height:1.5px;width:0;z-index:80;
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--violet-ink) 22%,transparent),
    var(--violet-ink) 38%)}
/* .fade-top ĐÃ BỎ. Nó là một mảng nền cao 132px nằm đè lên nội dung ở z-index
   55 — tức là quét một vệt trắng (hoặc đen) lên đúng phần trên của ảnh tác
   phẩm, thấy rõ nhất khi cuộn qua khu vực thẻ dự án. Việc "làm nội dung tan
   dần khi trôi lên" giờ do chính thanh đầu trang lo: nền của nó nới xuống
   thêm 26px và được bo mép dưới bằng mặt nạ chuyển dần, nên chỉ mờ đúng vùng
   ngay sau thanh chứ không phủ xuống cả một phần tư màn hình. */

/* ---------- thanh trên ---------- */
/* KHÔNG đặt isolation:isolate ở đây. Nó biến header thành "backdrop root",
   mà backdrop-filter chỉ lấy được thứ nằm trong backdrop root của mình —
   tức là hai lớp ::before/::after sẽ đi blur đúng phần nội dung của chính
   header, bằng không blur gì cả. Mà nó cũng thừa: position:fixed kèm
   z-index:60 đã tạo sẵn stacking context để mẹo z-index:-1 hoạt động. */
header{
  position:fixed;top:0;left:0;right:0;z-index:60;
  border-bottom:1px solid transparent;transition:border-color .3s ease;
}
header::before{
  /* Nới xuống dưới đáy thanh 26px rồi bo mép bằng mặt nạ: nội dung chui vào
     vùng đó là mờ dần rồi biến mất, không có đường cắt ngang. Nới bằng inset
     âm chứ không tăng chiều cao thanh, để không đụng tới bố cục bên trong. */
  /* pointer-events:none là BẮT BUỘC, không phải cho gọn. Hộp của phần tử phụ
     vẫn được tính khi dò điểm chạm và được quy về chính thẻ header — mà lớp
     này thò xuống dưới đáy thanh, nên nó nuốt luôn cú bấm vào bất cứ thứ gì
     nằm trong dải đó. Kiểm bằng elementsFromPoint: header hiện ra TRƯỚC cả
     nút bên dưới nó. */
  content:"";position:absolute;inset:0 0 -26px;z-index:-1;opacity:0;pointer-events:none;
  background:linear-gradient(color-mix(in srgb,var(--ground) 86%,transparent) 62%,
                             color-mix(in srgb,var(--ground) 40%,transparent) 84%,
                             transparent);
  /* saturate ở đây KHÔNG phải để cho đẹp. Thanh này trong suốt, nên vật gì
     bão hoà màu trôi qua dưới nó — tấm nền lime sau chữ tên, ảnh bìa dự án —
     đều hằn lên thành một vệt màu bám vào thanh. Rút bão hoà xuống thì cái
     trôi qua chỉ còn là kết cấu xám, không còn là một cục màu.
     Mặt nạ bên dưới làm nhoà dần cả hiệu ứng này, nên chỗ nội dung còn hiện
     rõ thì màu vẫn nguyên vẹn — không bị quét xám xuống giữa trang. */
  backdrop-filter:blur(13px) saturate(.3);
  -webkit-backdrop-filter:blur(13px) saturate(.3);
  -webkit-mask-image:linear-gradient(#000 60%,rgba(0,0,0,.55) 82%,transparent);
  mask-image:linear-gradient(#000 60%,rgba(0,0,0,.55) 82%,transparent);
  transition:opacity .3s ease;
}
/* Lớp thứ hai: blur nhẹ, phủ rộng hơn xuống dưới. Một lớp blur duy nhất bao
   giờ cũng lộ mép — chỗ hết mặt nạ là chỗ ảnh đang nhoè bỗng nét trở lại.
   Xếp hai lớp thì độ nhoè đi thành bậc thang 13 → 4 → 0 thay vì rơi thẳng.
   ::after vẽ sau ::before nên nó nhận luôn kết quả của lớp trước làm nền,
   hai lớp cộng dồn chứ không tranh nhau. */
header::after{
  content:"";position:absolute;inset:0 0 -54px;z-index:-1;opacity:0;pointer-events:none;
  backdrop-filter:blur(4px) saturate(.62);
  -webkit-backdrop-filter:blur(4px) saturate(.62);
  -webkit-mask-image:linear-gradient(#000 52%,transparent);
  mask-image:linear-gradient(#000 52%,transparent);
  transition:opacity .3s ease;
}
header.stuck::before,header.stuck::after{opacity:1}
header.stuck{border-color:var(--hair)}
header .bar{display:flex;align-items:center;gap:12px;padding:18px 0;transition:padding .4s var(--ease)}
header.stuck .bar{padding:11px 0}
.brand{
  display:flex;align-items:center;gap:12px;
  font-family:var(--display);font-weight:700;font-size:17px;letter-spacing:-.015em;
}
/* .brand canvas ĐÃ BỎ cùng khối lập phương nhỏ cạnh chữ qucannh. */
header nav{margin-left:auto;display:flex;gap:2px}
header nav a{
  padding:8px 14px;border-radius:999px;font-size:13.5px;line-height:1.5;color:var(--muted);
  transition:color .25s,background .25s;
}
header nav a{position:relative}
header nav a::after{
  content:"";position:absolute;left:14px;right:14px;bottom:5px;height:1px;
  background:var(--violet-ink);transform:scaleX(0);transform-origin:right center;
  transition:transform .38s var(--ease);
}
header nav a:hover{color:var(--text)}
header nav a:hover::after{transform:scaleX(1);transform-origin:left center}
/* mục đang đọc: gạch chân giữ nguyên, không cần rê chuột */
header nav a.on{color:var(--text)}
header nav a.on::after{transform:scaleX(1);transform-origin:left center}
.hbtn{
  width:36px;height:36px;border-radius:999px;display:grid;place-items:center;color:var(--muted);
  border:1px solid var(--hair);transition:color .25s,border-color .25s;
}
.hbtn:hover{color:var(--text);border-color:var(--hair-2)}
.hbtn svg{transition:transform .5s var(--ease)}
.hbtn.spin svg{transform:rotate(180deg)}
@keyframes pageIn{from{opacity:0}to{opacity:1}}
body{animation:pageIn .5s ease both}
@media (max-width:900px){header nav{display:none}}

/* ---------- hero ---------- */
.hero{position:relative;min-height:100svh;display:flex;align-items:flex-end;padding:130px 0 46px;overflow:hidden}
#scene{position:absolute;inset:0;width:100%;height:100%;display:block;z-index:1}
.hero::after{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(to top,var(--ground) 1%,color-mix(in srgb,var(--ground) 56%,transparent) 32%,transparent 64%),
             linear-gradient(to right,color-mix(in srgb,var(--ground) 82%,transparent) 8%,color-mix(in srgb,var(--ground) 40%,transparent) 40%,transparent 62%);
}
.hero .wrap{position:relative;z-index:2}
/* Cân nhắc trước khi đặt filter lên .hero h1: một bộ lọc ở đây sẽ chặn đứng
   mọi backdrop-filter của phần tử con bên trong. Hiện không có con nào cần,
   nhưng nếu sau này dựng lại tấm kính mờ sau tên thì phải nhớ điều này. */
.hero h1{
  margin:0;font-size:clamp(58px,13.5vw,215px);cursor:default;
}
/* chữ 3D: mỗi dòng là một nhóm ba chiều, mỗi từ đùn thành nhiều lớp theo trục Z.
   Không dùng overflow:hidden hay background-clip:text để dấu tiếng Việt luôn đủ nét. */
.hero h1 .ln{display:block;perspective:1200px;perspective-origin:34% 50%}
.hero h1 .ln i{
  display:block;font-style:normal;transform-style:preserve-3d;
  transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));will-change:transform;
}
.hero h1 .w{
  position:relative;display:inline-block;transform-style:preserve-3d;
  color:var(--text);transform-origin:50% 100%;
  opacity:0;transform:translateY(.3em) rotateX(-30deg) scale(.975);
}
/* Tên ở hero nằm trên một mảng lime đặc, giống vệt bôi highlight.
   Mảng lime ở CẢ HAI bản. Màu chữ thì khác nhau, và đây là chỗ dễ sai nhất:
     bản sáng  chữ đen #0A0910   → 17.8:1
     bản tối   chữ #77767E       → 3.6:1
   Nền của chữ ở đây là mảng lime chứ không phải nền trang, nên đừng để chữ
   trắng ở bản tối: trắng trên lime chỉ đạt 1.18:1, gần như không đọc được.
   Muốn giữ chữ trắng tinh thì phải đổi mảng sang tím #7E3BED (được 3.7:1).

   Hai chỗ dễ sai, đừng sửa lại:
   - Mép dưới nới sâu hơn hẳn ba mép kia (.15em so với .015em): hộp dòng dừng
     gần đường chân chữ, còn đuôi q/g/y thò xuống thêm khoảng .12em. Để nông
     là đuôi chữ lòi ra ngoài mảng.
   - Phải là ::before, KHÔNG được đặt nền lên chính .w: .w nằm trong ngữ cảnh
     3D nên mặt phẳng nền của nó ở z=0, che hết các lớp đùn khối ở z âm. */
/* Tắt ngữ cảnh 3D ở khối chữ. Nó chỉ cần cho các lớp đùn khối, mà những lớp
   đó đang tắt (EX_LAYERS = 0 trong js/app.js). Giữ preserve-3d thì z-index
   không có tác dụng, vầng sáng sẽ nhảy lên nằm đè lên chữ.
   Bật lại lớp đùn khối thì phải trả preserve-3d về. */
.hero h1 .ln i,.hero h1 .w{transform-style:flat}
.hero h1 .w{color:var(--text);z-index:0}                  /* bản sáng (mặc định): chữ đen */
:root[data-theme="dark"] .hero h1 .w{color:#FFFFFF}       /* bản tối: trắng trên mảng tím */
/* Tấm kính mờ phía sau tên.

   Ba điều kiện phải giữ, đổi một cái là hỏng:
   1. Mép dưới nới sâu hơn hẳn ba mép kia (.15em so với .015em): hộp dòng dừng
      gần đường chân chữ, còn đuôi chữ q/g/y thò xuống thêm khoảng .12em.
   2. Phải là ::before có z-index âm, KHÔNG đặt nền lên chính .w: .w nằm trong
      ngữ cảnh 3D, nếu đặt nền trực tiếp thì nó nằm ở z=0 và che mất mọi thứ.
      Ngữ cảnh 3D cũng đã bị tắt ở trên để z-index có tác dụng.
   3. Không đặt filter lên .hero h1 — bộ lọc ở phần tử cha sẽ chặn đứng
      backdrop-filter ở đây, kính hết mờ.

   Không viền, không vệt sáng mép: để tấm kính chìm vào nền chứ không thành
   cái khung. Chất kính đến từ ba thứ — màu phủ mỏng, làm mờ mạnh hậu cảnh,
   và lớp hạt nhám ở ::after. */
.hero h1 .w::before{
  content:"";position:absolute;inset:-.015em -.05em -.15em;
  z-index:-1;pointer-events:none;transform-origin:left center;
  background:color-mix(in srgb,var(--lime) 22%,transparent);
  border-radius:.30em;                   /* co giãn theo cỡ chữ, không cắm px cứng */
  /* Bản làm mờ thường — dùng cho mọi máy, và là bản dự phòng cho Safari vốn
     không nhận url() trong backdrop-filter. Bản bẻ cong ảnh nền qua bộ lọc
     SVG nằm trong khối @media (min-width:1000px) bên dưới, vì đó là phép
     tính đắt nhất trang. ĐỪNG chép nó ngược lên đây. */
  backdrop-filter:blur(16px) saturate(1.6) brightness(1.05);
  -webkit-backdrop-filter:blur(16px) saturate(1.6) brightness(1.05);
  /* Vát cạnh kiểu thấu kính: sáng ở mép trên trái, tím đọng ở mép dưới phải,
     một vành lime rất mảnh chạy quanh. Đây mới là thứ làm nó ra "khối nước"
     chứ không phải mảng phẳng. */
  box-shadow:
    inset 3px 4px 10px -3px rgba(255,255,255,.60),
    inset -4px -6px 14px -6px rgba(126,59,237,.55),
    inset 0 0 0 1px rgba(198,255,52,.30),
    0 22px 50px -28px rgba(0,0,0,.45);
}
:root[data-theme="dark"] .hero h1 .w::before{
  background:color-mix(in srgb,var(--violet) 30%,transparent);
  box-shadow:
    inset 3px 4px 10px -3px rgba(255,255,255,.34),
    inset -4px -6px 14px -6px rgba(198,255,52,.30),
    inset 0 0 0 1px rgba(169,140,255,.34),
    0 22px 50px -28px rgba(0,0,0,.85);
}
/* Vệt sáng trượt chéo mặt kính, kiểu ánh sáng phản trên khối nước. Dùng
   ::after vì nó vẽ sau ::before nên nằm trên mặt kính, mà vẫn dưới chữ nhờ
   z-index âm. */
.hero h1 .w::after{
  content:"";position:absolute;inset:-.015em -.05em -.15em;z-index:-1;
  border-radius:.30em;pointer-events:none;transform-origin:left center;
  background:linear-gradient(118deg,
    rgba(255,255,255,.42) 0%,
    rgba(255,255,255,.10) 22%,
    rgba(255,255,255,0)   42%,
    rgba(255,255,255,0)   66%,
    rgba(198,255,52,.22)  88%,
    rgba(126,59,237,.24) 100%);
}
/* Bẻ cong ảnh nền qua bộ lọc SVG là phép tính đắt nhất trên trang: nền phía
   sau là khối 3D đang chạy, nên mỗi khung hình trình duyệt phải lọc lại từ
   đầu. Chỉ bật trên màn rộng — điện thoại vừa yếu vừa là nơi hiệu ứng này
   khó thấy nhất, ở đó bản làm mờ thường khai báo bên trên là đủ đẹp. */
@media (min-width:1000px) and (prefers-reduced-motion:no-preference){
  .hero h1 .w::before{
    backdrop-filter:blur(7px) saturate(1.7) brightness(1.05) url(#liquid);
  }
}
/* Máy không theo kịp: js/hero-raymarch.js đo nhịp khung hình sau khi trang
   mở xong, thấy giật kéo dài thì gắn class perf-lite lên <html>. Lúc đó quay
   về tấm kính mờ thường — y hệt bản điện thoại và Safari vẫn dùng — thay cho
   bản bẻ cong qua bộ lọc SVG. Máy chạy êm thì class này không bao giờ xuất
   hiện và kính lỏng vẫn nguyên.
   Không cần đặt quy tắc này trong @media: .perf-lite thêm một lớp vào bộ chọn
   nên nó luôn thắng quy tắc ở trên, bất kể thứ tự. */
.perf-lite .hero h1 .w::before{
  backdrop-filter:blur(16px) saturate(1.6) brightness(1.05);
  -webkit-backdrop-filter:blur(16px) saturate(1.6) brightness(1.05);
}
/* Tấm kính kéo mở ra từ trái khi trang vừa tải. */
@keyframes plateWipe{
  from{transform:scaleX(0)}
  to{transform:scaleX(1)}
}
.ready .hero h1 .w::before,
.ready .hero h1 .w::after{animation:plateWipe .75s cubic-bezier(.2,.9,.25,1) .06s both}
.hero h1 em{font-style:normal;color:var(--violet-ink)}
.hero h1 em .w{color:var(--violet-ink)}
.ready .hero h1 .w{
  opacity:1;transform:none;
  transition:transform .95s cubic-bezier(.2,1.04,.3,1) var(--d,0ms),
             opacity .6s ease var(--d,0ms);
}
/* lớp càng sâu càng nhoè — mô phỏng độ sâu trường ảnh, mặt chữ trước vẫn nét */
.hero h1 .ex{
  position:absolute;left:0;top:0;white-space:pre;pointer-events:none;user-select:none;
  transform:translate3d(calc(var(--i) * -.0035em),calc(var(--i) * .0042em),calc(var(--i) * -.008em));
  color:var(--ex-far);
  color:color-mix(in srgb,var(--ex-near),var(--ex-far) calc(var(--t) * 100%));
  filter:blur(calc(var(--t) * var(--t) * 2.2px));
  opacity:calc(1 - var(--t) * .68);
}
.hero h1 em .ex{
  color:var(--ex-em-far);
  color:color-mix(in srgb,var(--ex-em-near),var(--ex-em-far) calc(var(--t) * 100%));
}
.hero-foot{
  /* Bốn cột: đoạn giới thiệu · Based in · Status · Contact.
     Thêm cột thì PHẢI sửa ở đây, không thì cột mới bị nhét vào hàng thứ hai
     và cả dải mất cân. */
  display:grid;grid-template-columns:minmax(0,1.15fr) auto auto auto;gap:24px 46px;align-items:end;
  margin-top:40px;padding-top:22px;border-top:1px solid var(--hair);
  opacity:0;transform:translateY(16px);
}
.ready .hero-foot{opacity:1;transform:none;transition:opacity 1s var(--ease) .45s,transform 1s var(--ease) .45s}
.hero-foot p{margin:0;max-width:46ch;font-size:16.5px;color:var(--muted)}
.hero-foot p b{color:var(--text);font-weight:500}
.hero-foot .col span{display:block}
.hero-foot .col .k{margin-bottom:6px}
/* Thẻ a mặc định là inline, mà cả trang đã đặt a{text-decoration:none} nên
   nếu để nguyên thì email và số điện thoại vừa dính liền một dòng vừa trông
   y hệt chữ thường — không ai biết bấm được. */
.hero-foot .col a{
  display:block;font-size:16.5px;line-height:1.55;
  background-image:linear-gradient(var(--violet-ink),var(--violet-ink));
  background-repeat:no-repeat;background-position:0 100%;background-size:0 1px;
  transition:background-size .35s var(--ease),color .25s ease;
}
.hero-foot .col a:hover{background-size:100% 1px;color:var(--violet-ink)}
@media (max-width:1180px){.hero-foot{grid-template-columns:minmax(0,1fr) auto auto;gap:22px 40px}}
.avail{display:inline-flex;align-items:center;font-size:14.5px}
@media (max-width:820px){.hero-foot{grid-template-columns:1fr;gap:18px}}

/* ---------- băng chữ chạy ---------- */
.ticker{border-block:1px solid var(--hair);overflow:hidden;padding:16px 0;position:relative;z-index:1}
.ticker .track{display:flex;width:max-content}
.ticker span{
  display:flex;align-items:center;gap:26px;padding:0 26px;white-space:nowrap;
  font-family:var(--display);font-weight:400;font-size:15px;color:var(--muted);
}
.ticker span::after{content:"";width:5px;height:5px;border-radius:50%;background:var(--violet-ink)}
/* chấm ngăn cách xen kẽ hai màu nhấn */
.ticker span:nth-child(even)::after{background:var(--lime)}

/* ---------- khung mục ---------- */
section{position:relative;z-index:1}
.sec{padding:140px 0 0;content-visibility:auto;contain-intrinsic-size:auto 900px}
.sec-top{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.85fr);gap:34px;align-items:end;margin-bottom:52px}
.sec-top h2{margin:16px 0 0;font-size:clamp(38px,6vw,84px)}
.sec-top .side{color:var(--muted);font-size:16.5px;max-width:44ch;padding-bottom:10px}
@media (max-width:900px){.sec-top{grid-template-columns:1fr;gap:18px}}
.reveal{opacity:0;transform:translateY(26px);transition:opacity .85s var(--ease),transform .85s var(--ease)}
.reveal.in{opacity:1;transform:none}

/* ---------- mờ dần theo cuộn ----------
   Nội dung hiện lên khi vào khung nhìn và nhoè dần khi trôi lên khỏi mép trên.
   Dùng scroll-driven animation: trình duyệt tự tính theo vị trí cuộn, chạy
   ngoài luồng chính nên không ăn khung hình. Trình duyệt chưa hỗ trợ thì rơi
   về cách hiện dần bằng JS như cũ, không hỏng gì. */
/* KHÔNG có filter:blur() trong hai keyframe này — đọc trước khi thêm vào.
   Chúng chạy theo vị trí cuộn, nên trình duyệt tính lại giá trị ở TỪNG điểm
   ảnh cuộn được. Blur là phép đắt nhất: nó buộc vẽ lại cả lớp thay vì chỉ
   dịch lớp đã vẽ sẵn. Bản đầu để 2.5px và 3px thì lướt qua khu vực thẻ dự án
   bị khựng thấy rõ; bản sau hạ còn 1.2px lúc hiện vào.
   Nay bỏ hẳn, đúng như ghi chú cũ đã dặn "muốn mượt tuyệt đối thì đặt về 0".
   Chỉ còn opacity và transform — hai thứ trình duyệt dịch được trên lớp đã vẽ
   sẵn mà không phải vẽ lại. Mờ 1.2px trong một thoáng hiện vào gần như không
   ai nhận ra, còn cái giật khi cuộn thì ai cũng thấy. */
@keyframes scrollIn{
  from{opacity:0;transform:translateY(22px) scale(.985)}
  to{opacity:1;transform:none}
}
@keyframes scrollOut{
  from{opacity:1;transform:none}
  to{opacity:.12;transform:translateY(-14px) scale(.99)}
}
/* Chỉ nhạt đi, không dịch chuyển. Dùng cho hero: hero đã có sẵn transition
   transform lúc rèm mở (scale 1.05 → 1), mà animation luôn thắng transition
   trên cùng một thuộc tính — keyframe có transform là nuốt mất đoạn đó. */
@keyframes fadeOff{
  from{opacity:1}
  to{opacity:0}
}
/* ĐỪNG dịch chuyển .plate / .medium theo cuộn: transform không tính vào chiều
   cao thẻ nên dòng tên và dòng phụ tràn ra ngoài hộp và bị cắt mất phần dưới. */
/* gạch đầu nhãn mục vẽ dài ra khi mục lọt vào khung nhìn */
@keyframes ruleGrow{ from{transform:scaleX(0)} to{transform:scaleX(1)} }

@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    .eyebrow::before{
      transform-origin:left center;
      animation:ruleGrow .9s var(--ease) both;
      animation-timeline:view();
      animation-range:entry 15% entry 65%;
    }
    /* .card CỐ TÌNH không nằm trong nhóm này. Hiệu ứng chạy theo vị trí
       cuộn không đảm bảo thẻ nào cũng được thấy lúc hiện ra: thẻ đã nằm sẵn
       trong khung nhìn khi ta cuộn tới thì đã qua mất đoạn "hiện vào", nên
       nó bật ra đầy đủ mà chẳng có chuyển động nào. Thẻ dự án dùng cách khác
       ở dưới: hiện một lần, lần lượt từng thẻ, do JS bấm giờ. */
    /* .group-h nằm trong danh sách này chứ không dùng .reveal: nhóm được JS
       dựng lại mỗi lần đổi bộ lọc, mà .reveal thì IntersectionObserver chỉ
       quét đúng một lần lúc mở trang — phần tử sinh ra sau đó không ai theo
       dõi, nằm im ở opacity 0. Hiệu ứng chạy theo vị trí cuộn thì không cần
       ai đăng ký, thêm bao nhiêu phần tử cũng tự chạy. */
    .group-h,.tile,.sec-top,.contact .cols,.shotlist,.steps{
      opacity:1;transform:none;transition:none;
      animation:scrollIn linear both,scrollOut linear both;
      animation-timeline:view(),view();
      animation-range:entry 6% entry 62%, exit 30% exit 96%;
      /* filter đã rút khỏi danh sách: keyframe scrollIn hết blur rồi, còn khai
         will-change:filter là bắt trình duyệt dựng sẵn lớp lọc cho từng khối
         mà không bao giờ dùng tới — tốn bộ nhớ GPU vô ích. */
      will-change:opacity,transform;
    }

    /* Thẻ dự án mờ dần khi trôi lên khỏi mép trên — phần "hiện vào" vẫn do JS
       lo, xem chú thích ngay trên.
       Móc vào .card.done chứ KHÔNG phải .card, và đây là chỗ dễ sai nhất:
       đoạn hiện vào là transition opacity, mà animation luôn thắng transition
       trên cùng một thuộc tính. Đặt lên .card thì thẻ hiện ra tức thì, mất
       sạch đoạn hiện dần. .done chỉ được gắn sau khi transition opacity chạy
       xong (js/app.js, transitionend), nên hai thứ không giẫm chân nhau.
       Bắt đầu muộn hơn nhóm trên (35% thay vì 30%) vì thẻ cao gần cả màn
       hình, nhạt sớm quá là chưa đọc xong tên dự án đã mờ. */
    .card.done{
      animation:scrollOut linear both;
      animation-timeline:view();
      animation-range:exit 35% exit 94%;
    }
    /* Không đặt will-change ở đây: dưới file còn .card.done{will-change:auto}
       cùng độ ưu tiên nhưng đứng sau nên nó thắng — viết vào chỉ thành dòng
       chết. Mà cũng không cần: opacity với transform chạy theo cuộn thì trình
       duyệt tự tách lớp, khỏi phải nhắc. */

    /* Hero mờ theo khi cuộn xuống. Chỉ opacity — lý do xem keyframe fadeOff.
       Hero cao đúng một màn hình nên đoạn exit trải vừa vặn một lần cuộn. */
    .hero{
      animation:fadeOff linear both;
      animation-timeline:view();
      animation-range:exit 10% exit 92%;
    }
    /* Bên trong hero vẫn để yên: chữ có chuyển động riêng lúc rèm mở. */
    .hero *{animation:none}
  }
}
.split .w{
  display:inline-block;opacity:0;transform:translateY(.4em);filter:blur(8px);
  transition:opacity .75s var(--ease) var(--d,0ms),transform .75s var(--ease) var(--d,0ms),
             filter .75s var(--ease) var(--d,0ms);
}
.split.in .w{opacity:1;transform:none;filter:blur(0)}

/* ---------- tường tác phẩm ---------- */
#work{position:relative}
#work .wrap{position:relative;z-index:1}

/* ---------- hai nhóm: Clients và Personal Projects ---------- */
/* Thay cho hàng nút lọc. Tiêu đề nhóm nói thẳng đây là việc cho khách hay
   việc tự làm, nên dòng "hạng mục · khách hàng" dưới mỗi thẻ thành thừa và
   đã bỏ. */
/* Đổi bộ lọc: lớp cũ lùi ra rồi lớp mới mới dựng, thay vì innerHTML bị ghi đè
   tức thì. Chỉ opacity và transform — hai thứ trình duyệt xử lý được trên
   luồng riêng, không bắt tính lại bố cục. */
.groups{transition:opacity .22s ease,transform .22s var(--ease)}
.groups.doi{opacity:0;transform:translateY(12px)}
.group + .group{margin-top:104px}
.group-h{
  display:flex;align-items:baseline;gap:16px;margin:0;
  padding-bottom:18px;border-bottom:1px solid var(--hair);
}
.group-h .eyebrow{font-size:12px}
.group-n{
  margin-left:auto;font-family:var(--mono);font-size:12px;color:var(--muted);
  font-variant-numeric:tabular-nums;
}
@media (max-width:900px){.group + .group{margin-top:72px}}

/* Nút lọc: All · Clients · Personal Projects.
   KHÔNG dùng position:sticky và KHÔNG có viên pill trượt như bản trước. Bản
   đó ghim vào mép trên màn hình nên nằm trọn sau thanh đầu trang, còn viên
   pill lime thì đậu lì ở đấy, hắt qua nền mờ của thanh thành một chấm xanh lá
   treo lơ lửng. Nền tô thẳng vào chip đang chọn, không có gì để trôi lạc. */
.filters{display:flex;flex-wrap:wrap;gap:9px;padding:6px 0 30px}
.chip{
  border:1px solid var(--hair);border-radius:999px;padding:9px 18px;
  font-family:var(--sans);font-size:13.5px;letter-spacing:-.005em;color:var(--muted);
  transition:color .28s ease,border-color .28s ease,background .28s ease;
}
.chip:hover{color:var(--text);border-color:var(--hair-2)}
/* Chữ trên nền lime PHẢI lấy --on-lime. Lime trên nền trắng chỉ đạt 1.2:1,
   để màu chữ thường là gần như không đọc được. */
.chip[aria-pressed="true"]{
  background:var(--lime);border-color:var(--lime);color:var(--on-lime);
}

.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:70px 44px;padding:52px 0 0}
.grid .card:nth-child(even){margin-top:88px}
@media (max-width:900px){
  .grid{grid-template-columns:1fr;gap:56px}
  .grid .card:nth-child(even){margin-top:0}
}
/* Thẻ hiện ra một lần, chạy theo đồng hồ chứ không theo vị trí cuộn. Nhờ
   vậy thẻ nào cũng có chuyển động đầy đủ, và vì chỉ chạy đúng một lần rồi
   thôi nên blur ở đây rẻ hơn nhiều so với blur chạy theo cuộn.
   Độ trễ giữa các thẻ do js/app.js đặt, xem transitionDelay ở đó. */
.card{
  display:block;width:100%;text-align:left;perspective:1100px;
  opacity:0;transform:translateY(38px) scale(.97);filter:blur(6px);
  transition:opacity .9s var(--ease),transform 1s var(--ease),filter .8s var(--ease);
  will-change:opacity,transform,filter;
}
.card.in{opacity:1;transform:none;filter:blur(0)}
/* Chạy xong thì gỡ will-change: giữ mãi là bắt trình duyệt nuôi một lớp
   riêng cho từng thẻ suốt thời gian còn lại. */
.card.done{will-change:auto}
.card .shot{
  position:relative;display:block;aspect-ratio:4/3;border-radius:22px;overflow:hidden;
  background:var(--raised);border:1px solid var(--hair);padding:8px;
  transform-style:preserve-3d;clip-path:inset(0 0 100% 0 round 22px);
  transition:border-color .35s ease,box-shadow .35s ease,
             transform .5s var(--ease),clip-path 1s var(--ease) .1s;
}
.card.in .shot{clip-path:inset(0 0 0 0 round 22px)}
.card .shot canvas,.card .shot img,.card .shot video{border-radius:15px}
.card:hover .shot{border-color:var(--hair-2);box-shadow:var(--shadow)}
/* PHẢI liệt kê cả video, không chỉ canvas và img: thiếu nó thì thẻ video
   giữ nguyên cỡ gốc của file, không lấp đầy khung bìa. */
.card .shot canvas,.card .shot img,.card .shot video{width:100%;height:100%;display:block;object-fit:cover;transition:transform .8s var(--ease)}
.card:hover .shot canvas,.card:hover .shot img,.card:hover .shot video{transform:scale(1.035)}
.card .num{
  position:absolute;left:20px;top:19px;z-index:2;font-family:var(--mono);font-size:11px;
  color:var(--muted);background:color-mix(in srgb,var(--ground) 70%,transparent);
  backdrop-filter:blur(6px);border-radius:999px;padding:4px 10px;
}
.card .go{
  position:absolute;right:20px;bottom:20px;z-index:2;display:flex;align-items:center;gap:7px;
  font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;
  background:var(--violet-ink);color:var(--on-lime);border-radius:999px;padding:8px 14px;
  opacity:0;transform:translateY(8px);transition:opacity .34s var(--ease),transform .34s var(--ease);
}
.card:hover .go{opacity:1;transform:none}
.card .plate{display:flex;align-items:baseline;gap:14px;margin-top:18px}
.card h3{
  margin:0;font-family:var(--display);font-weight:700;
  font-size:clamp(21px,2.2vw,30px);letter-spacing:-.012em;line-height:1.24;
}
.card h3{
  background-image:linear-gradient(var(--text),var(--text));
  background-size:0 1px;background-repeat:no-repeat;background-position:0 100%;
  transition:background-size .5s var(--ease);
}
.card:hover h3{background-size:100% 1px}
/* .card .yr ĐÃ BỎ — năm không còn hiện ở góc phải thẻ nữa. */
/* .card .medium ĐÃ BỎ — dòng "hạng mục · khách hàng" dưới mỗi thẻ giờ do
   tiêu đề nhóm nói thay. */
.empty{font-family:var(--mono);font-size:13px;color:var(--muted);padding:60px 0}

/* Mục "Lab — Generative systems" ĐÃ BỎ: toàn bộ khối .lab và cả .btn.
   .btn đi theo vì nút "New seed" của mục đó là chỗ cuối cùng còn dùng nó —
   nút "Open full project" trong panel đã bỏ từ trước. Đã kiểm lại cả ba file
   html và toàn bộ js, không còn class="btn" nào. */

/* ---------- tiểu sử ---------- */
.bento{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;padding-top:10px}
.tile{border:1px solid var(--hair);border-radius:22px;padding:28px;background:var(--raised)}
/* Ô này cao bằng HAI hàng ô thống kê, để lưới ra đúng hình:
     hàng 1-2  [ wide  wide ] [ số ] [ số ]
                              [ số ] [ số ]
     hàng 3    [ tools tools ]
   justify-content là CĂN GIỮA chứ không phải space-between, và đây là chỗ
   từng hỏng: ô cao hai hàng (khoảng 322px) nhưng nội dung chỉ còn một đoạn
   văn sau khi bỏ bớt chữ. space-between đẩy đoạn văn lên sát đỉnh và dòng
   "Ready for the next brief" xuống sát đáy, toàn bộ chỗ thừa dồn thành một
   mảng trống giữa ô — nhìn ra là trang tải thiếu.
   Căn giữa thì chỗ thừa chia đều trên và dưới, đọc ra là ô thoáng.
   Muốn ô đầy trở lại thì thêm chữ, đừng đổi lại space-between. */
.tile.wide{grid-column:span 2;grid-row:span 2;display:flex;flex-direction:column;justify-content:center;gap:26px}
.tile.wide p{margin:0;font-size:18.5px;line-height:1.7}
.tile.wide p + p{margin-top:16px;color:var(--muted);font-size:16px}
.tile b{
  display:block;color:var(--violet-ink);font-family:var(--display);font-weight:700;
  font-size:clamp(32px,3.4vw,50px);letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1;
}
.tile span.k{display:block;margin-top:12px}
.tile.tools{grid-column:span 2}
.tools-row{display:flex;flex-wrap:wrap;gap:9px;margin-top:16px}
.tools-row span{
  border:1px solid var(--hair);border-radius:999px;padding:7px 14px;font-size:13px;color:var(--muted);
  transition:color .25s,border-color .25s;
}
.tools-row span:hover{color:var(--violet-ink);border-color:var(--violet-ink)}
.tools-row span:nth-child(even):hover{color:var(--lime-ink);border-color:var(--lime-ink)}
@media (max-width:900px){
  .bento{grid-template-columns:repeat(2,minmax(0,1fr))}
  .tile.wide,.tile.tools{grid-column:span 2;grid-row:auto}
}

/* ---------- liên hệ ---------- */
.contact{padding:130px 0 0;content-visibility:auto;contain-intrinsic-size:auto 700px}
.mail{
  display:inline-block;font-family:var(--display);font-weight:700;
  font-size:clamp(19px,4.6vw,62px);letter-spacing:-.025em;line-height:1.16;
  background-image:linear-gradient(var(--violet-ink),var(--violet-ink));
  background-size:0 2px;background-repeat:no-repeat;background-position:0 100%;
  transition:background-size .55s var(--ease),color .3s;white-space:nowrap;max-width:100%;
}
.mail:hover{background-size:100% 2px;color:var(--violet-ink)}
.contact .cols{display:flex;flex-wrap:wrap;gap:28px 76px;margin-top:52px;padding-top:28px;border-top:1px solid var(--hair)}
.contact .cols div{display:flex;flex-direction:column;gap:8px}
.contact .cols a,.contact .cols p{margin:0;color:var(--muted);font-size:15px;transition:color .25s}
.contact .cols a:hover{color:var(--violet-ink)}
footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:14px;padding:70px 0 44px;color:var(--muted)}
footer span{font-family:var(--mono);font-size:11px;letter-spacing:.06em}

/* ---------- panel tác phẩm ---------- */
.scrim{position:fixed;inset:0;z-index:75;background:rgba(4,3,2,.66);backdrop-filter:blur(3px);opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s}
.scrim.open{opacity:1;visibility:visible}
.peek{
  position:fixed;top:0;right:0;bottom:0;z-index:76;width:min(840px,96vw);
  background:var(--ground);border-left:1px solid var(--hair);border-radius:30px 0 0 30px;overflow:hidden;
  transform:translateX(100%);transition:transform .42s var(--ease);
  display:flex;flex-direction:column;
}
.peek.open{transform:none}
/* overflow-x:clip chặn thanh cuộn ngang lúc mở trang. Hero chờ ở
   transform:scale(1.05) cho tới khi body.ready bật — ở tỉ lệ đó nó rộng hơn
   khung nhìn 36px mỗi bên, đủ để trình duyệt hiện thanh cuộn ngang trong một
   hai giây đầu. Đã đo: khung 1425px, scrollWidth 1461px.
   PHẢI là clip chứ không phải hidden. hidden biến main thành vùng cuộn, mà
   vùng cuộn thì nuốt mất position:sticky và làm hỏng các hiệu ứng chạy theo
   vị trí cuộn bên trong. clip cắt mà không tạo vùng cuộn. */
main{overflow-x:clip;transition:transform .5s var(--ease)}
/* filter:blur(3px) trên main ĐÃ BỎ. Nó làm mờ LẦN THỨ HAI đúng vùng mà .scrim
   phía trên đã làm mờ bằng backdrop-filter:blur(3px). Tệ hơn, filter đặt lên
   main bắt trình duyệt vẽ lại toàn bộ trang — cả canvas hero và video đang
   chạy — rồi mới làm mờ, lặp lại mỗi khung hình suốt lúc panel mở. Đó là cơn
   khựng khi bấm vào thẻ.
   Một lớp mờ của scrim cộng nền tối 66% là đủ che; bỏ lớp thứ hai gần như
   không thấy khác. Giữ scale để trang vẫn lùi ra sau. */
body.peeking main{transform:scale(.985)}
.peek-bar{display:flex;align-items:center;gap:10px;padding:16px 24px;border-bottom:1px solid var(--hair);flex:none}
.peek-bar .nm{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.peek-bar .sp{flex:1}
.peek-body{overflow:auto;padding-bottom:90px}
.peek-shot{aspect-ratio:16/9;background:var(--raised);position:relative;overflow:hidden}
.peek-shot canvas,.peek-shot img,.peek-shot video{width:100%;height:100%;display:block;object-fit:cover}
.peek-in{width:min(720px,100% - 64px);margin:0 auto;padding-top:40px}
.peek h3{margin:12px 0 0;font-size:clamp(30px,4.6vw,54px)}
.peek .desc{margin:26px 0 0;font-size:18px;line-height:1.75;color:var(--muted);max-width:60ch}
.specs{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px;margin-top:40px}
.specs div{border:1px solid var(--hair);border-radius:16px;padding:16px 17px}
.specs .k{display:block;margin-bottom:8px}
.specs .v{font-size:15px}
.h4{margin:48px 0 16px}
.credits{margin:0;display:grid;grid-template-columns:auto 1fr;gap:11px 28px;font-size:15px;border-top:1px solid var(--hair);padding-top:20px}
.credits dt{color:var(--muted)}
.credits dd{margin:0}

/* Hai lớp này thay cho thuộc tính style="..." từng viết thẳng trong HTML.
   Không phải để cho gọn: Content-Security-Policy nghiêm chặn mọi style nội
   tuyến, nên còn một thuộc tính style= là phải mở 'unsafe-inline' cho cả
   trang — mà mở ra thì gần như mất hết tác dụng của CSP. */
.svg-defs{position:absolute}
.mail-row{margin-top:18px}

/* ---------- tác phẩm trong panel ---------- */
.peek-facts{
  margin:30px 0 0;display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));
  gap:18px 26px;border-top:1px solid var(--hair);padding-top:22px;
}
.peek-facts dt{
  font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--muted);margin-bottom:7px;
}
.peek-facts dd{margin:0;font-size:15px;line-height:1.5}

/* Một cột. Panel rộng nhất cũng chỉ 840px, chia đôi thì mỗi ô còn chưa tới
   360px — ảnh tác phẩm mà bày cỡ đó thì xem bằng thừa. Cuộn dọc rẻ hơn. */
.peek-gal{display:grid;gap:16px;margin-top:38px}
.peek-gal figure{
  position:relative;margin:0;aspect-ratio:16/9;border-radius:18px;overflow:hidden;
  background:var(--raised);border:1px solid var(--hair);
}
.peek-gal figure canvas,.peek-gal figure img,.peek-gal figure video{
  width:100%;height:100%;display:block;object-fit:cover;
}
.peek-gal figure.is-video{cursor:pointer}
.peek-gal figure.is-zoom{cursor:zoom-in}
/* ---------- video nhúng từ YouTube ---------- */
/* Ảnh chờ + nút play, chưa có iframe. Bấm vào mới thay bằng iframe thật.
   Trước lúc bấm thì trang chưa gọi sang YouTube một lần nào. */
.ytlite{
  display:block;width:100%;height:100%;padding:0;position:relative;
  cursor:pointer;background:var(--raised);
}
.ytlite img{width:100%;height:100%;display:block;object-fit:cover;transition:transform .8s var(--ease)}
.ytlite:hover img{transform:scale(1.03)}
.ytlite .play{
  position:absolute;left:14px;bottom:12px;display:flex;align-items:center;gap:7px;
  font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  color:#fff;text-shadow:0 1px 10px rgba(0,0,0,.6);pointer-events:none;
}
.ytlite .play svg{width:15px;height:15px;display:block}
/* Lối thoát ra YouTube thật. Nhỏ, nằm góc trên phải, không tranh chỗ với
   nút play — nhưng luôn có mặt, kể cả khi trình phát nhúng hỏng. */
.yt-out{
  position:absolute;right:12px;top:11px;z-index:2;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:rgba(255,255,255,.86);background:rgba(0,0,0,.42);
  border:1px solid rgba(255,255,255,.22);border-radius:999px;padding:5px 11px;
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  transition:color .25s,background .25s,border-color .25s;
}
.yt-out:hover{color:#fff;background:rgba(0,0,0,.66);border-color:rgba(255,255,255,.5)}

/* ---------- xem ảnh phóng to ---------- */
/* Nền tối đặc ở cả hai giao diện, không lấy theo --ground: xem ảnh thì thứ
   quanh nó càng tối càng đỡ chói mắt, kể cả khi trang đang ở bản sáng. */
.lb{
  position:fixed;inset:0;z-index:95;display:flex;
  background:rgba(6,5,10,.94);
  opacity:0;visibility:hidden;transition:opacity .25s ease,visibility .25s;
}
.lb.open{opacity:1;visibility:visible}
/* Ảnh nhích lên một chút khi mở, thay vì chỉ hiện ra. Đặt trên .lb-stage chứ
   không trên chính thẻ img: img đang giữ transform của phần phóng to kéo thả,
   ghi đè lên đó là hỏng cả hai. */
.lb-stage{transform:scale(.972);transition:transform .42s var(--ease)}
.lb.open .lb-stage{transform:none}
.lb-stage{
  flex:1;display:grid;overflow:hidden;
  /* Hàng và cột PHẢI khai rõ minmax(0,1fr), đừng để grid tự co theo nội dung.
     Hàng co theo nội dung là hàng có chiều cao "không xác định", mà phần trăm
     thì cần một mốc để quy ra — nên height:100% của ảnh bên trong bị coi như
     auto, ảnh dọc cao quá khung rồi bị overflow:hidden cắt cụt trên dưới.
     Đo lúc còn place-items:center: ảnh cao 1044px trong khung cao 780px. */
  grid-template:minmax(0,1fr) / minmax(0,1fr);
  place-items:stretch;
  padding:64px 24px 56px;touch-action:none;
}
.lb-stage img{
  /* ĐỪNG quay lại max-width/max-height:100%. Khung này là grid với hàng cao
     theo nội dung, nên phần trăm không có mốc để quy ra — trình duyệt coi
     max-height như không có, ảnh dọc tràn xuống dưới rồi bị overflow:hidden
     cắt mất. Đã đo: ảnh cao 1044px nằm trong khung cao 780px.
     width/height 100% cộng object-fit thì cỡ luôn xác định. Dùng scale-down
     chứ không phải contain: contain kéo cả ảnh nhỏ phóng to lên cho đầy
     khung, nhìn nhoè. */
  width:100%;height:100%;object-fit:scale-down;display:block;
  transform-origin:center center;
  /* Không đặt transition cho transform: lúc kéo và lúc chụm hai ngón, mỗi
     khung hình một giá trị mới — có transition là hình đuổi theo tay chậm
     một nhịp, rê thấy dính. */
  cursor:zoom-in;user-select:none;-webkit-user-drag:none;
}
.lb.zoomed .lb-stage img{cursor:grab}
.lb.zoomed .lb-stage img:active{cursor:grabbing}

.lb-bar{
  position:absolute;top:16px;right:18px;z-index:2;
  display:flex;align-items:center;gap:8px;
}
.lb-zoom{
  font-family:var(--mono);font-size:11px;letter-spacing:.12em;
  color:rgba(255,255,255,.6);margin-right:6px;font-variant-numeric:tabular-nums;
  min-width:44px;text-align:right;
}
.lb-b{
  width:36px;height:36px;border-radius:999px;display:grid;place-items:center;
  color:rgba(255,255,255,.82);border:1px solid rgba(255,255,255,.2);
  background:rgba(255,255,255,.06);
  transition:color .2s,border-color .2s,background .2s;
}
.lb-b svg{width:16px;height:16px;display:block}
.lb-b:hover{color:#fff;border-color:rgba(255,255,255,.5);background:rgba(255,255,255,.14)}
.lb-x{margin-left:8px}

.lb-hint{
  position:absolute;left:0;right:0;bottom:16px;text-align:center;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;
  color:rgba(255,255,255,.34);pointer-events:none;
  transition:opacity .3s ease;
}
.lb.zoomed .lb-hint{opacity:0}
@media (max-width:700px){
  .lb-stage{padding:60px 10px 44px}
  .lb-hint{display:none}
  .lb-zoom{display:none}
}
.peek-gal figure iframe,.gal figure iframe{width:100%;height:100%;display:block;border:0}
/* Nhãn chữ ở góc, không phải nút tròn đè giữa ảnh — che mất đúng phần đáng
   xem nhất của khung hình. */
.peek-gal .play{
  position:absolute;left:14px;bottom:12px;display:flex;align-items:center;gap:7px;
  font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  color:#fff;text-shadow:0 1px 10px rgba(0,0,0,.6);pointer-events:none;
  transition:opacity .3s ease;
}
.peek-gal .play svg{width:15px;height:15px;display:block}
.peek-gal figure.playing .play{opacity:0}
/* Đang phát thì bỏ cắt cúp, để nguyên tỉ lệ thật của video */
.peek-gal figure.playing{aspect-ratio:auto}
.peek-gal figure.playing video{height:auto;object-fit:contain}

.peek-brief{margin:38px 0 0;font-size:16.5px;line-height:1.8;color:var(--muted);max-width:62ch;white-space:pre-line}
.peek-steps{margin:0;padding:0;list-style:none;counter-reset:s}
/* PHẢI chỉ định cột cho CẢ h4 lẫn p. Ô này có ba phần tử con (::before, h4,
   p) mà chỉ có hai cột, nên nếu để grid tự xếp thì nó rải theo thứ tự: số
   vào cột 1, tiêu đề cột 2, rồi đoạn mô tả rơi xuống cột 1 của dòng sau —
   tức là bị ép vào cột rộng 44px, mỗi chữ một dòng. Đã đo: p rộng đúng 34px,
   cao 459px. Nhìn tưởng lỗi chữ, thật ra là lỗi xếp ô. */
.peek-steps li{
  counter-increment:s;border-top:1px solid var(--hair);padding:26px 0;
  display:grid;grid-template-columns:44px minmax(0,1fr);column-gap:20px;
}
.peek-steps li::before{
  content:counter(s,decimal-leading-zero);
  grid-column:1;grid-row:1 / span 2;
  /* tím giống .steps-lg bên trang chi tiết — cùng một khối nội dung thì đừng
     để hai nơi hiện hai màu khác nhau */
  font-family:var(--mono);font-size:11.5px;letter-spacing:.12em;color:var(--violet-ink);
  padding-top:5px;font-variant-numeric:tabular-nums;
}
.peek-steps h4{grid-column:2;grid-row:1;margin:0;font-size:17px;font-weight:500;letter-spacing:-.01em}
.peek-steps p{
  grid-column:2;grid-row:2;
  margin:9px 0 0;font-size:15px;line-height:1.75;color:var(--muted);max-width:58ch;
}
/* Gạch cuối thừa: khối tiếp theo đã có gạch trên của riêng nó */
.peek-steps li:last-child{padding-bottom:6px}

/* .peek-perma ĐÃ BỎ cùng với dòng "Open as its own page". Panel đã chứa đủ
   nội dung, còn muốn chia sẻ thì địa chỉ trên thanh trình duyệt đã là
   ?p=<slug> ngay khi mở panel. */

/* Ảnh bìa trong panel bấm được để xem phóng to. Video thì không: bấm vào
   video là để phát. */
.peek-shot.is-zoom{cursor:zoom-in}

/* ---------- bảng tìm ---------- */
.pal-scrim{
  position:fixed;inset:0;z-index:85;background:rgba(4,3,2,.62);backdrop-filter:blur(4px);
  display:flex;align-items:flex-start;justify-content:center;padding-top:13vh;
  opacity:0;visibility:hidden;transition:opacity .18s ease,visibility .18s;
}
.pal-scrim.open{opacity:1;visibility:visible}
.pal{
  width:min(660px,92vw);background:var(--raised);border:1px solid var(--hair-2);
  border-radius:24px;overflow:hidden;box-shadow:var(--shadow);
  transform:translateY(-8px) scale(.985);transition:transform .18s var(--ease);
}
.pal-scrim.open .pal{transform:none}
.pal input{
  width:100%;padding:21px 24px;border:0;border-bottom:1px solid var(--hair);
  background:none;color:var(--text);font-family:inherit;font-size:17px;
}
.pal input::placeholder{color:var(--muted)}
.pal .list{max-height:52vh;overflow:auto;padding:8px}
.pal .item{display:flex;align-items:baseline;gap:16px;width:100%;padding:12px 16px;border-radius:2px;font-size:15px;text-align:left}
.pal .item .idx{font-family:var(--mono);font-size:10.5px;color:var(--muted)}
.pal .item .t{font-size:15px}
.pal .item .m{margin-left:auto;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;color:var(--muted)}
.pal .item.sel,.pal .item:hover{background:var(--hair)}
.pal .item.sel .m{color:var(--violet-ink)}
.pal .none{padding:28px;text-align:center;color:var(--muted);font-size:14px}
.pal .foot{display:flex;gap:18px;padding:12px 20px;border-top:1px solid var(--hair);font-family:var(--mono);font-size:10px;letter-spacing:.08em;color:var(--muted)}
kbd{border:1px solid var(--hair);border-radius:3px;padding:1px 6px}

.toast{
  position:fixed;left:50%;bottom:30px;transform:translate(-50%,14px);z-index:90;
  background:var(--text);color:var(--ground);border-radius:999px;padding:12px 22px;font-size:13.5px;
  opacity:0;visibility:hidden;transition:opacity .22s ease,transform .22s ease,visibility .22s;
}
.toast.show{opacity:1;visibility:visible;transform:translate(-50%,0)}

@media (max-width:640px){
  .wrap{width:calc(100% - 32px)}
  .sec{padding-top:88px}
  .peek-in{width:calc(100% - 34px)}
  .grid{gap:44px}
}
/* ==========================================================================
   CHUYỂN ĐỘNG BỔ SUNG
   Rà cả trang rồi chỉ thêm vào những chỗ CÒN ĐỨNG IM. Cố tình không đụng tới
   những hiệu ứng đã có — thẻ hiện lần lượt, bìa nghiêng theo chuột, băng chữ
   chậm lại khi rê vào, các khối mờ dần theo cuộn. Chồng thêm lên chỉ làm loãng.
   Khối này nằm ngay TRƯỚC @media prefers-reduced-motion ở cuối file, mà khối
   đó tắt animation/transition bằng !important, nên mọi thứ dưới đây tự động
   im lặng với người đã tắt hiệu ứng trong hệ điều hành.
   ========================================================================== */

/* --- 1. ĐÃ BỎ: dấu chấm cạnh chữ qucannh trên thanh đầu trang ---
   Từng thêm một chấm tròn nhỏ sau chữ, cho khớp với icon trên tab. Bỏ theo
   yêu cầu. Icon trên tab thì GIỮ NGUYÊN — đó là hai thứ khác nhau, xem thẻ
   <link rel="icon"> trong index.html và tools/tao-favicon.js.
   Cũng đã bỏ luôn dòng .brand{gap:5px} để quy tắc gap gốc ở phần thanh đầu
   trang đứng một mình, khỏi phải đọc hai chỗ mới biết gap bằng bao nhiêu. */

/* --- 1b. ĐÃ BỎ: quy tắc text-transform:uppercase cho tên dự án ---
   Từng ép viết hoa toàn bộ tên ở thẻ, panel và trang chi tiết. Bỏ đi để tên
   hiện ĐÚNG NHƯ LƯU trong js/data.js — muốn dự án nào viết hoa thì gõ hoa
   thẳng vào trường title ở đó, như MAN UP, THAY QUẦN LUÔN và ANTISHOWOFF.
   Cách đó đúng hơn hẳn: viết hoa là quyết định của từng cái tên, không phải
   một luật áp chung cho mọi dự án. */

/* --- 2. Chấm trạng thái "Available for work" thở chậm ---
   Đây là dòng khách hàng liếc tìm đầu tiên. Một chấm đập chậm khiến nó đọc ra
   "đang thật sự mở nhận việc", khác hẳn một dòng chữ nằm chết. 2.4s một nhịp:
   đủ chậm để không giật mắt khi đang đọc phần khác. */
.avail::before{
  content:"";width:7px;height:7px;border-radius:50%;flex:none;margin-right:9px;
  background:var(--lime-ink);
  animation:availPulse 2.4s ease-in-out infinite;
}
@keyframes availPulse{
  0%,100%{opacity:1;transform:scale(1)}
  50%{opacity:.42;transform:scale(.76)}
}

/* --- 3. Phản hồi khi bấm ---
   Trước đây bấm vào chip lọc hay nút tròn đều không có gì nhúc nhích. Trên
   máy có chuột còn thấy được trạng thái hover, nhưng trên điện thoại KHÔNG hề
   có hover — chạm vào là im re cho tới khi nội dung đổi. Một cú lún 0.18s cho
   biết máy đã nhận ngón tay.
   Thẻ dự án cố tình không có ở đây: transform của .shot đang do js/app.js ghi
   thẳng vào thuộc tính style (hiệu ứng nghiêng theo chuột), mà style nội tuyến
   thắng mọi quy tắc trong file này — viết vào cũng không chạy. */
.chip,.tools-row span{
  transition:color .28s ease,border-color .28s ease,background .28s ease,
             transform .18s var(--ease);
}
.hbtn{transition:color .25s,border-color .25s,transform .18s var(--ease)}
.chip:active,.tools-row span:active,.hbtn:active{transform:scale(.93)}
.mail:active,.contact .cols a:active,.brand:active{opacity:.55}

/* --- 4. Ô thống kê (bento) khi rê chuột ---
   Sáu ô này trước giờ hoàn toàn bất động.
   KHÔNG dùng transform để nhấc ô lên, dù đó là cách quen tay nhất: .tile nằm
   trong danh sách chạy theo cuộn ở trên, và keyframe scrollIn/scrollOut có
   điều khiển transform với fill-mode both — nghĩa là transform của .tile LÚC
   NÀO cũng thuộc quyền animation. Animation luôn thắng transition trên cùng
   một thuộc tính, nên transform hover sẽ không bao giờ chạy.
   Viền và bóng đổ thì không keyframe nào đụng tới, dùng được. */
.tile{transition:border-color .4s var(--ease),box-shadow .5s var(--ease)}
.tile:hover{border-color:var(--hair-2);box-shadow:var(--shadow)}
.tile b{transition:color .4s ease}
.tile:hover b{color:var(--lime-ink)}

/* --- 5. Liên kết cột Liên hệ: gạch chân chạy ngang ---
   Cả trang dùng đúng một kiểu gạch chân — nền linear-gradient cao 1px, chạy
   background-size từ 0 lên 100% (xem .mail, .hero-foot .col a, header nav a).
   Riêng cột dưới chân trang chỉ đổi màu, lạc nhịp với phần còn lại.
   align-self:flex-start là bắt buộc: cột cha là flex dọc nên thẻ a bị kéo dài
   bằng cả cột, không có nó thì gạch chân chạy hết chiều ngang cột chứ không
   dừng ở cuối chữ. */
.contact .cols a{
  align-self:flex-start;
  background-image:linear-gradient(var(--violet-ink),var(--violet-ink));
  background-size:0 1px;background-repeat:no-repeat;background-position:0 100%;
  transition:background-size .45s var(--ease),color .25s;
}
.contact .cols a:hover{background-size:100% 1px}

/* --- 6. Vòng nét khi đi bằng bàn phím ---
   Trước chỉ khai focus-visible đúng một chỗ, nên bấm Tab qua trang gần như
   không biết mình đang đứng ở đâu. Dùng outline chứ không phải box-shadow:
   outline không tính vào bố cục nên không đẩy gì sang chỗ khác, và nó tự bám
   theo hình bo tròn của phần tử. */
a:focus-visible,button:focus-visible{
  outline:2px solid var(--violet-ink);outline-offset:3px;
}
.card:focus-visible{outline-offset:7px}

@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    /* --- 7. Chân trang hiện lên theo cuộn ---
       Mọi khối khác đều có lối vào riêng, chỉ dòng bản quyền là bật ra đột
       ngột. Chỉ lấy scrollIn, không lấy scrollOut: chân trang là thứ cuối
       cùng của trang nên đoạn "trôi ra" không bao giờ chạy hết, gắn vào chỉ
       khiến nó nhạt đi vô cớ khi người ta cuộn tới đáy. */
    footer{
      animation:scrollIn linear both;
      animation-timeline:view();
      animation-range:entry 10% entry 70%;
    }
  }
}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation:none!important;transition-duration:.01ms!important}
  .reveal,.card{opacity:1;transform:none}
  .hero h1 .w,.split .w{transform:none;opacity:1;filter:none}
  .hero-foot{opacity:1;transform:none}
  .card .shot{clip-path:none}
  body.peeking main{transform:none;filter:none}
}
