/* move.css — star365 공용 모션 레이어 v0.3
 * 단일 출처: /opt/move/dist/move.css
 * 색은 정의하지 않는다. 각 플랫폼의 --bg/--ink/--ac 토큰을 그대로 쓴다.
 *
 * 핵심 규칙 3가지
 *  1) JS 가 죽어도 콘텐츠는 보인다 — 숨김 상태는 html.mv-ready 가 있을 때만 적용된다.
 *  2) transform/opacity/filter 만 움직인다 — width/height/top/left 는 건드리지 않는다(레이아웃 재계산 0).
 *  3) prefers-reduced-motion 이면 전부 즉시 최종 상태로 간다.
 *
 * v0.3 에서 더해진 규칙
 *  4) 진입은 '시간 기반'(반드시 끝난다), 스크롤 연출은 '스크롤 기반'(프레임 정확).
 *     → 진입만 JS(IntersectionObserver)가 맡고, parallax·progress·scrub 은 CSS 스크롤 타임라인이 맡는다.
 *  5) 한 요소를 두 엔진이 잡지 않는다 — 네이티브가 되면 JS/GSAP 은 그 요소에서 손을 뗀다(검증 C8).
 */

:root {
  /* 시간 — 4단계만 쓴다. 이 밖의 숫자를 직접 쓰지 않는다 */
  --mv-d1: 0.18s;   /* 마이크로: hover, 버튼 눌림 */
  --mv-d2: 0.38s;   /* 기본: 진입, 카드 */
  --mv-d3: 0.62s;   /* 큰 덩어리: 섹션, 히어로 */
  --mv-d4: 0.90s;   /* 연출: 배경, 매스 */

  /* 가속 곡선 — 3개만 쓴다 */
  --mv-out:   cubic-bezier(0.16, 1, 0.3, 1);      /* 들어올 때(감속). GSAP power3.out 과 같은 느낌 */
  --mv-inout: cubic-bezier(0.65, 0, 0.35, 1);     /* 상태 전환 */
  --mv-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* 살짝 넘어갔다 돌아옴. 확인/성공에만 */

  /* 거리 — 화면 크기에 비례시키지 않는다. 24px 넘으면 '튄다'는 인상이 된다 */
  --mv-rise: 14px;
  --mv-slide: 24px;
  --mv-scale: 0.985;
  --mv-blur: 6px;

  /* 순차 지연 */
  --mv-stagger: 0.055s;
  --mv-stagger-max: 8;   /* 이보다 뒤 항목은 더 늦추지 않는다(긴 목록에서 마지막이 1초 뒤에 뜨는 사고 방지) */

  /* 깊이 — parallax 기본값. 0.3 을 넘기지 않는다 */
  --mv-depth: 0.15;
}

