/* ==========================================================================
   heowinsall — F «SPIRAL» 표현
   구조 = Story(세로 문서 5장) → Work(WebGL 나선) → Contact
   ── 이 파일이 지키는 것 (home-e에서 그대로 계승한 잠긴 결정) ──────────────
   · 색 토큰은 tokens.css가 단일 소스 — 여기서 :root에 색을 정의하지 않는다
   · 라디우스 전면 0 · 그림자 0 — 부유는 명도 계단이 단독으로 진다
   · 서체 1종(Pretendard) + 커버 이름 1곳만 Wanted Sans
   · 베이스 = 세로 정적 문서. html.anim일 때만 뷰 전환·나선으로 승격한다
   ========================================================================== */

*{margin:0;padding:0;box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:auto}
body{
  background:var(--canvas); color:var(--ink);
  font-family:var(--fk); font-variant-numeric:tabular-nums;
  word-break:keep-all; overflow-wrap:break-word;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}
a{color:inherit;text-decoration:none}
.sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
.mono{font-family:var(--fm)}

/* home-e R44 계승 — 포커스 링 전면 폐기.
   ⚠WCAG 2.4.7 실패라는 사실은 그대로 상속된다. 되살리면 여기 한 줄이다. */
:focus-visible{outline:none}

.skip{position:absolute;left:12px;top:-72px;z-index:200;background:var(--frame);
  border:1px solid var(--ink);padding:0 18px;font-size:14px;font-weight:600;
  display:inline-flex;align-items:center;min-height:44px;transition:top .12s}
.skip:focus{top:12px}

/* 그레인 1패스 — home-e와 같은 물성 */
body::after{
  content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opacity:.025;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='128' height='128'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/></filter><rect width='128' height='128' filter='url(%23n)'/></svg>");
}

/* ==========================================================================
   BASE — 세로 정적 문서 (폴백 완결)
   .anim이 없어도(1023 이하 · reduced · WebGL 실패 · 모듈 로드 실패)
   Story → 작업 목록 → Contact이 순서대로 읽히는 온전한 문서다.
   ========================================================================== */
.story,.worklist,.s-contact{max-width:1120px;margin:0 auto;padding:0 24px}
.sec{padding:96px 0}

/* s0 커버 */
.s-cover{text-align:center;padding:120px 0 96px}
.kicker{font-size:12px;letter-spacing:.08em;color:var(--ink-cap);margin-bottom:14px}
.name{font-family:var(--fd);font-size:clamp(44px,9vw,124px);font-weight:500;
  letter-spacing:-.03em;line-height:1.12}
.scrollhint{margin-top:40px;font-size:11px;letter-spacing:.14em;color:var(--ink-ui)}

/* s1 헤드라인 */
.figh{font-weight:400;letter-spacing:-.01em;font-size:clamp(22px,3.2vw,40px);line-height:1.6}
.figrole{margin-top:48px;font-size:15px;color:var(--ink-sub);font-weight:400}

/* s2 일하는 방식과 이력
   ★★두 열의 **끝이 맞는다**(●사용자 2026-08-08). 현행은 좌 394.5 / 우 469.3으로 74.8px 어긋났다.
     ⇒ 좌측 블록 간격 48 → **85.4px**. 고른 값이 아니라 계산된 값이다:
       (우측 469.3 − 좌측 블록 합 298.5) ÷ 2 = 85.4.
     타이포·폭·줄바꿈은 한 값도 안 건드렸다 — **이 한 줄만** 바뀐다.
   ⚠**CSS로 스스로 맞추려 했다가 틀렸다.** `align-items:stretch`+`space-between`이면 값이 필요 없지만,
     `html.anim .sec{min-height:100vh}`라 이 섹션은 **화면 높이를 갖는다** — 두 열이 100vh까지 늘어나
     블록 간격이 380px로 벌어졌다(실측). 아래 주석의 「두 열만 상단 앵커다」가 그 경고였다.
   ⚠그래서 이 값은 **내용이 바뀌면 조용히 어긋난다.** 이력에 행이 하나 늘면 다시 계산해야 한다.
     ⇒ `_gates/_s2.mjs`가 매번 다시 계산해 대조한다. 게이트가 새 값을 알려 주므로 손으로 재지 않는다. */
.s-way{display:flex;gap:64px;align-items:flex-start}
.f2L,.f2R{display:flex;flex-direction:column}
.f2L{flex:1 1 0;gap:85.4px}
.f2R{flex:1 1 0;gap:40px}
.f2blk{display:flex;flex-direction:column;gap:8px}
.f2h{color:var(--ink);font-weight:500;line-height:1.5;font-size:16px}
.f2b{color:var(--ink-cap);font-weight:400;line-height:1.5;font-size:15px}
.f2b p{margin:0}
.f2lab{color:var(--ink-cap);font-weight:400;line-height:1.5;font-size:13px;margin-bottom:4px}
.f2row{display:flex;gap:6px 24px;padding:16px 0;flex-wrap:wrap;
  border-bottom:1px solid var(--hair)}
.f2yr{color:var(--ink);font-weight:500;line-height:1.5;font-size:15px;flex:0 0 auto}
.f2val{display:flex;flex-direction:column;gap:4px;flex:1 1 240px;min-width:0}
.f2m{color:var(--ink);font-weight:500;line-height:1.5;font-size:15px}
.f2s{color:var(--ink-ui);font-weight:400;line-height:1.5;font-size:13px}
.f2badge{display:block;flex:0 0 auto}
.f2badge img{display:block;width:96px;height:48.8px}

/* s3·s4 프로젝트 이력 — home-e의 지면 문법을 계승하되 세로 문서 비례로 다시 잡는다.
   ★유형 라벨은 어떤 밀도에서도 지우지 않는다(잠긴 결정) — 지우면 Proposal이 수행처럼 읽힌다. */
