/* ── ☆☆☆ 이머시브 ─────────────────────────────────────────
   평범한 확대 뷰(dialog#viewer)가 카드를 크게 보여준다면, 여기서는 카드 "안으로"
   들어간다. 하늘 / 색 환경 / 빛줄기 / 먼지 / 카드 / 자막 / 앞잎사귀가 각각 다른
   평면에 놓이고, 마우스나 폰 기울기에 따라 서로 다른 속도로 움직인다.

   정직하게 적어 둘 것 — 진짜 이머시브 카드는 원화를 레이어로 나눠 그린다.
   여기 art/*.webp 는 프레임까지 인쇄된 완성 카드 한 장이라 배추만 오려낼 수가 없다.
   그래서 카드는 통째로 '주인공 평면' 하나를 차지하고, 앞뒤 여섯 평면은 전부 CSS 로
   만들어 냈다(색 환경은 같은 그림을 크게 흐려 깐 것). 레이어 원화가 생기면
   .dio-subject 하나만 여러 장으로 쪼개면 되고, 나머지 구조는 그대로 쓴다.

   style.css 는 건드리지 않고 여기서만 얹는다. 다음 PR 이 다른 레어도(ex / 각인 /
   크라운)를 붙일 때 기본 카드 코드와 섞여 있지 않아야 되돌리기 쉽다. */

/* ── 꾹 누르기 게이지 ───────────────────────────────────────
   "꾹 누르면 뭔가 된다"는 건 눌러보기 전엔 알 수가 없다. 누르는 동안 테두리가
   차오르지 않으면 사용자는 카드가 멈춘 줄 안다.

   @property 가 없는 브라우저에서는 conic-gradient 안의 var(--hold-p) 가 무효라
   링이 아예 안 그려진다. 그래서 .card 의 밝기 변화를 따로 둔다 — 링이 없어도
   "반응하고 있다"는 신호는 남는다. */

@property --hold-p {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

/* 누르고 있다는 신호. 링만으로는 약하다 — 2px 짜리 선이 카드 자체의 밝은 테두리에
   묻혀서, 3초를 버티는 동안 아무 일도 안 일어나는 것처럼 보인다. 카드가 살짝
   눌려 들어가는 것이 제일 확실한 신호다. (.card 는 지금 transform 을 안 쓴다 —
   기울기는 .tilt 가, 회전은 .spin 이 갖고 있어서 여기서 겹칠 게 없다.) */
.stage.is-holding .card {
  filter: brightness(1.1) saturate(1.14);
  transform: scale(.955);
  transition: transform 200ms cubic-bezier(.2, .8, .2, 1);
}

.stage.is-holding::after {
  content: "";
  position: absolute;
  inset: -9px;
  z-index: 3;
  padding: 5px;          /* 곧 선 두께다. 2px 이면 카드 테두리에 묻혀서 안 보인다 */
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent2) 70%, transparent));
  border-radius: 5% / 3.6%;
  pointer-events: none;
  background: conic-gradient(from -90deg,
    color-mix(in srgb, var(--accent2) 55%, #ffffff) var(--hold-p),
    rgba(0, 0, 0, .55) 0);
  /* 안쪽을 뚫어 테두리만 남긴다 — padding 이 곧 선 두께가 된다 */
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  animation: hold-fill var(--hold-ms, 520ms) linear forwards;
}

@keyframes hold-fill {
  from { --hold-p: 0deg; }
  to { --hold-p: 360deg; }
}

/* 캡션의 진입 버튼. 꾹 누르기는 발견해야 아는 제스처라 유일한 길이면 안 된다 —
   키보드와 스크린리더에는 이 버튼이 유일한 길이다.
   --accent 는 .stage 안에 갇혀 있어서 캡션이 못 본다. main.js 가 li 에 한 번 더 얹는다. */
