/* frame.css — 브라우저 창과 사이트가 함께 하나의 영상 프레임으로 읽히게 한다.
   ★ 사이트 안에서 Safari 메뉴바를 흉내 내지 않는다. 윗변은 **실제 브라우저 크롬**이 맡는다.

   프레임 네 변
     위   Safari 검은 크롬 (F01) · 아주 얇은 내부 선 (F02) · 내부 프레임 (F03)
     좌우 이전·다음 작품 (검은 테두리가 아니라 실제 작품이다)
     아래 화면을 닫는 검은 면 */

:root {
  /* ★ 이 사이트는 라이트 모드가 없다 — 색은 영상만 가지고 나머지는 검정이다.
     이걸 선언해야 스크롤바·폼 요소·브라우저 UI 가 밝은 쪽으로 튀지 않는다. */
  color-scheme: dark;

  --ui: -apple-system, BlinkMacSystemFont, "Pretendard", "Apple SD Gothic Neo",
        system-ui, "Segoe UI", Roboto, sans-serif;

  --ink: #000;
  --paper: #ededee;

  /* 퍼센트 하나로 정하지 않는다 — 최소·반응·최대를 함께 쓴다.
     ★ 좌우와 하단은 **같은 굵기**다(1440 안팎에서 50px). 처음엔 하단을 0.6 배로 눌렀는데,
       실제로 보니 세 변이 같아야 하나의 프레임으로 읽혔다. 같은 식을 쓴다. */
  --frame: clamp(48px, 4.5vw, 96px);
  --side-frame: var(--frame);
  --bottom-frame: var(--frame);

  --top-frame: 0px;               /* F01 기본. F02/F03 이 덮어쓴다. */

  --side-dim: .78;                /* 열린 가운데를 누르는 정도. 양 끝은 비네트가 따로 완전히 덮는다 */
  --side-blur: 7px;               /* 움직임이 읽히지 않게 뭉개는 정도 */
  --edge-v: clamp(38px, 7dvh, 64px);   /* 좁은 화면에서 위아래로 걸치는 두께 */

  /* ★ 시트·하단 띠의 좌우 여백은 **영상의 좌우 선에 맞춘다.** 예전엔 따로 clamp() 를
     써서 영상보다 안쪽/바깥쪽으로 어긋났다 — 목차를 올리면 글씨가 영상 왼쪽 선을 넘어
     좌측 버튼 자리를 침범했고, 오른쪽 소리 아이콘은 영상 오른쪽 선을 넘어갔다.
     좁은 화면에서는 `--side-frame` 이 0 이 되므로 최소 16px 은 남겨 둔다. */
  --gutter: max(16px, var(--side-frame));

  /* 유일한 액센트. 새 색을 만든 것이 아니라 **필름 센서에 이미 쓰던 따뜻한 색**을
     그대로 가져왔다 — 이 사이트에 색은 이것 하나뿐이어야 한다. */
  --accent: rgba(226,192,142,.95);

  /* ── 디자인 규칙(토큰) — docs/DESIGN-RULES.md ──────────────────────
     ★ 글자·밝기·여백은 **이 이름으로만** 쓴다. 새 CSS 에 숫자를 직접 적지 않는다.
       (예외: 프레임 두께처럼 화면 비율로 흐르는 값, 0/1 켜고 끄기, 그림 효과) */
  --t-title: clamp(28px, 3vw, 40px);   /* 큰 제목 — 작품 제목 */
  --t-head: 20px;                      /* 중간 제목 — 분류 이름(머리줄) */
  --t-body: 15px;                      /* 본문 — 값 줄, 연혁 제목 */
  --t-small: 13px;                     /* 작은 글씨 — 장소, 칸 이름, 안내 */
  --t-label: 10.5px;                   /* 표지 — 대문자 달 이름, 개수, 필터 */

  --o-hi: .92;                         /* 또렷함 — 제목 */
  --o-mid: .45;                        /* 보조 — 장소·값·목록 */
  --o-lo: .3;                          /* 흐림 — 달 이름, 쉬는 조작부, 빈 칸 */
  --o-faint: .15;                      /* 거의 없음 — 눈금, 음소거 */

  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;

  /* 시트 높이. 그라데이션이 시트 **바로 위**에 앉아야 해서 한 곳에서 관리한다
     (좁은 화면에서는 아래 미디어 쿼리가 덮어쓴다). */
  --sheet-h: clamp(124px, 20vh, 174px);   /* 30% 낮췄다 — 정보가 많지 않아 위쪽 여백만 남았다 */

}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0; height: 100%;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--ui);
  overflow: hidden;
  overscroll-behavior: none;
  -webkit-text-size-adjust: 100%;
}

.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; clip-path: inset(50%); overflow: hidden;
}

/* ── 무대 ─────────────────────────────────────────────────────────── */
.stage {
  position: fixed; inset: 0;
  display: flex; flex-direction: column;
  background: var(--ink);
  overflow: hidden;      /* 내려가 숨은 연혁이 화면 밖으로 삐져나오지 않게 */
  /* ★ 데스크톱 웹에서 끌기(스크럽·좌우 넘김·볼륨)를 하다 보면 그 안의 글씨가 딸려 와
     드래그 선택되는 일이 있다 — 여긴 읽는 화면이 아니라 조작하는 화면이라 막아 둔다.
     복사해야 할 만큼 긴 글이 없으니 잃을 것도 없다. */
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}

/* ★ 홈 화면에서 열면 Safari 크롬이 없다 — 그러면 **윗변을 우리가 만들어야** 한다.
   상태바가 사이트 위로 겹쳐 오므로(black-translucent) 그 높이만큼 검게 비워 둔다.
   이때가 이 디자인의 완성형이다: 네 변이 모두 검정이고, 상태바와 경계가 없다. */
@media (display-mode: standalone) {
  .stage { padding-top: env(safe-area-inset-top); }
}
/* iOS 구버전은 display-mode 를 못 알아듣는다 — JS 가 붙여 주는 쪽도 함께 본다 */
.stage.is-standalone { padding-top: env(safe-area-inset-top); }

/* 위 — F01 은 0, F02 는 얇은 선, F03 은 제대로 된 변 */
.topbar {
  position: relative; flex: 0 0 auto;
  height: var(--top-frame); background: var(--ink); overflow: hidden;
  transition: height .78s cubic-bezier(.4,0,.16,1);
}

.band { flex: 1 1 auto; display: flex; min-height: 0; }

/* ── 좌우 = 이전·다음 작품 ────────────────────────────────────────── */
.side {
  position: relative;
  flex: 0 0 var(--side-frame);     /* 처음이든 끝이든 언제나 같은 두께 */
  width: var(--side-frame);
  /* 원본 비율로 바꿀 때 프레임이 닫히며 들어온다 — 변수만 바꿔선 안 움직이므로 여기서 전환한다 */
  transition: flex-basis .55s cubic-bezier(.4,0,.16,1), width .55s cubic-bezier(.4,0,.16,1),
              background-color .3s ease;
  margin: 0; padding: 0; border: 0;
  background: var(--ink); color: inherit;
  cursor: pointer; overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}
/* 좌우는 검은 테두리가 아니라 **실제 이전·다음 작품**이다.
   (한때 완전히 비워 프레임으로만 둘렀는데, 모바일에서도 데스크톱에서도 어색해 되돌렸다.) */
.side .shot {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
  filter: blur(var(--side-blur)) saturate(.9);
  transform: scale(1.16);
  opacity: 0;                                  /* 살아 있는 한 장만 보인다 */
  transition: filter .32s ease, transform .32s ease, opacity .5s ease;
}
.side .shot.is-live { opacity: 1; }
/* 손이 오면 그 편이 실제로 돌아간다 — 정지 그림보다 "저기 뭔가 있다"가 훨씬 강하다.
   평소엔 붙어 있지 않으므로 대역폭도 CPU 도 쓰지 않는다. */
.side .peek {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
  filter: blur(2px) saturate(1);
  transform: scale(1.06);
  opacity: 0; transition: opacity .3s ease;
}
.side .peek.is-live { opacity: 1; }
.side .peek.is-live ~ .shot { opacity: 0; }

/* ── 이웃을 덮는 어둠은 **두 겹**이다 ────────────────────────────────
 *
 * 화면의 네 변은 언제나 까맣다 — 그래야 프레임으로 읽힌다.
 * 그런데 좌우를 통째로 검게 비웠더니 "옆에 이전·다음 작품이 있다"가 사라졌다.
 * 그래서 **양 끝은 완전한 검정으로 두고 가운데만 조금 열어** 둔다.
 *
 *  ① 비네트 — 언제나 그대로. 끝은 진짜 #000 이고 손이 와도 밝아지지 않는다.
 *  ② 그 위에 균일한 어둠 한 겹 — 열린 가운데의 밝기를 정하고, 손이 오면 이것만 옅어진다.
 *
 * ★ 두 겹으로 나눈 이유가 핵심이다. 한 겹에 비네트를 그려 놓고 그 판 전체에
 *   opacity 를 곱하면 **가장자리조차 그 비율만큼밖에 어두워지지 않는다** —
 *   그래서 아무리 비네트를 넣어도 양 끝이 까매지지 않았다.
 *
 * 반경은 축 방향으로만 좁힌다. 데스크톱에서 이 판은 세로로 긴 띠라
 * 가로 반경을 판 밖(190%)까지 밀어 좌우로는 감쇠를 없애고 세로로만 닫는다.
 * 좁은 화면에서는 판이 가로로 눕으므로 아래 미디어 쿼리에서 축을 뒤집는다 —
 * 어느 배치에서도 "긴 쪽 양 끝이 까맣다"가 그대로 유지된다.
 */
.side .dim {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 190% 36% at 50% 50%,
    rgba(8,8,10,0) 0%, rgba(8,8,10,.45) 42%, rgba(8,8,10,1) 78%);
}
.side .dim::after {
  content: ''; position: absolute; inset: 0;
  background: var(--ink);
  opacity: var(--side-dim);
  transition: opacity .5s ease;
}
/* 손이 가까워지면 **폭은 그대로 두고** 밝기와 선명도로만 반응한다.
   한때 폭을 2.1배로 넓혀 "끌려 나오는" 느낌을 주려 했는데, flex-basis 가 매 프레임
   레이아웃을 다시 계산해 눈에 띄게 버벅였다 — 되돌렸다. 시차 이동도 함께 뺐다. */
/* 옅어지는 것은 균일한 겹뿐이다 — 비네트는 그대로라 위아래 끝은 여전히 까맣다.
   손이 와도 프레임이 풀리지 않고 **가운데만 열린다.** */
.side:hover .dim::after, .side:focus-visible .dim::after { opacity: calc(var(--side-dim) - .3); }
.side:hover .shot.is-live, .side:focus-visible .shot.is-live {
  filter: blur(2.5px) saturate(1);
  transform: scale(1.08);
}
.side:focus-visible { outline: none; }


/* ── 중앙 ─────────────────────────────────────────────────────────── */
.center {
  position: relative;
  cursor: pointer;
  flex: 1 1 auto; min-width: 0;
  background: var(--ink);
  overflow: hidden;
  /* ★ 아주 살짝만 — 네 모서리를 깎아서 프레임과 같은 검정이 그 자리를 채운다.
     각지지 않다는 걸 재는 게 아니라 겨우 알아챌 만큼만. overflow:hidden 이라
     영상·포스터도 이 모양대로 잘려서, 진짜 모서리에 검정이 살짝 스민 것처럼 보인다.

     ★ 다만 이건 **좌우·아래에 프레임이 다 있을 때만** 성립한다. 모서리를 깎는 이유가
       "안의 것이 프레임 **안에** 들어 있다" 를 강하게 만드는 것인데, 감쌀 프레임이
       없으면 깎인 모서리는 그냥 둥근 카드가 된다. 그래서 원본비(프레임이 닫혀 들어와
       좌우가 사라진다)와 좁은 화면(영상이 폭을 꽉 채운다)에서는 각지게 되돌린다. */
  border-radius: 10px;
  transition: border-radius .55s cubic-bezier(.4,0,.16,1);
}
.center .lqip, .center .poster, .center .vid {
  position: absolute; inset: 0; width: 100%; height: 100%;
}

/* 원본비로 볼 때는 감쌀 프레임이 없다 — 각지게 되돌린다(위 .center 설명 참조) */
.stage.is-contain .center { border-radius: 0; }

/* ★ 시트가 올라오면 영상의 **아래도 둥글게 잘린다.**
 *
 *   전에는 영상 상자 자체를 시트 높이만큼 줄여서 이걸 얻었는데, 상자가 줄면
 *   object-fit:cover 가 다시 잘라 내면서 **보던 화면이 줄었다 늘었다** 했다 — 그게 거슬렸다.
 *   그래서 상자는 그대로 두고 **clip-path 로 오려낸다.** 레이아웃이 변하지 않으니
 *   영상의 구도는 1px 도 움직이지 않고, 아랫변만 둥글게 닫힌다.
 *   시트가 올라오는 것과 같은 시간·같은 곡선으로 함께 닫힌다. */
.center {
  clip-path: inset(0 0 0 0 round 10px);
  transition: clip-path .55s cubic-bezier(.4,0,.16,1),
              border-radius .55s cubic-bezier(.4,0,.16,1);
}
.stage.toc-open .center { clip-path: inset(0 0 var(--sheet-h) 0 round 10px); }
/* 원본비에는 감쌀 프레임이 없다 — 각지게, 오려내지도 않는다 */
.stage.is-contain .center { clip-path: inset(0 0 0 0 round 0); }