.histh{font-size:13px;font-weight:500;letter-spacing:.02em;color:var(--ink-ui);margin-bottom:28px}
.histh .mono{font-weight:400}
.hgrid,.hcell{display:flex;flex-direction:column}
.hgrid{gap:36px}
.hrow{display:flex;flex-wrap:wrap;gap:36px}
.hcell{flex:1 1 320px;min-width:0}
.hy{color:var(--ink-ui);font-weight:500;letter-spacing:.02em;font-size:13px;margin-bottom:10px}
.hitem{display:block;padding:5px 0;font-size:15px;line-height:1.5;letter-spacing:-.01em}
.hn{font-weight:500;color:var(--ink)}
.hn i{font-style:normal;font-weight:400}
.hb{display:inline-block;vertical-align:-1px;margin-left:2px}
.ht{display:block;margin-top:1px;font-size:13px;color:var(--ink-ui)}
/* 제안 행은 이름·유형 **둘 다** 한 단 옅게(home-e R58 계승).
   ⚠--ink-faint는 대비 2.57로 AA 미달이다 — 의도적 후퇴이며 본문 대역에는 쓰지 않는다. */
.hitem.hp .hn,.hitem.hp .ht{color:var(--ink-faint)}

/* 나선 진입 표제 — .anim에서는 뷰 전환의 방아쇠라 화면에 안 보인다 */
.s-gate{text-align:center}
.gatelab{font-size:11px;letter-spacing:.14em;color:var(--ink-ui)}

/* ── 작업 목록 (정적 폴백의 «목록 화면») ─────────────────────────────────
   나선은 이 목록의 표현일 뿐이다. 링크·데이터의 진리원은 여기다. */
.worklist{list-style:none;padding-top:0;padding-bottom:96px}
.wi{display:flex;align-items:baseline;justify-content:space-between;gap:16px;
  padding:14px 0;border-bottom:1px solid var(--hair)}
.worklist li:first-child .wi{border-top:1px solid var(--hair)}
.wn{font-size:15px;font-weight:500}
.wm{font-size:12px;color:var(--ink-cap);white-space:nowrap}
.wi:hover .wn{color:var(--pop-ink)}

/* Contact */
.s-contact{text-align:center;padding-bottom:120px}
.fh{font-size:clamp(28px,4.4vw,56px);font-weight:700;letter-spacing:-.02em}
.clead{margin-top:20px;font-size:15px;line-height:1.6;color:var(--ink-sub)}
.mailrow{display:flex;gap:20px;align-items:center;justify-content:center;
  flex-wrap:wrap;margin-top:20px}
.mailbtn{position:relative;display:inline-flex;align-items:center;max-width:100%;
  background:transparent;border:0;padding:0;cursor:pointer;font-family:inherit;color:inherit}
.mailbtn .addr{font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
  border-bottom:1px solid var(--ink-ui);padding-bottom:3px;transition:color .2s,border-color .2s}
.mailbtn:hover .addr{color:var(--ink);border-bottom-color:var(--ink)}
/* 체크는 흐름에서 빼내 주소 오른쪽에 띄운다 — 중앙정렬의 기준이 «주소»가 되게(home-e R38) */
.mailbtn .ck{position:absolute;left:100%;top:50%;margin-left:14px;
  stroke:var(--pop-ink);stroke-dasharray:var(--ck-len,12.2);stroke-dashoffset:var(--ck-len,12.2);
  opacity:0;transform:translateY(-50%) scale(.86);transform-origin:50% 50%;
  transition:opacity .18s linear,stroke-dashoffset 0s linear .18s,transform 0s linear .18s}
.mailbtn.copied .ck{opacity:1;stroke-dashoffset:0;transform:translateY(-50%) scale(1);
  transition:opacity .1s linear,
             stroke-dashoffset .34s cubic-bezier(.22,1,.36,1),
             transform .34s cubic-bezier(.22,1,.36,1)}
.ck{fill:none;stroke:var(--pop-ink);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}

/* 크롬·나선·슬롯·지면 틴트는 정적 모드에서 존재하지 않는다 */
#stage,#chrome,#slot,#bg,.ovbtn,#ovlabels{display:none}

/* 정적 모드 리빌 */
@media (prefers-reduced-motion:no-preference){
  html:not(.anim) .sec{opacity:0;transform:translateY(14px);
    transition:opacity .4s cubic-bezier(.16,1,.3,1),transform .4s cubic-bezier(.16,1,.3,1)}
  html:not(.anim) .sec.in{opacity:1;transform:none}
}

/* ==========================================================================
   ANIM — 승격 (≥1024 && !reduced)
   ========================================================================== */
html.anim body{background:var(--canvas)}

/* ── Story: 한 장이 한 화면 ────────────────────────────────────────────
   세로 문서라는 성질은 그대로 두고 «장»의 경계만 세운다(조사 유형 D의 구간 분할).
   스냅은 쓰지 않는다 — 스냅은 읽는 속도를 화면이 정해 버린다. */
html.anim .sec{min-height:100vh;display:flex;flex-direction:column;justify-content:center;
  padding:120px 0}
/* ★두 열은 **위를 맞춘다**(사용자 요청). 가운데 맞춤이면 짧은 열이 아래로 처져
   좌우가 서로 다른 높이에서 시작하고, 그러면 «같은 지면의 두 열»로 안 읽힌다.
   섹션 자체는 화면 가운데에 서고(위 `.sec`의 justify-content), 두 열만 상단 앵커다. */
html.anim .s-way{flex-direction:row;justify-content:flex-start;align-items:flex-start}
html.anim .s-cover{align-items:center}
html.anim .name{font-size:clamp(64px,9vw,124px)}
html.anim .figh{font-size:clamp(28px,3.2vw,40.8px);line-height:1.6}

