/* ==========================================================================
   animations.css — Hệ chuyển động
   Trích từ reference jmiiwedding.com/dathaaa (LadiPage), diễn đạt lại bằng
   token của tokens.css.

   Reference dùng 7 keyframes: fadeInUp / fadeInDown / fadeInLeft /
   fadeInRight / rotateInDownLeft / rotateInDownRight / pulse.
   Reveal chạy 1 lần (animation-iteration-count: 1), 1s–2s, delay bậc
   0s / 0.5s / 0.7s / 1s / 1.5s; chỉ "pulse" là infinite.
   Reference KHÔNG có prefers-reduced-motion — ta bắt buộc phải có.

   Quy ước:
   - Mọi thời lượng / easing / màu lấy từ tokens.css.
   - JS chỉ việc bật/tắt class (.is-visible, .is-open, .is-ticking).
   - Toàn bộ chuyển động bị vô hiệu trong @media (prefers-reduced-motion).
   ========================================================================== */

/* --- Token chuyển động bổ sung -----------------------------------------
   tokens.css chưa có thời lượng cho lớp trôi nổi chậm (6–11s) và cho
   hiệu ứng lấp lánh. Khai báo tập trung tại đây; nếu tokens.css được mở
   rộng thì chuyển các biến này về đó và xoá block này.               */
:root {
  --dur-drift: 8s;         /* lớp hoạ tiết trôi — mặc định */
  --dur-drift-slow: 11s;   /* lớp nền lớn, trôi chậm hơn */
  --dur-drift-fast: 6s;    /* nhánh lá nhỏ, trôi nhanh hơn */
  --dur-shimmer: 6.5s;     /* vệt sáng quét trên hoạ tiết vàng */
  --dur-glow: 7s;          /* nhịp toả sáng ánh nến */
  --dur-pulse: 2.4s;       /* nhịp đập nhẹ (mũi chỉ đường, gợi ý cuộn) */
  --dur-blink: 0.9s;       /* nửa nhịp nháy — chạy alternate nên chu kỳ = 1.8s */
}

/* ==========================================================================
   1. REVEAL KHI CUỘN VÀO KHUNG NHÌN
   JS (IntersectionObserver, chạy một lần) thêm .is-visible.
   Biên độ / độ trễ tuỳ biến qua --reveal-shift / --reveal-delay.
   Lưu ý layout: --reveal-shift dịch ngang 24px, nên phần tử .reveal--left
   / .reveal--right không được là khối tràn viền (full-bleed) — nếu không sẽ
   sinh thanh cuộn ngang. Khối cha nên có overflow-x: clip.
   ========================================================================== */
.reveal {
  --reveal-shift: 24px;
  --reveal-scale: 0.94;
  --reveal-tilt: 8deg;

  opacity: 0;
  transform: translateY(var(--reveal-shift));
  transition:
    opacity var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}

/* Biến thể hướng vào — tương ứng fadeInLeft / fadeInRight / fadeInDown
   và rotateInDownLeft / rotateInDownRight của reference.              */
.reveal--left {
  transform: translateX(calc(var(--reveal-shift) * -1));
}

.reveal--right {
  transform: translateX(var(--reveal-shift));
}

.reveal--down {
  transform: translateY(calc(var(--reveal-shift) * -1));
}

.reveal--scale {
  transform: scale(var(--reveal-scale));
}

.reveal--rotate-left {
  transform-origin: left bottom;
  transform: rotate(calc(var(--reveal-tilt) * -1));
}

.reveal--rotate-right {
  transform-origin: right bottom;
  transform: rotate(var(--reveal-tilt));
}

/* Trạng thái đã hiện — thắng mọi biến thể ở trên về specificity. */
.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* Xếp bậc tự động cho các con trực tiếp: mỗi con trễ thêm 80ms.
   Dùng khi không muốn viết --reveal-delay bằng tay trong render.js. */
.reveal-stagger > .reveal:nth-child(1)  { --reveal-delay: 0ms; }
.reveal-stagger > .reveal:nth-child(2)  { --reveal-delay: 80ms; }
.reveal-stagger > .reveal:nth-child(3)  { --reveal-delay: 160ms; }
.reveal-stagger > .reveal:nth-child(4)  { --reveal-delay: 240ms; }
.reveal-stagger > .reveal:nth-child(5)  { --reveal-delay: 320ms; }
.reveal-stagger > .reveal:nth-child(6)  { --reveal-delay: 400ms; }
.reveal-stagger > .reveal:nth-child(7)  { --reveal-delay: 480ms; }
.reveal-stagger > .reveal:nth-child(8)  { --reveal-delay: 560ms; }
.reveal-stagger > .reveal:nth-child(9)  { --reveal-delay: 640ms; }
.reveal-stagger > .reveal:nth-child(10) { --reveal-delay: 720ms; }
.reveal-stagger > .reveal:nth-child(n + 11) { --reveal-delay: 800ms; }