/* ── 쉼 — 한동안 조작이 없을 때 ───────────────────────────────────────
 *
 * ★ 통짜 검정으로 덮지 않는다. **비네트로 가장자리부터 닫히고 가운데는 남긴다** —
 *   이 사이트가 내내 말하는 "프레임" 이 조용히 조여드는 것으로 읽힌다.
 * ★ 그 위에 **필름 그레인**을 얹고 아주 느리게 떠다니게 한다. 영사기가 멎지 않고
 *   그대로 돌아가는 느낌이라, 멈춰 있는 화면인데도 살아 있다.
 * ★ 값싸게 만든다 — 그레인은 feTurbulence 로 구운 **정지 이미지 한 장**이고(요청 0개),
 *   움직이는 것은 transform 뿐이라 합성 단계에서 끝난다. 영상 자체를 블러하면 매 프레임
 *   다시 계산해야 해서 **쉬는 화면이 오히려 더 뜨거워진다** — 그 길은 택하지 않았다.
 * ★ `.center` 안이라 border-radius 로 함께 잘리고, pointer-events 는 흘려보낸다.
 */
/* ★ 어둡게 덮지 않는다. 비네트로 눌러 봤는데 결국 "화면이 꺼져 간다" 로 읽혀서 걷어냈다 —
   쉬는 것이지 꺼지는 것이 아니다. 남은 것은 **질감 하나뿐**이고, 영상은 제 밝기 그대로 돈다. */