/* ★스크롤 구동 — JS 0줄(조사 유형 C).
   ── 사용자 요청 「텍스트가 스크롤에 따라 딸려 올라가는 모션」 ─────────────
   등장만 하고 마는 것이 아니라, 요소가 화면을 **지나는 내내** 스크롤보다 조금 더 빨리
   위로 흐른다. 그래서 «페이지를 따라오는» 것이 아니라 «딸려 올라가는» 것으로 읽힌다.
   `view()` 타임라인은 요소가 뷰포트를 가로지르는 구간을 0~1로 주므로, 그 구간 전체에
   이동을 깔면 된다 — 스크롤 값을 JS로 읽을 필요가 없다.

   이동량은 요소마다 다르다(`--d`). 같은 양으로 움직이면 지면이 통째로 미끄러지는 것처럼 보이고,
   달라야 층이 생긴다 — 큰 표제가 많이, 본문이 적게.
   ⚠`--d`는 keyframe 안 calc에서 쓰인다. 애니메이션되는 값이 아니라 요소마다 고정된 상수다. */
/* ★★가운데 «정지 구간»이 있어야 한다 — 30%~70% 사이에서는 모든 글자가 정확히 0이다.
   ⚠처음엔 전 구간을 계속 흐르게 했는데, 그러면 글자가 **한 번도 정렬되지 않아**
     줄이 늘 물결친 채라 읽을 수가 없다(사용자 지적: "제대로 되는 부분이 없어서 이상하게 읽힌다").
     들어올 때와 나갈 때만 글자마다 따로 움직이고, 읽는 자리에서는 활자가 제자리에 선다.
   ⚠글자마다 시작 지점이 최대 8% 어긋나므로 **공통 정지 구간은 38%~70%**다 —
     그 폭을 확보하려고 평탄부를 넉넉히 잡았다. 좁히면 글자마다 정지 시점이 갈려 다시 물결친다. */
@keyframes fFlow{
  0%   {opacity:0; transform:translateY(calc(var(--d,44px) *  1))}
  30%  {opacity:1; transform:translateY(0)}
  70%  {opacity:1; transform:translateY(0)}
  100% {opacity:1; transform:translateY(calc(var(--d,44px) * -0.9))}
}
/* ★★줄 단위 마스크 리빌 (사용자 선택).
   바깥 `.ln`이 `overflow:hidden`인 마스크이고, 안쪽 `.lni`가 아래에서 올라온다.
   **줄이 통째로 움직이므로 줄 안 활자는 서로 어긋나지 않는다** — 문자 단위가
   «복잡하다»던 원인이 정확히 그 어긋남이었다.
   레퍼런스(pacome)도 줄 단위이고(SplitType type:"lines" · yPercent −100→0 · stagger 0.1),
   우리 커버 이름이 이미 같은 문법이라(.name{overflow:hidden} + eUp) 새 어휘가 아니다. */
/* ⚠`.lns`가 인라인이면 그 안의 블록 자식이 «block-in-inline»으로 쪼개져
   view() 타임라인의 주체가 제대로 잡히지 않는다 — 진행도가 1에 고정돼 애니메이션이
   아예 안 돌았다(실측: 어느 스크롤에서도 finished 1.000). 반드시 블록이어야 한다. */
.lns{display:block}
/* ★★`overflow:hidden`이 아니라 **`clip`**이다. 이유가 결정적이다 —
   `hidden`은 그 요소를 **스크롤 컨테이너로 만든다**. 그러면 안쪽 `.lni`의 `view()` 타임라인이
   문서가 아니라 «마스크»에 물리는데, 마스크는 절대 스크롤되지 않으므로 진행도가 고정된다
   (실측: 어느 스크롤에서도 progress 1.000 · finished — 애니메이션이 아예 안 돌았다).
   `clip`은 똑같이 잘라 내지만 스크롤 컨테이너를 만들지 않아 타임라인이 문서를 본다. */
.ln{display:block;overflow:clip}
.lni{display:block}
@keyframes fLine{
  from{transform:translateY(100%);opacity:0}
  to  {transform:translateY(0);opacity:1}
}

@supports (animation-timeline: view()){
  html.anim .split .lni{
    animation:fLine linear both;
    animation-timeline:view();
    /* ★대역은 `cover`의 **앞부분**만 쓴다. 뒤는 `both`가 끝 상태로 붙잡으므로
       올라온 뒤에는 활자가 완전히 정지한다 — 읽는 자리에서 흔들리지 않는다.
       ⚠`entry`를 쓰면 안 된다. 그 대역의 길이는 **줄 높이(≈65px)뿐**이라
         화면 맨 아래 가장자리에서 순식간에 끝나 버린다(실측: 어느 스크롤에서도 이미 finished).
         `cover`는 뷰포트+요소 높이(≈965px)라 26%가 250px쯤 되어 눈에 들어온다. */
    animation-range:cover 6% cover 30%;
  }
  /* 줄 번호가 곧 스태거다 — 뒤에 오는 줄일수록 조금 늦게 올라온다(4% ≈ 39px) */
  html.anim .split .ln:nth-child(2) .lni{animation-range:cover 10% cover 34%}
  html.anim .split .ln:nth-child(3) .lni{animation-range:cover 14% cover 38%}
  html.anim .split .ln:nth-child(n+4) .lni{animation-range:cover 18% cover 42%}

  /* 덩어리는 아주 약하게만 흐른다 — 스크롤과 이어진 느낌은 남기되 활자는 안 흔든다 */
  html.anim .f2b,html.anim .f2rows,html.anim .hcell{
    animation:fFlow linear both;
    animation-timeline:view();
    animation-range:cover 0% cover 100%;
  }
  html.anim .f2b{--d:22px}
  html.anim .f2rows{--d:20px}
  html.anim .hcell:nth-child(odd){--d:24px}
  html.anim .hcell:nth-child(even){--d:34px}
  /* 커버는 이 흐름에서 뺀다 — 첫 화면은 프리롤(아래 eUp)이 단독으로 맡는다. */
}
/* 폴백 — view() 미지원 시 IntersectionObserver가 .in을 붙인다.
   여기서는 글자 단위로 가지 않는다(스크롤에 물리지 않으면 «딸려 올라감»이 성립하지 않고,
   글자마다 지연만 준 등장은 다른 물건이다). 덩어리로 한 번 올라오고 끝난다. */