/* ==========================================================================
   2. TRÔI NỔI CHẬM CHO LỚP HOẠ TIẾT LÁ / HOA
   Nhấc lên vài px kèm nghiêng 1–2 độ, ease-in-out, alternate vô hạn.
   Delay ÂM để các lớp bắt đầu ở pha khác nhau ngay từ khung hình đầu,
   không nhấp nhô đồng loạt.
   ========================================================================== */
@keyframes drift-float {
  from {
    transform: translateY(0) rotate(0deg);
  }
  to {
    transform: translateY(var(--drift-shift, -10px)) rotate(var(--drift-tilt, 1.5deg));
  }
}

.drift {
  animation-name: drift-float;
  animation-duration: var(--dur-drift);
  animation-timing-function: var(--ease-in-out);
  animation-direction: alternate;
  animation-iteration-count: infinite;
  animation-delay: var(--drift-delay, 0s);
}

.drift--slow {
  --drift-shift: -14px;
  --drift-tilt: 1deg;
  animation-duration: var(--dur-drift-slow);
}

.drift--fast {
  --drift-shift: -7px;
  --drift-tilt: 2deg;
  animation-duration: var(--dur-drift-fast);
}

/* Lệch pha mặc định theo thứ tự trong DOM (áp cho tối đa 8 lớp/section).
   Ghi đè bằng --drift-delay nếu cần kiểm soát chính xác hơn.         */
.drift:nth-child(2) { --drift-delay: -1.3s; }
.drift:nth-child(3) { --drift-delay: -2.6s; }
.drift:nth-child(4) { --drift-delay: -3.9s; }
.drift:nth-child(5) { --drift-delay: -0.7s; }
.drift:nth-child(6) { --drift-delay: -2s; }
.drift:nth-child(7) { --drift-delay: -3.2s; }
.drift:nth-child(8) { --drift-delay: -4.5s; }

/* ==========================================================================
   3. MỞ PHONG BÌ Ở MÀN INTRO
   --------------------------------------------------------------------------
   TRẠNG THÁI MẶC ĐỊNH LÀ "GẤP KÍN": .envelope__flap úp xuống che mép trước,
   dấu sáp còn nguyên. Nút "Mở thiệp" nhờ vậy mới có việc để làm. Bản trước
   ship sẵn nắp ĐÃ MỞ mà vẫn dán sáp — một phong bì vừa mở vừa niêm phong —
   và animation cũ quay cả tấm ảnh mặt sau 650×710 quanh mép TRÊN, tức là lật
   nguyên thân phong bì ra sau chứ không phải giở nắp.

   JS thêm .is-open lên .envelope → BỐN nhịp nối tiếp (số ms ở tokens.css):
     nhịp 0 (chỉ khi có mặt trước, xem mục 3.0)
       0    → 900ms   .envelope__face   lật mặt trước ↔ mặt sau
     rồi chuỗi mở, mốc 0 của nó dời muộn --envelope-offset (0 hoặc 760ms):
     +0    → 260ms   .envelope__seal   nứt rồi tan    envelope-seal-break
     +220  → 1120ms  .envelope__flap   giở lên        envelope-flap-open
     +640  → 1720ms  .envelope__card   trồi lên + giữ envelope-card-rise
   Mốc cuối = --envelope-offset + --dur-envelope-total; envelope.js CỘNG đúng
   hai token đó để đặt timeout dự phòng ⇒ ĐỔI THỜI LƯỢNG Ở ĐÂY THÌ PHẢI CỘNG
   LẠI TOKEN, nếu không lớp phủ có thể bị ẩn khi animation còn đang chạy.

   ⚠ MỌI GIÁ TRỊ DỰ PHÒNG TRONG var(--dur-…, X) PHẢI BẰNG ĐÚNG TOKEN. Ba dòng
   trong file này từng ghi 720ms / 760ms / 520ms trong khi token là 900 / 1080
   / 640 — chết lâm sàng khi tokens.css nạp được, nhưng chúng âm thầm mã hoá
   một DÒNG THỜI GIAN NGẮN HƠN, tức đúng lúc tokens.css lỗi (kịch bản duy nhất
   dự phòng có việc) thì CSS và timeout của JS lại nói hai chuyện khác nhau.

   CẢNH BÁO nội dung: .envelope__card mặc định opacity 0 (thiếp đang NẰM TRONG
   phong bì) nên KHÔNG đặt nội dung thiết yếu (tên, ngày, địa điểm) chỉ ở trong
   thiếp này — bản thật phải nằm ở #hero.
   ========================================================================== */