/* ── 접근성: 이 블록이 다른 모든 규칙을 이긴다 ───────────────────────── */
@media (prefers-reduced-motion: reduce) {
  :root {
    --mv-d1: 0.01ms; --mv-d2: 0.01ms; --mv-d3: 0.01ms; --mv-d4: 0.01ms;
    --mv-rise: 0px; --mv-slide: 0px; --mv-scale: 1; --mv-blur: 0px;
    --mv-stagger: 0s; --mv-depth: 0;
  }
  .mv-ready [data-move] { opacity: 1 !important; transform: none !important; filter: none !important; }
  [data-move="marquee"] > :not(.mv-pause) { animation: none !important; }

  /* 🔑 스켈레톤 반짝임도 '상시 모션'이다. v0.2 는 마퀴만 껐고 이건 계속 돌았다 —
   *   화면 어딘가가 끝없이 깜빡이는 상태가 남는다(빛 민감성엔 이쪽이 더 나쁘다).
   *   검증 C7 이 이걸 잡아서 알게 됐다. 자리는 그대로 잡되 움직임만 없앤다. */
  [data-move="skeleton"] {
    animation: none !important;
    background: var(--line, #E4E6E4) !important;
  }

  /* 스크롤 타임라인도 여기서 끊는다 — @supports 블록 뒤에 와도 !important 로 이긴다.
   * (애니메이션 자체를 없애야 한다. animation-timeline 만 none 으로 두면 시간축으로 재생된다) */
  [data-move-parallax],
  [data-move="progress"],
  [data-move-scrub],
  [data-move-scrub] [data-move-track],
  [data-move-scrub] [data-move-step] { animation: none !important; transform: none !important; opacity: 1 !important; }

  /* 오버레이는 '즉시 나타나고 즉시 사라진다' — 다만 display/overlay 는 여전히 discrete 로 처리돼야
   * 닫힐 때 잔상이 남지 않는다. 시간만 0 으로 만든다(위 --mv-d2 가 이미 0.01ms) */
  [data-move="overlay"], [data-move="toast"] { transform: none !important; }
}

/* ── 진입(enter) ─────────────────────────────────────────────────────
 * .mv-ready 가 <html> 에 붙었을 때만 숨긴다 → 스크립트 로드 실패 = 그냥 정적 페이지
 *
 * 🔑 숨김 대상은 '진입 7종'만 명시한다. :not() 으로 빼는 방식은 쓰지 않는다 —
 *    :not() 이 특정도를 올려서 아래 .mv-in(보이기) 규칙을 이겨버린다. 실제로 그 사고가 났다.
 */
.mv-ready [data-move="rise"],
.mv-ready [data-move="fade"],
.mv-ready [data-move="left"],
.mv-ready [data-move="right"],
.mv-ready [data-move="zoom"],
.mv-ready [data-move="soft"],
.mv-ready [data-move="cover"] {
  opacity: 0;
  transition:
    opacity   var(--mv-d2) var(--mv-out) var(--mv-delay, 0s),
    transform var(--mv-d2) var(--mv-out) var(--mv-delay, 0s),
    filter    var(--mv-d2) var(--mv-out) var(--mv-delay, 0s);
}

.mv-ready [data-move="rise"]   { transform: translate3d(0, var(--mv-rise), 0); }
.mv-ready [data-move="fade"]   { transform: none; }
.mv-ready [data-move="left"]   { transform: translate3d(calc(var(--mv-slide) * -1), 0, 0); }
.mv-ready [data-move="right"]  { transform: translate3d(var(--mv-slide), 0, 0); }
.mv-ready [data-move="zoom"]   { transform: scale(var(--mv-scale)); }
.mv-ready [data-move="soft"]   { transform: translate3d(0, var(--mv-rise), 0); filter: blur(var(--mv-blur)); }
.mv-ready [data-move="cover"]  { transform: translate3d(0, var(--mv-slide), 0); clip-path: inset(0 0 100% 0); }

/* 도착 상태 — IntersectionObserver 가 .mv-in 을 붙인다 */
.mv-ready [data-move].mv-in {
  opacity: 1;
  transform: none;
  filter: none;
  clip-path: inset(0 0 0 0);
}

/* 합성 레이어는 '곧 움직일 것'에만 준다.
 * 🔑 v0.2 는 숨은 요소 전부에 will-change 를 상시로 켜 두었다 — 화면 밖 수백 개가 레이어를 붙든다.
 *   지금은 JS 가 화면 300px 앞에서 .mv-arm 을 붙이고, 전환이 끝나면 .mv-done 이 놓아준다(검증 C9). */
.mv-ready [data-move].mv-arm  { will-change: opacity, transform; }
.mv-ready [data-move].mv-done { will-change: auto; }

/* 목록 순차 지연 — JS 가 --mv-i 만 심어준다 */
.mv-ready [data-move][style*="--mv-i"] { --mv-delay: calc(var(--mv-i) * var(--mv-stagger)); }

/* 큰 덩어리는 조금 느리게 */
.mv-ready [data-move-size="lg"] { transition-duration: var(--mv-d3); }
.mv-ready [data-move-size="xl"] { transition-duration: var(--mv-d4); }

/* ── 상시 모션 ───────────────────────────────────────────────────── */

/* 마퀴: 로고/티커. 폭은 JS 가 재지 않는다 — 원본을 2벌 깔고 -50% 이동 */
[data-move="marquee"] { overflow: hidden; display: flex; position: relative; }
/* ⚠️ `> *` 에는 아래의 멈춤 버튼도 포함된다 — 버튼까지 같이 흘러가 버린다.
 *   흐르는 것은 '항목 줄'뿐이므로 버튼만 빼 준다. */
[data-move="marquee"] > :not(.mv-pause) {
  display: flex; flex: 0 0 auto; gap: inherit;
  animation: mv-marquee var(--mv-speed, 32s) linear infinite;
}
/* WCAG 2.2.2 — 5초 넘게 자동으로 움직이는 것에는 멈추는 수단이 있어야 한다.
 * hover 만으로는 키보드·터치 사용자가 멈출 수 없다 → focus-within + 명시 버튼(JS가 넣는다) */
[data-move="marquee"]:hover > :not(.mv-pause),
[data-move="marquee"]:focus-within > :not(.mv-pause),
[data-move="marquee"][data-move-paused="1"] > :not(.mv-pause) { animation-play-state: paused; }
@keyframes mv-marquee { to { transform: translate3d(-50%, 0, 0); } }

[data-move="marquee"] .mv-pause {
  position: absolute; inset-inline-end: 6px; inset-block-start: 50%;
  translate: 0 -50%;
  z-index: 2; cursor: pointer;
  font: inherit; font-size: .72rem; line-height: 1;
  padding: 5px 8px; border-radius: 999px;
  border: 1px solid var(--line, #E4E6E4);
  background: var(--bg, #fff); color: var(--ink, #111);
  opacity: .35; transition: opacity var(--mv-d1) var(--mv-out);
}
[data-move="marquee"]:hover .mv-pause,
[data-move="marquee"] .mv-pause:focus-visible { opacity: 1; }

/* 자석 버튼: JS 가 --mv-mx/--mv-my 를 넣는다 */
[data-move="magnetic"] {
  transform: translate3d(var(--mv-mx, 0px), var(--mv-my, 0px), 0);
  transition: transform var(--mv-d1) var(--mv-out);
}

/* 눌림 — 클릭이 '먹혔다'는 신호. 이게 없으면 느리다는 인상이 남는다 */
[data-move-press] { transition: transform var(--mv-d1) var(--mv-out); }
[data-move-press]:active { transform: scale(0.975); }

/* 스켈레톤: 로딩 자리를 잡아 CLS 를 0 으로 만든다 */
[data-move="skeleton"] {
  background: linear-gradient(90deg, var(--line, #E4E6E4) 25%, var(--acs, #EEF2F0) 37%, var(--line, #E4E6E4) 63%);
  background-size: 400% 100%;
  animation: mv-skeleton 1.4s ease infinite;
  border-radius: 6px;
}
@keyframes mv-skeleton { 0% { background-position: 100% 50% } 100% { background-position: 0 50% } }

/* 포커스 링 — 모션을 넣으면 키보드 사용자가 먼저 손해를 본다. 여기서 되돌린다 */
[data-move]:focus-visible,
[data-move] :focus-visible { outline: 2px solid var(--ac, #1F5F4E); outline-offset: 2px; }

/* ── 상태 변화: 오버레이 · 토스트 ─────────────────────────────────────
 * 🔑 이 레이어가 원래 풀려던 문제가 '상태 변화가 화면에 안 남는다' 였는데,
 *   v0.2 에는 정작 모달·토스트가 없었다. 여기서 채운다.
 *
 * 나갈 때(닫힐 때) 애니메이션이 되려면 display/overlay 를 discrete 로 전환해야 한다.
 *   transition-behavior: allow-discrete  — Chrome 117 · Firefox 129 · Safari 17.4 (2026-09-17 MDN BCD)
 *   @starting-style                      — Chrome 117 · Firefox 129 · Safari 17.5
 * 둘 다 없는 브라우저에서는 '그냥 즉시 열리고 즉시 닫힌다' — 깨지지 않는다.
 */
[data-move="overlay"], [data-move="toast"] {
  opacity: 0;
  transition:
    opacity   var(--mv-d2) var(--mv-out),
    transform var(--mv-d2) var(--mv-out),
    overlay   var(--mv-d2) allow-discrete,
    display   var(--mv-d2) allow-discrete;
}
[data-move="overlay"] { transform: translate3d(0, var(--mv-rise), 0) scale(var(--mv-scale)); }
[data-move="toast"]   { transform: translate3d(0, var(--mv-slide), 0); }

/* 열린 상태 — popover / <dialog open> / 직접 붙이는 .mv-open 셋 다 지원한다 */
[data-move="overlay"]:popover-open,
[data-move="toast"]:popover-open,
dialog[data-move="overlay"][open],
dialog[data-move="toast"][open],
[data-move="overlay"].mv-open,
[data-move="toast"].mv-open { opacity: 1; transform: none; }

/* 시작 상태 — 이게 없으면 '열릴 때'는 애니메이션이 안 된다(닫힐 때만 된다) */
@starting-style {
  [data-move="overlay"]:popover-open,
  [data-move="toast"]:popover-open,
  dialog[data-move="overlay"][open],
  dialog[data-move="toast"][open],
  [data-move="overlay"].mv-open,
  [data-move="toast"].mv-open { opacity: 0; }
  [data-move="overlay"]:popover-open,
  dialog[data-move="overlay"][open],
  [data-move="overlay"].mv-open { transform: translate3d(0, var(--mv-rise), 0) scale(var(--mv-scale)); }
  [data-move="toast"]:popover-open,
  dialog[data-move="toast"][open],
  [data-move="toast"].mv-open { transform: translate3d(0, var(--mv-slide), 0); }
}

/* 뒷배경도 같이 페이드한다 — 안 하면 배경만 툭 끊긴다 */
[data-move="overlay"]::backdrop,
[data-move="toast"]::backdrop {
  background: rgb(0 0 0 / 0.38);
  opacity: 0;
  transition: opacity var(--mv-d2) var(--mv-out), overlay var(--mv-d2) allow-discrete, display var(--mv-d2) allow-discrete;
}
[data-move="overlay"]:popover-open::backdrop,
dialog[data-move="overlay"][open]::backdrop,
[data-move="toast"]:popover-open::backdrop,
dialog[data-move="toast"][open]::backdrop { opacity: 1; }
@starting-style {
  [data-move="overlay"]:popover-open::backdrop,
  dialog[data-move="overlay"][open]::backdrop,
  [data-move="toast"]:popover-open::backdrop,
  dialog[data-move="toast"][open]::backdrop { opacity: 0; }
}

/* ── 스크롤 진행 바 ─────────────────────────────────────────────────
 * 네이티브가 되면 CSS 가 직접 계산한다(아래 @supports). 안 되면 JS 가 --mv-progress 를 갱신한다. */
[data-move="progress"] {
  transform-origin: 0 50%;
  transform: scaleX(var(--mv-progress, 0));
}

/* ── 스크롤 무대(stage) ───────────────────────────────────────────
 * 고정은 GSAP pin 이 아니라 CSS sticky 로 한다.
 * 🔑 pin 은 스페이서를 나중에 끼워 넣어 아래 내용을 밀어낸다 — 이 데모에서 CLS 1.57 이 그렇게 났다.
 *   sticky 는 높이가 처음부터 CSS 에 있으므로 레이아웃이 변하지 않는다(CLS 0).
 *   덤으로 JS 가 없어도 '고정되는 섹션'으로 정상 동작한다.
 */
[data-move-scrub="horizontal"],
[data-move-scrub="pin"] { height: var(--mv-stage, 300vh); position: relative; }

[data-move-stage] {
  position: sticky; top: 0; height: 100vh;
  display: flex; align-items: center; overflow: hidden;
}
/* GSAP·네이티브 둘 다 없으면 손으로 가로 스크롤이라도 되게 둔다 */
[data-move-scrub="horizontal"] [data-move-stage] { overflow-x: auto; scrollbar-width: thin; }
[data-move-track] { display: flex; flex: 0 0 auto; width: max-content; }

/* pin 무대의 단계들 — 엔진이 없으면 첫 단계만 보인다(겹쳐 읽히지 않게) */
[data-move-scrub="pin"] [data-move-step] { position: absolute; left: 0; right: 0; margin-inline: auto; }
[data-move-scrub="pin"] [data-move-step]:not(:first-child) { opacity: 0; }

/* 엔진이 하나도 없을 때 — 무대를 평범한 문서로 되돌린다. 숨은 채로 남는 글이 없어야 한다 */
[data-move-scrub].mv-nogsap { height: auto; }
[data-move-scrub].mv-nogsap [data-move-stage] { position: static; height: auto; padding: 48px 0; display: block; }
[data-move-scrub].mv-nogsap [data-move-step] { position: static; opacity: 1; margin: 0 auto 40px; }

/* ═══════════════════════════════════════════════════════════════════
 * 네이티브 스크롤 타임라인 (CSS Scroll-driven Animations)
 * animation-timeline: view()/scroll() — Chrome 115+ · Safari 26+ · Firefox 미출시(2026-09-17 MDN BCD)
 *
 * 🔑 여기 있는 것들은 **JS 도 GSAP 도 없이** 동작하고, 메인 스레드를 쓰지 않는다.
 *   지원하지 않는 브라우저(Firefox)에서는 아래 블록이 통째로 무시되고
 *   move.js 의 rAF 폴백 / GSAP 경로가 대신 맡는다. 둘이 동시에 잡지 않는 건 move.js 가 보장한다.
 *
 * 🔑 진입(rise/fade/…)은 여기에 넣지 않았다. 스크롤에 물리면 '스크롤을 멈춘 자리에서
 *   글이 반투명한 채로 멈춘다'. 진입은 시간 기반이라야 반드시 끝난다.
 * ═══════════════════════════════════════════════════════════════════ */
@supports (animation-timeline: view()) {

  /* 1. 패럴랙스 — 깊이는 --mv-depth (JS 가 data-move-parallax 값을 옮겨 심거나, 직접 style 로 준다) */
  @keyframes mv-parallax {
    from { transform: translate3d(0, calc(var(--mv-depth) * -60px), 0); }
    to   { transform: translate3d(0, calc(var(--mv-depth) *  60px), 0); }
  }
  [data-move-parallax] {
    animation: mv-parallax linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }

  /* 2. 스크롤 진행 바 — 스크롤 리스너 0개 */
  @keyframes mv-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  [data-move="progress"] {
    animation: mv-progress linear both;
    animation-timeline: scroll(root block);
  }

  /* 3. scrub reveal — 스크롤에 물린 등장. GSAP 없이 된다 */
  @keyframes mv-scrub-reveal {
    from { opacity: 0; transform: translate3d(0, 40px, 0); }
    to   { opacity: 1; transform: none; }
  }
  [data-move-scrub="reveal"] {
    animation: mv-scrub-reveal linear both;
    animation-timeline: view();
    animation-range: entry 10% cover 35%;
  }

  /* 4. 가로 무대 — 세로 스크롤을 가로 이동으로.
   *    이동 거리를 JS 가 재지 않는다: 컨테이너 쿼리 단위로 '무대폭 − 트랙폭' 을 CSS 가 직접 계산한다.
   *    (min(0px, …) 이라 트랙이 무대보다 좁으면 아예 움직이지 않는다) */
  [data-move-scrub="horizontal"] [data-move-stage] { container-type: inline-size; overflow-x: hidden; }
  @keyframes mv-track {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(min(0px, calc(100cqw - 100%)), 0, 0); }
  }
  /* 🔑 타임라인은 '트랙'이 아니라 '무대 섹션'에서 온다.
   *   트랙은 sticky 무대 안에 있어 스스로는 화면을 벗어나지 않는다 —
   *   view() 를 트랙에 걸면 진행도가 움직이지 않고 한 값에 멈춘다(실측: 어느 위치에서나 -254px).
   *   그래서 섹션에 이름 붙인 타임라인(--mv-stage-tl)을 걸고 자손이 그걸 가리킨다. */
  [data-move-scrub="horizontal"] [data-move-track] {
    animation: mv-track linear both;
    animation-timeline: --mv-stage-tl;
    animation-range: contain 0% contain 100%;
  }

  /* 5. 단계 무대(pin) — 단계 i 는 전체 구간의 i/n ~ (i+1)/n 에서만 보인다.
   *    JS 가 --mv-i/--mv-n 만 한 번 심는다(프레임마다 도는 코드 없음). */
  [data-move-scrub="horizontal"],
  [data-move-scrub="pin"] { view-timeline: --mv-stage-tl block; }
  @keyframes mv-step {
    0%   { opacity: 0; transform: translate3d(0, 18px, 0); }
    14%  { opacity: 1; transform: none; }
    82%  { opacity: 1; transform: none; }
    100% { opacity: 0; transform: translate3d(0, -18px, 0); }
  }
  [data-move-scrub="pin"] [data-move-step] {
    animation: mv-step linear both;
    animation-timeline: --mv-stage-tl;
    animation-range:
      contain calc(var(--mv-i, 0) / var(--mv-n, 1) * 100%)
      contain calc((var(--mv-i, 0) + 1) / var(--mv-n, 1) * 100%);
  }
  /* 첫 단계만 보이던 폴백 규칙을 되돌린다 — 네이티브가 전부 관리한다 */
  [data-move-scrub="pin"] [data-move-step]:not(:first-child) { opacity: 0; }
}

/* ── 페이지 전환 (View Transitions) ───────────────────────────────────
 * 기본값은 '아무 일도 없음'이다. 켜는 것은 각 플랫폼의 선택(dist/move-nav.css 를 추가로 불러온다).
 * 여기서는 켜졌을 때의 '속도와 곡선'만 규격에 맞춘다 — 기본 0.25s ease 는 이 레이어의 토큰이 아니다.
 * MOTION.md 의 금지 항목("화면 전체를 가리는 페이지 전환")을 지키려고 밝기/덮개 없이 교차 페이드만 쓴다.
 */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--mv-d2);
  animation-timing-function: var(--mv-out);
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}