@supports not (animation-timeline: view()){
  html.anim .s-lead .figh,html.anim .s-lead .figrole,
  html.anim .f2blk,html.anim .f2sec,
  html.anim .histh,html.anim .hcell{
    opacity:0;transform:translateY(20px);
    transition:opacity .5s cubic-bezier(.22,1,.36,1),transform .5s cubic-bezier(.22,1,.36,1)}
  html.anim .in .figh,html.anim .in .figrole,
  html.anim .sec.in .f2blk,html.anim .sec.in .f2sec,
  html.anim .sec.in .histh,html.anim .sec.in .hcell{opacity:1;transform:none}
}

/* 커버 프리롤 0.75s — home-e E12 계승(전용 로더 없음) */
html.anim .name{overflow:hidden}
html.anim .name .w{display:inline-block}
@keyframes eUp{from{transform:translateY(100%)}to{transform:translateY(0)}}
@keyframes eIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:none}}
html.anim .s-cover .kicker{animation:eIn .3s cubic-bezier(.22,1,.36,1) both}
html.anim .s-cover .name .w{animation:eUp .45s cubic-bezier(.22,1,.36,1) both}
html.anim .s-cover .scrollhint{animation:eIn .3s .45s cubic-bezier(.22,1,.36,1) both}

/* 나선 진입 문턱 — 보이지는 않지만 자리는 차지한다(스크롤이 여기 닿으면 f.js가 넘긴다) */
html.anim .s-gate{min-height:60vh;opacity:0;pointer-events:none}

/* ── 뷰 전환 ───────────────────────────────────────────────────────────
   상태는 셋뿐이다: v-story / v-work / v-contact.
   나선이 «진짜 무한»이라 스크롤로는 그 밖으로 못 나간다(사용자 결정) —
   그래서 출구는 하단 크롬의 퀵 네비뿐이고, 그 사실이 이 규칙에 들어 있다. */
html.anim #story,html.anim .worklist,html.anim .s-contact{
  transition:opacity .5s cubic-bezier(.65,0,.35,1)}

/* ★Story는 **있던 자리에 선 채로** 사라진다.
   ⚠`position:fixed; inset:0`만 주면 문서 높이가 무너져 scrollY가 0으로 튀고,
     사라지는 0.5초 동안 Story가 **맨 처음(커버 heowinsall)으로 되감겨** 스쳐 보였다.
     빠져나가는 순간의 좌표를 `--sy`로 받아 그만큼 위로 밀어 두면 화면이 움직이지 않는다.
     (transform을 쓰는 이유 — top을 쓰면 레이아웃이 다시 잡히고, transform은 합성만 한다.) */
html.anim.v-work #story,html.anim.v-contact #story{
  opacity:0;pointer-events:none;
  position:fixed;left:0;right:0;top:0;bottom:auto;overflow:hidden;
  transform:translateY(calc(-1 * var(--sy,0px)))}
html.anim .worklist{display:none}          /* 나선이 목록을 대신한다 */
html.anim .s-contact{opacity:0;pointer-events:none;position:fixed;inset:0;
  display:flex;flex-direction:column;justify-content:center;align-items:center;
  max-width:none;padding:0 24px;z-index:20}
html.anim.v-contact .s-contact{opacity:1;pointer-events:auto}

/* ── 지면 틴트 ────────────────────────────────────────────────────────
   나선 캔버스(#stage)가 투명하고 그 **뒤에** 이 레이어가 깔린다(z-index 1 ↔ 2).
   두 겹 `<i>`를 번갈아 쓰며 불투명도만 오간다 — CSS가 background-image를 전환하지 못해서다.
   전환 0.9s는 카드 이동(0.5s대)보다 느리다: 색이 «따라온다»로 읽혀야지 같이 튀면 안 된다. */
html.anim #bg{display:block;position:fixed;inset:0;z-index:1;pointer-events:none;
  opacity:0;transition:opacity .5s cubic-bezier(.65,0,.35,1)}
html.anim.v-work #bg{opacity:1}
#bg i{position:absolute;inset:0;opacity:0;
  transition:opacity .9s cubic-bezier(.65,0,.35,1)}
#bg i.on{opacity:1}

/* 나선 무대 */
html.anim #stage{display:block;position:fixed;inset:0;z-index:2;
  opacity:0;pointer-events:none;transition:opacity .6s cubic-bezier(.65,0,.35,1)}
html.anim.v-work #stage{opacity:1;pointer-events:auto}
#stage canvas{display:block;width:100%;height:100%}
/* 나선 위에서는 문서 스크롤이 없다 — 휠은 회전으로 간다.
   `overscroll-behavior:none`은 브라우저가 스크롤을 상위로 넘기거나 바운스시키지 못하게 막는다.
   Story에서 스크롤로 들어온 직후에는 제스처가 아직 문서에 물려 있어서, 이게 없으면
   그 잔여 제스처가 화면을 미세하게 흔들거나 브라우저 UI를 끌어당긴다. */
html.anim.v-work,html.anim.v-work body{overflow:hidden;height:100%;overscroll-behavior:none}
html.anim.v-contact,html.anim.v-contact body{overflow:hidden;height:100%;overscroll-behavior:none}

/* ── 크롬 ──────────────────────────────────────────────────────────── */
/* ★우상단 OVERVIEW. 하단 크롬과 같은 활자(11px · 자간 .05em · --ink-cap)를 써서
   «또 하나의 크롬»으로 읽히게 한다 — 새 위계를 만들지 않는다. */
html.anim .ovbtn{display:inline-flex;align-items:center;position:fixed;right:32px;top:20px;z-index:92;
  min-height:44px;padding:0;background:transparent;border:0;cursor:pointer;
  font-family:var(--fe);font-size:11px;font-weight:500;letter-spacing:.12em;
  color:var(--ink-cap);text-transform:uppercase;transition:color .2s}