.center::before {                      /* 그레인 */
  content: ''; position: absolute; inset: -14%; z-index: 8;
  pointer-events: none; opacity: 0;
  background-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20width='180'%20height='180'><filter%20id='n'><feTurbulence%20type='fractalNoise'%20baseFrequency='0.9'%20numOctaves='3'%20stitchTiles='stitch'/><feColorMatrix%20type='saturate'%20values='0'/></filter><rect%20width='180'%20height='180'%20filter='url%28%23n%29'/></svg>");
  background-repeat: repeat;
  mix-blend-mode: overlay;
  /* ★ 만졌을 때 되돌아오는 쪽(그레인이 걷히는 쪽) — 예전엔 .8s 로 즉시 걷혔는데,
     그러니 "다시 켜진다" 처럼 툭 끊겨 보였다. 천천히 밝아지도록 늦춘다. */
  transition: opacity 2.6s ease-out;
}
/* 소리가 물러나는 시간(sound.js REST_FADE)과 같은 15초에 걸쳐 스며든다 */
.stage.is-resting .center::before {
  opacity: .34; transition: opacity 15s linear;
  will-change: transform;
  animation: grainDrift 1.6s steps(3) infinite;
}
/* 몇 픽셀씩 툭툭 옮겨 놓기만 한다 — 진짜 필름 그레인이 그렇게 튄다 */
@keyframes grainDrift {
  0%   { transform: translate3d(0, 0, 0); }
  33%  { transform: translate3d(-1.5%, 1%, 0); }
  66%  { transform: translate3d(1%, -1.5%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .stage.is-resting .center::before { animation: none; }
}
/* 두 장 중 살아 있는 쪽만 보인다. 교대는 새 영상이 재생을 시작한 뒤에 일어난다. */
.center .vid { z-index: 2; }
.center .vid.is-live { z-index: 3; }
.center .lqip { background: #000 center/cover no-repeat; }
.center .poster, .center .vid { object-fit: cover; object-position: center; display: block; }
/* 포스터는 **다 받은 뒤에만** 보인다. 받는 중이거나 실패했을 때 깨진 아이콘이 스치지 않게. */
.center .poster { opacity: 0; transition: opacity .3s ease; }
.center .poster.is-ready { opacity: 1; }
/* 판 두 장을 미리 받아 두는 구조는 그대로. 준비된 순간 그냥 바꿔 낀다.
   (한때 위아래로 밀어 넘기는 슬라이드를 넣었다가 걷어냈다 — 경박했고,
    좌우에 이웃이 있는 넓은 화면에서 세로로 움직이니 방향도 맞지 않았다.) */
.center .vid { opacity: 0; }
.center .vid.is-live { opacity: 1; }

/* ── 스크롤로 넘기는 동안 ─────────────────────────────────────────────
   `is-scrubbing` = 손이 붙어 있다. 전환을 끄고 민 만큼 **그대로** 따라온다.
   `is-settling`  = 손을 뗐거나 문턱을 넘었다. 남은 구간만 스르륵 잇는다.
   ★ 둘 다 아닐 때는 예전 그대로 즉시 갈아 끼운다 — 클릭·스와이프 넘김은 느려지지 않는다. */
/* (한때 전환 한가운데에 그 편의 lqip 를 색안개처럼 덮어 봤다 — 걷어냈다.
    겹침의 탁함은 가려졌지만 담백한 맛이 사라졌다. 디졸브만으로 둔다.) */
.stage.is-scrubbing .center .vid,
.stage.is-scrubbing .side .dim::after { transition: none; }
.stage.is-settling .center .vid { transition: opacity .34s cubic-bezier(.22,.61,.36,1); }
.stage.is-settling .side .dim::after { transition: opacity .34s cubic-bezier(.22,.61,.36,1); }

/* ── 눌러서 넘길 때 ───────────────────────────────────────────────────
   스크럽보다 **느리다.** 스크럽은 손이 놓은 자리를 마저 잇는 마무리라 짧아야 하고,
   넘김은 "다음 편으로 간다"는 한 번의 결정이라 여유가 있어야 한다.
   느리게 넘어가면 받아 오는 시간이 그 안에 숨고, 한 편에 머무는 시간도 늘어난다 —
   빨리 넘어갈수록 영상은 안 보고 넘기기만 하게 된다.
   좌우·상하 이웃을 누르든, 스와이프하든, 목차에서 고르든 모두 이 길을 지난다. */
.stage.is-crossing .center .vid { transition: opacity .62s cubic-bezier(.33,0,.2,1); }
/* ★★ 옛 영상은 **녹아 없어지지 않는다 — 새 영상이 다 덮은 뒤에 치운다.**
   예전엔 옛 판은 1→0, 새 판은 0→1 로 **동시에** 움직였다. 한가운데서 둘 다 반투명이 되니
   맨 밑에 깔린 포스터(스틸)가 비쳐 올라왔고, 그게 "썸네일 → 영상" 순서로 두 번 툭툭
   바뀌는 것으로 보였다. 영상 위에 영상을 얹는 디졸브라면 **아래 판은 끝까지 불투명**해야 한다.
   새 판(is-live)이 z-index 로 위에 서므로, 옛 판은 전환 시간만큼 기다렸다가 한 번에 꺼진다. */
.stage.is-crossing .center .vid:not(.is-live) { transition: opacity 0s linear .62s; }
.stage.is-crossing .center .poster { transition: opacity .62s cubic-bezier(.33,0,.2,1); }
/* 이웃 그림도 같은 속도로 갈아 끼워진다 — 가운데만 녹고 옆은 툭 바뀌면 따로 논다 */
.stage.is-crossing .side .shot { transition: opacity .62s cubic-bezier(.33,0,.2,1); }

/* 원본 비율로 보기 — 세로·시네마스코프를 확인할 때 쓴다.
   ★ 이때 생기는 내부 여백은 프레임과 **같은 검정으로 뭉개지면 안 된다.**
     아주 미세하게 다른 검정을 깔아 "영상의 여백"과 "사이트의 프레임"을 구분한다. */
/* ★ 검은 띠를 만들지 않는다. 원본 비율로 바꿀 때 생길 빈 자리만큼 **프레임이 닫혀 들어와**
   그 자리를 대신 채운다. 상자 비율이 영상 비율과 같아지면 cover 와 contain 이 같은 그림이 되므로
   그동안 object-fit 을 건드릴 필요가 없다 — 전환에서 튀는 순간이 아예 없다.
   프레임이 다 닫힌 뒤에만 contain 으로 확정해 반올림 오차로 한 줄이 잘리는 것을 막는다.
   (프레임을 너무 두껍게 만들어야 하는 세로 영상에서는 닫기를 멈추고 남는 만큼만 여백으로 둔다 — 그때만 이 배경이 보인다.) */
.stage.is-contain .center { background: #14141a; }
.stage.is-contain.is-settled .center .poster,
.stage.is-contain.is-settled .center .vid { object-fit: contain; }
/* lqip 는 **확정된 뒤에만** 숨긴다. 넘기는 순간에 숨겨 두면 포스터가 바뀌는 사이 화면이 빈다. */
.stage.is-contain.is-settled .center .lqip { opacity: 0; }

/* 지금 보고 있는 편 — 좌하단에 **계속 떠 있는다.**
   원본 비율로 펼쳤을 때만 물러난다(그때는 화면만 보는 상태다). */
.card {
  position: absolute; left: 0; right: 0; bottom: 0;
  /* ★ 시트(z-index 8)보다 **위**에 둔다. 시트가 올라올 때 이 글씨가 그 아래로 깔리면
     "덮인다" 가 되고, 위에 있으면 **떠오른다** 가 된다 — 같은 움직임인데 층이 뜻을 바꾼다.
     (영상 판이 z-index 3 이라 이게 없으면 애초에 영상 뒤로 깔린다.) */
  z-index: 10;
  /* 영상 아래·왼쪽에서 더 떨어뜨린다 — 글씨가 화면 모서리에 붙으면 영상에 얹힌
     자막처럼 보이고, 떨어뜨릴수록 "프레임 안에 놓인 표제" 로 읽힌다. */
  padding: 0 clamp(26px, 3.4vw, 56px) clamp(26px, 3.2vw, 48px);
  pointer-events: none;
  opacity: 0; transform: translateY(8px);
  transition: opacity .6s ease, transform .5s ease;
}
/* 3 초 또렷하게 머물다 흐려진다 — 사라지지는 않는다(무엇을 보고 있는지는 늘 알 수 있게). */
.card.is-on { opacity: 1; transform: none; }
.card.is-on.is-faded { opacity: .34; }
/* 손이 오면 다시 또렷해진다 */
.stage.is-awake .card.is-on { opacity: 1; }
/* ★ 시트가 올라오면 영상 위의 표제는 **물러난다.** 같은 제목을 두 군데 띄우지 않는다 —
   영상이 위로 물러설 때 이 글씨가 딸려 올라가고, 그동안 옅어지면서 시트의 제목이
   그 자리를 이어받는다. 하나가 올라가 자리를 넘겨주는 것으로 보인다. */
/* 시트가 올라오면 영상 위의 표제는 그냥 사라진다 — 같은 제목을 두 군데 띄우지 않는다.
   (어차피 위 clip-path 가 그 자리를 오려 내므로 남겨 둘 수도 없다.) */
.stage.toc-open .card.is-on,
.stage.toc-open.is-awake .card.is-on { opacity: 0; transition: opacity .28s ease; }

/* ★ 펼쳐 보는 중에는 **완전히 사라진다.** 흐리게도 남기지 않는다 — 영상만 보는 상태다.
   `.stage.is-awake .card.is-on` 이 더 강해서, 마우스만 움직여도 글씨가 되살아나고 있었다.
   같은 자리에서 이기도록 선택자를 맞춘다. */
.stage.is-contain .card.is-on,
.stage.is-contain.is-awake .card.is-on { opacity: 0; transform: translateY(8px); }
/* ★ 시기는 여기서 뺐다. 화면에서 가장 크게 읽히던 값인데 정작 중요도는 가장 낮았다 —
   이전인지 이후인지는 아래 연혁이 이미 말해 주고, 정확한 달은 상세에서 찾으면 된다.
   남은 순서가 곧 중요도다: 제목(대개 도시) → 나라. */
/* ★ 나라가 먼저, 제목이 나중이다. 나라는 없을 수도 있지만 제목은 늘 있다 —
   순서를 이렇게 두면 **제목의 위치가 고정된다.** 나라 유무와 무관하게 제목은 늘
   카드 맨 아래(둘 중 마지막 줄)에 있어서, 나라가 있고 없고에 위아래로 흔들리지 않는다.
   반대(제목이 먼저)였을 때는 나라가 없는 편마다 제목이 그만큼 아래로 내려와 있었다. */
.c-place { margin: 0; font: 400 var(--t-small)/1.3 var(--ui); color: var(--paper); opacity: var(--o-mid); }
.c-title {
  margin: var(--s-2) 0 0;
  /* 얇게 해봤는데 도로 별로라고 함 — 두껍게 되돌리고 크기는 오히려 더 키웠다.
     대신 완전한 흰색 대신 약간 투명하게 둔다 — `.card.is-on`(1)/`.is-faded`(.34)와
     곱해져서, 또렷할 때도 순백은 아니고 시간이 지나면 지금처럼 더 옅어진다. */
  font: 600 var(--t-title)/1.06 var(--ui); letter-spacing: -.02em;
  color: var(--paper); opacity: var(--o-hi);
}

/* 상세 — 눌렀을 때만. 시기·기기·센서는 찾아보는 값이지 늘 읽는 값이 아니다. */
.card { pointer-events: none; }
.card .c-title, .card .c-place { pointer-events: auto; cursor: pointer; }
/* ★★ 상세는 **장소 줄 위에서** 열린다(HTML 순서는 장소·제목·상세 그대로, 보이는 순서만 바꾼다).
   카드는 영상 아랫변에 붙어 있어서, 제목 **아래**로 열면 카드가 위로 자라며 제목이 밀려 올라갔다.
   위에서 열면 늘어나는 쪽이 위라 **제목과 장소 줄은 제자리**에 있다. */
.card { display: flex; flex-direction: column; align-items: flex-start; }
.c-more {
  order: -1;
  display: grid; grid-template-columns: auto 1fr;
  gap: var(--s-1) var(--s-4); margin: 0; max-width: 30ch;
  pointer-events: auto;
  /* 접힌 상태 — 높이 0 · 투명 · 조금 아래. display:none 은 전환이 안 걸려서 쓰지 않는다 */
  max-height: 0; overflow: hidden; opacity: 0; transform: translateY(6px);
  transition: max-height .45s cubic-bezier(.4,0,.16,1), margin .45s cubic-bezier(.4,0,.16,1),
              opacity .3s ease, transform .45s cubic-bezier(.4,0,.16,1);
}
.card.is-open .c-more {
  max-height: 10em; opacity: 1; transform: none;
  margin-bottom: var(--s-3);
}
.c-more[hidden] { display: none; }
.c-more dt {
  font: 500 var(--t-label)/1.5 var(--ui); letter-spacing: .14em; text-transform: uppercase;
  color: var(--paper); opacity: var(--o-lo);
}
.c-more dd {
  margin: 0; font: 400 var(--t-small)/1.5 var(--ui);
  color: var(--paper); opacity: var(--o-mid);
  font-variant-numeric: tabular-nums;
}
/* 접혀 있을 때는 자리도 차지하지 않는다 — 위 max-height 0 이 맡는다 */

/* ── 연혁 ─────────────────────────────────────────────────────────────
   가로로 흐르는 시간축. **한 칸이 한 편이고, 칸 전체가 눌린다.**

   ★ 처음에는 실선 축 위에 작은 점을 찍었다. 두 가지가 잘못됐다 —
     하나, 누를 곳이 점만 하게 **보여서** 실제로는 칸 전체가 눌리는데도 조준하게 됐다.
     둘, 22 편을 한 화면에 다 욱여넣느라 칸이 78px 까지 좁아져 제목이 잘렸다.
   그래서 축과 점을 걷어내고 칸을 넓혀 주인공으로 만들었다. 다 안 보여도 된다 —
   훑어 내리는 것이 목적이지 전체를 조망하는 게 목적이 아니다. */
/* ★ 따로 떠 있는 판이 아니다. **아래 검은 띠가 그대로 자라 올라온 것**이다.
   그래서 하단 프레임 바로 위에 이어 붙고, 배경도 프레임과 **똑같은 검정**이다 —
   반투명도 흐림도 쓰지 않는다. 그것들이 있으면 위에 얹힌 별개의 패널로 읽힌다. */
.toc {
  /* ★ 좌우 프레임(이전·다음 작품) 을 **덮지 않는다.** 시트는 아래에서 그 사이로 올라오는
     것이지 화면을 가로지르는 판이 아니다 — 덮어 버리면 네 변으로 둘러싼 프레임이 깨진다.
     그래서 안쪽 여백(--gutter)도 0 이다. 시트의 왼쪽 끝이 곧 영상의 왼쪽 선이라 이미 맞다. */
  position: absolute; left: var(--side-frame); right: var(--side-frame); z-index: 8;
  --gutter: 0px;
  bottom: var(--bottom-frame);
  /* ★ 열었을 때 높이는 **카테고리와 무관하게 같다.** 내용이 바뀔 때마다 판이 오르내리면
     그때마다 위의 영상까지 흔들려 화면이 들썩인다. 그림이 들어갈 만큼으로 한 번에 잡는다. */
  height: var(--sheet-h);
  background: var(--ink);
  /* ★ 시트 자체는 더 이상 스크롤되지 않는다. 가로로 흐르는 것은 오른쪽 칸뿐이다 —
     그래야 왼쪽(정보 / 되돌아가기)이 스크롤과 함께 밀려나지 않는다. */
  overflow: hidden;
  overscroll-behavior: none;
  scrollbar-width: none;
  transform: translateY(100%);          /* 평소엔 띠 뒤로 내려가 숨는다 */
  /* ★ 올라오는 시간을 **영상이 물러서는 시간(.band 의 margin-bottom)과 같게** 맞춘다.
     예전엔 시트가 .3s 로 먼저 도착하고 영상이 .55s 로 뒤늦게 물러서서, 영상의 아래
     모서리가 시트가 다 올라온 **뒤에야** 뒤늦게 둥글어졌다. 둘은 한 동작이라 같이 움직여야 한다. */
  transition: transform .55s cubic-bezier(.4,0,.16,1),
              height .55s cubic-bezier(.4,0,.16,1),
              bottom .55s cubic-bezier(.4,0,.16,1);   /* 프레임 두께가 변하면 함께 따라간다 */
}
/* 그림에는 자리가 더 필요하다 — 같은 시트가 그만큼 자란다(다른 화면으로 가지 않는다) */
.toc.is-browse { overflow-x: hidden; }   /* 높이는 위와 같다 — 바뀌지 않는다 */
.toc::-webkit-scrollbar { width: 0; height: 0; }
.toc[hidden] { display: none; }
.toc.is-on { transform: translateY(0); }

/* ── 분류 탭 ──────────────────────────────────────────────────────────
   ★ 탭을 바꿔도 **자리는 하나**다. 아래 한 칸의 내용만 통째로 교체된다.
     분류마다 화면을 따로 만들면 그때부터 사이트가 아니라 목록이 된다. */
/* ★ 탭 줄은 없앴다. 다섯 개를 늘어놓아 봐야 그중 넷은 **바로 아래 정보의 각 줄**과
   같은 것을 가리키고 있었다 — 같은 문을 두 개 만든 셈이라 자리만 넓게 먹었다.
   지금은 들어가는 문이 정보의 값 자체이고, 나오는 문은 이 머리줄 하나뿐이다. */
.sheet-cat { display: flex; align-items: center; }
.sheet-cat[hidden] { display: none; }
/* ★ `‹ TIMELINE` 처럼 화살표와 작은 대문자를 한 줄에 붙이면 **버튼처럼만** 보이고,
   정작 "지금 무엇을 보고 있나" 는 작게 눌려 안 읽힌다.
   그래서 정보 화면과 **같은 위계**를 쓴다 — 되돌아가는 화살표는 위에 작게,
   분류 이름은 그 아래 제목 크기로. 왼쪽 칸은 언제나 "지금 보는 것의 이름" 자리다. */
.cat-back {
  display: inline-flex; flex-direction: column; align-items: flex-start; gap: var(--s-2);
  margin-left: -3px;
  border: 0; background: transparent; color: var(--paper);
  padding: var(--s-1); text-align: left;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.cat-back:focus-visible { outline: none; }
.cat-back svg {
  width: 17px; height: 17px; display: block;
  opacity: var(--o-mid); transition: opacity .2s ease, transform .2s ease;
}
.cat-back:hover svg, .cat-back:focus-visible svg { opacity: 1; transform: translateX(-3px); }
.cat-name {
  font: 600 var(--t-head)/1.1 var(--ui); letter-spacing: -.02em;
  color: var(--paper); opacity: var(--o-hi);
}

/* ── 시트는 두 칸이다 ─────────────────────────────────────────────────
   왼쪽 = 지금 편의 정보(또는 분류 안에서는 되돌아가기). **고정이다.**
   오른쪽 = 내용. 여기만 가로로 흐른다.
   ★ 예전엔 시트 전체가 하나의 가로 스크롤러라, 연혁을 끝까지 훑으면 되돌아가기 화살표가
     화면 밖으로 밀려나 왼쪽 끝까지 되짚어 가야 했다. 칸을 나누면 그 문제가 사라진다. */
.sheet-body { position: relative; height: 100%; display: flex; align-items: stretch; }
.sheet-left {
  flex: 0 0 auto; width: clamp(190px, 25%, 300px);
  min-width: 0; display: flex; flex-direction: column; justify-content: center;
  padding-left: var(--gutter);
}
/* ★ 넓은 화면에서는 왼쪽 여백을 **거의 두지 않는다.** 좌우에 이미 검은 프레임이
   서 있고 시트도 같은 검정이라, 그 프레임 자체가 여백으로 읽힌다 — 거기에 여백을
   또 얹으면 글씨만 가운데로 밀려 어정쩡해진다. 좁은 화면에는 그 프레임이 없어서
   `--gutter` 가 그대로 필요하다(아래 좁은 화면 분기). */
.toc.is-info .sheet-left { padding-left: 0; }
/* ★ 정보를 볼 때는 왼쪽을 **넓게** 쓴다. 제목이 긴 편이 많은데 좁은 칸에 가두면
   두 줄로도 모자라 잘렸다. 분류 안(되돌아가기 화살표만 있는 상태)에서는 좁게 되돌린다 —
   거기서는 넓은 왼쪽이 그냥 빈 자리다. */
.toc.is-info .sheet-left { width: clamp(260px, 44%, 600px); }
.sheet-right {
  flex: 1 1 auto; min-width: 0; height: 100%;
  overflow-x: auto; overflow-y: hidden;
  overscroll-behavior: none;
  scrollbar-width: none;
}
.sheet-right::-webkit-scrollbar { width: 0; height: 0; }

/* ── 아래 영역의 기본 자리 — 지금 보고 있는 편 ──────────────────────────
   ★ 좁은 화면에서는 글씨가 **여기에만** 뜬다. 영상 위에 겹치지 않는다.
     카테고리를 고르면 같은 자리가 그 분류로 바뀔 뿐, 화면이 늘어나지 않는다. */
/* ★ `justify-content: center` 는 내용이 칸보다 길면 **양쪽으로** 넘친다 — 위가 잘린다.
   상세가 많은 편(FX30 처럼 날짜·기기·센서·비율이 다 있는 편)만 윗여백이 사라진 게 이것이다.
   `safe` 를 붙이면 넘치는 순간에만 위를 기준으로 붙어 잘리는 쪽이 아래가 된다. */
.sheet-info {
  padding: var(--s-1) var(--s-4) var(--s-3) 0;
  display: flex; flex-direction: column;
  justify-content: center;
  justify-content: safe center;
  overflow: hidden;
}
.sheet-info[hidden] { display: none; }
.si-title {
  margin: 0;
  /* ★ 줄높이 1.1 은 `Q`·`g` 의 아래 꼬리를 자른다. -webkit-line-clamp 의 칸 높이가
     정확히 (줄수 × 줄높이) 라, 글자 잉크가 그보다 아래로 내려가면 그대로 잘려 나간다.
     1.16 이면 꼬리가 칸 안에 들어온다. */
  /* 줄 이름을 걷어낸 만큼 제목을 키운다(데스크톱은 조금: 22~34 → 26~40) */
  font: 600 var(--t-title)/1.16 var(--ui); letter-spacing: -.025em;
  color: var(--paper);
  /* 왼쪽 칸은 좁다 — 한 줄로 자르면 대부분 "…" 로 끝난다. 두 줄까지 편다. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ★★ **폭과 무관하게** 글줄은 눌리지 않는다.
   `.sheet-info` 는 세로 flex 이고 `overflow: hidden` 이라, 내용이 칸보다 높으면
   flex 가 아이들을 **줄여서** 맞추려 든다. 그러면 `overflow: hidden` 을 제 안에
   가진 제목이 제 글자를 잘라 먹는다 — 화면에 보이는 것이 "글씨 아랫부분이 싹둑".

   이 못박기가 예전에는 `max-width: 860px` 안에만 있었다. 그래서 **그보다 넓은
   중간 폭**(창을 줄이다 보면 지나가는 구간)에서 같은 증상이 그대로 남아 있었다.
   폭에 따라 달라질 이유가 없는 규칙이라 밖으로 꺼낸다. 내용이 정말 넘치면
   제목을 찌그러뜨리는 대신 아래쪽이 잘리게 두는 편이 옳다. */
.si-title, .si-place, .si-more { flex: 0 0 auto; }
.si-place {
  margin: var(--s-1) 0 0;
  font: 400 var(--t-small)/1.3 var(--ui); color: var(--paper); opacity: var(--o-mid);
}
.si-place[hidden] { display: none; }
/* 장소 옆 날짜 — 누르면 연혁. 생김새는 장소 글씨와 똑같고, 손이 가면만 밝아진다. */
.si-date {
  font: inherit; color: inherit; letter-spacing: inherit;
  border: 0; background: transparent; padding: 0; margin: 0;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  font-variant-numeric: tabular-nums;
}
.si-date:hover, .si-date:focus-visible { color: var(--paper); outline: none; text-decoration: underline; text-underline-offset: 3px; }

/* ── 시트 글씨가 들어오는 방식 ────────────────────────────────────────
 *
 * ★ 흔한 페이드나 타자기 대신 **필름 퍼포레이션**에서 가져왔다 — 홀수 글자는 위에서,
 *   짝수 글자는 아래에서 들어와 가운데 선에서 맞물린다. 필름이 톱니에 물려 한 칸
 *   전진하는 그 모습이다. 이 사이트가 내내 말하는 것이 프레임과 필름이니 거기서 나온다.
 * ★ transform 과 opacity 만 쓴다 — 둘 다 합성 단계에서 처리되어 레이아웃을 다시 계산하지
 *   않는다. 글자 수만큼 요소가 늘어도 프레임을 떨어뜨리지 않는다.
 * ★ 글자마다 18ms 씩 밀린다. 왼쪽부터 차례로 물려 들어가는 것처럼 읽힌다.
 */
/* 단어 묶음 — 줄바꿈은 이 묶음 사이에서만 일어난다(frame.js splitChars 참조) */
.si-title .w { display: inline-block; white-space: nowrap; }
.si-title .ch {
  display: inline-block;
  will-change: transform, opacity;
}
.sheet-info.is-in .si-title .ch {
  animation: chSprocket .46s cubic-bezier(.22,.9,.3,1) backwards;
  animation-delay: calc(var(--i) * 18ms);
}
@keyframes chSprocket {
  from { opacity: 0; transform: translateY(var(--from, -0.5em)); }
  to   { opacity: 1; transform: none; }
}
/* 짝수 글자만 반대편(아래)에서 올라온다 — 그래서 맞물리는 것으로 보인다 */
.si-title .ch:nth-child(even) { --from: 0.5em; }

/* 나머지 줄은 글자로 쪼개지 않는다 — 제목만 톱니고, 아래는 그 뒤를 따라 한 덩이로 뜬다.
   따라오는 순서가 곧 읽는 순서다(지역 → 시기). */
.sheet-info.is-in .si-place { animation: lineUp .44s ease .18s backwards; }
.sheet-info.is-in .si-more  { animation: lineUp .44s ease .26s backwards; }
@keyframes lineUp {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
/* 오른쪽 미리보기 셋도 차례로 — 왼쪽 글씨가 다 선 뒤에 따라온다 */
.sheet-info.is-in ~ * .pv-cell,
.preview-host .pv-cell { animation: lineUp .5s ease backwards; }
.preview-host .pv-cell:nth-child(1) { animation-delay: .30s; }
.preview-host .pv-cell:nth-child(2) { animation-delay: .38s; }
.preview-host .pv-cell:nth-child(3) { animation-delay: .46s; }

@media (prefers-reduced-motion: reduce) {
  .sheet-info.is-in .si-title .ch,
  .sheet-info.is-in .si-place,
  .sheet-info.is-in .si-more,
  .preview-host .pv-cell { animation: none; }
}
/* 상세는 한 줄로 흘려 적는다 — 아래 영역은 넓지만 높지는 않다.
   ★ 이 값들 자체가 이미 카테고리다(기기·센서·비율) — 누르면 바로 그 분류로 간다.
   그래서 여기가 곧 카테고리를 고르는 자리를 겸한다 — 글씨를 눌러볼 만한 크기로 키웠다. */
.si-more {
  display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-1);
  margin: var(--s-4) calc(-1 * var(--s-2)) 0;
}
.si-row {
  display: flex; align-items: baseline; gap: var(--s-2);
  border: 0; background: transparent; color: inherit;
  padding: var(--s-1) var(--s-2); margin: 0; border-radius: 3px;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  opacity: 1; transition: opacity .2s ease, background-color .2s ease;
}
.si-row:hover, .si-row:focus-visible { opacity: 1; background: rgba(237,237,238,.06); outline: none; }
/* ★ 날짜·기기·센서·비율은 **중요하지 않다.** 궁금할 때 찾아 읽는 값이지,
   화면에서 눈을 끌어야 할 것이 아니다. 제목보다 확실히 뒤로 물러나게 눌러 둔다. */
.si-v {
  font: 500 var(--t-body)/1.4 var(--ui); color: var(--paper); opacity: var(--o-mid);
  font-variant-numeric: tabular-nums;
}
/* 손이 가면 그때 또렷해진다 — 누를 수 있는 것임은 그대로 읽혀야 한다 */
.si-row:hover .si-v, .si-row:focus-visible .si-v { opacity: var(--o-hi); }

/* ★ 좁은 화면 — **반드시 위 기본값들보다 뒤에** 와야 한다.
   전에는 이 블록이 `.si-row` 기본 규칙보다 앞에 있어서, 같은 특이도에 밀려
   `padding: 3px` 이 통째로 죽어 있었다(5px 이 그대로 먹고 있었다). */
@media (max-width: 860px), (hover: none) and (pointer: coarse) {
  /* ★ 좌우 두 개씩 접었더니 칸 폭이 반토막나 값이 잘렸다(긴 카메라 이름 등).
     한 줄에 하나씩 세로로 편다 — 오른쪽 그림이 한 칸뿐이라 폭이 남는다. */
  /* ★ 값이 짧아져서(Dec 2025 · FX30 · APS-C · 2.35:1) **옆으로 흘린다.** 세로로 쌓으면
     네 줄이 시트 높이를 다 먹는다 — 시트를 3분의 1 줄인 뒤로는 들어가지도 않는다. */
  .si-more {
    margin-top: var(--s-2);
    display: flex; flex-direction: row; flex-wrap: wrap;
    gap: 0 var(--s-1);
  }
  .si-row { padding: var(--s-1) var(--s-2); min-width: 0; }
  .si-v { font-size: var(--t-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* 제목이 두 줄로 접히는 자리라 조금 낮춘다 — 두 줄이 온전히 들어가야 한다 */
  /* ★ 모바일은 **많이** 키운다(20 → 30px). 줄 이름이 빠져 아래가 가벼워졌다. */
  /* ★★ 제목은 **절대 두 줄이 되지 않는다.** 길면 "…" 로 자른다.
     제목이 접히면 그 아래 값 줄(Dec 2025 · 2.35:1)의 자리가 편마다 오르내린다 —
     그 줄은 누르는 단추라 늘 같은 자리에 있어야 한다. */
  .si-title {
    font-size: var(--t-title);
    display: block; -webkit-line-clamp: unset;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* 장소 줄도 **없는 편에서 자리를 비워 둔다** — 있는 편과 없는 편에서 값 줄 높이가 같게 */
  /* 장소 · 날짜는 한 줄 — 길면 "…"(Jade Dragon Snow Mountain, China · Aug 2026) */
  .si-place { line-height: 1.35; height: 1.35em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .si-place[hidden] { display: block; visibility: hidden; }
  .si-place { font-size: var(--t-small); margin-top: var(--s-1); }
}

.toc-rows {
  position: relative;
  margin: 0; padding: 0 var(--gutter); list-style: none;
  display: flex; align-items: stretch;
  height: 100%; width: max-content; min-width: 100%;
}
.toc-rows[hidden] { display: none; }

/* 한 칸 = 한 편. 넉넉히 넓고, 세로로는 시트를 꽉 채운다 — 어디를 눌러도 눌린다. */
.toc-row {
  position: relative; flex: 0 0 auto;
  width: clamp(124px, 13vw, 172px); height: 100%;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--s-3);
  margin: 0; padding: 0 var(--s-3); border: 0;
  background: transparent; color: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.toc-row:focus-visible { outline: none; }

/* 달 — **바뀌는 자리에만** 적힌다. 그래야 연혁으로 읽히고, 같은 글자로 도배되지 않는다. */
.toc-when {
  font: 500 var(--t-label)/1 var(--ui); letter-spacing: .2em; text-transform: uppercase;
  color: var(--paper); opacity: var(--o-lo);
  white-space: nowrap;
  transition: opacity .22s ease;
}
/* ★ 같은 달이 이어지는 칸은 손이 가도, 지금 보고 있어도 **끝까지 비어 있다.**
   (한때 그때만 드러내 줬는데, 결국 칸마다 같은 글자가 뜨는 것과 같았다.
    지금이 언제인지는 화면 왼쪽 아래 정보가 이미 말해 준다.) */
.toc-when.is-same { opacity: 0 !important; }
.toc-row:hover .toc-when,
.toc-row:focus-visible .toc-when,
.toc-row.is-cur .toc-when { opacity: var(--o-mid); }

/* 제목이 주인공이다. 두 줄까지 펴진다 — 한 줄로 자르면 대부분이 "…"로 끝났다. */
.toc-title {
  margin: 0;
  font: 500 var(--t-body)/1.34 var(--ui); letter-spacing: -.01em;
  color: var(--paper); opacity: var(--o-mid);
  text-align: center;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  transition: opacity .22s ease;
}
.toc-row:hover .toc-title, .toc-row:focus-visible .toc-title { opacity: 1; }
.toc-row.is-cur .toc-title { opacity: 1; font-weight: 600; }

/* 눈금 — 이게 곧 손이 닿았다는 표시다.
   ★ 칸 뒤에 면(그라데이션이든 단색이든)을 깔지 않는다. 대신 **이미 있는 선이 자란다** —
     평소엔 짧은 눈금, 손이 오면 칸 폭만큼 늘어나 밑줄이 되고, 지금 보는 편은 밝고 두껍다.
     면을 깔면 무엇을 하든 상자로 읽히지만, 선이 자라는 것은 연혁의 축으로 읽힌다. */
.toc-dot {
  width: 16px; height: 2px; border-radius: 2px;
  background: var(--paper); opacity: var(--o-faint);
  transition: opacity .22s cubic-bezier(.22,.61,.36,1),
              width .26s cubic-bezier(.22,.61,.36,1),
              height .22s ease;
}
.toc-row:hover .toc-dot, .toc-row:focus-visible .toc-dot { width: 100%; opacity: var(--o-mid); }
.toc-row.is-cur .toc-dot { width: 100%; height: 3px; opacity: 1; }

/* ── 정보 오른쪽의 미리보기 셋 ────────────────────────────────────────
   ★ 왼쪽 값들이 사실은 **누를 수 있는 입구**라는 것을 밑줄이나 테두리 없이 알리는 방법.
     그 분류가 어떤 그림인지 미리 하나씩 열어 두면, 눌러서 들어가는 것임이 저절로 읽힌다.
   ★ 셋 다 자리가 고정이라 편을 넘겨도 그림만 갈리고 눈은 흔들리지 않는다. */
.preview-host { height: 100%; }
.preview-host[hidden] { display: none; }
/* ★ 그림은 **오른쪽 끝에 붙인다.** 남는 폭을 칸들이 나눠 갖게 두었더니(space-evenly +
   flex:1) 두 칸 사이가 222px 까지 벌어져, 같은 줄에 있는 물건으로 안 읽혔다.
   왼쪽 제목이 영상의 왼쪽 선에 맞았듯, 그림은 영상의 오른쪽 선에 맞는다.
   ★ 위아래 여백과 칸 사이 간격을 **같은 값**으로 둔다 — 그래야 네 변이 한 세트로 읽힌다.
     예전엔 위 26 / 아래 14 로 어긋나 있었다. */
.pv {
  /* 이 한 값이 위·아래 여백이자 칸 사이 간격이다 — 셋이 같아야 한 세트로 읽힌다.
     좁을수록 그림이 커진다(칸 크기 = 시트 높이 − 이 값 ×2). */
  --pv-space: clamp(10px, 1.1vw, 16px);
  height: 100%;
  /* ★ `center` 로 두고 칸에 `height:100%` 를 주면, `aspect-ratio` 가 폭을 되계산하는
     사이 가운데 정렬이 어긋나 칸이 6px 씩 아래로 밀렸다(위 24 / 아래 12).
     `stretch` 로 두면 칸 높이가 곧 안쪽 높이라 계산이 끼어들 자리가 없다. */
  display: flex; align-items: stretch; justify-content: flex-end;
  gap: var(--pv-space);
  padding: var(--pv-space) var(--gutter) var(--pv-space) clamp(10px, 1.6vw, 24px);
}
/* ★ 칸을 **정사각으로 못박는다.** 그림이 정사각이라 결국 칸 높이가 크기를 정하는데,
   폭을 따로 나눠 갖고 있으면 그 남는 폭이 전부 칸 사이 빈틈이 된다.
   높이에서 폭을 끌어오면(aspect-ratio) 칸이 곧 그림이라 빈틈이 생길 자리가 없고,
   시트가 높아질수록 그림도 같이 커진다. */
.pv-cell {
  flex: 0 0 auto; min-width: 0;
  /* ★ 높이는 **확정값이어야** 한다 — `.pv-body` 가 컨테이너 쿼리(cqh/cqw)로 그림 크기를
     재기 때문에, 높이를 내용에 맡기면 서로를 기다리다 0 이 된다(실제로 그랬다).
     확정 높이에서 폭을 끌어오면 칸이 곧 정사각 그림이다. */
  height: 100%; aspect-ratio: 1 / 1;
  display: flex; align-items: center; gap: var(--s-1);
  opacity: .78; transition: opacity .2s ease;
}
.pv-cell[hidden] { display: none; }
/* ★ 이 편이 그 값을 안 가졌을 때. 상자는 자리 지키느라 남지만, 보이는 그림은 목록의
   첫 번째일 뿐 이 편의 기기가 아니다 — 더 흐리게 두어 "이 편의 것" 으로 안 읽히게
   한다. 누르면 그 분류를 둘러보는 문으로는 그대로 쓸 수 있다. */
.pv-cell.is-unowned { opacity: var(--o-lo); }
.pv-cell.is-unowned:hover, .pv-cell.is-unowned:focus-within { opacity: .6; }
.pv-cell:hover, .pv-cell:focus-within { opacity: 1; }
.pv-body {
  /* ★ align-items 를 center 로 두면 그림 상자가 내용 높이로 쪼그라든다(스트레치가 꺼진다).
     상자가 칸 높이를 다 차지해야 "최대 크기" 가 성립한다 — stretch 로 둔다. */
  display: flex; align-items: stretch;
  flex: 1 1 auto; min-width: 0; max-width: 230px; height: 100%;
  border: 0; background: transparent; color: inherit;
  padding: 0; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.pv-body:focus-visible { outline: none; }

/* ★ 정방형이다. 세로로 길면 상자가 사진기 그림에는 맞아도 센서·비율의 납작한
   사각형에는 위아래가 남아 허전했다. 정방형이 둘 다 무리 없이 받는다.
   높이가 곧 한 변이고(시트 높이가 한계), 남는 폭은 좌우로 흘려 보낸다. */
/* ★ 정방형이 실제로는 **세로로 길었다.** `height:100%` 로 한 변을 정해 놓고
   `max-width:100%` 를 같이 걸어 두면, 칸이 좁을 때 폭만 깎여 세로로 긴 상자가 된다
   (aspect-ratio 는 높이가 이미 확정된 쪽을 되돌리지 못한다).
   두 변을 **같은 값 하나**로 묶는다 — 칸의 높이와 폭 중 작은 쪽이 한 변이다. */
.pv-art {
  position: relative;
  flex: 0 0 auto; margin: 0 auto;
  height: min(100%, 100cqw); width: min(100%, 100cqh);
  aspect-ratio: 1 / 1;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
/* 컨테이너 단위(cqw/cqh)를 쓰려면 부모가 스스로를 '기준'이라고 선언해야 한다 */
.pv-body { container-type: size; }
/* 상자가 곧 한계다 — 따로 상한을 두지 않고 상자에 꽉 차게 둔다.
   ★ 사진기 PNG 는 그림 자체에 여백이 있을 수 있어 `scale` 로 조금 부풀려 상쇄한다.
     object-fit:contain 이라 비율은 그대로고, 넘치는 부분은 상자가 잘라 준다. */
.pv-art .br-svg, .pv-art .br-ghost { max-width: 100%; max-height: 100%; }
/* --cam-scale 은 카탈로그(cameras.json)의 scale 이 정한다 — 그림마다 여백이 달라
   같은 상자에서 유난히 커 보이는 것을 개별로 눌러 준다. 1.12 는 공통 여백 보정. */
/* ★ max-width/height 로 두면 **다 받기 전에는 크기가 0** 이라 작게 떴다가 로딩이 끝나는
   순간 제 크기로 튄다. width/height 를 100% 로 못박고 object-fit 으로 안에서 맞추면
   상자를 처음부터 차지하므로 튀는 순간이 없다 — 그림만 나중에 채워질 뿐이다. */
.pv-art .br-cam-img,
.pv-art .br-ghost.pv-cam {
  /* ★★ 사진기 그림의 한계는 **옆 칸의 풀프레임 선과 같은 사각형**이다(93% × 62.5%, 가운데).
     상자 전체(정방형)에 꽉 채웠더니 세로로 긴 instax 가 선 사각형보다 위아래로 25px 씩
     튀어나와 "상자를 뚫고 나온" 것처럼 보였다 — 사람 눈에 상자는 정방형 칸이 아니라 그 선이다.
     (옆 칸 기준 틀: 240 격자에서 폭 224 · 높이 150 → 93% · 62.5%) */
  width: 93%; height: 62.5%;
  /* ★ 기본 `.br-cam-img` 의 **84px 상한**을 풀어야 상자를 다 쓴다. height 100% 만 줘서는
     그 상한이 그대로 남아, 세로로 긴 그림(instax)이 131px 상자에서 84px 에 멈췄다. */
  max-width: none; max-height: none;
  object-fit: contain;
  transform: scale(var(--cam-scale, 1));
}
.pv-art .br-ghost.pv-cam { color: rgba(237,237,238,.55); }
/* SVG 에는 hidden 프로퍼티가 없다 — 클래스로 가린다(browse.js paint 참조) */
.pv-art .is-off { display: none; }

/* ★ 한때 미리보기에서 기준 틀을 껐다 — "견줄 상대가 없는 정보 화면에서는 그냥
   그림마다 둘러쳐진 상자 테두리로 보인다"는 이유였다. 그 판단은 센서와 비율이
   각자 칸을 갖고 있던 시절의 것이다.
   지금은 둘을 한 칸에 합쳐서 **그 칸이 하는 일이 곧 비교**다 — 풀프레임 틀이
   없으면 instax(1/5인치) 같은 작은 판은 그냥 작은 사각형일 뿐, 얼마나 작은지가
   사라진다. 그래서 되살린다. */

/* ★ 사진기가 바뀔 때의 전환은 browse.js 의 모자이크(SVG mask)가 맡는다 — 여기는
   그 mask 가 씌워질 자리라는 것 말고는 특별히 손댈 CSS 가 없다. */

/* ── 글씨와 그림을 **짝지어 밝힌다** ───────────────────────────────────
   같은 값을 왼쪽은 글씨로, 오른쪽은 그림으로 들고 있다. 한쪽에 손이 가면 다른 쪽도
   함께 밝아져야 "이 둘이 같은 것" 이라는 게 설명 없이 읽힌다.
   ★ `:has()` 로 부모를 거슬러 짚는다 — JS 로 이벤트를 걸고 클래스를 붙였다 떼는 것보다
     싸고(리스너 0개), 마우스가 빠르게 지나가도 상태가 남지 않는다. */
.sheet-body:has(.pv-cell[data-kind~="camera"]:hover) .si-row[data-kind="camera"],
.sheet-body:has(.pv-cell[data-kind~="sensor"]:hover) .si-row[data-kind="sensor"],
.sheet-body:has(.pv-cell[data-kind~="ratio"]:hover)  .si-row[data-kind="ratio"],
.sheet-body:has(.si-row[data-kind="camera"]:hover) .pv-cell[data-kind~="camera"],
.sheet-body:has(.si-row[data-kind="sensor"]:hover) .pv-cell[data-kind~="sensor"],
.sheet-body:has(.si-row[data-kind="ratio"]:hover)  .pv-cell[data-kind~="ratio"] {
  opacity: 1;
}
.sheet-body:has(.pv-cell[data-kind~="camera"]:hover) .si-row[data-kind="camera"] .si-v,
.sheet-body:has(.pv-cell[data-kind~="sensor"]:hover) .si-row[data-kind="sensor"] .si-v,
.sheet-body:has(.pv-cell[data-kind~="ratio"]:hover)  .si-row[data-kind="ratio"] .si-v {
  color: var(--accent);
}
/* 손이 얹힌 그림 자체도 또렷해진다 */
.pv-cell:hover .br-svg .br-fig { stroke: var(--accent); }

/* ── Browse — 조건별로 모아 보기 ──────────────────────────────────────
 *
 * ★ 칸마다 **그림 자체가 링크**다. "모아보기" 버튼을 따로 달지 않는다 —
 *   달는 순간 그림은 장식이 되고 누를 것만 하나 더 늘어난다.
 *
 * 색은 새로 만들지 않는다. 이 사이트에는 강조색이 없다(docs/TONE.md 1).
 * 필름/디지털, 가로/세로 같은 대비는 **선의 성질**로 나눈다 — 채운 면과 점선.
 */
.browse-host { height: 100%; overflow: hidden; }
.browse-host[hidden] { display: none; }
.br { display: flex; flex-direction: column; height: 100%; }

/* ★ 150px 고정값이 문제였다 — 좁은 화면에서는 이 칸 자체가 150px 보다 작아서
   그림이 칸을 넘쳐 아래 텍스트를 덮었다. min() 으로 "칸을 넘지 않으면서, 그 안에서도
   compact 하게" 둘 다 만족시킨다 — 화면 크기마다 다른 값을 따로 적을 필요가 없다. */
.br-svg { width: 100%; height: 100%; max-height: min(100%, 84px); overflow: visible; }

/* 기준 틀 — 센서는 풀프레임, 비율은 16:9. 늘 같은 자리에 있어 비교의 눈금이 된다.
   ★★ **선이 아니라 면이다.** 예전에는 흐린 실선 사각형이었는데, 무언가를 완전히
      에워싸는 선은 뜻과 무관하게 "그릇" 으로 읽힌다 — 기준 틀이 아니라 이미지
      상자의 테두리가 생긴 것처럼 보였다. 이 창에서 기준 틀을 한 번 껐던 이유도
      바로 그것("그림마다 둘러쳐진 상자 테두리로 보인다")이었다.
      면으로 두면 에워싸지 않으므로 테두리로 오해할 여지가 없고, 정작 하려던 일
      (풀프레임 **넓이** 대비 이 센서의 **넓이**)에는 면이 선보다 곧바로 읽힌다.
      흐린 바닥판 = 풀프레임, 그 위에 밝은 판 = 지금 이 센서. */
.br-ref {
  /* ★★ 기준 틀(풀프레임 / 16:9)은 **회색 선**이다 — 면으로 칠하지 않는다.
     한때 "테두리처럼 보인다"는 말을 잘못 알아듣고 옅은 회색 면으로 바꿨는데, 원하던 것은
     정반대였다: 배경을 깔지 말고, 풀프레임 자리를 선으로만 그어 두는 것.
     아래 .br-fig 설명의 "흐린 실선 = 기준 틀" 이 원래 뜻이다. */
  fill: none;
  stroke: rgba(237,237,238,.32);
  stroke-width: 1; vector-effect: non-scaling-stroke;
  /* 비율에서는 이 기준 틀도 방향을 따라 돌아눕는다 — 실물과 같은 전환을 걸어 둔다 */
  transition: x .55s cubic-bezier(.33,0,.2,1), y .55s cubic-bezier(.33,0,.2,1),
              width .55s cubic-bezier(.33,0,.2,1), height .55s cubic-bezier(.33,0,.2,1);
}
/* 고른 것 — 채운 면(디지털·가로) 대 점선(필름·세로).
   ★ 비율에서는 이 틀이 **모양을 바꾸며 눕고 선다.** 다시 그리는 게 아니라 변형이라
     `x/y/width/height` 에 전환을 걸어 두면 16:9 가 9:16 으로 돌아눕는 것이 보인다. */
.br-fig {
  transition: x .55s cubic-bezier(.33,0,.2,1), y .55s cubic-bezier(.33,0,.2,1),
              width .55s cubic-bezier(.33,0,.2,1), height .55s cubic-bezier(.33,0,.2,1);
  fill: rgba(237,237,238,.14);
  stroke: rgba(237,237,238,.9);
  stroke-width: 2; vector-effect: non-scaling-stroke;
}
/* ★ 필름은 **색**으로 가른다 — 점선이 아니다.
   예전엔 점선 하나가 세 가지 뜻을 겸했다(필름 / 세로 / 기준보다 큼). 게다가 패턴도
   4 3 과 2 2 로 제각각이라, instax 처럼 '필름이면서 기준보다 큰' 것은 둘이 겹쳐
   뜻을 알 수 없었다. 지금은 점선을 아예 쓰지 않는다 —
     · 흐린 실선   = 기준 틀(풀프레임 / 16:9)
     · 채운 실선   = 지금 고른 것
     · 따뜻한 색   = 필름,  중성색 = 디지털
   기준보다 크다는 것은 별도 표시 없이 **실제로 넘쳐 나가는 것**으로 보인다.
   (`.is-vert` 는 지웠다 — 선언만 되어 있고 어디에도 붙지 않는 죽은 규칙이었다.
    세로 여부는 이제 기준 틀이 함께 돌아눕는 것으로 읽힌다.) */
.br-fig.is-film {
  fill: rgba(226,192,142,.18);
  stroke: rgba(226,192,142,.95);
}
/* ★ 비율은 센서와 무관한 값이 아니라 **그 센서를 얼마나 잘라내는가**다 — 그래서 따로
   상자를 두지 않고 센서 사각형 위에 크롭선으로 겹친다. 채운 면(지금 고른 센서)과
   구분되도록 점선으로, 위에 얹히는 것이라 배경 없이 선만 긋는다. */
.br-crop {
  fill: none; stroke: rgba(237,237,238,.85);
  stroke-width: 1.3; stroke-dasharray: 2.5 2;
  vector-effect: non-scaling-stroke;
  transition: x .3s ease, y .3s ease, width .3s ease, height .3s ease, opacity .2s ease;
}
.br-crop.is-off { opacity: 0; }

/* 카메라 — 그림은 나중에 채워진다. 없으면 조용한 대체 그림. */
.br-cam { width: 100%; height: 100%; display: grid; place-items: center; }
/* ★ 그림은 **흰 종이에 검은 선**으로 그려져 온다(도면이니 당연하다).
   검은 사이트에 그대로 얹으면 흰 네모가 그대로 보이거나, 투명 PNG 라도
   검은 선이 검은 배경에 묻혀 아무것도 안 보인다.

   그래서 두 줄로 해결한다 —
     `invert(1)`     흰 종이는 검정이 되고 검은 선은 흰 선이 된다.
     `screen`        검정은 아래를 그대로 통과시킨다. 남는 건 흰 선뿐이다.

   ★ 이러면 **투명 PNG 인지 아닌지가 아예 상관없어진다.** 배경이 흰색이든 투명이든
     같은 결과가 나온다 — 파일을 손대지 않고 어떤 그림이 와도 받아 준다. */
.br-cam-img {
  max-width: 100%; max-height: min(100%, 84px);
  object-fit: contain; display: block;
  /* ★ 래스터(PNG)라 선 굵기를 **실제로 바꿀 수는 없다.** 대신 선명하게 보이게 하는 것을
     할 수 있는 만큼 다 했다:
       ① 원본을 640px 로 다시 뽑았다(전엔 320px 이라 레티나에서 확대되고 있었다)
       ② 굽는 단계에서 언샤프 마스크 — 가장자리만 또렷해진다
       ③ 여기서 대비를 올려 흐린 중간 회색을 걷어 낸다
       ④ 확대(scale)는 흐려지는 원인이라 1.12 → 1.0 으로 없앴다
     `image-rendering` 은 손대지 않는다 — 선 그림을 축소할 때 crisp-edges 를 주면
     계단이 생겨 오히려 지저분해진다. */
  filter: invert(1) brightness(1.22) contrast(1.7);
  mix-blend-mode: screen;
  opacity: .9;
}
.br-ghost { width: auto; height: 100%; max-height: min(100%, 84px); color: rgba(237,237,238,.3); }

/* ── 데스크탑 갤러리 ────────────────────────────────────────────────
 *
 * ★ 넓은 화면에서는 strip(글자 줄)+stage(그림 하나) 조합 대신 **한 줄짜리 갤러리**로
 *   바꾼다. 칸마다 그림·이름·"n개 보기"를 전부 갖고 있어서 훑다가 바로 눌러 간다 —
 *   좁은 화면의 "고르기→보기" 두 단계가 필요 없다.
 * ★ JS 의 NARROW 판정(860px, 또는 터치 포인터)과 정확히 반대 조건으로 켠다 —
 *   같은 화면에서 strip/stage 와 grid 가 동시에 보이는 일이 없어야 한다.
 */
/* ★ **넓든 좁든 같은 갤러리다.** 예전엔 좁은 화면만 "칩 줄에서 고르고 → 아래 그림 하나로
   확인" 하는 두 단계였는데, 낮아진 판에서 그 둘이 서로 자리를 뺏어 글자 줄도 그림도
   반쪽이 됐다. 한 칸이 그 자체로 결과인 갤러리는 낮고 넓은 판에 그대로 들어맞는다 —
   손가락으로 옆으로 훑는 것도 칩 줄을 훑던 동작과 같다.
   (strip/stage 배치는 늘 숨겨져 있어서 코드째 걷어냈다.) */
.br-grid {
  display: flex; flex: 1 1 auto; min-height: 0;
  align-items: stretch; gap: var(--s-3);
  padding: var(--s-1) var(--gutter) var(--s-4);
  overflow-x: auto; overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.br-grid::-webkit-scrollbar { width: 0; height: 0; }

.br-tile {
  flex: 0 0 auto; width: 130px;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
  gap: var(--s-2);
  border: 0; background: transparent; color: inherit;
  padding: var(--s-2) var(--s-2) var(--s-1); border-radius: 3px;
  cursor: pointer; text-align: center;
  -webkit-tap-highlight-color: transparent;
  transition: opacity .2s ease;
}
.br-tile:hover, .br-tile:focus-visible { opacity: .78; outline: none; }
.br-tile.is-empty { cursor: default; opacity: var(--o-lo); }

/* 좁은 화면 — 판이 낮으니 칸도 낮게. 폭도 조금 줄여 한 화면에 서너 칸이 보이게 한다
   (한 칸만 보이면 옆으로 더 있다는 걸 알 수 없다). */
@media (max-width: 860px), (hover: none) and (pointer: coarse) {
  .br-grid { gap: var(--s-1); padding: var(--s-1) var(--gutter) var(--s-3); }
  .br-tile { width: 104px; gap: var(--s-1); padding: var(--s-1) var(--s-1) var(--s-1); }
  .br-tile-art .br-svg, .br-tile-art .br-cam-img, .br-tile-art .br-ghost { max-height: 52px; }
  .br-tile-name { font-size: var(--t-small); }
  .br-tile-cta { font-size: var(--t-label); }
}

/* ★★ 그림 자리를 **못박는다.** 예전에는 `flex: 1 1 auto` 라 남는 자리를 그림이 다
   가져갔는데, 그 "남는 자리" 는 이름이 몇 줄이냐에 따라 달라진다. 그래서 같은 줄에
   놓인 카드인데 그림이 68~102px 로 제각각이었다(측정값) — 한 줄 이름은 크고 세 줄
   이름(Blackmagic Cinema Camera 6K)은 작았다. 나란히 놓인 것들의 크기가 다르면
   그것부터 눈에 걸려 정작 그림이 안 보인다.
   그림·이름·링크 **셋 다 높이를 정해** 카드끼리 줄을 맞춘다. */
.br-tile-art {
  flex: 0 0 auto; height: var(--tile-art-h, 76px); width: 100%;
  display: flex; align-items: center; justify-content: center;
}
/* 분류 그림(카메라·센서·비율)을 그대로 쓰되, 칸 안에서는 작게 */
.br-tile-art .br-svg, .br-tile-art .br-cam-img, .br-tile-art .br-ghost {
  max-height: 62px;
}
/* pv-art 쪽 --cam-scale 보정을 그리드 칸에도 그대로 적용 — 안 그러면 여백이 적은
   그림(instax 등)만 유난히 크게 튀어나와 보인다 */
.br-tile-art .br-cam-img, .br-tile-art .br-ghost {
  transform: scale(var(--cam-scale, 1));
}

.br-tile-name {
  font: 500 var(--t-small)/1.25 var(--ui); letter-spacing: -.005em;
  color: var(--paper);
  /* 두 줄까지만. 세 줄짜리 이름 하나 때문에 카드 전체가 어긋나느니 끝을 자른다 —
     전체 이름은 `aria-label` 과 넓은 화면에 그대로 남아 있다. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  /* 한 줄짜리 이름도 **두 줄 자리를 지킨다** — 그래야 아래 링크가 카드마다 같은 높이에 선다 */
  height: calc(2em * 1.25);
}
.br-tile-cta {
  font: 500 var(--t-label)/1 var(--ui); letter-spacing: .04em;
  color: var(--paper); opacity: var(--o-mid);
}
.br-tile-cta:not(:empty)::after { content: ' →'; }

/* 조용한 재시도 — 실패했을 때만 */
.retry {
  position: absolute; left: 50%; bottom: 18px; transform: translateX(-50%);
  border: 0; background: transparent; color: var(--paper);
  font: 500 var(--t-label)/1 var(--ui); letter-spacing: .18em;
  opacity: var(--o-mid); cursor: pointer; min-height: 44px;
}
.retry[hidden] { display: none; }

/* ── 처음 한 번만 뜨는 조작법 안내(onboard.js) ─────────────────────────
 *
 * ★ 따로 로딩 화면을 두지 않는다. 첫 영상 위에 이 막 하나를 얹어서, 로딩이
 *   실제로 얼마나 걸리든 그 시간이 "안내를 읽는 시간"으로 가려지게 한다.
 * ★ 처음엔 **꽉 찬 검정**이고, 8초에 걸쳐 서서히 옅어지되 끝나도 70% 는 남는다.
 *   아이콘은 그보다 훨씬 빨리(.7초) 떠오른다.
 * ★ 아이콘은 눌러도 반응하지 않는다 — 뭘 눌러도 "이 화면 전체"를 지우는 것으로만
 *   받는다. 잘못 눌러 원치 않는 화면 전환이 같이 일어나면 안 된다.
 *
 * ★ 판 **전체의 opacity 는 절대 건드리지 않는다.** 영상 바로 위에서 opacity 가 변하면
 *   브라우저가 그 판을 합성 레이어로 올렸다 내렸다 하면서 아래 영상까지 다시 래스터화하고,
 *   그때 영상이 한 번 튕겨 보인다. 그래서 검정은 언제나 **background-color 의 알파**로만
 *   여닫는다 — 칠하는 일이라 레이어 구성을 안 건드린다.
 *
 * ★ 규칙 순서 주의: 아래 `is-out`(걷힘) 묶음은 반드시 `is-in`(들어옴) **뒤에** 와야 한다.
 *   둘은 명시도가 같아서 먼저 쓴 쪽이 진다. 한때 is-out 을 위에 뒀더니 is-in 이 이겨서
 *   검정이 그대로 멈춰 있다가 JS 가 판을 떼는 순간 툭 사라졌다.
 */
.onb {
  position: absolute; inset: 0; z-index: 30;
  cursor: pointer;
  opacity: 1;
}

/* ★ 완전한 검정(알파 1)이 아니라 **아주 조금 비치는** 검정에서 시작한다.
   꽉 막힌 검정은 화면이 꺼진 것처럼 보이는데, 이만큼만 비쳐도 "뒤에 무언가 있다" 가
   읽힌다 — 그 상태에서 8 초에 걸쳐 .7 까지 열린다. */
.onb-dim {
  position: absolute; inset: 0;
  background-color: rgba(0, 0, 0, .99);
  transition: background-color 8s linear;
}
.onb.is-in .onb-dim { background-color: rgba(0, 0, 0, .5); }

.onb-icon {
  position: absolute;
  display: flex; flex-direction: column; align-items: center;
  color: var(--paper); pointer-events: none;
  opacity: 0; transition: opacity .7s ease;
}
/* ★ 안내지 주인공이 아니다. 또렷하게 그려 두면 첫 화면이 아이콘 판으로 읽힌다 —
   뒤에 영상이 있다는 게 먼저 보여야 한다. 알아볼 만큼만 남긴다. */
.onb.is-in .onb-icon { opacity: var(--o-mid); }
.onb-icon svg { width: 34px; height: 34px; display: block; }
/* 가운데(탭 해서 보기)와 아래(목차)는 더 크게 — 이 둘이 이 화면의 요점이다.
   가운데는 정사각이 아니라 가로로 넓고 낮다(40:26) — height:auto 라야 안 찌그러진다. */
.onb-center svg { width: 56px; height: auto; }
/* 한 문장이 되었으니(‘탭 해서 보기’) 대문자 변환·넓은 자간은 뺀다 —
   그건 한두 단어짜리 표지에나 어울리고, 문장에 걸면 읽기만 어려워진다.

   ★ **흐름에서 빼서** 아래에 매단다. 그러지 않으면 글씨가 있는 아이콘(가운데)만
     세로로 길어져서, 같은 top:50% 에 놓아도 그림 자체는 라벨 높이의 절반만큼
     위로 밀린다 — 좌우 화살표와 높이가 안 맞아 보이던 게 이것 때문이었다. */
.onb-label {
  position: absolute; top: calc(100% + 14px); left: 50%; transform: translateX(-50%);
  font: 500 var(--t-small)/1 var(--ui); letter-spacing: .01em;
  color: var(--paper); opacity: var(--o-mid); white-space: nowrap;
}

/* ★ 화면 한가운데가 아니라 **개구부(영상이 뜨는 자리) 한가운데**에 놓는다.
   아래에는 아랫변 프레임이 있어서, 화면 기준 50% 로 두면 영상보다 그만큼 아래로 처진다. */
.onb-center { left: 50%; top: calc(50% - var(--bottom-frame) / 2); transform: translate(-50%, -50%); }
.onb-prev   { left: calc(var(--side-frame) / 2); top: calc(50% - var(--bottom-frame) / 2); transform: translate(-50%, -50%); }
.onb-next   { right: calc(var(--side-frame) / 2); top: calc(50% - var(--bottom-frame) / 2); transform: translate(50%, -50%); }
.onb-up     { left: 50%; top: calc(var(--edge-v, 40px) + 16px); transform: translate(-50%, 0); }
.onb-down   { left: 50%; bottom: calc(var(--sheet-h, var(--bottom-frame)) + 16px); transform: translate(-50%, 0); }

/* ★ 좁은 화면에서는 아랫변이 `--bottom-frame` 이 아니라 **아래 판(--sheet-h)** 이다.
   그런데 여기 좌표는 전부 `--bottom-frame` 을 기준으로 잡혀 있어서, 안내가 영상 한가운데가
   아니라 판 위에 걸쳐 그려졌다 — 위아래 화살표가 "영상 밖"에 있는 것처럼 보였다.
   좁은 화면에서는 개구부(위 ~ 판 윗선)를 기준으로 다시 잡는다. */
@media (max-width: 860px), (hover: none) and (pointer: coarse) {
  /* ★★ 좁은 화면에서는 **화면 높이의 한가운데**에 모은다.
     개구부(영상) 기준으로 두니 안내 전체가 화면 위쪽 영상 쪽으로 치우쳐 보였다 — 안내는
     화면 전체를 덮는 판이라 판의 한가운데가 맞다. 이제 스와이프는 아래 시트에서도 먹으므로
     "영상 안에서만 쓸어라" 라고 가리킬 이유도 없어졌다.
     위아래 화살표는 가운데를 기준으로 **같은 거리**만큼 벌린다 — 짧은 화면에서는 덜 벌어진다. */
  .onb { --onb-gap: clamp(110px, 19vh, 170px); }
  .onb-center { top: 50%; }
  .onb-up   { top: calc(50% - var(--onb-gap)); bottom: auto; transform: translate(-50%, -50%); }
  .onb-down { top: calc(50% + var(--onb-gap)); bottom: auto; transform: translate(-50%, -50%); }
}

/* 10초를 가만있으면 **아이콘이** 밝아졌다 덜 밝아졌다 한다.
   ★ 한때 가운데 아이콘만 확대했는데 그건 "여길 눌러라" 로 읽힌다 — 틀린 안내다.
     아무 데나 눌러도 닫히므로 특정 지점을 가리키면 안 된다. 그래서 **넷이 함께** 숨을 쉰다.
   ★ 검은 막을 흔드는 것도 해 봤지만 화면 전체가 밝아졌다 어두워져 산만했다 — 아이콘만 움직인다. */
.onb.is-nudge .onb-icon { animation: onbPulse 2.6s ease-in-out infinite; }
@keyframes onbPulse {
  0%, 100% { opacity: .5; }
  50%      { opacity: .38; }
}
@media (prefers-reduced-motion: reduce) {
  .onb.is-nudge .onb-icon { animation: none; }
}

/* ── 걷힐 때 ──────────────────────────────────────────────────────────
   반드시 위 is-in / is-nudge 보다 **뒤에** 있어야 한다(위 설명 참조).
   animation 을 명시적으로 끄는 것도 같은 이유다 — 애니메이션은 transition 을 이기므로,
   숨쉬는 중에 걷으면 그 자리에 멈춰 있다가 툭 사라진다. */
.onb.is-out { pointer-events: none; }
.onb.is-out .onb-dim {
  animation: none;
  background-color: rgba(0, 0, 0, 0);
  transition: background-color .48s ease;
}
.onb.is-out .onb-icon {
  animation: none;
  opacity: 0;
  transition: opacity .34s ease;
}

/* ── 아래 = 화면을 닫는 검은 면 ───────────────────────────────────────
   영상 컨트롤 바가 아니다. 감상 중에는 거의 빈 검은 프레임이어야 한다. */
/* 하단 프레임 — 누르면 지금 작품의 정보가 다시 잠깐 뜬다 */
/* 아래 검은 띠 — 닫혀 있을 때는 이게 곧 연혁을 여는 버튼이다.
   ★ 숨어 있는 연혁(z-index 8)보다 위에 그려져야 그것을 가려 준다. */
.bottom {
  position: relative; z-index: 9;
  cursor: pointer;
  flex: 0 0 var(--bottom-frame);
  height: var(--bottom-frame);
  transition: flex-basis .55s cubic-bezier(.4,0,.16,1), height .55s cubic-bezier(.4,0,.16,1);
  background: var(--ink);
  display: flex; align-items: center;
  padding: 0 var(--gutter) env(safe-area-inset-bottom);
}

/* 진행도 — 영상 안쪽 맨 아래의 얇은 흰 띠.
 *
 * ★ **글씨와 한 몸으로 밝아지고 흐려진다.** 예전엔 마우스를 올려야만 또렷해져서
 *   "손을 올려야 작동하는 것" 처럼 보였다 — 그건 진행도가 할 말이 아니다.
 *   `.card`(제목)와 같은 부모(.center) 안에 있고 순서상 뒤에 오므로, 그 상태를
 *   형제 선택자로 그대로 물려받는다: 글씨가 또렷하면 띠도 또렷하고, 3초 뒤
 *   글씨가 흐려지면 띠도 같이 흐려진다. 원본비로 볼 때는 둘 다 완전히 물러난다.
 * ★ 색은 다시 흰색 계열이다. 검정으로 눌러 봤더니 어두운 장면에서 아예 사라졌다.
 * ★ 손이 오면 두꺼워지고 선명해지는 것은 남긴다 — 그건 밝기가 아니라
 *   "잡아 끌 수 있다" 는 다른 정보다.
 */
.scrub {
  position: absolute; left: 0; right: 0; bottom: 0; height: 20px;
  z-index: 6; cursor: pointer;
  display: flex; align-items: flex-end;
  opacity: 1; transition: opacity .6s ease;
}
.trace {
  position: relative; width: 100%; height: 4px;
  overflow: hidden; pointer-events: none;
  transition: height .18s ease;
}
.trace span {
  display: block; height: 100%; width: 100%;
  background: rgba(255,255,255,.4);
  filter: blur(1.6px);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: filter .18s ease, background-color .18s ease;
}
.scrub:hover .trace, .scrub.is-grab .trace { height: 7px; }
.scrub:hover .trace span, .scrub.is-grab .trace span {
  filter: blur(0.6px); background: rgba(255,255,255,.72);
}

/* 글씨를 따라간다 — `.card` 의 상태를 형제로 물려받는다(위 설명 참조). */
.card.is-faded ~ .scrub { opacity: .34; }
.stage.is-awake .card.is-faded ~ .scrub { opacity: 1; }
/* ★ 원본비로 볼 때는 방해 없이 — 반드시 위 규칙들보다 **뒤에, 더 강하게** 와야 한다.
   (전에 걷힘 규칙을 앞에 뒀다가 진 적이 있다 — 같은 실수를 반복하지 않는다.) */
.stage.is-contain .card ~ .scrub,
.stage.is-contain.is-awake .card ~ .scrub { opacity: 0; pointer-events: none; }

/* 필요할 때만 나타나는 기능. 아이콘을 줄지어 놓지 않는다. */
.acts {
  margin-left: auto; display: flex; gap: var(--s-2); align-items: center;
  opacity: 0; transition: opacity .3s ease;
  pointer-events: none;
}
.stage.is-awake .acts { opacity: 1; pointer-events: auto; }

/* ★ 소리를 만지는 중에는 **사라지지 않는다.**
   `.acts` 는 손이 멎으면 2.2 초 뒤 opacity 0 이 된다(위 `is-awake`). 그런데 꾹 누르기는
   5 초짜리 제스처라, 그 한가운데서 칸이 통째로 사라져 물결이 까맣게 없어져 버렸다 —
   "최소 속도일 때와 최대 속도일 때 안 보인다" 가 이것이었다.
   ★ opacity 는 부모에 걸려 있어 자식이 되돌릴 수 없다. 그래서 무대에 표식을 두고
     그 칸 전체를 살려 둔다. 붙잡고 있는 동안과 곡이 흐르는 동안 켜진다. */
.stage.vol-live .acts { opacity: 1; pointer-events: auto; }

/* ★ 원본비로 볼 때는 **아무것도 남기지 않는다.** 손이 움직이면 흐릿하게 돌아오게
   해 뒀었는데, 잠깐이라도 뜨는 것 자체가 거슬린다 — 여기서는 영상만 있으면 된다.
   `vol-live`(꾹 누르는 중·곡이 흐르는 중)보다도 뒤에 와서 그것까지 덮는다.
   소리는 자판으로 계속 조작할 수 있다(m 음소거, ←→ 크기). */
.stage.is-contain .acts,
.stage.is-contain.is-awake .acts,
.stage.is-contain.vol-live .acts { opacity: 0; pointer-events: none; }
/* ★ 아이콘은 작게, **누를 자리는 크게.** 둘은 다른 것이다 —
   보이는 것이 작아 조용한데 실제로 눌리는 영역은 손가락에 맞는다.
   (예전엔 34px 정사각이라 아이콘도 작고 누르기도 어려웠다 — 최악의 조합이었다.) */
.act {
  border: 0; background: transparent; color: var(--paper);
  display: grid; place-items: center;
  width: 48px; height: 100%; min-height: 44px; padding: 0;
  opacity: var(--o-lo); cursor: pointer;
  transition: opacity .2s ease;
  -webkit-tap-highlight-color: transparent;
}
.act svg { width: 17px; height: 17px; display: block; }
.act:hover, .act:focus-visible { opacity: .95; outline: none; }
.act.is-on { opacity: .95; }

/* ── 소리 ─────────────────────────────────────────────────────────────
 *
 * ★ **파형 한 줄**이다. 진폭이 곧 음량이고, 끄면 잔잔한 직선이 된다.
 *   아이콘도 슬라이더도 없다 — 물결이 이는가 잦아드는가로 상태가 그대로 읽힌다.
 *   좌우로 끌면 크기가 바뀌고, 그냥 누르면 켜고 끈다. 켜고 끄기와 크기가 한 물건이다.
 *
 *   (한때 계단 모양 막대 다섯 개였다. 크기는 읽혔지만 "소리"로는 안 읽혔다 —
 *    막대는 신호의 은유이지 소리의 은유가 아니다. 물결이 훨씬 곧장 읽힌다.)
 */
.vol {
  display: grid; place-items: center;
  width: 72px; height: 100%; min-height: 44px;
  color: var(--paper);
  cursor: pointer; touch-action: none;
  -webkit-tap-highlight-color: transparent;
  /* 평소엔 더 어둡게(.5 → .3) — 영상 옆에서 눈을 끌지 않는다. 손이 가면 그대로 밝아진다. */
  opacity: var(--o-lo); transition: opacity .25s ease;
}
.vol:hover, .vol:focus-visible, .vol.is-grab { opacity: .95; outline: none; }
.vol.is-muted { opacity: var(--o-faint); }
/* ★ 밖으로 삐져나가 그려지는 것이 없게 잘라 둔다 — 흐려질 때 상자 밖 조각이 점으로 남았다(frame.js drawWave).
   가장 큰 물결(진폭 9.2 + 선 반폭 0.8)도 높이 20 안에 들어간다. */
.vol svg { width: 100%; height: 20px; display: block; overflow: hidden; }
/* 파형이 바뀌는 것 자체가 애니메이션이다 — 진폭이 물결처럼 잦아들고 다시 인다 */
.vol path { transition: d .42s cubic-bezier(.22,.61,.36,1); }
.vol.is-grab path { transition: d .12s linear; }

/* ── 꾹 누르는 동안 ───────────────────────────────────────────────────
   ★ 여기에 키프레임을 두지 않는다. 물결은 **JS 가 위상을 밀어 그린다**(frame.js waveTick).
     CSS 로 하려면 속도를 바꿀 때마다 `animation-duration` 을 갈아야 하는데, 그 순간
     진행 위치가 다시 계산되어 툭 튄다 — 서서히 빨라지는 것을 만들 수가 없다.
   ★ 그래서 여기서는 **밝기만** 맡는다. 모양은 전부 JS 쪽이다.
   ★ 흐르는 동안에는 `d` 에 걸린 전환을 꺼야 한다 — 매 프레임 새 경로가 오는데
     그때마다 0.42초짜리 전환이 걸리면 서로 밀려 뭉갠다. */
.vol.is-charging { opacity: .95; }
/* ★ 물결이 흐르는 동안에는 `d` 전환을 끈다 — 위 `.vol path` 와 `.vol.is-grab path` 의
   전환이 매 프레임 오는 새 경로와 밀려 뭉갠다(frame.js setWave 참조).
   `is-grab`(음량 끌기) 보다 뒤에 와야 이긴다. */
.vol.is-waving path { transition: none; }
/* ★ "이제 놓아도 된다" — 5초 차오름이 끝나는 그 프레임에만 짧게 튀는 신호.
   `is-armed` 자체는 손을 뗄 때까지 계속 붙어 있지만, `animation` 은 클래스가
   막 걸리는 순간에만 재생되고 저절로 끝난다(반복 없음) — 그래서 딱 한 번만 튄다.
   물결 속도가 계속 빨라지는 것만으로는 "지금이 그 순간"이 잘 안 느껴진다고 해서 더함. */
.vol.is-armed { animation: vol-armed .34s ease-out; }
@keyframes vol-armed {
  0% { transform: scale(1); filter: brightness(1); }
  32% { transform: scale(1.16); filter: brightness(1.8); }
  100% { transform: scale(1); filter: brightness(1); }
}

/* ── 모바일 — 데스크톱 프레임을 그대로 줄이지 않는다 ─────────────────
   좌우는 가장자리 힌트 수준으로 얇아지고, 감상 영역과 스와이프를 우선한다.
   아래는 브라우저 하단 바와 겹쳐 답답해지지 않게 최소로만 남긴다. */
/* `pointer: coarse` 단독으로 걸면 터치스크린 노트북에서도 프레임이 무너진다.
   진짜 손가락 기기는 hover 가 없다 — 두 조건을 함께 본다. */
/* ── 좁은 화면 — 위아래로 바뀐다 ─────────────────────────────────────
   데스크톱 프레임을 그대로 줄이지 않는다. 좌우에 있던 이전·다음이 **위아래로 옮겨 간다** —
   좁은 화면에서 옆으로 넘기는 것보다 아래로 넘기는 쪽이 훨씬 자연스럽다(숏츠와 같은 방향).
   화살표 같은 표식은 두지 않는다. 위아래에 걸친 이웃 자체가 넘김 장치다. */
@media (max-width: 860px), (hover: none) and (pointer: coarse) {
  /* ★ 좁은 화면에서는 이웃 판을 아예 걷어낸다.
     한동안 위아래에 걸어 뒀는데, 비네트로 가장자리를 닫아 놓으니 그 판이
     **영상과 검은 프레임 사이에 낀 회색 상자**처럼 보였다 — 프레임이 두 겹으로 읽혔다.
     대신 그 자리를 아래 탐색 시트가 통째로 가져간다. 시트가 항상 열려 있으므로
     화면은 자연스럽게 "위아래가 검고 가운데만 영상"인 형태가 된다.
     세로가 짧아질수록 영상이 덜 어색해지므로, 이쪽이 화면 비율에도 맞다. */
  .side { display: none; }
  .band { flex-direction: column; }

  .card { padding-bottom: clamp(12px, 3vw, 20px); }

  /* ★ 진행도 바를 걷어낸다. 두 가지 이유다 —
     하나, 4px 짜리 띠는 손가락으로 잡아 끌 수가 없다. 있어 봐야 못 쓴다.
     둘, 이건 흐릿하게 번지는 **흰 줄**이라(rgba(255,255,255,.62) + blur)
     바로 아래 붙은 '다음 작품' 판과의 경계에 흰 선이 그어진 것처럼 보인다. */
  .scrub { display: none; }

  /* ★ 좁은 화면에서는 조작이 **늘 보여야** 한다 — 손이 다가오는 일이 없어
     "가까이 가면 드러난다"가 성립하지 않는다. 대신 아주 조용한 밝기로 둔다. */
  .acts { opacity: .55; pointer-events: auto; }
  .stage.is-awake .acts { opacity: 1; }

  /* ★ 시트는 **늘 열려 있다.** 그래서 내려가 숨는 동작을 쓰지 않는다.
     아래 검은 면이 곧 탐색이고, 그것이 화면의 아랫변 프레임을 겸한다. */
  /* ★ 좁은 화면에서도 탭 줄은 없다. 정보의 각 줄이 곧 분류의 입구이고,
     돌아오는 길은 머리줄의 화살표 하나다 — 세로 공간을 한 줄도 더 쓰지 않는다. */

  /* ★ 좁은 화면에서는 모서리를 깎지 않는다 — 좌우에 프레임이 없어서(영상이 화면 폭을
     꽉 채운다) 둥근 모서리가 "프레임"이 아니라 "카드"로 읽힌다.
     ★ border-radius 만 끄면 모자란다 — 아래 clip-path 에도 `round` 가 들어 있어
       그것까지 같이 꺼야 한다. 그리고 여기서는 **아래를 오려낼 이유도 없다**:
       좁은 화면은 이미 `.band` 의 margin-bottom 으로 시트만큼 비켜나 있어서,
       clip-path 로 또 잘라내면 시트 높이만큼 두 번 잘려 나간다(실제로 그랬다). */
  /* ★ 이제 위(검은 띠)와 아래(시트)가 모두 영상을 **감싸므로** 모서리를 데스크톱처럼 둥글게 한다.
     아래를 clip-path 로 오려내지는 않는다 — 좁은 화면은 .band 의 margin-bottom 으로 이미 비켜나 있다. */
  /* ★ 아래는 **4px 오려낸 자리**에서 둥글린다. 영상 상자는 시트 밑으로 4px 파고들어 있어서
     (.band margin-bottom 의 −4px — 경계의 틈을 막는 겹침), 상자 끝에서 둥글리면 그 곡선이
     시트에 가려 아래 모서리만 각져 보였다. 보이는 끝선에서 둥글려야 네 모서리가 같다. */
  .center,
  .stage.toc-open .center { border-radius: 10px; clip-path: inset(0 0 4px 0 round 10px); }

  /* 좁은 화면에는 좌우 프레임이 없다 — 시트가 폭을 그대로 쓴다.
     대신 글씨가 화면 끝에 붙지 않게 여백을 넉넉히 준다(16 → 22). */
  .toc { left: 0; right: 0; --gutter: var(--s-5); }
  /* ★ 글씨 왼쪽, 그림 오른쪽 — 좁은 화면에서도 좌우 배치를 지킨다.
     한때 그림을 글씨 **아래 줄**로 내려 봤는데, 그러면 상세 네 줄까지 세로로 쌓여
     시트가 통째로 길어지고 영상이 그만큼 짧아졌다.
     대신 **그림을 하나만** 둔다(아래 fit 참조) — 한 칸이면 폭을 얼마 안 먹어서
     글씨가 쓸 자리가 충분하다.
     ★ 연혁·Camera/Sensor/Ratio 그리드도 같은 .sheet-right 를 쓰므로
       **정보 화면일 때만**(.toc.is-info) 건드린다. */
  /* 좁은 화면에는 좌우 프레임이 없다 — 여백을 프레임에 맡길 수 없으니 직접 준다 */
  .toc.is-info .sheet-left { flex: 1 1 auto; width: auto; min-width: 0; padding-left: var(--gutter); }
  /* ★★ 모바일 정보 화면에는 **그림 칸(기기·센서)이 없다.** 제목 오른쪽을 먹어 긴 제목이
     두 줄로 접혔다. 그림이 하던 일(그 분류로 들어가기)은 아래 값 줄이 이미 한다. */
  .toc.is-info .sheet-right { display: none; }

  /* ★★ 정보 줄은 **분류로 들어가는 단추**다(누르면 그 카테고리로 간다). 그런데
     높이가 24px 이라 엄지로 겨냥하기에 작았다 — 이 저장소는 같은 문제를 분류 화면
     머리줄에서 이미 44px 로 고쳤는데(`.cat-back`), 정작 더 자주 누르는 이 줄들은
     그대로였다.
     44px 로 맞추려면 네 줄에 176px 이 필요해 시트가 79px 길어지고 그만큼 영상이
     짧아진다 — 이 화면이 명시적으로 거부한 거래다(위 주석). 그래서 32px 로
     타협한다: 시트는 31px 만 자라고(203→234, 화면의 3.8%), 겨냥은 3분의 1 쉬워진다.
     줄이 화면 폭을 거의 다 쓰는 가로로 긴 표적이라, 같은 높이의 정사각 단추보다
     실제로 누르기 쉽다는 점도 감안했다. */
  .si-row { min-height: 32px; padding-top: var(--s-1); padding-bottom: var(--s-1); }
  /* `.pv` 의 위 패딩 26px 은 넓은 화면에서 그림을 아래로 내리려던 값이다 —
     좁은 화면에서는 한 칸뿐이라 가운데 두는 편이 낫다.
     ★ `.pv-art` 는 높이가 정해져 있어(정사각) 부모의 stretch 가 먹지 않는다 —
       위에 붙어 버리므로 `margin:auto` 로 남는 자리를 위아래로 나눠 갖게 한다. */
  .toc.is-info .pv { padding: var(--s-2) var(--gutter) var(--s-2) 0; }
  .toc.is-info .pv-art { margin: auto; }
  /* ★ 넓은 화면에서는 칸이 정사각(높이=폭)이지만, 여기서는 시트가 높고 칸은 좁아
     그대로 두면 칸이 시트 높이만큼 넓어져 넘친다 — 폭이 정하게 되돌린다. */
  .toc.is-info .pv-cell { aspect-ratio: auto; flex: 1 1 auto; }
  /* ★ 제목이 시트 윗변에 딱 붙어 있었다. 내용이 칸보다 높으면 `safe center` 가
     가운데 정렬을 포기하고 위로 붙이는데(잘리는 쪽을 막으려는 동작), 그때 여백이
     하나도 없어 답답했다. 위아래로 숨 쉴 자리를 준다. */
  .toc.is-info .sheet-info { padding-top: var(--s-5); padding-bottom: var(--s-3); }   /* 제목 위 여백 14 → 24 */
  /* ★★ 제목은 **시트 윗변에 못박는다.** 가운데 정렬이면 제목 자리가 상세 줄 수에 달려
     있다 — 날짜·비율 두 줄인 편과 날짜·기기·센서·비율 네 줄인 편에서 제목이 32px 씩
     오르내렸다. 넘길 때마다 가장 눈에 띄어야 할 글씨가 들썩이면 화면이 흔들려 보인다.
     제목과 상세는 **따로다** — 제목은 늘 같은 자리, 상세는 그 아래로 필요한 만큼 붙는다. */
  .toc.is-info .sheet-left { justify-content: flex-start; }
  .toc.is-info .sheet-info { justify-content: flex-start; }

  /* ── 분류 화면(연혁·Camera·Sensor·Ratio)은 통째로 **모바일용 배치**다 ────────
     넓은 화면의 좌우 2단을 그대로 좁히면 이름 칸이 화면 절반을 먹고, 남은 절반에
     칸들이 우겨넣어져 글씨가 10px 로 줄고 오른쪽이 잘려 나간다 — 실제로 그랬다.
     좁은 화면에서는 **머리줄을 위로 올리고 아래를 폭 전체로 쓴다.**
     세로 스와이프는 작품을 넘기는 동작이라 여기서 쓸 수 없다 — 아래 칸들은
     **옆으로 튕겨** 넘기고, 스냅으로 손끝에서 착 멎게 한다. */
  .toc:not(.is-info) .sheet-body { flex-direction: column; align-items: stretch; }
  /* ★★ 머리줄도 **정보 화면 제목과 같은 여백**을 쓴다(위 24 · 옆 24).
     위에 딱 붙어 있어서 뒤로가기가 시트 윗변에 걸려 보였다. 단추 자체가 44px 높이라
     그 안에 이미 위아래 빈 곳이 있으므로 12px 부터 띄우면 글자 윗선이 제목과 맞는다. */
  .toc:not(.is-info) .sheet-left {
    flex: 0 0 auto; width: 100%;
    justify-content: flex-start;
    padding: var(--s-3) var(--gutter) 0;
  }
  /* 화살표를 단추 안쪽 여백만큼 당겨, 화살표 끝이 제목 글자와 같은 세로선에 서게 */
  .toc:not(.is-info) .cat-back { margin-left: calc(-1 * var(--s-2)); }
  .toc:not(.is-info) .sheet-right { flex: 1 1 auto; width: 100%; min-height: 0; }

  /* 머리줄 — 화살표와 이름을 **한 줄로** 눕히고 손가락에 맞는 높이를 준다.
     넓은 화면에서는 화살표가 위, 이름이 아래인 2층이지만 여기서는 그 높이가 아깝다. */
  .cat-back {
    flex-direction: row; align-items: center; gap: var(--s-2);
    min-height: 44px; padding: var(--s-1) var(--s-2) var(--s-1) var(--s-1);
  }
  .cat-back svg { width: 19px; height: 19px; opacity: var(--o-mid); }
  .cat-name { font-size: var(--t-head); }

  /* 칸 — 엄지로 누를 것이라 크게. 폭이 넉넉해져서 이름도 제 크기로 적을 수 있다. */
  .br-grid {
    gap: var(--s-2); padding: 0 var(--gutter) var(--s-3);
    scroll-snap-type: x proximity;
    scroll-padding-left: var(--gutter);
  }
  .br-tile {
    width: 118px; gap: var(--s-2); padding: var(--s-2) var(--s-2) var(--s-2);
    /* 세 조각의 높이가 모두 정해졌으므로 가운데 정렬이 안전하다 — 칸이 높아져도
       모든 카드가 똑같이 내려앉는다. (예전 어긋남은 정렬 탓이 아니라 그림이
       남는 자리를 다 가져간 탓이었다.) */
    justify-content: center;
    scroll-snap-align: start;
    --tile-art-h: 70px;
  }
  .br-tile-art .br-svg, .br-tile-art .br-cam-img, .br-tile-art .br-ghost { max-height: 56px; }
  .br-tile-name { font-size: var(--t-small); line-height: 1.3; height: calc(2em * 1.3); }
  /* 작품이 없는 칸은 링크가 비어 있다 — 그래도 자리는 지켜야 칸끼리 바닥이 맞는다 */
  .br-tile-cta { font-size: var(--t-label); min-height: 1.25em; }

  /* 연혁도 같은 규칙 — 칸을 넓히고 제목을 키운다.
     ★ 여기엔 스냅을 걸지 않는다. 스냅은 "여기가 선택됐다"고 말하는 장치인데
       연혁은 훑어보다 **눌러서** 가는 목록이다(frame.js 의 같은 판단). 게다가
       실제로 옆으로 흐르는 것은 `.toc-rows` 가 아니라 부모인 `.sheet-right` 라
       여기 걸어 봐야 먹지도 않는다. */
  .toc-row { width: 132px; padding: 0 var(--s-3); }
  .toc-title { font-size: var(--t-body); }
  .toc-when { font-size: var(--t-label); }

  /* ★ 좁은 화면에서는 왼쪽 칸이 세로로 쌓인다(제목 + 지역 + 상세 네 줄) + 그 아래 이미지박스 줄.
     전체 높이를 30% 줄였을 때 여기까지 같이 줄어 **제목이 위로 밀려 잘렸다**(내용 153 / 칸 146).
     좁은 화면만 조금 되돌린다 — 여기서는 그 네 줄이 세로로 자리를 먹기 때문이다. */
  /* ★ 상세 네 줄을 한 줄씩 펴고 이미지박스까지 아래로 내리면서 필요한 높이가 늘었다 —
     그만큼 --sheet-h 도 키운다(128~168px → 168~224px). 영상이 그만큼 짧아지지만
     시트가 잘리는 것보다는 낫다. */
  /* ★ 정보 줄을 24 → 32px 로 키우면서 네 줄이 32px 더 먹는다. 25vh(812 화면에서
     203px)로는 내용 203 / 칸 203 이 되어 여백이 0 이 됐다 — 제목이 윗변에 붙던
     그 상태로 되돌아간다. 29vh 로 올려 숨 쉴 자리를 되돌린다(203 → 232).
     영상은 29px 짧아지지만(화면의 3.6%), 누를 수 있는 단추가 낫다. */
  /* ★★ 아래 검은 면(시트 + 띠)을 **3분의 1 줄이고, 그만큼 위에 검은 띠를 둔다.**
     812 화면 기준: 아래 232 + 45 = 277 → 173 + 12 = 185 (−92), 위 0 → 92.
     영상 높이는 그대로다 — 영상이 아래로 92px 내려와 화면 가운데 쪽에 앉는다.
     음량이 위 띠로 옮겨 가서 아래 띠(--bottom-frame)는 12px 만 남긴다. */
  .stage {
    --sheet-h: clamp(130px, 21.3vh, 173px);
    --top-frame: clamp(56px, 11.3vh, 96px);
    --bottom-frame: 12px;
  }
  /* ★ 카테고리를 바꿔도 **높이는 그대로**다.
     아래 영역은 정보든 연혁이든 센서든 같은 자리다 — 내용이 바뀔 때마다 판이 오르내리면
     그때마다 위의 영상 크기까지 흔들려 화면이 들썩인다. */
  .stage.sheet-tall { --sheet-h: clamp(130px, 21.3vh, 173px); }
  .toc { transform: none; height: var(--sheet-h); }

  /* ★ 영상 영역을 시트 **위까지**로 줄인다.
     시트는 떠 있는 판이라, 그냥 두면 영상은 화면 끝까지 잡아 늘여 놓고
     아래 3 분의 1 을 시트가 가린다 — 세로로 길게 크롭해 놓고 그 절반을 버리는 셈이다.
     자리를 실제로 비켜 주면 영상 자체가 짧아진다. 세로가 짧을수록 덜 어색하다. */
  /* ★ 시트와 **1px 겹치게** 둔다.
     둘의 경계는 소수점에서 만난다(18vh 는 146.16px 같은 값이다). 브라우저가 각자
     반올림하면 그 사이에 한 줄이 비고, 거기로 뒤가 비쳐 **얇은 띠**로 보인다.
     넉넉히 겹쳐 두면 그 줄이 생길 자리가 아예 없어진다 — 시트가 위에 있으니 가려진다.
     (1px 로는 모자랐다. 프레임 두께까지 각자 반올림되어 어긋남이 2px 을 넘는다.) */
  .band {
    margin-bottom: calc(var(--sheet-h) - 4px);
    transition: margin-bottom .34s cubic-bezier(.4,0,.16,1);
  }

  /* ★ 좁은 화면에는 원본 비율 모드가 없다(frame.js 참조) —
     세로로 맞추면 영상이 화면을 반도 못 채우고 회색 여백만 남는다.
     그래서 그 상태를 위한 규칙도 여기 두지 않는다. */

  /* ★ 연혁을 좌우로 훑는 것은 **목록을 보는 일**이지 넘기는 일이 아니다.
     한때 가운데 온 칸으로 자동으로 넘어가게 했는데, 상하 스와이프도 넘김이라
     같은 화면에서 두 방향이 모두 넘김이 되어 버렸다 — 어느 쪽이 무엇인지 알 수 없었다.
     지금은 넘김은 **상하 스와이프 하나뿐**이고, 연혁은 눌러야 그 편으로 간다.
     그래서 착 붙는 스냅도 쓰지 않는다(스냅은 "여기가 선택됐다"고 말하는 장치다). */
  .toc { scroll-snap-type: none; }
  /* 전역 `.toc.is-browse` 규칙이 특이도로 이기므로 여기서 다시 못박는다 */
  .toc.is-browse { height: var(--sheet-h); }

  /* ★ 좁은 화면에서는 글씨를 **영상 위에 올리지 않는다.**
     아래 영역이 넓으니 거기가 글씨 자리다 — 영상은 영상만 보이게 둔다. */
  .card { display: none; }
}

/* ★ 원본 비율로 보는 동안에는 넓든 좁든 **같은 그림**이다.
   좁은 화면이라고 위아래에 이웃을 걸어 두면, 몰입해서 보는 중에 판이 통째로 다른 배치가 된다.
   이 상태에서 이웃은 아예 참여하지 않는다 — 저희가 살던 방향(위아래)으로 닫혀 사라지고,
   남는 자리는 프레임이 대신 채운다. 다시 눌러 나오면 그때 폭에 맞는 배치로 돌아온다. */
.stage.is-narrow.is-contain .side {
  flex-basis: 0; height: 0; opacity: 0;
  transition: flex-basis .45s cubic-bezier(.4,0,.16,1), height .45s cubic-bezier(.4,0,.16,1),
              opacity .3s ease;
}

@media (prefers-reduced-motion: reduce) {
  .toc { transition: none !important; }
  .card, .c-more, .side .shot, .side .dim, .center .vid, .acts, .toc { transition: none !important; }
}

/* ── 좁은 화면: 음량은 위 검은 띠 왼쪽 ─────────────────────────────────
   frame.js(placeActs)가 좁은 화면에서 `.acts` 를 `.topbar` 로 옮겨 붙인다. */
@media (max-width: 860px), (hover: none) and (pointer: coarse) {
  .topbar .acts {
    position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
    height: 44px; margin: 0;
  }
  /* 모바일은 20% 작게 — 누를 자리 높이(44px)는 그대로 둔다 */
  .topbar .vol { width: 58px; }
  .topbar .vol svg { height: 16px; }
}

/* 모바일에서 달라지는 토큰은 여기 한 곳에서만 바꾼다 */
@media (max-width: 860px), (hover: none) and (pointer: coarse) {
  :root { --t-title: 30px; --t-head: 18px; }
}