.envelope {
  position: relative;

  /* Độ sâu phối cảnh. Tách thành biến vì .envelope__face--back PHẢI LẶP LẠI
     đúng giá trị này (sections-hero.css): `perspective` chỉ áp cho CON TRỰC
     TIẾP, mà từ khi có mặt lật thì nắp phong bì không còn là con trực tiếp của
     .envelope nữa — nó nằm trong mặt sau. Thiếu dòng lặp đó thì cú giở nắp mất
     hết chiều sâu, chỉ còn co dẹt theo phương đứng. */
  --envelope-perspective: 900px;
  perspective: var(--envelope-perspective);

  /* MỐC 0 CỦA CHUỖI MỞ, tính từ lúc bấm nút. Không có mặt trước → chuỗi chạy
     ngay (0ms). Có mặt trước → cả ba nhịp lùi lại đúng một lần, bằng đúng
     một biến. Đây là lý do các luật bên dưới đều viết
     `calc(var(--envelope-offset) + var(--dur-envelope-…-in))` thay vì nhân đôi
     mỗi luật thành hai bản có/không lật: nhân đôi thì kiểu gì cũng có ngày sửa
     bản này quên bản kia. */
  --envelope-offset: 0ms;
}

/* ==========================================================================
   3.0 LẬT PHONG BÌ — MẶT TRƯỚC ↔ MẶT SAU
   --------------------------------------------------------------------------
   Màn intro mở ra ở MẶT TRƯỚC (mặt ghi người nhận, xem sections-hero.css mục
   0B). Bấm "Mở thiệp" → phong bì lật nửa vòng quanh trục dọc, để lộ MẶT SAU
   (nắp + dấu sáp) rồi mới chạy chuỗi mở.

   ⚠ VÌ SAO KHÔNG DÙNG KIỂU FLIP-CARD KINH ĐIỂN (một wrapper `preserve-3d`
   chứa hai mặt):
   `transform-style: preserve-3d` kéo TẤT CẢ con vào chung MỘT ngữ cảnh 3D, mà
   trong ngữ cảnh 3D thứ tự vẽ do TOẠ ĐỘ Z quyết định, z-index chỉ còn dùng để
   phân xử các lớp ĐỒNG PHẲNG. Sáu lớp mặt sau ở đây sống chết bằng z-index —
   nhất là @keyframes envelope-flap-open, nó ĐỔI z-index 6 → 2 giữa chừng để
   nắp chui xuống dưới tấm thiếp. Đưa chúng vào preserve-3d là cầm chắc vỡ
   thứ tự lớp ở đúng cái animation tinh vi nhất của cả trang.

   CÁCH DÙNG Ở ĐÂY: hai mặt là hai phần tử ANH EM, mỗi mặt tự quay nửa vòng
   của mình (trước 0 → -180deg, sau 180deg → 0). Cùng chiều, cùng thời lượng
   nên mắt đọc ra là MỘT tờ giấy đang lật. Mỗi mặt vẫn là một ngữ cảnh PHẲNG
   → z-index bên trong mặt sau giữ nguyên hành vi cũ từng pixel.

   ĐỔI MẶT bằng `visibility` trong keyframe (hidden ↔ visible tại đúng 50%),
   KHÔNG chỉ dựa vào backface-visibility: `visibility` là bước nhảy rời rạc,
   chắc chắn, không phụ thuộc trình duyệt làm tròn góc quay quanh mốc 90deg.
   backface-visibility vẫn đặt thêm ở sections-hero.css như lớp bảo hiểm thứ
   hai cho khe sub-pixel ngay tại 90deg.

   Chiều quay ÂM (0 → -180deg): góc âm cho mép PHẢI tiến về phía người xem rồi
   vòng sang trái — đúng động tác cầm phong bì lật bằng tay phải. Góc dương
   đẩy mép phải ra sau, trông như tờ giấy bị gió thổi ngược.
   ========================================================================== */
.envelope.has-front {
  --envelope-offset: var(--dur-envelope-front-offset, 760ms);
}

/* Lớp bảo hiểm THỨ HAI cho khe sub-pixel ngay tại 90deg — `visibility` trong
   keyframe mới là cơ chế chính. Cố ý CHỈ áp khi .has-front: không có mặt trước
   thì mặt sau chẳng bao giờ quay, thêm backface-visibility vào chỉ tổ ép trình
   duyệt tạo layer thừa cho một phần tử đứng yên. */