.im-badge {
  margin-top: 7px;
  padding: 4px 10px;
  border: 1px solid color-mix(in srgb, var(--accent, #ffffff) 40%, transparent);
  border-radius: 999px;
  color: color-mix(in srgb, var(--accent, #ffffff) 70%, #ffffff);
  background: rgba(255, 255, 255, .04);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .06em;
  cursor: pointer;
  transition: color .18s ease, border-color .18s ease;
}

.im-badge:hover,
.im-badge:focus-visible {
  color: #ffffff;
  border-color: color-mix(in srgb, var(--accent, #ffffff) 80%, transparent);
}

/* ── 무대 ──────────────────────────────────────────────────
   구조가 두 겹인 이유: .dio-layer 는 진입 애니메이션이 transform 을 쓰고,
   .dio-move 는 패럴랙스가 transform 을 쓴다. 한 요소에 둘을 걸면 서로 덮어쓴다. */

.immersive {
  inset: 0;
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: clip;
  color: var(--ink);
  background: #040603;
}

.immersive[open] { display: block; }

/* style.css 의 .viewer 와 같은 제약이다 — 여기에 backdrop-filter 를 걸면 Chrome 이
   별도의 backdrop root 를 만들고, 그 안에서는 자식의 mix-blend-mode 가 무시된다.
   .foil / .glare 가 블렌딩 없이 덮여서 카드 그림이 안 보인다. */
.immersive::backdrop { background: #000000; }

.is-immersed { overflow: hidden; }
.is-immersed .page { filter: blur(6px); pointer-events: none; }

.dio {
  /* 패럴랙스 입력. -1 ~ 1. immersive.mjs 가 매 프레임 갱신한다. */
  --px: 0;
  --py: 0;

  /* 주인공 평면(.dio-subject)의 패럴랙스 세기(px). **창이 같은 값을 읽는다.**
     창은 카드라는 물체에 뚫린 구멍이라 그 물체와 같이 움직여야 한다. 여기 두는
     이유가 그것이다 — .dio-subject .dio-move 안에 숫자로 박아 두면 자루(.dio-world)가
     못 읽어서, 틀만 밀리고 창은 제자리인 채로 그 차이가 검은 틈이 된다.
     세로는 가로의 --par-y 배만 민다. 화면이 가로로 넓어서 같은 양을 세로로 주면
     너무 크게 흔들린다. */
  --subject-par: 52;
  --par-y: .68;

  /* 누끼 높이. 이 장면의 크기 기준자다 — 주인공·카드·접지그림자·표면 이슬,
     그리고 앞 잎사귀까지 전부 여기서 나온다.
     **여기(.dio)에 있어야 한다.** 예전엔 .dio-subject .dio-move 에 있었는데,
     그러면 주인공 평면 밖에서는 못 읽어서 앞 잎사귀가 px 로 남았다. 화면이 작아져도
     잎만 안 줄어드니 폰에서 누끼 대비 20~59% 를 덮었다 (데스크톱은 14~42%). */
  --hh: min(58svh, 560px);

  /* 예전엔 여기서 --mouse-x/--glare 를 넘겨 style.css 의 .foil/.glare 를 굴렸다.
     주인공이 카드가 아니라 누끼가 되면서 포일을 얹을 표면이 없어져 전부 걷어냈다. */

  position: absolute;
  inset: 0;
  overflow: hidden;
  cursor: grab;
  touch-action: none;   /* 끌어서 장면을 돌린다 — 스크롤로 넘겨 주면 안 된다 */
}

/* ── 창 ────────────────────────────────────────────────────
   "카드 안으로 들어간다" 를 만드는 장치. --winK 는 **창이 몇 배로 벌어졌나**이고,
   1 이면 카드의 그림 영역 크기, 커질수록 화면 밖으로 벌어진다.

   @property 로 선언해야 애니메이션이 된다. 커스텀 속성은 기본적으로 문자열이라
   1 에서 4 로 "보간" 이 안 되고 툭 튄다. syntax 를 <number> 로 못박아야 숫자로 센다.
   inherits: true 라서 .dio 에 건 애니메이션이 .dio-world 와 틀 이미지에 같이 내려간다 —
   **둘이 같은 값을 읽는 게 이 연출의 핵심이다.** 창은 px 로, 틀은 scale 로 커지지만
   기준점(창 중심)이 같아서 결과가 정확히 일치한다. 따로 맞추려 들면 반드시 어긋난다.

   @property 를 모르는 브라우저에서는 --winK 가 1 에 머물러 창이 안 열린다. 그래도
   is-entering 이 걷히면 clip 자체가 사라지므로 장면은 정상으로 끝난다. */
@property --winK {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

/* 창 밖. 검은 자리를 없애는 판이다.

   창은 카드의 그림 영역이라 거의 정사각인데(462x453) 화면은 아니다(1905x897).
   창은 축마다 **같은** 배율로 벌어져야 한다 — 축마다 다르게 벌리면 같은 배율을
   읽는 틀 이미지가 늘어나 테두리가 찌그러진다. 그래서 짧은 축이 먼저 화면을 덮고
   긴 축은 한참 남는다. 실측으로 위아래는 1570ms 에, 좌우는 1764ms 에 덮였다 —
   그 사이 **194ms 동안 좌우가 각각 화면의 17% 씩 검게 비었다.** 폰이면 반대로
   위아래가 빈다. 화면이 넓을수록 이 구간이 길어진다.

   창을 더 빨리 벌려서 해결할 수는 없다. 그건 짧은 축이 화면 밖으로 더 나갈 뿐
   긴 축이 따라잡는 순서를 못 바꾼다. 그러니 창을 손대지 말고 **빈 자리 쪽을
   채운다** — 같은 텃밭을 어둡고 흐리게 깐 판을 자루 밖에 한 장 둔다.
   창 안은 또렷하고 밖은 초점이 나간 상태가 되어, 검은 구멍이 아니라 깊이로 읽힌다.

   자루(.dio-world) **밖**에 있어야 한다. 안에 넣으면 같이 잘려서 아무 의미가 없다.
   창이 다 열리면 밝은 .dio-back 이 위를 덮으므로 알아서 안 보이게 된다 —
   끄는 코드는 필요 없다.

   blur 는 화면만 한 레이어에 거는 거라 싸지 않다. 터치 기기에서는 뺀다 —
   원화(1376x768)가 이미 3배 넘게 늘어나 있어서 어둡게만 깔아도 충분히 물러난다. */
.dio-void {
  position: absolute;
  inset: 0;
  background: var(--back) center / cover no-repeat;
  filter: blur(12px) brightness(.32) saturate(.7);
  transform: scale(1.09);   /* blur 가 가장자리를 비우므로 그만큼 키운다 */
  pointer-events: none;
}

@media (hover: none) and (pointer: coarse) {
  .dio-void { filter: brightness(.32) saturate(.7); transform: scale(1.03); }
}

/* 판이 제 크기로 돌아온 몫. 1 이면 아직 --set-s 로 서 있는 상태, --set-inv 면 다 컸다.

   판(.dio-plate)은 카드가 화면에 들어오도록 --set-s 로 줄어든 채 서 있다가, 창을
   통과하면서 배율 1 로 돌아온다. 그런데 **틀은 판 안에 있고 자루는 판 밖에 있다.**
   판만 커지면 틀은 따라 커지는데 자루는 제자리라, 그 차이가 창 가장자리에 검은
   띠로 드러난다. 그래서 자루도 이 값을 곱해서 읽는다.

   --winK 와 같은 원칙이고, 한 걸음 더 갔다: 판의 배율 자체를 이 변수로 쓴다
   (.dio-plate 의 마지막 키프레임이 scale(--set-s * --pg) 다). 판과 자루가 **같은
   변수 하나**를 읽으므로 애니메이션 두 개의 시작 시각이나 이징이 어긋날 여지가
   아예 없다. 따로 맞추려 들면 반드시 어긋난다. */
@property --pg {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

/* 세상을 담는 자루. 이게 있어야 "창 안에만 세상이 보이는" 상태를 만들 수 있다.
   **주인공 평면(.dio-subject)은 일부러 이 밖에 있다** — 틀은 창을 두르고 있어서
   창으로 자르면 틀 자체가 지워지고, 누끼는 창보다 작아서 자를 일이 없다.
   앞뒤로 두 자루인 이유도 그것이다: 앞잎·이슬은 주인공보다 앞에 그려져야 한다. */
.dio-world {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* 창 = 틀에서 뒤가 비치는 자리. 중심과 반지름을 immersive.mjs 가 재서 넣고, 여기서
   배율만 곱한다. inset() 의 %는 이 상자(=.dio) 기준이라 100% 를 그대로 쓸 수 있다.

   배율은 --winK(창이 벌어진 몫)와 --pg(판이 커진 몫)의 **곱**이다. 틀은 판 안에서
   --winK 로 커지고 판 자신이 --pg 로 커지니, 화면에서 틀의 배율이 곧 이 곱이다.
   자루가 같은 곱을 읽어야 둘이 안 어긋난다.

   중심에는 두 가지가 더해진다.

   하나는 패럴랙스다. 틀이 든 주인공 평면은 마우스·자이로를 따라 최대 가로 52px,
   세로 35px 밀리는데 자루는 안 밀려서, 예전엔 그 차이가 그대로 창 가장자리의 검은
   틈이 됐다 (px=-1 에서 왼쪽 52px, py=-1 에서 위쪽 35px). 같은 --subject-par 을
   읽어 같이 민다. 이 이동은 판 밖(.dio-move)에 걸리므로 --pg 를 안 곱한다.

   다른 하나는 판이 커진 몫이다. 판은 **자기 중심(=화면 중심)** 기준으로 커지므로, 화면
   중심에서 벗어나 있는 창 중심은 그만큼 밀려난다. 창이 누끼 경계상자보다 아래로
   넓어지면서(카드 아래쪽 판들까지 덮느라) 화면 중심과 42px 벌어져 있어서, 안 옮기면
   그 몫이 그대로 어긋난다. inset() 의 50% 는 각 변이 알아서 폭/높이로 푼다. */
.immersive.is-entering .has-window .dio-world {
  --winKp: calc(var(--winK) * var(--pg));
  --wcx: calc(50% + (var(--win-cx) - 50%) * var(--pg)
               + var(--px) * var(--subject-par) * 1px);
  --wcy: calc(50% + (var(--win-cy) - 50%) * var(--pg)
               + var(--py) * var(--subject-par) * var(--par-y) * 1px);
  clip-path: inset(
    calc(var(--wcy) - var(--win-hh) * var(--winKp))
    calc(100% - var(--wcx) - var(--win-hw) * var(--winKp))
    calc(100% - var(--wcy) - var(--win-hh) * var(--winKp))
    calc(var(--wcx) - var(--win-hw) * var(--winKp)));
}

.dio:active { cursor: grabbing; }

.dio-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* inset 은 반드시 0 이어야 한다. 처음엔 -14% 로 키워서 패럴랙스에 밀려도 가장자리가
   비지 않게 했는데, 그러면 이 안의 자막·버튼이 "화면"이 아니라 "커진 상자" 기준으로
   붙어서 전부 뷰포트 밖으로 나간다. 가장자리 여유는 크기(--k)로 벌고 위치는 건드리지
   않는다 — 배경 평면만 키우면 되고, 자막이 있는 평면은 --k 를 1 로 둔다. */
.dio-move {
  --par: 0;   /* 패럴랙스 세기(px). 뒤에서 앞으로 갈수록 커진다 */
  --k: 1;     /* 가장자리 여유. 밀려나는 만큼만 키운다 */
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  transform:
    translate3d(calc(var(--px) * var(--par) * 1px),
                calc(var(--py) * var(--par) * var(--par-y) * 1px),
                0)
    scale(var(--k));
  will-change: transform;
}

/* 0. 새벽 하늘 */
.dio-sky .dio-move {
  --par: 5;
  --k: 1.04;
  background:
    radial-gradient(ellipse 70% 55% at 50% 6%,
      color-mix(in srgb, var(--accent) 30%, transparent), transparent 70%),
    radial-gradient(ellipse 95% 60% at 18% 104%,
      color-mix(in srgb, var(--accent2) 22%, transparent), transparent 72%),
    linear-gradient(178deg, #0b1a11 0%, #06100a 46%, #020604 100%);
}

/* 1. 배경 — 텃밭 원화. 사실상 이 연출의 전부다.
   잎사귀 하나하나, 이랑, 지평선처럼 **윤곽이 있는** 것들이라 밀리면 눈에 보인다.
   예전엔 같은 카드 그림을 blur(72px) 로 깔았는데, 흐린 덩어리는 아무리 밀어도
   움직이는 게 보이지 않았다 — 패럴랙스는 눈이 따라갈 기준점이 있어야 성립한다. */
/* --k 를 크게 잡으면 안 된다. 원화를 일부러 2:1 로 넓혀 놨는데 여기서 또 확대하면
   cover 가 좌우를 맞추면서 위아래를 잘라내 **하늘이 사라지고 텃밭만** 남는다.
   패럴랙스로 밀리는 12px 을 덮을 만큼만 키운다. */
.dio-back .dio-move {
  --par: 12;
  --k: 1.06;
  background: var(--back) center / cover no-repeat;
}

/* 원화가 755x670 밖에 안 돼서 화면을 채우려면 3배 넘게 늘려야 한다. 늘어나 흐려진 걸
   가리고 주인공을 띄우려고 어둡게 깔고 비네팅을 씌운다.
   이건 레이어(.dio-back)에 건다 — .dio-move 에 걸면 비네팅까지 같이 밀려서
   화면 한쪽 구석이 밝아진다. */
/* 위아래를 다르게 덮는다. 사방을 고르게 어둡게 하면 하늘까지 뭉개져서 장면이
   텃밭 한 덩어리로 보인다. 흙(아래)은 깊게 눌러 주인공을 띄우고, 하늘(위)은 살린다. */
.dio-back::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top,
      rgba(2, 6, 3, .84) 0%, rgba(2, 6, 3, .22) 34%, transparent 58%),
    radial-gradient(ellipse 92% 80% at 50% 54%, transparent 46%, rgba(2, 6, 3, .5) 100%);
}

/* 2. 빛줄기 — 아주 천천히 돈다. 회전은 ::before 가 맡는다.
   .dio-move 의 transform 은 패럴랙스가 이미 쓰고 있어서 여기에 또 걸 수 없다. */
/* 배경이 흐린 덩어리였을 땐 빛줄기가 화면을 채워 줬는데, 이제 배경에 잎사귀가
   가득해서 세게 걸면 다 씻겨 나간다. 있는 듯 없는 듯한 정도로만 남긴다. */
.dio-rays {
  mix-blend-mode: screen;
  opacity: .3;
}

.dio-rays .dio-move { --par: 18; --k: 1.1; }

.dio-rays .dio-move::before {
  content: "";
  position: absolute;
  inset: -35%;
  background: conic-gradient(from 200deg at 50% 45%,
    transparent 0deg, rgba(255, 255, 255, .18) 7deg, transparent 19deg,
    transparent 44deg, rgba(255, 255, 255, .11) 53deg, transparent 63deg,
    transparent 176deg, rgba(255, 255, 255, .09) 184deg, transparent 194deg,
    transparent 298deg, rgba(255, 255, 255, .14) 310deg, transparent 320deg);
  filter: blur(24px);
  animation: dio-spin 70s linear infinite;
}

@keyframes dio-spin { to { transform: rotate(1turn); } }

/* 3. 먼지 — 카드 뒤 */
.dio-motes .dio-move { --par: 30; --k: 1.16; }

.mote {
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  width: calc(var(--s) * 1px);
  aspect-ratio: 1;
  border-radius: 50%;
  opacity: var(--o);
  background: color-mix(in srgb, var(--accent2) 65%, #ffffff);
  /* 번짐을 두 겹으로 준다. 한 겹만 쓰면 아무리 키워도 "점"에서 안 벗어난다 —
     좁고 밝은 심지(accent2)와 넓고 옅은 후광(accent)이 같이 있어야 빛으로 읽힌다. */
  box-shadow:
    0 0 calc(var(--s) * 3px) color-mix(in srgb, var(--accent2) 58%, transparent),
    0 0 calc(var(--s) * 9px) color-mix(in srgb, var(--accent) 34%, transparent);
  /* 흐르기와 밝아지기를 **따로** 돌린다. 한 애니메이션에 묶으면 밝기가 흐름과
     같은 박자로 뛰어서 34개가 동시에 깜빡이는 게 눈에 보인다. */
  animation:
    mote-drift calc(var(--t) * 1s) ease-in-out calc(var(--d) * -1s) infinite alternate,
    mote-glow calc(var(--gt) * 1s) ease-in-out calc(var(--gd) * -1s) infinite alternate;
}

@keyframes mote-drift {
  to { transform: translate3d(calc(var(--dx) * 1px), calc(var(--dy) * 1px), 0); }
}

@keyframes mote-glow {
  from { opacity: calc(var(--o) * .3); }
  to { opacity: var(--o); }
}

/* 4. 주인공 — 카드가 아니라 **누끼**다.
   프레임도 제목바도 없이 배추만 장면 안에 서 있다. 이게 "배경 위의 직사각형"과
   "장면 안의 배추"를 가르는 지점이다. 그래서 여기엔 .foil/.glare 를 얹지 않는다 —
   포일은 카드 표면의 성질이고, 지금 표면이라는 게 없다.

   그림자는 box-shadow 가 아니라 drop-shadow 를 쓴다. box-shadow 는 사각형을 따라가고
   drop-shadow 는 **알파 실루엣**을 따라가서, 잎사귀 윤곽대로 그림자가 진다. */
.dio-subject .dio-move {
  --par: var(--subject-par);
  perspective: 1500px;
}

/* 카드와 누끼를 같이 담는 판. 들어올 때 이 판이 통째로 날아오고 확대된다.
   둘이 한 판에 있어야 확대되는 동안 서로 어긋나지 않는다. */
.dio-plate {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  /* 이 한 줄이 있어야 안쪽 요소의 translateZ 가 살아난다. 없으면 판이 납작해져서
     Z 를 아무리 줘도 뭉개진다 (원근 자체는 .dio-subject .dio-move 의 perspective 가 준다).
     누끼는 filter 를 쓰고 있어 3D 자식을 못 갖지만, 어차피 z=0 바닥면이라 상관없다. */
  transform-style: preserve-3d;
}

/* 원본 카드(프레임 포함). 평소엔 안 보이고 들어오는 동안만 나타났다 녹는다.
   위치는 **누끼에 맞춰 역산**한다 — cards.mjs 의 scene.fit 이 "카드 안에서 누끼가
   차지하는 자리"라, 그걸 뒤집으면 "누끼에 맞추려면 카드가 어디 있어야 하는지"가 나온다.
   --card-x/y/h 는 immersive.mjs 가 누끼 높이의 배수로 계산해 넣는다.
   이게 맞아야 틀이 녹는 동안 캐릭터가 한 픽셀도 안 움직인다. */
/* 틀. 카드 그림과 **똑같은 상자**에 깔려 있다가, 카드가 녹으면 드러난다.
   left/top/height 를 카드와 한 글자도 다르지 않게 쓰는 게 중요하다 — 어긋나면
   카드가 녹는 순간 틀이 덜컥 움직인다. (두 원화의 비율이 0.7196 대 0.7200 으로
   0.06% 다른데, 638px 높이에서 폭 0.3px 차이라 눈에 안 띈다.)

   창 중심(--win-ox/oy)을 기준으로 --winK 만큼 커진다. 창과 같은 변수라 항상 붙어 다닌다. */
.dio-plate-frame {
  position: absolute;
  left: calc(50% + var(--card-x) * var(--hh));
  top: calc(50% + var(--card-y) * var(--hh));
  height: calc(var(--card-h) * var(--hh));
  width: auto;
  opacity: 0;
  transform-origin: var(--win-ox, 50%) var(--win-oy, 46%);
  transform: scale(var(--winK, 1));
}

.dio-plate-art {
  position: absolute;
  left: calc(50% + var(--card-x) * var(--hh));
  top: calc(50% + var(--card-y) * var(--hh));
  height: calc(var(--card-h) * var(--hh));
  width: auto;
  opacity: 0;
}

.dio-hero {
  display: block;
  height: var(--hh);
  width: auto;
  transform: rotateX(calc(var(--py) * -6deg)) rotateY(calc(var(--px) * 9deg));
  /* 배추 원화는 대낮 조명인데 장면은 해 뜨기 직전이다. 광원이 안 맞으면 아무리
     자리를 잘 잡아도 오려 붙인 것처럼 보인다. 살짝 눌러 새벽 쪽으로 당긴다. */
  filter:
    brightness(.92) saturate(1.03) contrast(1.02)
    drop-shadow(0 24px 38px rgba(0, 0, 0, .62))
    drop-shadow(0 0 64px color-mix(in srgb, var(--accent) 34%, transparent));
}

/* 캐릭터 표면의 이슬이 앉는 판. 누끼와 **정확히 같은 상자**여야 방울을 % 로 찍을 수
   있어서, 높이(--hh)와 가로세로비를 누끼에서 그대로 가져온다 (--subject-ar 은
   immersive.mjs 가 이미지 원본 크기를 읽어 넣는다).
   .dio-plate 가 grid 라서 그냥 두면 누끼 **아래 줄**에 놓인다. 둘 다 1/1 로 못 박아
   같은 칸에 겹친다. 기울기도 누끼와 같은 식을 써야 방울이 배추에 붙어 있는 걸로 읽힌다 —
   여기서 각도가 어긋나면 방울만 따로 미끄러진다. */
.dio-hero,
.dio-rind,
.dio-skin { grid-area: 1 / 1; }

.dio-skin {
  position: relative;
  height: var(--hh);
  aspect-ratio: var(--subject-ar, 1);
  pointer-events: none;
  transform: rotateX(calc(var(--py) * -6deg)) rotateY(calc(var(--px) * 9deg));
  transform-style: preserve-3d;
}

/* 겉잎 — 같은 누끼를 고리 모양으로 오려 **겹겹이** 앞에 세운다. 기울이면 각 겹이
   속과 다른 속도로 어긋나고, 그 어긋남이 배추의 두께가 된다. 방울이나 빛과 달리
   캐릭터 자신이 갈라져 움직이는 거라 제일 직관적이다.

   파일 맨 위에 "레이어 원화가 생기면 .dio-subject 를 여러 장으로 쪼갠다"고 적어 뒀는데,
   이건 새 원화 없이 그 구조를 미리 해 보는 것이다. 다만 한계가 분명하다 — 겹이 속과
   **같은 픽셀**이라 비켜도 새로 드러나는 게 없다. 진짜 레이어 원화가 오면 마스크를 빼고
   겹마다 다른 그림을 끼우면 되고, 나머지 구조(Z · 원근 보정 · 그림자)는 그대로 쓴다.

   마스크 경계를 넓게 흐리는 게 중요하다. 딱 자르면 어긋나는 순간 "두께"가 아니라
   "인쇄가 밀린 두 장"으로 보인다. 원 마스크가 잎 한복판을 가로지르기 때문이다.

   scale 이 먼저(오른쪽부터) 걸리고 그 다음 Z 로 밀린다. 원근이 키우는 만큼을 미리
   줄여 두는 것 — 안 하면 겹이 정지 상태에서도 속보다 크게 나와서 깊이가 아니라
   테두리가 어긋난 걸로 보인다. --k 는 immersive.mjs 가 (원근 - Z) / 원근 으로 넣는다. */
.dio-rind {
  grid-area: 1 / 1;
  height: var(--hh);
  aspect-ratio: var(--subject-ar, 1);
  pointer-events: none;
  transform:
    rotateX(calc(var(--py) * -6deg)) rotateY(calc(var(--px) * 9deg))
    translateZ(calc(var(--z) * 1px)) scale(var(--k, 1));

  /* 앞 겹이 뒤에 드리우는 그림자. 가림이 없으면 앞뒤가 아니라 나란히 놓인 걸로 읽힌다.
     띄운 높이에 비례해 길어진다. 광원이 왼쪽 위라 그림자는 오른쪽 아래로 진다.

     진하기(--sa)는 겹마다 따로 준다. 뒤쪽 겹까지 또렷하게 그림자를 지면 고리 경계가
     테두리 선처럼 드러나서, 두께가 아니라 오려 붙인 원으로 보인다.

     filter 를 **바깥 div** 에 걸어야 한다. 마스크와 같은 요소에 걸면 filter 가 마스크보다
     먼저 적용돼서, 고리가 아니라 배추 실루엣 **전체**가 그림자를 진다. */
  filter: drop-shadow(
    calc(var(--z) * .055px) calc(var(--z) * .1px)
    calc(var(--z) * .13px) rgba(0, 0, 0, var(--sa, .5)));
}

/* 고리로 오려내기. 안쪽 --r0~--r1 에서 서서히 나타나고 --r2~--r3 에서 사라진다.
   맨 바깥 겹은 --r2/--r3 로 실루엣에 닿기 전에 흐려진다 — 안 그러면 배경과 맞닿는
   가장자리에서 속과 겹쳐 보인다. 같은 픽셀이라 그 자리가 제일 티가 난다.

   정지점은 커지는 순서여야 한다. --r2 를 --r1 보다 작게 주면 브라우저가 뒤 값을
   --r1 로 끌어올려서, 부드러운 페이드가 아니라 그 자리 하드컷이 된다. */
.dio-rind img {
  display: block;
  width: 100%;
  height: 100%;
  mask: radial-gradient(circle at 50% 46%,
    transparent calc(var(--r0) * 1%), #000 calc(var(--r1) * 1%),
    #000 calc(var(--r2, 200) * 1%), transparent calc(var(--r3, 201) * 1%));
  -webkit-mask: radial-gradient(circle at 50% 46%,
    transparent calc(var(--r0) * 1%), #000 calc(var(--r1) * 1%),
    #000 calc(var(--r2, 200) * 1%), transparent calc(var(--r3, 201) * 1%));
  /* 누끼의 색 보정만 따라간다. 실루엣 그림자는 누끼가 이미 지고 있어서 여긴 뺀다 */
  filter: brightness(.92) saturate(1.03) contrast(1.02);
}

/* 방울을 배추 **표면 위로 띄운다**. 이게 없으면 방울이 원화와 같은 평면에 있어서,
   기울여도 인쇄된 물방울과 똑같이 움직인다 — 상대 이동이 0 이라 눈이 깊이로 읽을 게 없다.
   Z 를 주면 9도 기울일 때 원화 대비 몇 픽셀 어긋나고, 그 어긋남이 곧 입체감이다. */
.dio-skin .dew { transform: translateZ(calc(var(--z) * 1px)); }

/* --z 는 immersive.mjs 가 **제일 앞 겹보다 높게** 잡는다. 낮게 두면 고리 위에 앉힌
   방울이 겉잎 뒤로 숨어 몇 개가 통째로 사라진다. */

/* 캐릭터에 붙은 방울은 캐릭터와 같이 커지고 작아져야 한다. 폰에서 --hh 가 400px 로
   줄 때 방울만 px 로 남으면 배추에 비해 우스꽝스럽게 커진다. 그래서 --hh 의 비율로 잰다
   (--s 단위가 렌즈 이슬과 다르다 — 여기선 "누끼 높이의 %"다). */
.dio-skin .dew { --dw: calc(var(--hh) * var(--s) * .01); }

/* 접지 그림자 — "따로 논다"의 진짜 원인은 색이 아니라 **떠 있다**는 것이다.
   지면에 닿은 자국이 있어야 놓여 있는 걸로 읽힌다.
   .dio-move 는 grid place-items:center 라 그냥 두면 이 타원도 한가운데 서 버린다.
   그래서 absolute 로 빼서 주인공 밑동 높이에 맞춘다 — 주인공이 min(58svh,560px)
   높이로 가운데 서 있으니 그 절반만큼 내려온 자리가 밑동이다. */
.dio-ground {
  position: absolute;
  left: 50%;
  top: calc(50% + var(--hh) * .43);   /* 누끼 아래쪽 끝이 .5, 살짝 겹치게 */
  width: calc(var(--hh) * .58);
  height: calc(var(--hh) * .11);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(ellipse,
    rgba(0, 0, 0, .62), rgba(0, 0, 0, .26) 52%, transparent 74%);
  filter: blur(12px);
}

/* 5. 자막 — 카드보다 조금 앞. 이름·코드만 여기 있고, 조작하는 것(닫기 버튼)은
   패럴랙스에서 빼 뒀다. 누르려는 버튼이 시선 따라 움직이면 못 누른다. */
.dio-hud .dio-move { --par: 30; }

.dio-meta {
  position: absolute;
  left: clamp(20px, 5vw, 54px);
  bottom: clamp(20px, 5vh, 52px);
  display: grid;
  gap: 3px;
  text-shadow: 0 2px 18px rgba(0, 0, 0, .9);
}

.dio-rank {
  color: color-mix(in srgb, var(--accent2) 70%, #ffffff);
  font-size: 12px;
  letter-spacing: .3em;
}

.dio-meta strong {
  font-size: clamp(20px, 3.4vw, 30px);
  letter-spacing: -.04em;
}

.dio-code {
  color: var(--ink-dim);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
}

.dio-place {
  margin-top: 4px;
  color: color-mix(in srgb, var(--accent) 55%, #ffffff);
  font-size: 12px;
}

/* 이 둘은 .dio 의 직계 자식이라 화면 기준으로 붙는다 (레이어 안이 아니다) */
.dio-tools,
.dio-tip {
  position: absolute;
  z-index: 2;
}

/* 버튼이 둘(음소거 · 닫기)이 되면서 각자 right 를 박는 대신 한 줄로 묶었다.
   자리 값은 예전 .dio-exit 이 갖고 있던 것 그대로다.
   .dio-layer 가 아니므로 진입·퇴장 연출(dio-out)은 이 바를 건드리지 않는다. */
.dio-tools {
  top: clamp(16px, 3vh, 30px);
  right: clamp(16px, 4vw, 34px);
  display: flex;
  gap: 8px;
}

.dio-exit,
.dio-mute {
  padding: 8px 15px;
  border: 1px solid rgba(255, 255, 255, .22);
  border-radius: 999px;
  color: var(--ink);
  background: rgba(0, 0, 0, .42);
  font-size: 12px;
  /* 라벨이 "소리" ↔ "음소거" 로 길이가 바뀐다. 줄바꿈되면 알약이 두 줄이 된다 */
  white-space: nowrap;
  cursor: pointer;
  transition: border-color .18s ease, background-color .18s ease;
}

.dio-exit:hover,
.dio-exit:focus-visible,
.dio-mute:hover,
.dio-mute:focus-visible {
  border-color: rgba(255, 255, 255, .5);
  background: rgba(0, 0, 0, .62);
}

/* 음소거된 동안은 눌린 상태로 보이게 — 아이콘만으로는 지금 켜진 건지 꺼진 건지 애매하다 */
.dio-mute[aria-pressed="true"] {
  color: var(--ink-dim);
  background: rgba(0, 0, 0, .62);
}

.dio-tip {
  right: clamp(20px, 5vw, 54px);
  bottom: clamp(22px, 5vh, 54px);
  margin: 0;
  color: var(--ink-faint);
  font-size: 11px;
  letter-spacing: .04em;
}

/* 6. 앞 잎사귀 — 카드 앞을 지나간다. 흐린 게 핵심이다.
   카메라가 카드에 초점을 맞춘 것처럼 읽혀서 깊이가 생긴다. */
.dio-fore .dio-move { --par: 100; --k: 1.3; }

.leaf {
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  /* --s 와 --b 는 px 이 아니라 **누끼 높이의 %** 다 (.dio-skin .dew 와 같은 단위).
     px 로 두면 화면이 작아질 때 잎만 안 줄어서 캐릭터를 덮는다 — 표면 이슬이 이미
     같은 이유로 비율을 쓰고 있었는데 잎만 빠져 있었다. --k: 1.3 이 여기에 곱해지는
     것까지 감안한 값이다. */
  width: calc(var(--s) * var(--hh) * .01);
  height: calc(var(--s) * var(--hh) * .0145);
  /* (--x, --y) 를 잎의 **중심**으로 삼는다. 모서리 기준이면 offCenter 로 가운데를
     피해 놔도 큰 잎이 그대로 얼굴까지 뻗어서 피한 의미가 없다.
     transform 은 leaf-sway 가 통째로 쓰고 있으므로 margin 으로 당긴다. */
  margin-left: calc(var(--s) * var(--hh) * -.005);
  margin-top: calc(var(--s) * var(--hh) * -.00725);
  border-radius: 0 100% 0 100%;
  opacity: var(--o);
  background: linear-gradient(150deg,
    var(--accent), color-mix(in srgb, var(--accent) 25%, #000000));
  filter: blur(calc(var(--b) * var(--hh) * .01));
  animation: leaf-sway calc(var(--t) * 1s) ease-in-out calc(var(--d) * -1s) infinite alternate;
}

@keyframes leaf-sway {
  from { transform: rotate(calc(var(--r) * 1deg)); }
  to {
    transform:
      rotate(calc(var(--r) * 1deg + 13deg))
      translate3d(calc(var(--dx) * 1px), calc(var(--dy) * 1px), 0);
  }
}

/* 7. 이슬 — 카메라 유리에 맺힌 방울. 이 층만 --par 이 0 이다.
   뒤 일곱 평면이 전부 시선 따라 밀리는데 이것만 안 움직여서, 화면과 나 사이에
   유리가 한 장 있는 걸로 읽힌다. 여기에 패럴랙스를 주면 효과가 통째로 사라지고
   그냥 떠다니는 동그라미가 된다.
   backdrop-filter 로 진짜 굴절을 주고 싶겠지만 쓰면 안 된다 — 파일 위쪽에 적어 둔
   대로, dialog 안에서 backdrop root 가 생기면 .dio-rays 의 mix-blend-mode 가
   통째로 무시된다. 그래서 굴절은 그라디언트로 흉내 낸다. */
.dio-dew .dio-move { --par: 0; }

.dew {
  /* 방울의 기준 치수. 테두리 빛·그림자가 전부 여기서 나온다.
     렌즈 이슬은 px 그대로 쓰고, 캐릭터에 붙은 이슬은 아래에서 --hh 비율로 덮어쓴다. */
  --dw: calc(var(--s) * 1px);

  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  width: var(--dw);
  height: calc(var(--dw) * var(--e));
  opacity: var(--o);
  /* 물방울은 정원이 아니다. 위는 좁고 아래는 무게로 처진다 */
  border-radius: 50% 50% 50% 50% / 42% 42% 58% 58%;
  /* 속을 하얗게 채우면 물방울이 아니라 **비눗방울**이 된다. 실제 물방울은 뒤가
     거의 그대로 비치고, 빛을 바깥으로 꺾어서 몸통은 오히려 배경보다 어둡다.
     읽히게 만드는 건 채움이 아니라 (1) 아래 테두리에 맺힌 빛 (2) 점 하나짜리
     반사점 (3) 위쪽의 어두운 테, 이 셋뿐이다. */
  background:
    /* 1. 아래 테두리 — 방울을 통과한 빛이 여기로 모인다 */
    radial-gradient(ellipse 96% 92% at 50% 60%,
      transparent 62%, rgba(255, 255, 255, .46) 86%, rgba(255, 255, 255, .08) 100%),
    /* 2. 몸통 — 비어 있되 아주 살짝 어둡다 */
    radial-gradient(ellipse 100% 100% at 46% 40%,
      rgba(0, 0, 0, .17) 0%, rgba(0, 0, 0, .05) 58%,
      color-mix(in srgb, var(--accent) 18%, transparent) 90%, transparent 100%);
  box-shadow:
    inset 0 calc(var(--dw) * -.05) calc(var(--dw) * .08) rgba(255, 255, 255, .5),
    inset 0 calc(var(--dw) * .05) calc(var(--dw) * .08) rgba(0, 0, 0, .34),
    0 calc(var(--dw) * .06) calc(var(--dw) * .13) rgba(0, 0, 0, .3);
}

/* 초점이 나간 방울만. blur(0px) 도 공짜가 아니라서 — 값이 0 이어도 요소마다
   필터용 렌더 표면이 하나씩 생긴다. 15개 전부에 걸면 폰에서 그만큼 손해다. */
.dew.is-soft { filter: blur(calc(var(--b) * 1px)); }

/* 3. 반사점. 광원이 왼쪽 위(하늘)라 여기 하나만 또렷하게 찍힌다.
   크게 그리면 그 순간 방울이 아니라 구슬이 된다 — 점이어야 한다. */
.dew::after {
  content: "";
  position: absolute;
  left: 26%;
  top: 17%;
  width: 21%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: rgba(255, 255, 255, .92);
}

/* ── 방울 두 종류 더 ──────────────────────────────────────
   요소를 늘리지 않고 같은 방울에 클래스만 얹는다. 부하는 그대로고 실루엣만 달라진다. */

/* 흘러내리다 멈춘 자국. 위는 가늘게 끌리고 아래에 무게가 모인다.
   세로 길이(--e)는 immersive.mjs 가 이 종류에만 크게 준다. */
.dew.is-streak {
  border-radius: 50% 50% 50% 50% / 20% 20% 80% 80%;
  background:
    /* 아래 불룩한 곳의 테두리 빛 */
    radial-gradient(ellipse 94% 46% at 50% 82%,
      transparent 52%, rgba(255, 255, 255, .46) 82%, rgba(255, 255, 255, .07) 100%),
    /* 위로 갈수록 옅어지는 꼬리 */
    linear-gradient(to bottom,
      rgba(255, 255, 255, .12) 0%, rgba(0, 0, 0, .07) 34%,
      rgba(0, 0, 0, .16) 72%, rgba(0, 0, 0, .05) 100%);
}

/* 꼬리가 아니라 불룩한 아래쪽에 빛이 맺힌다 */
.dew.is-streak::after {
  left: 30%;
  top: 62%;
  width: 24%;
}

/* 큰 방울 옆에 작은 게 붙은 것. 이슬은 서로 잘 뭉친다.
   background 는 상속받되 그림자는 위성 크기에 맞춰 다시 잡는다 — 그대로 물려받으면
   본체 기준 두께라 작은 쪽이 테두리에 잡아먹힌다. */
.dew.is-twin::before {
  content: "";
  position: absolute;
  right: -32%;
  bottom: -4%;
  width: 44%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: inherit;
  box-shadow:
    inset 0 calc(var(--dw) * -.022) calc(var(--dw) * .035) rgba(255, 255, 255, .5),
    0 calc(var(--dw) * .026) calc(var(--dw) * .06) rgba(0, 0, 0, .3);
}

/* 몇 개만 흘러내린다. 전부 움직이면 비 오는 창문이 되고, 여긴 이슬이다. */
.dew.is-run {
  animation: dew-run calc(var(--t) * 1s) ease-in calc(var(--d) * -1s) infinite;
}

/* 0% 에서 곧바로 --o 로 켜면 방울이 허공에서 툭 튀어나온다. 앞뒤로 opacity 0 을
   두고 그 사이에서만 보이게 해서, 맺혔다 흘러내려 마르는 한 바퀴로 읽히게 한다.
   끝(fall)과 처음(0)이 안 보이는 구간에서 이어지므로 되감기는 눈에 안 띈다. */
@keyframes dew-run {
  0% { transform: translateY(0); opacity: 0; }
  10%, 42% { transform: translateY(0); opacity: var(--o); }
  92%, 100% { transform: translateY(calc(var(--fall) * 1px)); opacity: 0; }
}

/* ── 들어가기 ──────────────────────────────────────────────
   이 2.1초가 이 기능의 전부다. 그냥 다른 화면이 뜨면 "카드 안으로 들어갔다"가 아니라
   "화면이 바뀌었다"가 된다. 눌린 그 카드에서 출발해야 연결이 생긴다.

       0 ─ 24%   눌린 자리에서 가운데로 날아오며 커진다 (FLIP)
      24 ─ 44%   제자리 크기까지 커지고 **거기서 멈춘다**
      44 ─ 62%   카드 그림이 녹아 밑의 틀만 남는다
      46 ─ 64%   누끼가 그 자리에 나타나고, 창 안으로 배경이 들어온다
      62 ─ 100%  창과 틀이 같이 벌어져 화면 밖으로 나간다 → 통과

   **44~64% 구간에는 아무것도 움직이지 않는다.** 이게 설계의 전부다. 카드와 누끼는
   같은 판 위에 픽셀까지 맞춰 겹쳐 있어서, 배율이 같은 동안에는 크로스페이드가
   눈에 안 보인다 — 틀만 사라지고 캐릭터는 제자리에 남는 것처럼 읽힌다. 그래서
   판을 44% 에 세워 두고, 움직이는 일(창 열기)은 그 뒤로 미룬다.

   한 번은 카드 그림 자체를 화면 밖까지 부풀려 봤다. 통과감은 생겼지만 카드 속
   인쇄된 배추가 같이 커지면서 누끼와 배율이 어긋나 **큰 배추 위에 작은 배추**가
   겹쳐 뿌옇게 됐다. 정렬이 필요한 구간과 움직이는 구간은 반드시 갈라야 한다.

   길다 싶어도 줄이지 말 것 — 사람이 지루해하는 건 **꾹 누르는 시간**(520ms)이지
   연출 시간이 아니다. 여기서 서두르면 확대되는 게 아니라 튀어 오르는 걸로 보인다.

   끝난 상태가 각 요소의 평소 값과 같아서 도중에 클래스를 떼도 튀지 않는다. */

@keyframes plate-in {
  0% {
    /* immersive.mjs 가 눌린 카드를 재서 넣는다. 없으면(키보드 진입) 그냥 작게 시작 */
    transform: var(--flip-t, scale(.3));
    animation-timing-function: cubic-bezier(.2, .78, .3, 1);
  }
  24% {
    /* 중간에 한 번 카드 크기로 선다. 값은 immersive.mjs 가 출발 크기를 보고 정한다 —
       모바일은 그리드 카드가 이미 화면만 해서 고정값을 쓰면 도중에 작아진다. */
    transform: scale(var(--mid-s, .62));
    animation-timing-function: cubic-bezier(.42, 0, .3, 1);
  }
  /* **여기서 멈춰 서 있어야 한다.** 창의 자리는 immersive.mjs 가 이 배율 기준으로
     한 번 재서 넣은 값이라, 창이 열리는 62% 이후에도 판이 계속 커지면 어긋난다.
     예전엔 100% 까지 천천히 커졌는데 그 역할은 이제 창이 대신한다.

     배율이 1 이 아니라 --set-s 인 이유: 판의 1 은 **카드가 화면보다 큰 크기**다
     (카드 높이가 누끼 높이의 1.59배). 예전엔 카드가 1 에 닿기 전에 녹아서 그게
     드러나지 않았지만, 이제는 서서 그대로 보이므로 화면에 맞춰 줄여야 한다. */
  /* 끝값이 곧 **평소 값(변형 없음)** 이어야 한다. --set-s 로 서서 끝나게 뒀더니
     is-entering 이 걷히는 순간 판이 --set-s 에서 1 로 툭 튀었다. --pg 가 마지막에
     --set-inv(=1/--set-s)가 되므로 여기 곱은 정확히 1 로 끝난다.

     되돌리는 시각을 --pg 가 정한다(plate-pg). 배율 식을 여기 두는 게 요점이다 —
     판과 자루가 같은 변수를 읽어야 틀이 창과 안 어긋난다. */
  44%, 100% { transform: scale(calc(var(--set-s, 1) * var(--pg, 1))); }
}

/* 판이 제 크기로 돌아온다. **창이 열리는 구간과 같이 간다.**

   예전엔 84~100% 에 몰아서 돌렸다. 튀지는 않았지만 **배경이 다 들어오고 창도 다
   열린 뒤에 캐릭터만 240ms 동안 커지는** 순서라, 결국 "갑자기 커진다"로 보였다.
   데스크톱에서 +16%, 폰에서는 카드가 화면 폭에 눌려 --set-s 가 0.6 근처까지
   내려가므로 +68% 다. 다 끝난 화면에서 그만큼 움직이면 눈에 안 띌 수가 없다.

   그래서 창이 벌어지기 시작하는 62% 에 같이 출발한다. 카메라가 창을 통과하는 동안
   주인공도 같이 다가오는 것이 되어, 두 사건이 아니라 한 번의 달리로 읽힌다.

   곡선은 창(--winK)의 가속 곡선을 그대로 쓰면 안 된다. 그건 뒤로 갈수록 가파른데,
   그러면 커지는 양의 69% 가 창이 화면을 다 덮은 **뒤에** 몰려서 결국 "다 끝나고
   혼자 커진다"가 된다. 실제로 그렇게 재 봤다. 완만한 ease-in-out 으로 펴면 창이
   화면을 덮는 시점(84%)에 이미 73% 가 진행돼 있어서, 나머지는 착지로 읽힌다.

   이징이 --winK 와 정확히 같을 필요는 없다. 정합은 판과 자루가 **같은 --pg 를
   읽는다**는 데서 나오지 곡선이 같아서 나오는 게 아니다. 곡선은 느낌만 정한다. */
@keyframes plate-pg {
  0%, 62% { --pg: 1; animation-timing-function: cubic-bezier(.4, 0, .45, 1); }
  100% { --pg: var(--set-inv, 1); }
}

@keyframes plate-art-out {
  0%, 44% { opacity: 1; }
  62%, 100% { opacity: 0; }
}

@keyframes hero-in {
  0%, 46% { opacity: 0; }
  64%, 100% { opacity: 1; }
}

/* 창이 벌어진다. 배율은 immersive.mjs 가 화면을 재서 넣는다 (--win-k) —
   창은 거의 정사각인데 화면은 가로로 길어서, 다 덮는 데 필요한 배율이 화면마다
   다르다. 고정값(예전의 4.5)을 쓰면 조금만 넓거나 낮은 화면에서 **좌우에 검은 띠가
   남은 채로 연출이 끝난다.**
   가속 곡선인 건 카메라가 창을 통과하는 느낌이라 그렇다 — 등속이면 창이 열리는
   게 아니라 화면이 확대되는 걸로 보인다.

   84% 에 딱 다 덮고, 나머지 16% 는 화면 밖으로 더 나간다. 다 덮은 뒤에도 계속
   벌어져야 "통과했다"가 되고, 여기서부터는 틀이 화면 밖이라 뭘 해도 안 보인다. */
@keyframes window-open {
  0%, 62% { --winK: 1; animation-timing-function: cubic-bezier(.45, 0, .75, .5); }
  84% { --winK: var(--win-k, 4.5); animation-timing-function: cubic-bezier(.3, 0, .7, 1); }
  100% { --winK: var(--win-k-end, 5.5); }
}

/* 창 밖 판이 깔린다. **좌우가 비기 시작하는 1570ms 전에 다 깔려 있어야** 한다.
   74% = 1554ms 라 딱 그 직전이다. 그 전에는 카드가 어두운 방에 혼자 서 있는
   그림이고 그건 일부러 그런 것이라, 날아오는 동안에는 깔지 않는다. */
@keyframes void-in {
  0%, 50% { opacity: 0; }
  74%, 100% { opacity: 1; }
}

@keyframes dio-in {
  from { opacity: 0; transform: scale(var(--in-s, .86)); filter: blur(7px); }
  to { opacity: 1; transform: none; filter: blur(0); }
}

/* 창 배율은 .dio 에서 한 번만 돌린다. --winK 가 상속되므로 자루(clip-path)와
   틀(scale)이 같은 값을 읽는다. 틀 에셋이 없는 카드는 has-window 가 안 붙어서
   예전 연출 그대로 돈다. */
.immersive.is-entering .has-window { animation: window-open 2100ms both, plate-pg 2100ms both; }
.immersive.is-entering .has-window .dio-plate-frame { opacity: 1; }
.immersive.is-entering .dio-void { animation: void-in 2100ms linear both; }

.immersive.is-entering .dio-plate { animation: plate-in 2100ms both; }
.immersive.is-entering .dio-plate-art { animation: plate-art-out 2100ms linear both; }

.immersive.is-entering .dio-hero,
.immersive.is-entering .dio-rind,
.immersive.is-entering .dio-skin,
.immersive.is-entering .dio-ground { animation: hero-in 2100ms linear both; }


/* 주인공 평면은 위에서 따로 짰으므로 이 시차에서 뺀다 */
.immersive.is-entering .dio-layer:not(.dio-subject) {
  animation: dio-in 620ms cubic-bezier(.16, .84, .28, 1) both;
  /* 880ms = 42%. 판이 44% 에 서므로 그 직전부터 들어온다 — 창의 자리는 배율 1
     기준으로 잰 값이라, 판이 아직 커지는 동안 배경이 보이면 창과 어긋나 보인다. */
  animation-delay: calc(880ms + var(--i) * 45ms);
}

/* 들어오는 배율을 평면마다 다르게 준다 (--in-s).

   예전엔 여덟 장이 전부 `.86 → 1` 이었다. 다 같은 비율로 커지는 건 **줌**이지
   **달리**가 아니다 — 카메라가 실제로 앞으로 가면 가까운 것은 크게, 먼 것은 조금
   커진다. 그 차이가 "공간을 통과했다"를 만든다. 값이 작을수록 멀리서 크게 다가온다.

   **--i 로 일괄 계산하면 안 된다.** 뒤에서 앞이라는 순서와 거리가 일치하지 않는
   평면이 둘 있다:

     .dio-dew  렌즈에 맺힌 이슬이다. 카메라에 붙어 있으므로(거리 0) 앞으로 가도
               크기가 안 변한다. 여기서 크게 밀면 이슬이 유리에 있는 게 아니라
               허공에 떠 있는 게 된다 — --par: 0 으로 애써 만든 효과가 무너진다
     .dio-hud  자막이다. 장면 안의 물건이 아니라 화면에 얹은 UI라 달리를 안 탄다

   나머지는 --par(패럴랙스 세기)과 같은 순서로 준다. 그게 곧 거리다.
   숫자는 눈으로 맞춘 값이다. 앞잎(.dio-fore)이 이 연출을 제일 크게 판다 —
   --par 이 100 으로 제일 크고, 화면 기여도도 32% 로 1위인 평면이다. */
.immersive.is-entering .dio-sky   { --in-s: .96; }   /* --par 5   가장 멀다 */
.immersive.is-entering .dio-back  { --in-s: .92; }   /* --par 12 */
.immersive.is-entering .dio-rays  { --in-s: .88; }   /* --par 18 */
.immersive.is-entering .dio-motes { --in-s: .80; }   /* --par 30 */
.immersive.is-entering .dio-fore  { --in-s: .62; }   /* --par 100  가장 가깝다 */
.immersive.is-entering .dio-hud   { --in-s: .95; }   /* 자막 — 달리에서 뺀다 */
.immersive.is-entering .dio-dew   { --in-s: .98; }   /* 렌즈 위 — 카메라와 함께 간다 */

/* ── 나가기 ────────────────────────────────────────────────
   들어올 때를 되감지 않는다. 되감으면 나가는 데도 1.3초가 걸리는데, 닫기는 빨라야 한다.
   앞 평면부터 접히도록 시차만 뒤집는다. */

@keyframes dio-out {
  to { opacity: 0; transform: scale(.94); filter: blur(5px); }
}

.immersive.is-leaving .dio-layer {
  animation: dio-out 240ms ease both;
  animation-delay: calc((7 - var(--i)) * 18ms);   /* 7 = 맨 앞 평면(.dio-dew)의 --i */
}

@media (max-width: 760px) {
  /* .dio-hero 의 height 를 직접 덮으면 안 된다. 이 평면의 카드 자리·접지 그림자가
     전부 --hh 로 계산돼 있어서, 누끼만 줄이면 진입할 때 카드와 캐릭터가 어긋난다
     (틀이 녹는 순간 캐릭터가 26% 튀었다). 크기는 --hh 하나로만 바꾼다. */
  .dio { --hh: min(46svh, 400px); }
  .dio-meta { left: 20px; bottom: 78px; }
  .dio-tip { left: 20px; right: auto; bottom: 22px; }
}

@media (prefers-reduced-motion: reduce) {
  /* 꾹 누르기 게이지는 **끄지 않는다.** 장식이 아니라 진행 표시라, 이걸 멈추면
     "몇 초 더 눌러야 하는지" 를 알 길이 아예 없어진다 — 링이 안 차오르니 카드가
     안 눌리는 것으로 보인다. 대신 튀는 요소(글로우)만 뺀다. */
  .stage.is-holding::after { filter: none; }
  /* 카드가 눌려 들어가는 것도 남긴다. transition 은 style.css 가 이미 껐으므로
     즉시 적용된다 — 움직임이 아니라 상태 표시가 된다. */
  .dio-move,
  .dio-hero,
  .dio-rind,
  .dio-skin { transform: none; }
  .mote,
  .leaf,
  .dew.is-run,
  .dio-rays .dio-move::before { animation: none; }
  .immersive.is-entering .dio-layer:not(.dio-subject),
  .immersive.is-leaving .dio-layer,
  .immersive.is-entering .dio-void,
  .immersive.is-entering .dio-plate,
  .immersive.is-entering .dio-plate-art,
  .immersive.is-entering .dio-hero,
  .immersive.is-entering .dio-rind,
  .immersive.is-entering .dio-skin,
  .immersive.is-entering .dio-ground { animation: none; }
}