html.anim .ovbtn:hover{color:var(--ink)}
html.anim .ovbtn[aria-pressed="true"]{color:var(--ink);font-weight:700}
/* Story·Contact에서는 나선이 없으므로 «전부 보기»가 가리킬 것도 없다 */
html.anim .ovbtn{opacity:0;pointer-events:none;transition:opacity .3s,color .2s}
html.anim.v-work .ovbtn{opacity:1;pointer-events:auto}

/* 격자 이름표 — 타일 아래. 릴과 같은 문법(이름 좌 / 년도 우, 프레임 바깥) */
html.anim #ovlabels{display:block;position:fixed;inset:0;z-index:86;pointer-events:none}
#ovlabels .lb{position:absolute;display:flex;align-items:baseline;justify-content:space-between;
  gap:8px;white-space:nowrap;overflow:hidden;font-size:12px;line-height:16px;color:var(--ink);
  opacity:0;transform:translateY(4px);
  transition:opacity .3s cubic-bezier(.22,1,.36,1),transform .3s cubic-bezier(.22,1,.36,1)}
#ovlabels .lb.on{opacity:1;transform:none}
#ovlabels .lb .yr{font-size:11px;color:var(--ink-cap)}
/* 오버뷰에서는 고정 슬롯·위치 라벨이 할 일이 없다 — 타일마다 이름이 붙어 있다.
   ⚠`html.anim.v-work`를 **함께** 써서 특이도를 (0,4,1)로 올린다. `html.anim.ov`(0,3,1)만 쓰면
     `html.anim.v-work #slot{opacity:1}`과 동률이라 소스 순서에 기대게 되고, 실제로 졌다
     (같은 함정에 `.diving`에서 이미 한 번 물렸다 — 이 파일에서 두 번째다). */
html.anim.v-work.ov #slot,html.anim.v-work.ov .cright{opacity:0}

html.anim #chrome{display:flex;position:fixed;left:0;right:0;bottom:0;height:72px;z-index:90;
  align-items:center;justify-content:space-between;padding:0 32px;gap:24px;pointer-events:none}
html.anim #chrome>*{pointer-events:auto}
.cleft,.cright{font-size:11px;letter-spacing:.05em;color:var(--ink-cap);white-space:nowrap;flex:0 0 auto}
/* 위치 라벨은 **나선에서만** 뜻이 있다. Story·Contact에서는 잴 위치가 없으므로
   「— / —」 같은 빈 값을 띄우지 않고 통째로 물러난다(빈 값은 결함처럼 읽힌다). */
.cright{text-align:right;opacity:0;transition:opacity .3s}
html.anim.v-work .cright{opacity:1}
.cleft{display:inline-flex;align-items:center;gap:8px;min-height:44px;
  background:transparent;border:0;padding:0;cursor:pointer;font-family:inherit}
.cleft .addr{border-bottom:1px solid var(--ink-ui);padding-bottom:2px;
  transition:color .2s,border-color .2s}
.cleft:hover .addr{color:var(--ink);border-bottom-color:var(--ink)}
.cleft .ck{flex:0 0 auto;opacity:0;transform:translateX(-3px);
  transition:opacity .2s cubic-bezier(.22,1,.36,1),transform .2s cubic-bezier(.22,1,.36,1)}
.cleft.copied .ck{opacity:1;transform:none}

/* 중앙 축은 흐름에서 빼내 뷰포트 중심에 못박는다 — 좌우 라벨 길이가 축을 밀지 않게(home-e CRAFT-8) */
.cmid{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  display:flex;flex-direction:column;align-items:center;gap:8px;min-width:0}
.quick{display:flex;gap:2px;align-items:center}
.quick button{background:transparent;border:0;cursor:pointer;font-family:var(--fe);
  font-size:11px;font-weight:500;letter-spacing:.12em;color:var(--ink-cap);
  padding:6px 10px;min-height:28px;text-transform:uppercase}
.quick button:hover{color:var(--ink)}
.quick button[aria-current="true"]{color:var(--ink);font-weight:700}
.quick button[aria-current="true"]::after{content:"";display:block;height:1px;
  background:var(--ink);margin-top:4px}

/* ── ★고정 슬롯 — 나선의 라벨 (사용자 결정) ────────────────────────────
   카드가 48.7°씩 기울어 도는 지면에서 글자를 카드에 붙이면 같이 눕는다.
   그래서 이름·년도·분야는 **화면의 이 한 자리**에서만 교체된다.
   교체는 Arnaud Rocca의 «Leftovers Text» — 두 이름에 공통으로 있는 글자는
   사라지지 않고 새 자리로 **이동**한다(아래 .ch 규칙이 그 이동을 진다). */
html.anim #slot{display:block;position:fixed;left:0;right:0;bottom:104px;z-index:88;
  text-align:center;pointer-events:none;opacity:0;
  transition:opacity .4s cubic-bezier(.65,0,.35,1)}
html.anim.v-work #slot{opacity:1}
/* `position:relative` — 짝을 못 찾은 옛 글자를 **있던 자리에** 절대 배치로 남겨
   사라지게 하려면 기준 상자가 있어야 한다. 그냥 지우면 교체가 «깜빡임»으로 읽힌다. */
.slot-name{position:relative;font-size:15px;font-weight:500;color:var(--ink);
  height:22px;line-height:22px;white-space:nowrap}
.slot-meta{font-size:11px;letter-spacing:.05em;color:var(--ink-cap);height:16px;line-height:16px;
  margin-top:2px}
/* 글자 한 개 — 이동은 transform, 신규·소멸만 opacity가 진다 */
.slot-name .ch{display:inline-block;white-space:pre;
  transition:transform .52s cubic-bezier(.22,1,.36,1),opacity .26s linear}
.slot-name .ch.new{opacity:0}
/* 잔상 — 흐름에서 빠져 있던 자리에 그대로 서서 사라진다(f.js가 left를 박아 넣는다) */
.slot-name .ch.gone{position:absolute;top:0;opacity:0;pointer-events:none}