.envelope.has-front .envelope__face {
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

/* will-change CHỈ trong lúc mở, KHÔNG đặt sẵn ở trạng thái nghỉ. Hai mặt phủ
   gần trọn khung phong bì, mỗi mặt tách một layer riêng tốn ~6.5MB bộ nhớ GPU
   ở khổ 390px/DPR3 — mà trước khi khách bấm thì cả hai đứng im, giữ layer sẵn
   là trả tiền suốt thời gian ngồi nhìn. Gắn cùng khung hình với animation vẫn
   kịp: Chrome tự nâng layer cho animation transform, will-change ở đây chỉ là
   lời báo trước, không phải điều kiện. */
.envelope.has-front.is-open .envelope__face {
  will-change: transform;
}

@keyframes envelope-turn-front {
  from {
    transform: rotateY(0deg);
    visibility: visible;
  }
  49.99% {
    visibility: visible;
  }
  50% {
    visibility: hidden;
  }
  to {
    transform: rotateY(-180deg);
    visibility: hidden;
  }
}

@keyframes envelope-turn-back {
  from {
    transform: rotateY(180deg);
    visibility: hidden;
  }
  49.99% {
    visibility: hidden;
  }
  50% {
    visibility: visible;
  }
  to {
    transform: rotateY(0deg);
    visibility: visible;
  }
}

/* Trạng thái NGHỈ (chưa bấm): mặt trước quay về phía người xem, mặt sau úp
   ngược và đang ẩn. Chỉ áp khi .has-front — không có mặt trước thì mặt sau
   phải hiện y như trước giờ, không transform, không visibility. */
.envelope.has-front .envelope__face--front {
  transform: rotateY(0deg);
  visibility: visible;
}

.envelope.has-front .envelope__face--back {
  transform: rotateY(180deg);
  visibility: hidden;
}

.envelope.has-front.is-open .envelope__face--front {
  animation: envelope-turn-front var(--dur-envelope-flip, 900ms) var(--ease-in-out) both;
}

.envelope.has-front.is-open .envelope__face--back {
  animation: envelope-turn-back var(--dur-envelope-flip, 900ms) var(--ease-in-out) both;
}

/* --- Nắp phong bì -------------------------------------------------------
   Nắp là ảnh envelope-ivory-open.png bị clip-path cắt còn đúng tam giác trên;
   bản lề = transform-origin đặt trùng đường cắt đó, nên nắp quay quanh chính
   mép trên thân phong bì. sections-hero.css lo clip-path + toạ độ + z-index,
   file này chỉ lo góc quay.

   Góc ÂM (-180deg), KHÔNG phải +180deg: với origin ở bản lề, góc âm cho mũi
   nắp vòng ra PHÍA TRƯỚC người xem rồi mới dựng lên — đúng động tác mở thư.
   Góc dương làm nắp lộn ngược ra sau, xuyên qua ruột phong bì.

   KHÔNG dùng backface-visibility: hidden (bản cũ có): lúc gấp kín ta đang nhìn
   đúng MẶT SAU của phần tử, ẩn mặt sau đi thì phong bì đóng sẽ tàng hình. Tam
   giác đối xứng và vân giấy đều nên ảnh bị lật không hề lộ.
   Cũng bỏ transform-style: preserve-3d — nắp không có con 3D nào, mà giữ lại
   thì xung đột với clip-path (clip ép phẳng ngữ cảnh 3D). */
.envelope__flap {
  transform-origin: 50% var(--envelope-hinge, 42.39%);
  transform: rotateX(var(--envelope-flap-closed, -180deg));
  /* will-change chỉ dùng ở 4 phần tử của cả trang, tất cả đều thuộc màn phong
     bì và tất cả đều là chuyển động 3D toàn khung: nắp, thiếp, và hai mặt lật
     (mục 3.0). Đó là những chỗ tách layer có lợi rõ rệt.
     Dấu sáp và chữ script CỐ TÌNH không có: chỉ 260ms, diện tích rất nhỏ. */
  will-change: transform;
}

/* z-index đổi GIỮA CHỪNG là bắt buộc, không phải trang trí:
     · nửa đầu — nắp còn úp trước mép trước ⇒ phải nằm TRÊN mép trước;
     · nửa sau — nắp đã dựng vào vùng của tấm thiếp ⇒ phải nằm DƯỚI thiếp,
       không thì tam giác ivory đè lên tấm thiếp đang trồi ra.
   Đổi ở đúng 50% vì lúc đó nắp quay ngang (rotateX -90deg), bề cao chiếu bằng
   0 nên mắt không bắt được cú nhảy. Phải có CẢ HAI keyframe 49.99% và 50%:
   z-index là <integer> nên nếu để nội suy dần (6→5→4→3) nó sẽ lần lượt bằng
   đúng lớp mép trước rồi lớp thiếp, gây chớp sai thứ tự suốt nửa sau. */
@keyframes envelope-flap-open {
  from {
    transform: rotateX(var(--envelope-flap-closed, -180deg));
    z-index: var(--envelope-flap-z-shut, 6);
  }
  49.99% {
    z-index: var(--envelope-flap-z-shut, 6);
  }
  50% {
    z-index: var(--envelope-flap-z-open, 2);
  }
  to {
    transform: rotateX(0deg);
    z-index: var(--envelope-flap-z-open, 2);
  }
}

.envelope.is-open .envelope__flap {
  animation: envelope-flap-open var(--dur-envelope-flap, 900ms) var(--ease-in-out)
    calc(var(--envelope-offset, 0ms) + var(--dur-envelope-flap-in, 220ms)) both;
}

/* --- Dấu sáp + chữ trên mặt nắp -----------------------------------------
   Nhịp "bóc niêm": ép nhẹ rồi vỡ vụn. Chạy TRƯỚC nắp để khách kịp thấy nguyên
   nhân — sáp gần tan hết (260ms) thì nắp mới nhúc nhích (220ms, chồng 40ms).
   Chữ script nằm TRÊN mặt nắp nhưng là phần tử anh em nên không quay theo
   được; cho nó mờ đi cùng lúc với sáp thì mắt không kịp bắt lỗi. */
@keyframes envelope-seal-break {
  0% {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }
  35% {
    opacity: 1;
    transform: scale(1.07) rotate(-2.5deg);
  }
  100% {
    opacity: 0;
    transform: scale(0.86) rotate(3deg);
  }
}

@keyframes envelope-script-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

.envelope.is-open .envelope__seal {
  animation: envelope-seal-break var(--dur-envelope-seal, 260ms) var(--ease-in-out)
    calc(var(--envelope-offset, 0ms) + var(--dur-envelope-seal-in, 0ms)) both;
}

.envelope.is-open .envelope__script {
  animation: envelope-script-out var(--dur-envelope-seal, 260ms) var(--ease-out)
    calc(var(--envelope-offset, 0ms) + var(--dur-envelope-seal-in, 0ms)) both;
}

/* --- Thiếp --------------------------------------------------------------- */
@keyframes envelope-card-rise {
  0% {
    opacity: 0;
    transform: translateY(0) scale(0.97);
  }
  34% {
    opacity: 1;
  }
  /* Thiếp đạt vị trí cuối ở 70% rồi GIỮ YÊN tới 100%: nhịp dừng này (~30% thời
     lượng = 324ms) là để khách đọc kịp tên + ngày trước khi lớp phủ đóng.
     HAI MỐC % NÀY ĐI KÈM --dur-envelope-card: chúng được đẩy từ 28%/58% lên
     34%/70% khi thời lượng rút 1300 → 1080ms, cốt để mốc TUYỆT ĐỐI không đổi
     (0.58×1300 = 754ms ≈ 0.70×1080 = 756ms) — tức thiếp trồi lên đúng tốc độ
     cũ, chỉ đứng yên ngắn lại. Đổi thời lượng mà quên đổi hai số này là vô
     tình làm thiếp bay vọt lên. */
  70% {
    opacity: 1;
    transform: translateY(var(--envelope-lift, -38%)) scale(1);
  }
  100% {
    opacity: 1;
    transform: translateY(var(--envelope-lift, -38%)) scale(1);
  }
}

.envelope__card {
  opacity: 0;
  transform: translateY(0) scale(0.97);
  will-change: transform, opacity;
}

.envelope.is-open .envelope__card {
  /* Trễ tới lúc nắp sắp qua phương ngang (50% của nắp = 670ms) rồi thiếp mới
     hiện: trước mốc đó phong bì vẫn còn đọc là "đang đóng", thiếp mà sáng lên
     phía trên nó thì thành ra thiếp nổi bên ngoài một phong bì chưa mở. */
  animation: envelope-card-rise var(--dur-envelope-card, 1080ms) var(--ease-out)
    calc(var(--envelope-offset, 0ms) + var(--dur-envelope-card-in, 640ms)) both;
}

/* ==========================================================================
   4. ĐỔI SỐ ĐỒNG HỒ ĐẾM NGƯỢC
   JS gỡ rồi gắn lại .is-ticking mỗi lần chữ số thay đổi (reflow để
   animation chạy lại). Rất nhẹ: nhấc 2px + scale 1.06 + mờ nhẹ.
   Nhớ đặt font-variant-numeric: tabular-nums cho ô số ở CSS component
   để giây đổi không làm nhảy layout.
   ========================================================================== */
@keyframes digit-pop {
  from {
    opacity: 0.55;
    transform: translateY(-2px) scale(1.06);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.is-ticking {
  animation: digit-pop var(--dur-base) var(--ease-out) 1;
}

/* ==========================================================================
   5. NÚT / LINK VÀ VÒNG FOCUS DÙNG CHUNG
   Reference: nút nhấc nhẹ khi hover, lún xuống khi :active
   (.ladi-button:active { transform: translateY(2px) }).
   ========================================================================== */
.btn,
.btn--solid,
.link {
  transition:
    color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.btn:hover,
.btn--solid:hover {
  transform: translateY(-1px);
}

.btn:active,
.btn--solid:active {
  transform: translateY(1px);
}

/* Nút nền sage: đậm lại khi hover. */
.btn--solid:hover {
  background-color: var(--sage-deep);
}

.link:hover {
  color: var(--sage-deep);
}

/* Vòng focus dùng chung — luôn thấy được, màu --sage.
   LƯU Ý QUAN TRỌNG (đã sửa sau khi render thật):
   tabindex ÂM (`tabindex="-1"`) nghĩa là "chỉ focus được bằng JS, KHÔNG nằm
   trong luồng Tab" — đó là các mốc dời focus, không phải phần tử tương tác:
     · envelope.js đặt tabindex="-1" lên <h1> của hero rồi focus vào đó sau khi
       đóng màn phong bì → nếu không loại trừ, một khung viền sage 2px sẽ vẽ
       quanh TÊN CÔ DÂU CHÚ RỂ (đã quan sát được trong Chrome khi bấm "Mở thiệp").
     · rsvp.js đặt tabindex="-1" lên .rsvp-success__panel.
     · gallery.js / rsvp.js dùng cùng kỹ thuật cho các lớp phủ.
   Vì vậy chỉ bắt tabindex KHÔNG âm. Đừng bỏ :not() này ra. */
.focus-ring:focus-visible,
a:focus-visible,
button:focus-visible,
[role='button']:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:not([tabindex^='-']):focus-visible {
  outline: 2px solid var(--sage);
  outline-offset: 3px;
  box-shadow: 0 0 0 4px rgba(var(--sage-rgb), 0.22);
  transition: outline-offset var(--dur-fast) var(--ease-out);
}

/* Mốc dời focus: không bao giờ vẽ vòng focus, kể cả khi trình duyệt tự cho
   nó khớp :focus-visible (Chrome làm vậy khi focus được dời bằng JS). */
[tabindex^='-']:focus,
[tabindex^='-']:focus-visible {
  outline: none;
  box-shadow: none;
}

/* Bỏ viền mặc định khi focus bằng chuột, giữ nguyên cho bàn phím. */
.focus-ring:focus:not(:focus-visible),
a:focus:not(:focus-visible),
button:focus:not(:focus-visible),
[tabindex]:focus:not(:focus-visible) {
  outline: none;
}

/* ==========================================================================
   6. LẤP LÁNH / TOẢ SÁNG CHO HOẠ TIẾT VÀNG
   Rất tiết chế: một vệt --gold mờ quét ngang, và một nhịp --glow
   (ánh nến) rất chậm. Không dùng đồng thời trên cùng phần tử.
   ========================================================================== */
@keyframes gold-shimmer {
  0% {
    background-position: 180% 0;
  }
  60%,
  100% {
    background-position: -80% 0;
  }
}

.shimmer {
  position: relative;
  overflow: hidden;
}

.shimmer::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: linear-gradient(
    105deg,
    transparent 38%,
    var(--gold) 50%,
    transparent 62%
  );
  background-size: 260% 100%;
  background-repeat: no-repeat;
  opacity: 0.26;
  animation: gold-shimmer var(--dur-shimmer) var(--ease-in-out) infinite;
}

@keyframes gold-glow {
  from {
    opacity: 0.82;
    filter: drop-shadow(0 0 0 var(--glow));
  }
  to {
    opacity: 1;
    filter: drop-shadow(0 0 6px var(--glow));
  }
}

.glow {
  animation: gold-glow var(--dur-glow) var(--ease-in-out) infinite alternate;
}

/* Nhịp đập nhẹ — bản tiết chế của "pulse" trong reference (scale 1.1 → 1.05),
   dùng cho gợi ý cuộn ở hero và ghim "chỉ đường".                    */
@keyframes pulse-soft {
  from {
    transform: scale(1);
  }
  to {
    transform: scale(1.05);
  }
}

.pulse-soft {
  animation: pulse-soft var(--dur-pulse) var(--ease-in-out) infinite alternate;
}

/* Nháy — dùng cho trái tim khoanh ngày cưới trong bảng lịch.
   Vì sao KHÔNG dùng .pulse-soft ở đó: pulse-soft chỉ đổi scale 1 → 1.05, mà
   trái tim trong ô ngày chỉ rộng 1.5em (đo thật: 20×17px) nên biên độ đó ra
   đúng 1px — nhìn bằng mắt thường không thấy nó động đậy.

   Ba thứ đổi CÙNG LÚC để nhịp nháy đọc được ở cỡ nhỏ:
     · opacity 0.3 → 1  — phần "nháy" thật sự, tim mờ hẳn rồi sáng lại;
     · scale 0.92 → 1.12 — biên độ gấp 4 lần pulse-soft, đủ thấy ở 20px;
     · drop-shadow 0 → 4px — quầng sáng --ivory làm nét trắng bật khỏi nền sage
       ở đỉnh sáng, nếu không thì riêng opacity trông như ảnh đang tải dở.
   Toạ độ tim do `inset-*` lo (sections-event.css), KHÔNG có transform nào khác
   trên phần tử này nên scale ở đây không đè lên vị trí.

   opacity đáy để 0.3 chứ không phải 0: tim biến mất hẳn rồi hiện lại trông
   như lỗi render, còn 0.3 vẫn để lại một vệt mờ nên mắt đọc ra là "đang nháy".

   --blink-delay để chỗ gọi lệch pha nhiều trái tim; PHẢI là số ÂM. Delay dương
   giữ phần tử đứng yên ở khung hình đầu (opacity 0.3) suốt thời gian chờ, tức
   trái tim thứ hai sẽ mờ câm một lúc trước khi bắt nhịp. */
@keyframes blink-soft {
  from {
    opacity: 0.3;
    transform: scale(0.92);
    filter: drop-shadow(0 0 0 rgba(var(--ivory-rgb), 0));
  }
  to {
    opacity: 1;
    transform: scale(1.12);
    filter: drop-shadow(0 0 4px rgba(var(--ivory-rgb), 0.9));
  }
}

.blink-soft {
  animation: blink-soft var(--dur-blink) var(--ease-in-out) var(--blink-delay, 0s)
    infinite alternate;
}

/* Nháy — bản dành cho TRÁI TIM ĐỎ sau tên người nhận trên mặt trước phong bì
   (khách mở link có ?to=…). Vì sao không dùng thẳng .blink-soft: quầng sáng
   của nó là --ivory, đúng cho tim TRẮNG trên nền sage ở bảng lịch, nhưng tim
   này là tim ĐỎ nằm trên GIẤY IVORY — quầng ivory trên nền ivory là vô hình,
   nên riêng phần "bật khỏi nền" mất sạch, chỉ còn opacity nhấp nháy trơ trọi.

   Ba khác biệt so với blink-soft, đều có lý do:
     · quầng đổi sang --wax-red-rgb, alpha 0.55 (thấp hơn 0.9 của bản ivory):
       đỏ trên giấy sáng ăn mắt hơn trắng trên sage, để 0.9 thì trái tim viền
       loang như vết mực thấm.
     · opacity đáy 0.35 thay vì 0.3 — cùng lý do đã ghi ở blink-soft (tắt hẳn
       trông như lỗi render), nhích lên chút vì tim này nhỏ hơn tim bảng lịch.
     · scale đáy 0.94 thay vì 0.92: tim nằm NGAY SAU chữ ký tay, biên độ lớn
       quá thì nó nảy so với hàng chữ đứng yên, đọc ra là lỗi chứ không phải
       nhịp tim.

   Nhịp: --dur-blink = 0.9s chạy `alternate` ⇒ một chu kỳ đầy đủ 1.8s, tức
   ~0.55 lần nháy/giây. WCAG 2.3.1 cấm quá 3 lần/giây — còn rất xa ngưỡng.
   ĐỪNG rút --dur-blink xuống dưới 0.35s nếu sau này có ai chỉnh. */
@keyframes blink-heart {
  from {
    opacity: 0.35;
    transform: scale(0.94);
    filter: drop-shadow(0 0 0 rgba(var(--wax-red-rgb), 0));
  }
  to {
    opacity: 1;
    transform: scale(1.12);
    filter: drop-shadow(0 0 3px rgba(var(--wax-red-rgb), 0.55));
  }
}

.blink-heart {
  animation: blink-heart var(--dur-blink) var(--ease-in-out) infinite alternate;
}

/* ==========================================================================
   7. TÔN TRỌNG prefers-reduced-motion
   Tắt toàn bộ chuyển động. Nguyên tắc: KHÔNG phần tử nào bị bỏ lại ở
   opacity 0 — mọi nội dung phải hiện đầy đủ ngay lập tức.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) and (max-width: 0px) { /* guard tắt theo yêu cầu: thiệp LUÔN chạy animation. Xoá ' and (max-width: 0px)' để bật lại tôn trọng giảm chuyển động. */
  /* 7.1 Reveal: hiện hết, không dịch chuyển, không transition. */
  .reveal,
  .reveal--left,
  .reveal--right,
  .reveal--down,
  .reveal--scale,
  .reveal--rotate-left,
  .reveal--rotate-right,
  .reveal.is-visible {
    opacity: 1;
    transform: none;
    transition: none;
  }

  /* 7.2 Trôi nổi, lấp lánh, nhịp đập, nháy, đổi số: tắt hẳn.
     .blink-soft có khung hình đầu ở opacity 0.3, nhưng `animation: none` gỡ
     luôn mọi giá trị keyframe nên phần tử trở về opacity mặc định là 1 — trái
     tim vẫn hiện đủ, đúng nguyên tắc "không bỏ ai lại ở trạng thái ẩn". */
  .drift,
  .drift--slow,
  .drift--fast,
  .glow,
  .pulse-soft,
  .blink-soft,
  .blink-heart,
  .is-ticking {
    animation: none;
    transform: none;
    filter: none;
  }

  .shimmer::after {
    animation: none;
    background-position: 50% 0;
    opacity: 0.16;
  }

  /* 7.3 Phong bì: chuyển từ GẤP KÍN sang MỞ tức thì, không quay, không trượt.

     Trạng thái đóng ĐƯỢC GIỮ NGUYÊN ở chế độ này (nắp vẫn úp, sáp vẫn còn):
     giảm chuyển động nghĩa là bỏ đường đi, không phải bỏ điểm xuất phát. Nếu
     mở sẵn thì nút "Mở thiệp" lại thành vô nghĩa đúng như lỗi của bản cũ.

     ĐÃ BỎ luật `.envelope__card { opacity: 1 }` vô điều kiện của bản trước.
     Luật đó sinh ra khi phong bì mở sẵn; với phong bì gấp kín nó phản tác
     dụng — tấm thiếp sẽ hiện lù lù PHÍA TRÊN một phong bì còn dán sáp (thiếp
     nằm ở 14% khung, mép trên phong bì đóng ở 41.4%, hai vùng không đè nhau
     nên mép trước không che giúp được). Bỏ đi vẫn KHÔNG vi phạm luật "reduced
     motion phải hiện mọi nội dung", vì ba lẽ:
       1. Nội dung trong .envelope__card đều aria-hidden và chỉ là bản nhắc
          lại — tên/ngày thật nằm ở #hero, luôn hiện.
       2. envelope.js ở chế độ reduced-motion gọi finish() NGAY sau khi gắn
          .is-open, nên lớp phủ biến mất trong cùng khung hình; trạng thái mở
          của tấm thiếp thực tế không bao giờ được vẽ ra.
       3. Nếu .is-open không bao giờ được gắn thì thiếp vô hình là ĐÚNG — nó
          đang nằm trong một phong bì chưa mở. */
  .envelope__flap,
  .envelope__card,
  .envelope__seal,
  .envelope__script,
  .envelope__face {
    animation: none;
    transition: none;
    will-change: auto;
  }

  /* Cú lật (mục 3.0): bỏ đường đi, GIỮ hai điểm mút. Trước khi bấm vẫn là mặt
     trước — nếu nhảy sẵn sang mặt sau thì mặt trước vừa thiết kế xong không ai
     nhìn thấy, và nút "Mở thiệp" lại mất một nửa việc của nó. Bấm xong thì
     đổi mặt TỨC THÌ, không quay. Không phần tử nào bị bỏ lại ở trạng thái ẩn
     một cách sai: mặt nào đang úp thì đúng là phải ẩn.

     ⚠ `animation: none` PHẢI LẶP LẠI Ở ĐÂY, không được trông chờ vào dòng
     `.envelope__face` phía trên. @media KHÔNG cộng thêm độ đặc hiệu: luật gộp
     kia chỉ (0,1,0), trong khi mục 3.0 đặt animation ở
     `.envelope.has-front.is-open .envelope__face--front` = (0,4,0) — cao gấp
     bội và thắng. Đo thật khi gỡ guard: getComputedStyle(front).animationName
     vẫn ra "envelope-turn-front" và hai mặt vẫn quay trọn 900ms, tức khối
     "giảm chuyển động" chỉ ép được visibility còn cú lật thì không tắt nổi.
     Mọi phần tử động khác của mục 3 đều đã có bản giết ĐÚNG ĐẶC HIỆU ngay
     dưới đây (.envelope.is-open .envelope__flap …) — hai mặt lật từng là
     ngoại lệ duy nhất bị bỏ sót. */
  .envelope.has-front .envelope__face--front {
    transform: none;
    visibility: visible;
  }

  .envelope.has-front .envelope__face--back {
    transform: none;
    visibility: hidden;
  }

  .envelope.has-front.is-open .envelope__face--front {
    animation: none;
    will-change: auto;
    visibility: hidden;
  }

  .envelope.has-front.is-open .envelope__face--back {
    animation: none;
    will-change: auto;
    visibility: visible;
  }

  .envelope.is-open .envelope__flap {
    animation: none;
    transform: rotateX(0deg);
    z-index: var(--envelope-flap-z-open, 2);
  }

  .envelope.is-open .envelope__seal,
  .envelope.is-open .envelope__script {
    animation: none;
    opacity: 0;
  }

  .envelope.is-open .envelope__card {
    animation: none;
    opacity: 1;
    transform: translateY(var(--envelope-lift, -38%));
  }

  /* 7.4 Nút / link / focus: giữ thay đổi màu, bỏ dịch chuyển. */
  .btn,
  .btn--solid,
  .link {
    transition: none;
  }

  .btn:hover,
  .btn:active,
  .btn--solid:hover,
  .btn--solid:active {
    transform: none;
  }

  .focus-ring:focus-visible,
  a:focus-visible,
  button:focus-visible,
  [role='button']:focus-visible,
  input:focus-visible,
  select:focus-visible,
  textarea:focus-visible,
  summary:focus-visible,
  [tabindex]:not([tabindex^='-']):focus-visible {
    transition: none;
  }
}