/* ══════════════════════════════════════════════════════════════════════
   Contact — 지면 전체가 뒤집힌다
   ══════════════════════════════════════════════════════════════════════
   home-e의 잠긴 결정(«Contact은 배경색 자체가 없고, 닿으면 전 지면이 반전된다»)을
   이 방향에서도 지킨다. 릴에서는 «카메라가 62% 지점을 넘으면»이 방아쇠였지만
   여기 나선은 무한이라 그 지점이 없다 — 그래서 **뷰 전환 자체가 방아쇠**다.

   ⚠아래는 «색이 따라가야 하는 것의 **전수 목록**»이다. 요소를 더하거나 지우면
     여기도 반드시 같이 고쳐야 한다. 하나라도 빠뜨리면 검은 지면에 검은 글자가 남는다
     (릴에서 실제로 났던 결함 계열 — E-SPEC §4-12). */
html.anim body{transition:background-color .6s cubic-bezier(.65,0,.35,1)}
html.anim.v-contact body{background:var(--pjbg)}

html.anim .s-contact .fh,html.anim .clead,html.anim .mailbtn .addr,
html.anim .ovbtn,html.anim .cleft,html.anim .cleft .addr,html.anim .cright,
html.anim .quick button,html.anim .quick button::after,html.anim .ck{
  transition:color .5s cubic-bezier(.65,0,.35,1),
             border-color .5s cubic-bezier(.65,0,.35,1),
             background-color .5s cubic-bezier(.65,0,.35,1),
             stroke .5s cubic-bezier(.65,0,.35,1)}

html.anim.v-contact .s-contact{color:#F1F2F3}
html.anim.v-contact .clead{color:var(--g-sub)}
html.anim.v-contact .mailbtn .addr{border-bottom-color:var(--g-hair)}
html.anim.v-contact .mailbtn:hover .addr{color:#fff;border-bottom-color:#fff}
html.anim.v-contact .ovbtn,
html.anim.v-contact .cleft,html.anim.v-contact .cright{color:var(--g-cap)}
html.anim.v-contact .cleft .addr{border-bottom-color:var(--g-hair)}
html.anim.v-contact .cleft:hover .addr{color:#fff;border-bottom-color:#fff}
html.anim.v-contact .quick button{color:var(--g-sub)}
html.anim.v-contact .quick button:hover,
html.anim.v-contact .quick button[aria-current="true"]{color:#fff}
html.anim.v-contact .quick button[aria-current="true"]::after{background:#fff}
/* 검은 지면에서는 --pop-ink(#A50090)가 안 읽힌다 → --pop으로 올린다(릴과 같은 판단) */
html.anim.v-contact .ck{stroke:var(--pop)}

/* ══════════════════════════════════════════════════════════════════════
   «들어가기» — 썸네일을 누르면 그 카드가 다가와 화면을 채운다
   ══════════════════════════════════════════════════════════════════════
   카드 자체의 이동·확대·초점 풀림은 셰이더가 진다(spiral.js `dive`).
   여기서는 **그 위에 얹힌 것들이 물러나는 것**만 맡는다 — 크롬·슬롯·지면이 남아 있으면
   화면을 채우며 다가오는 카드 위에 글자가 떠 있어 «들어간다»가 깨진다.
   `--dive`는 0→1 진행도로 spiral.js가 매 프레임 써 넣는다. */
/* ⚠선택자에 `.anim`을 함께 쓴다 — `html.anim.v-work #slot{opacity:1}`과 특이도가 같아야
   소스 순서(이 블록이 뒤)로 이길 수 있다. `html.diving #slot`만 쓰면 (0,2,1)로 **진다**
   (실제로 슬롯만 안 사라지고 남아 있었다). */
html.anim.diving #chrome,html.anim.diving #slot,
html.anim.diving .ovbtn,html.anim.diving #ovlabels{
  opacity:calc(1 - var(--dive,0) * 2.4);pointer-events:none;transition:none}
/* 지면도 거의 다 뺀다 — 넘어가는 순간 카드 뒤에 남은 색이 착지 지면(흰색)과 부딪힌다 */
html.anim.diving #bg{opacity:calc(1 - var(--dive,0) * 1.1)}
html.anim.diving #stage{cursor:default}

/* ★★문서 전환 옵트인 — 이것이 «툭 흰 화면»의 진짜 원인이었다.
   크로스 도큐먼트 뷰 전환은 **양쪽 문서가 모두** 옵트인해야 성립한다.
   `case.css:640`에는 있는데 여기에 없어서 전환이 통째로 스킵됐고, 그래서 나선에서
   케이스로 넘어갈 때 새 문서의 흰 배경이 그대로 칠해졌다(사용자 지적).
   ⚠크로스 도큐먼트에서 `::view-transition-*` 의사요소는 **도착 문서**에 생긴다 —
     지속 시간·이징은 `case.css`가 쥔다. 여기서 할 수 있는 것은 옵트인과,
     떠나는 화면을 착지 지면 쪽으로 미리 물들여(uWash) 건널 거리를 좁히는 것이다. */
@view-transition{navigation:auto}
@media (prefers-reduced-motion:reduce){ @view-transition{navigation:none} }

/* ══════════════════════════════════════════════════════════════════════
   «나가기» — 케이스에서 돌아올 때의 이음매
   ══════════════════════════════════════════════════════════════════════
   카드가 물러나는 것 자체는 `spiral.js:undive`가 진다(들어가기의 되감기 · 638ms).
   여기서는 **케이스 지면이 걷히는 타이밍**만 맡는다.

   ★크로스 도큐먼트 VT의 의사요소는 **도착 문서**에 생긴다. 들어갈 때는 도착이 케이스라
     `case.css`가 타이밍을 쥐지만, **돌아올 때는 도착이 나선이라 여기가 쥔다.**
   ⚠나선 준비까지 실측 **273~499ms**다. 그동안 스냅샷을 걷으면 빈 캔버스가 드러나
     카드가 «뿅» 나타난다. ⇒ 스냅샷을 **붙잡았다가**, 되감기의 첫 프레임이 그려진 순간
     (`f.js`가 `.vt-out`을 붙인다) 걷는다. **시간 상수로 고정하지 않는 이유가 이것이다** —
     준비 시각은 회선이 정하지 우리가 정하지 않는다.
   ★상한 .76s = `OPEN_MS`. 그 안에 준비가 안 되면 스냅샷이 스스로 걷힌다(정직한 강등).
   ★걷는 데 걸리는 **.122s = OPEN_MS × (1 − OPEN_AT)** — 들어갈 때 문서를 넘긴 뒤
     **카드가 마저 다가오던 그 시간**이다. 대칭이 근거이고 지어낸 수가 아니다.
   ●사용자 「겹친다」 — 순차로 걷으면 그 사이에 정지감이 생긴다. */
/* ⚠**되감기가 실제로 도는 조건에서만** 붙잡는다. 무조건 걸면, 1023 이하·reduced에서는
     `f.js`가 `staticMode()`로 가 `boot()`이 아예 안 돌고 `.vt-out`도 영영 안 붙는다 —
     그러면 케이스 화면이 **752ms 얼어붙었다가 하드컷**된다(900px 실측: vHold 760ms · 도착 클래스 "").
     조건은 `f.js`의 `narrow || reduce`와 **같은 값**이어야 한다. 한쪽만 고치면 조용히 갈린다. */
@media (min-width:1024px) and (prefers-reduced-motion:no-preference){
  ::view-transition-old(root){animation:vHold .76s linear both}
  ::view-transition-new(root){animation:none;opacity:1}
  html.vt-out::view-transition-old(root){animation:vOut .122s linear forwards}
}
@keyframes vHold{from{opacity:1}99%{opacity:1}to{opacity:0}}
@keyframes vOut{to{opacity:0}}

/* ── 나선 지면 위의 커서 ──────────────────────────────────────────── */
@media (pointer:fine) and (hover:hover){
  html.anim.v-work #stage{cursor:grab}
  html.anim.v-work.dragging #stage{cursor:grabbing}
  html.anim.v-work.overcard #stage{cursor:pointer}
}

/* ── WebGL 실패 강등 ──────────────────────────────────────────────────
   컨텍스트를 못 얻으면 나선을 통째로 접고 세로 문서로 되돌린다.
   이 규칙이 없으면 «검은 화면 + 목록 없음»이 된다. */
html.no-gl #stage{display:none!important}
html.no-gl .worklist{display:block!important}
html.no-gl #slot{display:none!important}
html.no-gl.anim #story,html.no-gl.anim .s-contact{
  opacity:1!important;pointer-events:auto!important;position:static!important}
html.no-gl.anim,html.no-gl.anim body{overflow:visible!important;height:auto!important}
html.no-gl.anim .s-contact{max-width:1120px;margin:0 auto;display:block;text-align:center}

@media (prefers-reduced-motion:reduce){
  html.anim .sec,.slot-name .ch,#story,#stage,.s-contact{transition:none;animation:none}
}

/* ══════════════════════════════════════════════════════════════════════
   인트로 — 「화면 한 장이 근거를 달고 통과되는 1.5초」
   ══════════════════════════════════════════════════════════════════════
   마크업·기하는 f.js가 짓는다(개념과 유도 근거도 그쪽 머리말에 있다). **여기는 시간을 진다.**

   ★이징은 레퍼런스 사이트(devouringdetails.com)가 선언한 둘만 쓴다(실측 2026-08-07) —
     진행·와이프 = cubic-bezier(.23,.88,.26,.92)  DD의 유일한 커스텀 이징
     도착 마감   = ease-out                        DD 트랜지션 0.15s
     그 밖의 곡선은 손맞춤이지 문법이 아니다. ⚠사이트 다른 곳의 (.65,0,.35,1)·(.22,1,.36,1)을
     여기로 끌어오지 마라 — 그러면 «똑같이»가 절반만 남는다. 컨테이너 페이드만 사이트 값이다.
   ★막은 500ms 간격으로 열린다 — 1500(최소 대기) ÷ 대등한 절 셋.
   ⚠**reduced-motion 분기를 두지 않는다.** f.js가 reduce에서 staticMode로 가며 `.anim`을 떼고
     `#intro`는 `.anim`에서만 뜬다 ⇒ 이 블록이 한 번도 안 쓰인다. 옛 블록엔 있었고 죽은 코드였다. */
#intro{display:none}
html.anim #intro{display:flex;position:fixed;inset:0;z-index:120;
  align-items:center;justify-content:center;
  background:var(--canvas);color:var(--ink);
  transition:opacity .7s cubic-bezier(.65,0,.35,1)}
/* ★★2026-08-08 — **`visibility:hidden`을 반드시 같이 준다.**
   ●증상: 사파리에서 WORK에 도달해도 하단 크롬·오버뷰·썸네일이 하나도 안 눌리다가,
     **개발자 도구를 켜는 순간 동작**했다(사용자 실측). 그것이 곧 원인이다 —
     `opacity:0`만으로는 이 전체화면 고정 레이어(z 120)가 합성 레이어로 남고,
     사파리가 히트테스트 영역을 갱신하지 않으면 **투명한 판이 클릭을 계속 먹는다.**
     개발자 도구를 열면 레이아웃이 흔들려 레이어가 다시 계산되고, 그래서 «갑자기» 풀린다.
   ⇒ `pointer-events:none`은 규칙일 뿐이고, `visibility:hidden`은 **레이어를 히트테스트에서 뺀다.**
     지연 전이(`visibility 0s .7s`)를 걸어 페이드가 끝난 뒤에 걸리므로 퇴장 연출은 그대로다. */
html.anim.intro-done #intro{opacity:0;pointer-events:none;visibility:hidden;
  transition:opacity .7s cubic-bezier(.65,0,.35,1),visibility 0s .7s}

/* ★커버 프리롤은 인트로가 물러난 **뒤에** 돈다. 겹치면 이름이 인트로 뒤에서 혼자 올라가
   물러났을 때 이미 끝나 있다 — 첫 화면의 사건이 사라진다. */
html.anim.intro .s-cover .kicker,
html.anim.intro .s-cover .name .w,
html.anim.intro .s-cover .scrollhint{animation-play-state:paused}
/* 인트로 동안 문서가 스크롤되면 안 된다 — 뒤에서 Story가 지나가 버린다 */
html.anim.intro,html.anim.intro body{overflow:hidden;height:100%}

/* ── 저작 = 표시 1:1 (DD 카드 정본 viewBox의 2배, 펜과 피치는 1배) ──────
   ⚠**SVG를 늘리지 마라.** 획 1.5와 리브 피치 3이 DD 실측값이고 이 인트로의 밀도는 전부 그 둘에서 난다.
     크기가 필요하면 f.js의 K를 올려 **형태만** 다시 그린다 — 그래야 펜이 그대로 남는다. */
.iscr{overflow:visible;flex:0 0 auto}

/* ★★★**선의 등장은 «그어지는» 모션 하나뿐이다.**
     ●사용자 규정 2026-08-07 「라인 모션은 무조건 그어지는 모션으로 규정해」.
     그래서 키프레임을 **iDraw 하나로 잠근다** — 기준선도 나무도 전부 이것 하나를 쓴다.
     주석으로 적어 두면 다음 사람이 페이드나 스케일을 하나 더 만들기 쉬우므로, **구조로 막는다**:
     길이는 전부 --L이라는 같은 이름으로 받고(도형마다 값만 다르다), 다른 등장 키프레임은 없다.
   ⇒ DD의 카드는 «완성된 도면»이고 우리 것은 **그려지는 도면**이다. 이것이 첫째 차이다. */
@keyframes iDraw{from{stroke-dashoffset:var(--L)}to{stroke-dashoffset:0}}

/* ★★이징 — **등속(linear)이다. 예외가 없다.**
   ⚠DD의 cubic-bezier(.23,.88,.26,.92)는 **0.15~0.2초 트랜지션용으로 조율된 곡선**이다.
     600ms에 걸었더니 300ms에 이미 다 끝나 있고 뒤가 죽었다(이 파일이 실측으로 겪었다).
     레퍼런스의 값을 «그 값이 쓰이던 지속»에서 떼어 오면 곡선이 뜻을 잃는다.
   ⇒ 등속인 것은 손맞춤이 아니라 **「한 손·한 속도」라는 우리 주장이 등속을 요구하기 때문**이다.
   ★★그리고 **리듬은 곡선이 아니라 «어긋남»에서 온다**(2026-08-07 나무 승격).
     줄마다 제 길이가 다르므로 같은 속도로 그으면 **끝나는 시각이 저절로 어긋나고**,
     끝점 x 순서로 지연을 주므로 **왼쪽부터 오른쪽으로 하나씩 선다.**
     지연도 지속도 f.js가 손 속도(기준선 288 ÷ 600 = 0.48px/ms) 하나에서 계산한다 —
     ⇒ **CSS에는 고른 시간값이 하나도 없다.** 여기서 시간을 적어 넣지 마라. */
/* ⚠**단축 속성을 쓰지 마라.** `animation:iDraw var(--dur) var(--del) …`는 사파리가
     단축 파서에서 지속과 지연을 못 가려 선언을 통째로 흘리는 알려진 자리다(2026-08-08 회피).
     개별 속성은 값이 어느 자리인지 모호하지 않아 어느 엔진에서나 안전하다. */
.iscr .igd,.iscr .itr path{stroke-width:1.5;stroke-dasharray:var(--L);
  animation-name:iDraw;animation-duration:var(--dur);animation-delay:var(--del,0ms);
  animation-timing-function:linear;animation-fill-mode:both}

/* 나무 — 무채로 남는다. 그래야 «한 줄이 뿌리에서 끝까지 안 끊긴다»가 구조에서 안 깨진다
   (색을 줄 중간에서 가르려면 줄을 두 동강 내야 한다 — 랩에서 그렇게 해 보고 기각했다). */
.iscr .itr path{stroke:var(--ink)}

/* ★기준선 — 이 그림의 **유일한 직선이고 유일한 색**이다.
     ●인터뷰에서 제일 먼저 나온 답이 「기준이 분명하다」였고, 나무는 전부 휘므로 색이 그것 하나를 짚는다.
   ★액센트 — devouringdetails.com 실측(2026-08-07). 사이트 전역 fill 43·stroke 9, 이 색 하나뿐이다.
     sRGB 폴백을 먼저 두고 P3를 뒤에 둔다(P3가 sRGB 밖이라 폴백은 클리핑된 근사다).
   ⚠#FF5800은 우리 캔버스 위 **2.95:1**이다. 종전 각도기는 리브가 촘촘해 «덩어리»로 읽혀서 살았고,
     여기는 가는 선 하나다 — 그래도 색상 차가 커서 주황으로 읽힌다(실물 확인). 다만 **약한 액센트**다.
     굵게 만들 길은 없다(선언된 획 집합 {1.5, 1.35, 1}). 대신 길이를 288로 줄여
     «지평선»이 아니라 **«나무가 딛고 선 자리»**로 읽히게 했다. */
.iscr .igd{stroke:#FF5800;stroke:color(display-p3 .99 .4 .02)}

/* 퇴장 — DD `scaleOut` 실측값 그대로(op 1→0 · scale 1→.98 · blur 0→2px).
   컨테이너 페이드(.7s)보다 먼저 끝나 **그림이 먼저 물러나고 지면이 뒤따른다.** */
html.anim.intro-done .iscr{opacity:0;transform:scale(.98);filter:blur(2px);
  transition:opacity .3s ease,transform .3s ease,filter .3s ease}
