/* ============================================================================
   site.css — layers, components, routes
   Structure reproduces the measured skeleton in analysis/04-pages/home.json:
   a stack of five viewport-sized layers, not a document flow.
   ========================================================================== */

/* ---------------------------------------------------------------- 1. shell */

.wrap-all      { position: relative; width: 100%; height: 100vh; height: 100dvh; }
.wrap-sticky   { position: relative; width: 100%; height: 100%; }
.page-wrapper  { position: relative; width: 100%; height: 100%; }

/* Must stay free of transform/filter/opacity: the hero title blends against
   the filmstrip through this element's stacking context. */
.page-content {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  height: 100%;
}

/* ------------------------------------------------------------------ 2. nav */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 600;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: var(--s-2) var(--gutter);
  pointer-events: none;
  /* 어떤 이유로든 링크가 상자보다 넓어지면 밖으로 삐져나가 옆 패널을
     덮습니다. 크기 계산이 늦거나 실패해도 그 일만은 없게 잘라 둡니다. */
  overflow: hidden;
}
.nav > * { pointer-events: auto; }

/* 스크롤되는 라우트에서 본문이 고정 네비 아래로 지나가면 글자가 겹칩니다.
   이 시스템에는 elevation(그림자) 축이 없으므로, 측정된 어휘 그대로
   반투명 지면색 + backdrop-blur 로 가립니다. 세로 스크롤이 없는 홈에서는
   지나갈 것이 없으니 켜지 않습니다 — 상시 합성 레이어를 만들 이유가 없습니다. */
.nav::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(255, 255, 245, 0.82);
  backdrop-filter: blur(var(--blur-near));
  -webkit-backdrop-filter: blur(var(--blur-near));
  border-bottom: 1px solid var(--veil-dark-10);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-micro) var(--e-ease);
}
body.route-away .nav::before,
body.is-split .nav::before,
/* 세로 모드도 본문이 고정 네비 아래로 지나갑니다. */
body.is-vertical .nav::before { opacity: 1; }

.nav-logo {
  display: flex;
  align-items: baseline;
  gap: 0.12em;
  white-space: nowrap;
}
/* align-items: center, not baseline. A baseline-aligned flex item whose
   overflow is not visible has an unstable baseline across engines, and the
   tail below is overflow: hidden. Both halves are the same size and
   line-height, so centring and baselines coincide anyway. */
.nav-logo .logo-word { display: inline-flex; align-items: center; gap: 0.34em; }
.nav-logo .logo-group { display: inline-flex; align-items: center; }
/* 로고도 나머지 네비와 같은 폰트·자간·굵기입니다. 크기만 링크와 맞춥니다.
   (이전에는 .nav-logo 1.6em 위에 자식도 1.6em 이라 2.56em 으로 곱해졌고,
   .is-fk 가 자간을 0 으로 덮어 혼자 다른 조판이었습니다.) */
/* 분할이 열리면 네비가 쓸 수 있는 폭이 절반이 됩니다. 히어로 워드마크가
   그 폭을 따라 줄어드는 것처럼, 네비도 같이 줄어야 한 몸으로 반응하는
   것으로 읽힙니다. 배율은 app.js 의 fitNav() 가 잽니다 — 14px 바닥은
   거기서 지킵니다. */
.nav-logo,
.nav-links-wrap .nav-link,
.ig-link {
  font-size: calc(var(--t-nav) * var(--nav-fit, 1));
  transition: font-size var(--d-hero) var(--e-quart);
}
.nav-logo { letter-spacing: var(--ls-caps); }
.nav-logo .logo-mark,
.nav-logo .logo-tail {
  font-family: inherit;
  font-size: 1em;
  font-weight: inherit;
  letter-spacing: inherit;
  line-height: 1;
}

/* Word-mark expansion: the tail words are clipped to zero width and slide in
   from the left. [derived: GSAP width:auto 0.4s power2.out, then x -100% +
   opacity 0 to 1 0.5s power4.out] */
.nav-logo .logo-tail {
  display: inline-flex;
  overflow: hidden;
  /* min-width: auto is the flex-item default and it BEATS max-width. Without
     this the tail never collapses, and the -100% slide below then lands the
     tail on top of the initial. */
  min-width: 0;
  max-width: 0;
  opacity: 0;
  transition:
    max-width var(--d-quick) var(--e-quad-out),
    opacity var(--d-roll) var(--e-quart-out);
}
.nav-logo .logo-tail > span {
  display: inline-block;
  transform: translateX(-100%);
  transition: transform var(--d-roll) var(--e-quart-out);
}
/* 늘 펼쳐 둡니다. 접힌 "F S" 는 무엇의 약자인지 알 수 없고, 호버가
   없는 화면에서는 펼칠 방법도 없습니다. 슬라이드는 첫 등장에만 씁니다. */
.nav-logo .logo-tail { max-width: 12em; opacity: 1; }
.nav-logo .logo-tail > span { transform: translateX(0); }

/* 상단 메뉴는 .text-h4 를 쓰지만 여기서만 2배로 키웁니다. */
.nav-links-wrap { display: flex; align-items: center; gap: var(--s-25); }


/* Word roll: each link holds two stacked copies of its label; hovering rolls
   the pair up by 100%. [derived: yPercent -100/+100, 0.5s power2.inOut,
   stagger {amount: 0.1} distributed across the letters] */
.nav-link {
  position: relative;
  display: block;
  overflow: hidden;
  height: 1.2em;
  line-height: 1.2em;
}
.nav-link .word-track { display: flex; }
.nav-link .word {
  display: inline-block;
  transform: translateY(0);
  transition: transform var(--d-roll) var(--e-quad-io);
  transition-delay: var(--roll-delay, 0s);
}
.nav-link .word.is-space { width: 0.32em; }
.nav-link .word-face--in {
  position: absolute;
  inset: 0;
  display: flex;
}
.nav-link .word-face--in .word { transform: translateY(100%); }
.nav-link:hover .word-face--out .word,
.nav-link:focus-visible .word-face--out .word { transform: translateY(-100%); }
.nav-link:hover .word-face--in .word,
.nav-link:focus-visible .word-face--in .word { transform: translateY(0); }

.nav-link[aria-current="page"] .word-face--out .word {
  text-decoration: underline;
  text-underline-offset: 0.3em;
}

.ig-link { opacity: 1; transition: opacity var(--d-micro) var(--e-ease); }
.ig-link:hover { opacity: 0.6; }

/* ------------------------------------------------------- 3. burger + menu */

.burger {
  display: none;
  position: relative;
  width: 2.6em;
  height: 2em;
  flex-direction: column;
  justify-content: center;
  gap: 0.55em;
}
.burger-line {
  display: block;
  width: 100%;
  height: 0.17em;
  background: var(--page-fg);
  transition:
    transform var(--d-quick) var(--e-quart),
    margin var(--d-quick) var(--e-quart),
    opacity 0.01s linear var(--d-quick);
}
/* 메뉴는 검정 전면이라, 상아색 네비 띠가 그대로 남으면 상단만 회색
   판때기로 떠 보입니다. 띠를 끄고 로고와 햄버거를 반전색으로 넘깁니다. */
/* body 를 붙여야 합니다 — 위쪽 body.is-vertical/.is-split 규칙이 타입
   선택자 하나만큼 특이도가 높아, 클래스만으로는 지지 않고 밀립니다. */
body.menu-open .nav::before { opacity: 0; }
.menu-open .nav-logo { color: var(--invert-fg); }
/* 메뉴가 패널을 덮고 있는 동안 프로젝트 "닫기" 알약까지 남으면 닫기
   버튼이 둘로 보입니다. 뒤에 가려진 쪽을 감춥니다.
   (.cs-close.is-active 가 클래스 둘이라 body 를 붙여 특이도를 맞춥니다) */
body.menu-open .cs-close { opacity: 0; pointer-events: none; }
.menu-open .burger-line { background: var(--invert-fg); }

/* burger to X, the measured 1.4s master sequence collapsed onto class state */
.menu-open .burger-line.top    { transform: translateY(0.72em) rotate(45deg); }
.menu-open .burger-line.bottom { transform: translateY(-0.72em) rotate(-45deg); }
.menu-open .burger-line.middle { opacity: 0; transition-delay: 0s; }

.menu-mobile-wrap {
  position: fixed;
  inset: 0;
  z-index: 550;
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 0.2em;
  padding: var(--s-8) var(--gutter) var(--s-4);
  background: var(--invert-bg);
  color: var(--invert-fg);
  opacity: 0;
  transition: opacity var(--d-micro) var(--e-quart);
}
.menu-mobile-wrap.is-mounted { display: flex; }
.menu-open .menu-mobile-wrap { opacity: 1; }

/* 가장 긴 라벨(SHOWREEL)이 좁은 화면에서 넘쳐 잘립니다. overflow:hidden 은
   등장 연출용이라 넘친 글자를 조용히 먹어 버립니다. 폭에 상한을 겁니다. */
.is-menu-link {
  display: flex;
  align-items: baseline;
  gap: 0.42em;
  overflow: hidden;
}
/* 번호는 라벨을 밀어내지 않도록 자리를 고정합니다 — 01 과 06 의 폭이
   달라 줄마다 라벨 시작점이 어긋나면 목록으로 안 읽힙니다. */
.menu-num {
  flex: none;
  width: 1.6em;
  font-size: 0.3em;
  letter-spacing: var(--ls-caps);
  opacity: 0.45;
}
.menu-label { display: block; }
/* 번호 칸(0.48em)과 사이 여백(0.42em)이 라벨 몫을 약 0.9em 가져가므로
   상한도 그만큼 내려야 가장 긴 SHOWREEL 이 다시 잘리지 않습니다. */
.menu-mobile-wrap .is-menu-link { font-size: min(var(--t-display-m), 12.5vw); }
.is-menu-link > span {
  display: block;
  transform: translateY(100%);
  opacity: 0;
  transition:
    transform var(--d-quick) var(--e-cubic-out),
    opacity var(--d-quick) var(--e-cubic-out);
  transition-delay: var(--stagger-delay, 0s);
}
.menu-open .is-menu-link > span { transform: translateY(0); opacity: 1; }

.ig-link-menu {
  margin-top: var(--s-4);
  opacity: 0;
  transition: opacity var(--d-quick) var(--e-cubic-out);
  transition-delay: 0.5s;
}
.menu-open .ig-link-menu { opacity: 1; }

/* ----------------------------------------------------------------- 4. hero */

.section.is-hero {
  position: relative;
  z-index: var(--z-hero);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  transform: translateY(0);
  pointer-events: none;
}
/* body-level state machine, exactly as measured */
/* 하단바가 열려 있을 때에만 히어로가 그만큼 올라갑니다. 닫힌 화면에서
   미리 올라가 있으면 아무것도 없는 아래쪽을 비워 두고 글자만 떠 보입니다. */
body.timelineUp.strip-open .section.is-hero { transform: translateY(var(--hero-lift)); }
/* Only genuinely interactive hero children take pointer events. The display
   title is enormous and sits at z-index 100 directly over the filmstrip at
   z-index 10 — leaving it clickable swallows every wheel and drag aimed at
   the strip, which reads as "nothing on this site moves". */
.section.is-hero .filters-form { pointer-events: auto; }
.big-title-wrap,
.hero-subline { pointer-events: none; }
.big-title-wrap a,
.hero-subline a { pointer-events: auto; }

/* --- hero film band ------------------------------------------------------
   Full-bleed strip behind the word-mark. The reference has no video anywhere;
   this is an addition, and it is deliberately confined to the title band so
   the rest of the ivory system is untouched. */
.hero-media {
  position: absolute;
  top: -7%;
  bottom: -7%;
  left: 50%;
  width: 100vw;
  margin-left: -50vw;
  overflow: hidden;
  background: var(--invert-bg);
  z-index: 0;
}
.hero-media video,
.hero-fallback {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero-fallback {
  background-image: url("../img/hero-poster.svg");
  background-size: cover;
  background-position: center;
  opacity: 0.85;
  transform: scale(1.08);
  animation: heroDrift 26s ease-in-out infinite alternate;
}
@keyframes heroDrift {
  from { transform: scale(1.08) translate3d(-1.6%, 0.6%, 0); }
  to   { transform: scale(1.08) translate3d(1.6%, -0.6%, 0); }
}
/* A single slow accent sweep. The only gradient in the build — the measured
   system has zero, so it is scoped to this band alone. */
.hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 42%, rgba(219, 255, 0, 0.10) 50%, transparent 58%);
  background-size: 260% 100%;
  animation: heroSweep 9s linear infinite;
  pointer-events: none;
}
@keyframes heroSweep {
  from { background-position: 130% 0; }
  to   { background-position: -130% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-fallback { animation: none; }
  .hero-media::after { animation: none; background: none; }
}

/* 스크램블 중인 글자만 라임. 확정된 글자는 원래 색으로 돌아옵니다. */
.scr-on { color: var(--vivid-lime); }
.scr-on.v1 { color: var(--vivid-orange); }
.scr-on.v2 { color: var(--vivid-cyan); }
.scr-on.v3 { color: var(--vivid-magenta); }

/* The word-mark rides on top of the band, so it takes the inverted colour. */
.big-title-wrap.is-home .big-title { position: relative; z-index: 1; }
.no-tr, .no-tr * { transition: none !important; }

/* The reference carries exactly one mix-blend-mode: difference node and the
   measurement never identified which one (analysis lists it as inferred).
   Not guessed at here: blending is bounded by the nearest stacking-context
   ancestor, and the hero sits inside one (z-index 100), so a white
   difference-blended title would composite against an empty group and come
   out white on ivory — invisible. The inversion device is used where it
   cannot fail instead: at layer scale, on the project overlay. */
.big-title.is-home-title {
  color: var(--page-fg);
  text-align: center;
  white-space: nowrap;
  padding: 0 0.06em;
}
/* Only when an actual film is configured does the band appear and the
   word-mark flip to the inverted colour. Default is the ivory page. */
body.has-hero-video .big-title.is-home-title { color: var(--invert-fg); }
.hero-media { display: none; }
body.has-hero-video .hero-media { display: block; }

/* Tagline sits directly above the word-mark, centred, not pinned to a
   corner — it reads as one typographic block with the title. */
.hero-subline {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6em;
  margin-bottom: 0.4em;
  text-align: center;
}

/* ------------------------------------------------------------- 5. filters */

.filters-form {
  margin-top: 3.2em;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.25em;
}
.filter-btn {
  display: inline-flex;
  align-items: center;
  padding: var(--s-025) 0.9em;
  border-radius: 1em;
  background-color: transparent;
  color: var(--page-fg);
  transition:
    background-color var(--d-micro) var(--e-ease),
    color var(--d-micro) var(--e-ease);
}
.filter-btn:hover { background-color: var(--veil-dark-10); }

/* Selection state was not measurable: it lives in a runtime-generated
   style#dynamic-filter-css. Designed here from the system's own vocabulary,
   inversion for the state and the reserved --yellow for the marker.
   [filled: analysis gap G10] */
.filter-btn.is-selected {
  background-color: var(--invert-bg);
  color: var(--invert-fg);
}
.filter-btn.is-selected .filter-txt::before {
  content: "";
  display: inline-block;
  width: 0.45em;
  height: 0.45em;
  margin-right: 0.5em;
  border-radius: var(--radius-round);
  background: var(--yellow);
  vertical-align: 0.08em;
}
.filter-txt { font-size: max(var(--t-min), 0.95em); }

.filter-btn.is-project.is-contact { background-color: var(--ink-soft); color: var(--ivory); }
.filter-btn.is-project.is-contact:hover { background-color: var(--ink-softer); }
.filter-btn.is-project.is-contact.is-selected:hover { background-color: var(--ivory); color: var(--black); }

/* ---------------------------------------------------------- 6. filmstrip */

/* Bottom-anchored and exactly as tall as its content. Giving the strip a
   viewport-fraction height leaves the cards floating in the middle of the
   page; the cards belong on the bottom edge, sitting on the tick measure. */
.timeline--wrapper {
  /* 확장의 세 축을 한자리에 둡니다. --expand 가 걸리는 요소가 여기라,
     아래 규칙들이 모두 이 값을 상속받아 같이 움직입니다.
     타일 폭은 4열 기준입니다 — 하단바 카드(413)와 크게 다르지 않으므로
     "커졌다"는 폭이 아니라 높이가 만듭니다: 16:10 에서 4:5 로 가면서
     판이 2.2배 넓어지고, 동시에 흐림이 걷힙니다("켜지면서"). */
  --grid-gap: var(--s-25);
  --tile-w: calc((min(100vw - var(--gutter) * 2, 1440px) - var(--grid-gap) * 3) / 4);
  --card-w: calc(var(--slide-w) + (var(--tile-w) - var(--slide-w)) * var(--expand, 0));
  --plate-ar: calc(1.6 - 0.933 * var(--expand, 0));   /* 16:10 -> 2:3, 판 넓이 2.9배 */
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  z-index: var(--z-timeline);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* The top year rail is retired: the reference puts the index number directly
   above each card instead, and a second rail duplicated that information.
   Kept in the DOM so the scrubbing code keeps one API. */
.timeline-progress-top { display: none; }
.rail-track { position: absolute; inset: 0; will-change: transform; }
.rail-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 0.6em;
  padding-left: 0.6em;
  border-left: 1px solid var(--page-fg);
  white-space: nowrap;
}
.rail-tick.is-minor { border-left-color: var(--veil-dark-10); }
.rail-tick .rail-year { font-weight: var(--w-black); }
.rail-tick .rail-count { opacity: 0.45; }

/* The playhead does not move; the years move under it. It marks where the
   strip is being read, which is what makes the axis legible as time. */
.rail-playhead {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--yellow);
  mix-blend-mode: multiply;
  pointer-events: none;
}

/* Height is solved from the card geometry, so changing --slide-w keeps the
   strip correct without a second number to remember:
   label 1.5em + its 0.55em margin + 0.4em top pad + 0.6em bottom breathing
   + the plate, which is (slide - side padding) at 16:10. */
.timeline-coll-wrapper {
  position: relative;
  flex: none;
  height: calc(3.05em + (var(--card-w) - 1.2em) / var(--plate-ar));
  overflow: hidden;
  cursor: grab;
  touch-action: pan-y;
}
.timeline-coll-wrapper.is-dragging { cursor: grabbing; }

.timeline-coll-list {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: flex-end;
  will-change: transform;
}

.timeline-coll-item {
  position: relative;
  flex: 0 0 auto;
  width: var(--slide-w);
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-top: 0.4em;
  padding-bottom: 0.6em;
  transition: opacity var(--d-micro) var(--e-ease);
}

/* The reference sizes thumbnails through `padding`, which leaves the hovered
   card a different size from its neighbours. Per brief the cards are uniform
   here: fixed padding, one plate size, one shared baseline. Hover is carried
   by an outline instead, so nothing in the row changes size or shifts. */
.timeline-thumbnail-wrap {
  position: relative;
  width: 100%;
  padding: 0 0.6em;
}

.thumb-plate {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: var(--plate-ar, 1.6);
  overflow: hidden;
  border-radius: var(--radius);
  background-color: var(--film);
  /* backdrop-filter deliberately NOT applied here. The plate is always fully
     covered by an opaque capture, so the blur can never be seen — but it
     forces an extra composited layer and costs raster quality on the very
     image it sits under. The token is still used on translucent surfaces
     where it does something. */
  /* No border on the card, by request. The active state is carried by the
     label swap above the card and by the plate's own opacity. */
  outline: none;
  /* 펼쳐질수록 밝아집니다 — 요청하신 "점차 켜지면서". */
  opacity: calc(0.82 + 0.18 * var(--expand, 0));
  transition: opacity var(--d-pad) var(--e-pad);
}
.timeline-coll-item:hover .thumb-plate,
.timeline-coll-item.is-active .thumb-plate,
.timeline-coll-item:focus-within .thumb-plate { opacity: 1; }
.thumb-plate img { width: 100%; height: 100%; object-fit: cover; }
/* 정지 카드 — 세로 목록에서 쓰는 한 장. 맨 위를 보여 줍니다. */
.thumb-plate picture { position: absolute; inset: 0; display: block; }
.card-still {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* --- captured scroll sequence -------------------------------------------
   Two stacked copies of one full-page capture, translated by exactly -50%
   (one image height) at constant speed. When the loop restarts, copy two is
   already sitting where copy one began, so the wrap is invisible: the motion
   never stops, never reverses, and never pauses.

   `linear` is deliberate. Any ease has to decelerate to zero at the end of
   the cycle, and at the loop point that reads as a dead gap. */
.thumb-seq { position: absolute; inset: 0; overflow: hidden; }
.seq-reel {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  animation: seqScroll var(--seq-dur, 12s) linear infinite;
  will-change: transform;
}
.seq-shot {
  display: block;
  width: 100%;
  height: auto;
}
@keyframes seqScroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -50%, 0); }
}
/* Hovering holds the scroll so the card can be read before it is clicked. */
.timeline-coll-item:hover .seq-reel,
.timeline-coll-item:focus-within .seq-reel { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .seq-reel { animation: none; }
}

/* Live projects get a marker: this card is a real, reachable site. */
.timeline-coll-item[data-live] .thumb-plate::after {
  content: "LIVE";
  position: absolute;
  right: 0.5em;
  top: 0.5em;
  z-index: 2;
  padding: 0.15em 0.5em;
  border-radius: 1em;
  background: var(--yellow);
  color: var(--black);
  font-size: var(--t-xsmall);
  font-weight: var(--w-black);
  letter-spacing: var(--ls-caps);
}

.video-embed {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--d-micro) var(--e-ease);
}
.video-embed video { width: 100%; height: 100%; object-fit: cover; }
.embed-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.35em;
  width: 100%;
  height: 100%;
  padding: 0.8em;
  background: var(--invert-bg);
  color: var(--invert-fg);
  text-align: left;
}
.embed-card .p-xsmall { opacity: 0.55; }

/* Conditional hover: preview only reveals once a filter is engaged. Easy to
   miss when reproducing, the rule is scoped to .is-filtered. [derived] */
.timeline--wrapper.is-filtered .timeline-coll-item:hover .video-embed,
.timeline--wrapper.is-filtered .timeline-coll-item:focus-within .video-embed { opacity: 1; }

.timeline-coll-item.is-dimmed { opacity: 0.28; }

/* Slide label rides a `top` transition inside a clipped strip, above the
   card. Number at rest, project title when the card is active — one rail,
   two states. [derived: 0.45s easeInOutQuart on `top`]
   Inset matches .timeline-thumbnail-wrap so the label is flush with the
   card's left edge. */
.timeline-item-meta {
  position: relative;
  height: 1.5em;
  margin-bottom: 0.55em;
  overflow: hidden;
  padding: 0 0.6em;
}
.p-small.is-number,
.p-small.is-title {
  position: absolute;
  left: 0.6em;
  right: 0.6em;
  transition: top var(--d-rail) var(--e-quart);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.p-small.is-number { top: 0; font-weight: var(--w-medium); opacity: 0.55; }
.p-small.is-title  { top: 100%; font-weight: var(--w-black); }
.timeline-coll-item:hover .p-small.is-number,
.timeline-coll-item.is-active .p-small.is-number,
.timeline-coll-item:focus-within .p-small.is-number { top: -100%; }
.timeline-coll-item:hover .p-small.is-title,
.timeline-coll-item.is-active .p-small.is-title,
.timeline-coll-item:focus-within .p-small.is-title { top: 0; }

/* Perforation grid under a hard rule — the measure the cards sit on. Doubles
   as a scrubber. Two densities: a heavy tick per card, light ticks per
   quarter card. */
.timeline-bottom-frames {
  position: relative;
  flex: none;
  height: 1.7em;
  border-top: 1.5px solid var(--page-fg);
  cursor: ew-resize;
  overflow: hidden;
  transition: background-color var(--d-micro) var(--e-ease);
}
.timeline-bottom-frames:hover { background-color: var(--veil-dark-04); }
.frames-track {
  position: absolute;
  inset: 0;
  background-image:
    repeating-linear-gradient(to right, var(--page-fg) 0 1px, transparent 1px var(--slide-w)),
    repeating-linear-gradient(to right, var(--veil-dark-10) 0 1px, transparent 1px calc(var(--slide-w) / 4));
  background-size: 100% 0.75em, 100% 100%;
  background-repeat: repeat-x, repeat-x;
  background-position: 0 0, 0 0;
  will-change: transform;
}
.frames-window {
  position: absolute;
  bottom: 0;
  height: 0.2em;
  background: var(--page-fg);
  pointer-events: none;
}


/* --------------------------------------------------------- 7. route layers */

.other-pages-content {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  visibility: hidden;
  transition: visibility 0s linear var(--d-curtain);
}
.other-pages-content.is-active { visibility: visible; transition-delay: 0s; }

/* Project detail takes the right half and slides in; the timeline keeps
   running in the left half rather than being replaced. The split ratio is one
   token so it can be retuned in a single place. */
:root { --split: 50%; }

/* 프로젝트 화면도 패널과 같은 커튼으로 엽니다 — 가운데에서 위아래로.
   예전에는 오른쪽에서 밀려 들어왔는데, 사이트 안에서 화면이 바뀌는 방식이
   두 가지면 같은 동작으로 읽히지 않습니다. 하나로 맞춥니다. */
.cs-page-wrapper {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: var(--split);
  z-index: var(--z-detail);
  display: flex;
  visibility: hidden;
  clip-path: inset(50% 0 50% 0);
  transition:
    clip-path var(--d-curtain) var(--e-cubic-io),
    left var(--d-hero) var(--e-quart),
    visibility 0s linear var(--d-curtain);
}
.cs-page-wrapper.is-active {
  visibility: visible;
  clip-path: inset(0 0 0 0);
  transition-delay: 0s;
}
@media (prefers-reduced-motion: reduce) {
  .cs-page-wrapper { transition-duration: 0.01s; }
}
/* The seam. A hairline, not a shadow: this system has no elevation axis. */
.cs-page-wrapper::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--veil-light);
}

/* Static routes are one centred column under a display-sized word-mark —
   not a left-aligned document page. [confirmed from the screen recording] */
/* 패널은 showreel 과 같은 방식으로 열립니다: 가운데에서 위아래로 벌어지는
   커튼. reel 은 마스크의 height 를 0 -> 100% 로 키우는데, 여기서는 같은
   곡선을 clip-path 로 냅니다 — 레이아웃을 다시 잡지 않으므로 열리는 동안
   글자가 흔들리지 않습니다.
   커튼을 컨테이너가 아니라 패널마다 두는 이유: 컨테이너에 두면 About 에서
   Services 로 옮길 때처럼 패널끼리 바뀌는 경우에 커튼이 이미 열려 있어
   아무 일도 일어나지 않습니다. display:none 대신 겹쳐 두고 클립으로
   여닫아야 전환이 실제로 굴러갑니다. */
.page-panel {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--page-bg);
  visibility: hidden;
  clip-path: inset(50% 0 50% 0);
  transition:
    clip-path var(--d-curtain) var(--e-cubic-io),
    visibility 0s linear var(--d-curtain);
}
.page-panel.is-active {
  visibility: visible;
  clip-path: inset(0 0 0 0);
  transition-delay: 0s;
  z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
  .page-panel { transition-duration: 0.01s; }
}

.panel-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100%;
  padding: 8.5em var(--gutter) var(--s-4);
  text-align: center;
}
.panel-eyebrow { display: block; margin-bottom: 0.1em; opacity: 0.75; }
.panel-title { line-height: 0.85; margin-bottom: 0.5em; }
.panel-body { max-width: 60em; }
/* 본문은 .text-p-allcaps 를 쓰는데, 그 클래스는 라틴 캡스용입니다. 한글에
   그대로 걸리면 세 가지가 겹쳐 글자가 뭉개집니다:
     - weight 600 은 한글에서 상당히 두꺼운 세미볼드입니다. 작은 크기에서
       획 사이가 메워져 "깨진" 것처럼 보입니다.
     - 0.03em 대문자 트래킹은 한글 자간을 벌려 리듬을 무너뜨립니다.
     - uppercase 는 한글엔 무효지만 Next.js 를 NEXT.JS 로 바꿔 놓습니다.
   본문에서는 셋 다 풉니다. */
.panel-body .text-p-allcaps {
  /* 16 -> 20px, 두 단계. 읽는 글이므로 줄 사이도 함께 벌립니다 —
     글자만 키우면 줄이 붙어 오히려 읽기 어려워집니다. */
  font-size: 20px;
  font-weight: var(--w-regular);
  letter-spacing: 0;
  text-transform: none;
}
.panel-lede { margin-bottom: 2em; line-height: 1.65; }
.panel-block { margin-bottom: 1.8em; }
.panel-block h2 { margin-bottom: 0.4em; }
/* 불투명도로 낮추면 획까지 같이 얇아져 작은 글자가 흐려집니다. 색으로. */
.panel-block p { line-height: 1.7; opacity: 1; color: var(--ink-soft); }
.panel-list li { padding: 0.5em 0; }

/* 한 줄에 두 칸이 들어가면 0.55em 짜리 gap 으로는 붙어 보입니다. 가로는
   넉넉히, 세로는 라벨과 붙지 않을 만큼만. */
.field-row { display: grid; gap: 0.5em 0.9em; margin-bottom: var(--s-15); }
.field {
  font-size: var(--t-h6);
  text-transform: uppercase;
  letter-spacing: var(--ls-caps);
  padding: 0.9em 1em;
  border: 1px solid var(--veil-dark-10);
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font-family: inherit;
}
.field:focus-visible { border-color: var(--page-fg); }
.form-status { margin-top: var(--s-1); min-height: 1.4em; }
.contact-card {
  max-width: 34em;
  margin-bottom: var(--s-35);
  border-top: 1px solid var(--veil-dark-10);
}
.contact-row {
  display: flex;
  align-items: baseline;
  gap: var(--s-15);
  padding: var(--s-1) 0;
  border-bottom: 1px solid var(--veil-dark-10);
}
.contact-label { flex: 0 0 4.5em; opacity: 0.5; }
.contact-value { font-weight: var(--w-medium); }
a.contact-value { transition: opacity var(--d-micro) var(--e-ease); }
a.contact-value:hover { opacity: 0.6; }

.filters-form-contact { max-width: 34em; margin-top: var(--s-2); }
.filters-form-contact .field-row label { display: block; opacity: 0.5; }

/* contentinfo, which the reference has no equivalent of. Parked in the
   perforation strip so it costs the layout nothing. */
.site-footer {
  position: fixed;
  left: var(--gutter);
  bottom: calc(var(--strip-block) + 2.4em);
  z-index: 11;
  opacity: 0.4;
  pointer-events: none;
}
body.route-away .site-footer { opacity: 0; }
.site-footer { transition: opacity var(--d-quick) var(--e-quart); }

/* Case-study overlay is dark: the inversion pair, applied at layer scale. */
.cs-page-wrapper { background: var(--invert-bg); color: var(--invert-fg); }
.cs-inner {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.big-title-wrap.is-project { display: flex; flex-direction: column; align-items: center; gap: 0.3em; }
.big-title-wrap.is-project .big-title { font-size: 2.2em; line-height: 0.9; }

/* Media block fills whatever is left. */
.cs-media {
  position: relative;
  flex: 1 1 auto;
  min-height: 16em;
  overflow: hidden;
  background: var(--ink-soft);
}
.cs-media img { width: 100%; height: 100%; object-fit: cover; }
.cs-frame { width: 100%; height: 100%; border: 0; display: block; background: var(--white); }
.cs-open {
  position: absolute;
  right: var(--s-1);
  bottom: var(--s-1);
  padding: 0.4em 0.9em;
  border-radius: 1em;
  background: var(--yellow);
  color: var(--black);
  transition: opacity var(--d-micro) var(--e-ease);
}
.cs-open:hover { opacity: 0.75; }

.cs-still {
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--ink-soft);
  aspect-ratio: 16 / 9;
}
.cs-still img { width: 100%; height: 100%; object-fit: cover; }
.cs-note { margin-top: var(--s-2); max-width: 42em; }


.cs-close {
  position: fixed;
  top: var(--s-2);
  right: var(--gutter);
  z-index: 640;   /* 네비(600)보다 위 — 절대 가려지지 않게 */
  display: none;
  padding: var(--s-05) var(--s-1);
  border-radius: 1em;
  background: var(--veil-light);
  color: var(--invert-fg);
  transition: opacity var(--d-micro) var(--e-ease);
}
.cs-close.is-active { display: block; }
.cs-close:hover { opacity: 0.6; }


/* A full-bleed panel route (about/services/contact) hides the home layers.
   A project route does not — it splits the screen and the timeline keeps
   running on the left. Two different states, so two different classes. */
.timeline--wrapper,
.section.is-hero {
  transition:
    opacity var(--d-quick) var(--e-quart),
    right var(--d-hero) var(--e-quart),
    width var(--d-hero) var(--e-quart),
    clip-path var(--d-curtain) var(--e-cubic-io),
    transform var(--d-hero) var(--e-quart);
}
/* 커튼이 여닫으므로 페이드는 쓰지 않습니다. 겹치면 둘 다 흐려 보입니다. */
body.route-away .timeline--wrapper,
body.route-away .section.is-hero { pointer-events: none; }

/* --- split state --- */
body.is-split .timeline--wrapper { right: var(--split); }
body.is-split .section.is-hero { width: var(--split); }
body.is-split .nav { right: var(--split); }
body.is-split .site-footer { opacity: 0; }
/* Half the bar cannot hold five links plus the social link at full spacing;
   the social link is the one that can go, and the gap tightens. */
body.is-split .ig-link { display: none; }
/* 절반 폭에서는 gap 이 예산을 가장 많이 먹습니다. 다섯 칸을 좁혀야
   여섯 링크가 16px 바닥 위에서 들어갑니다. */
body.is-split .nav-links-wrap { gap: var(--s-1); }
body.is-split .nav { padding-left: var(--s-15); padding-right: var(--s-15); }
/* The display tier cannot stay at 19em in half a viewport. Overriding the
   token rescales the title and everything keyed to it in one move. */
body.is-split { --t-display-xl: 7em; }
body.is-split .filters-form { opacity: 0; pointer-events: none; }
.filters-form { transition: opacity var(--d-quick) var(--e-quart); }

/* ------------------------------------------------------------ 8. reel */

.reel-wrap {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  /* Held hidden while closed so the controls inside the zero-height mask stay
     out of the tab order. Kept visible through the 3s collapse on the way
     out via the delayed visibility step. */
  visibility: hidden;
  transition: visibility 0s linear var(--d-reel);
}
.reel-wrap.is-open { pointer-events: auto; visibility: visible; transition-delay: 0s; }

/* The wipe is overflow + height, not clip-path or mask. [derived] */
.reel-mask {
  position: relative;
  width: 100%;
  height: 0;
  overflow: hidden;
  background: var(--invert-bg);
  transition: height var(--d-reel) var(--e-cubic-io);
}
.reel-wrap.is-open .reel-mask { height: 100%; }
.reel-stage {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: var(--s-1);
  color: var(--invert-fg);
  text-align: center;
  padding: var(--gutter);
}
.reel-stage video { width: 100%; height: 100%; object-fit: cover; }
.main-vid-prog-wrap {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  bottom: var(--s-2);
  display: flex;
  align-items: center;
  gap: var(--s-15);
  opacity: 0;
  transition: opacity var(--d-pad) var(--e-cubic-io);
  transition-delay: var(--d-reel);
}
.reel-wrap.is-open .main-vid-prog-wrap { opacity: 1; }
.main-vid-prog-bar { flex: 1 1 auto; height: 2px; background: var(--veil-light); }
.main-vid-prog-fill { display: block; height: 100%; width: 0; background: var(--ivory); }
.fullscreen-btn, .muted-btn, .close-content {
  color: var(--invert-fg);
  opacity: 1;
  transition: opacity var(--d-micro) var(--e-ease);
}
.fullscreen-btn:hover, .muted-btn:hover, .close-content:hover { opacity: 0.6; }
.close-content {
  position: absolute;
  top: var(--s-2);
  right: var(--gutter);
  opacity: 0;
  transition: opacity var(--d-pad) var(--e-cubic-io);
  transition-delay: var(--d-reel);
}
.reel-wrap.is-open .close-content { opacity: 1; }

/* ----------------------------------------------------------- 9. loader */

.loading-wrap {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--page-bg);
  transition: opacity var(--d-quick) var(--e-quart), visibility 0s linear var(--d-quick);
}
.loading-wrap.is-done { opacity: 0; visibility: hidden; }
.loader-circle-img {
  width: 3em;
  height: 3em;
  border: 2px solid var(--veil-dark-10);
  border-top-color: var(--page-fg);
  border-radius: var(--radius-round);
  animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@media (hover: none) { .loading-wrap { background-color: rgb(255, 255, 245); } }

/* --------------------------------------------------------- 10. responsive */

/* 연락 페이지에서는 연락 FAB 이 중복이고 폼을 가립니다. */
body[data-route="contact"] .contact-fab,
body[data-route="inquiry"] .contact-fab { display: none; }

@media screen and (max-width: 991px) {
  .nav-links-wrap { display: none; }
  .burger { display: flex; }
  /* 링크 목록이 버거로 들어가면 남은 셋이 space-between 으로 퍼지면서
     인스타그램이 네비 한가운데 떠 메뉴 항목처럼 보입니다. 버거 메뉴 안에
     이미 같은 링크가 있으므로 상단에서는 내립니다. */
  .ig-link { display: none; }
  .timeline-bottom-frames { height: 1.5em; }
}

@media screen and (max-width: 767px) {
  /* --- 모바일 조판 --- */
  /* 최상단 바 높이를 한 곳에서 정합니다. 손가락 타깃 44px 은 줄일 수
     없으므로 위아래 여백만 깎습니다: 24+44+24=92 -> 10+44+10=64px (-30%).
     아래의 패널 여백과 닫기 위치가 이 값을 따라갑니다. */
  :root { --nav-pad: 0.625rem; --nav-h: calc(var(--nav-pad) * 2 + 44px); }
  .nav { padding: var(--nav-pad) var(--gutter); }
  .section.is-hero { padding: 0 var(--gutter); }
  /* 태그라인이 타이틀만큼 넓어지면 위계가 무너집니다. 한 급 낮춥니다. */
  .hero-subline { font-size: var(--t-allcaps); flex-wrap: wrap; row-gap: 0.15em; }
  .filters-form { margin-top: 2.2em; gap: 0.35em; }

  /* --- 터치 타깃 ---
     손가락으로 누르는 것은 44px 이 기준입니다. 보이는 크기를 키우는 대신
     눌리는 영역만 넓히면 조판이 흔들리지 않습니다. */
  .filter-btn { padding: 0.62em 0.9em; }
  .field { padding: 0.85em 1em; }
  .inq-add { padding: 0.62em 1em; min-height: 44px; }
  .contact-fab { padding: 0.72em 1.1em; min-height: 44px; }
  .nav-logo { min-height: 44px; align-items: center; }
  .burger { width: 44px; height: 44px; }
  .inq-check { min-height: 44px; }
  .inq-check input { width: 1.35em; height: 1.35em; }
  a.contact-value { display: inline-flex; align-items: center; min-height: 44px; }

  /* --- 겹침 여유 --- */
  .contact-fab { bottom: calc(var(--strip-block) + 2.4em); }
  body.route-away .contact-fab { bottom: var(--s-15); }
  /* 떠 있는 버튼 아래로 마지막 내용이 가리지 않도록 */
  .page-panel { padding-bottom: 7em; }
  .inq-pick { padding-bottom: var(--s-15); }
  .inq-section { margin-top: var(--s-25); }

  /* A half-and-half split is unreadable on a phone. The project panel takes
     the whole screen instead, and the home layers step back the way they do
     for a panel route. Same state class, different geometry. */
  :root { --split: 0%; }
  body.is-split .timeline--wrapper,
  body.is-split .section.is-hero { right: 0; opacity: 0; pointer-events: none; }
  /* 네비는 남깁니다. 패널이 화면을 다 덮으므로 예전에는 같이 숨겼는데,
     그러면 프로젝트를 보는 동안 메뉴로 갈 방법이 사라집니다 — 로고도
     햄버거도 없이 작은 X 하나만 남습니다. 메뉴 오버레이(550)는 패널(500)
     위에 뜨므로 프로젝트 위에서 바로 열 수 있습니다. */
  body.is-split .nav { right: 0; }
  /* 둘 다 살아 있으니 이제는 자리를 나눠야 합니다. 닫기를 햄버거 왼쪽으로
     옮깁니다 — 예전에는 네비를 통째로 숨겨 겹칠 일이 없었습니다.
     rem 으로 잡는 이유: 이 버튼은 아래에서 font-size 를 0 으로 만들어
     글자를 지우는데, 그러면 em 으로 쓴 값이 전부 0 이 되어 버튼이 화면
     모서리로 튑니다. 햄버거 폭(44px)과 그 좌우 여백을 실제 길이로 셉니다. */
  body.is-split .cs-close {
    /* 짧아진 바 안에서 세로 가운데. */
    top: var(--nav-pad);
    right: calc(1.5rem + 44px + 0.5rem);
  }
  body.is-split .section.is-hero { width: 100%; }
  body.is-split { --t-display-xl: 6em; }

  .filter-btn.is-submit { padding-top: 0.62em; }
  .timeline-bottom-frames { height: 1.3em; }
  .timeline-item-meta { height: 1.4em; }

  /* 네비를 다시 살렸으므로 제목이 그 아래에서 시작해야 합니다. 4.5em 은
     네비(패딩 둘 + 44px 탭 타깃)보다 짧아 제목 윗부분이 띠에 먹힙니다.
  .big-title-wrap.is-project .big-title { font-size: 2.6em; }
  /* 상아색 띠 위로 올라오므로 밝은 베일 알약은 글자가 묻힙니다. 반전시킵니다. */
  body.is-split .cs-close { background: var(--invert-bg); color: var(--invert-fg); }

  /* 좁은 화면에서는 "Close" 라는 낱말이 들어갈 자리가 없습니다. 로고를 늘
     펼쳐 두므로(FLOW STUDIO, 209px) 햄버거까지 넣고 나면 남는 폭이
     100px 이 안 됩니다. 글자 대신 x 를 씁니다 — 읽어 주는 이름은
     aria-label 로 그대로 "Close" 입니다. */
  .cs-close {
    display: none;
    width: 44px;
    height: 44px;
    padding: 0;
    border-radius: var(--radius-round);
  }
  .cs-close.is-active {
    /* flex 로 잡힙니다(앞선 규칙). flex 에서는 place-items 의 가로 축이
       무시되므로 justify-content 를 따로 줘야 x 가 가운데에 섭니다. */
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .cs-close::before {
    content: "×";           /* 곱셈 기호. CSS 이스케이프 대신 문자를 직접 씁니다 */
    font-size: 1.5rem;
    line-height: 1;
  }
  /* 글자는 지우지 않고 화면에서만 뺍니다. font-size 를 0 으로 만들면
     읽어 주는 프로그램에는 남아도 "0px 로 그려지는 글자"가 생기고,
     em 으로 잡아 둔 값까지 함께 무너집니다. */
  .cs-close-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .cs-media { min-height: 60vw; }
  .panel-grid { grid-template-columns: 1fr; gap: var(--s-25); }
  /* 좌우 여백이 .page-panel 과 .panel-inner 에 이중으로 걸려 있었습니다.
     390px 화면에서 128px, 즉 3분의 1이 여백이라 입력칸이 262px 밖에
     안 됐습니다. 가로는 .panel-inner 한 쪽에만 맡깁니다. */
  .page-panel { padding: 7em 0 var(--s-4); }

  .site-footer { display: none; }
}

@media screen and (max-width: 479px) {
  .page-panel { padding-top: 6.5em; }
  /* 위와 같은 이유 — 네비 높이는 화면이 좁아져도 44px 탭 타깃 그대로입니다. */
  .contact-row { flex-direction: column; gap: 0.2em; }
  .contact-label { flex: none; }
}

/* Touch: the strip is the only navigation, so it must win the horizontal
   gesture. pinch-zoom is left to the browser — taking it would break zoom. */
@media (hover: none) {
  .timeline-coll-wrapper { touch-action: pinch-zoom; }
  /* No hover on touch, so the active card carries the label swap alone. */
  .timeline-coll-item:hover .thumb-plate { opacity: 0.82; }
  .timeline-coll-item.is-active .thumb-plate { opacity: 1; }
}

@media only screen and (orientation: landscape) and (max-width: 767px) {
  body.timelineUp .section.is-hero { transform: translateY(-2em); }
  .timeline-bottom-frames { height: 1.1em; }
}

/* --------------------------------------------------------- 11. inquiry */
/* 고객 자료 요청서. 패널은 중앙 정렬이지만 폼은 좌측 정렬입니다 — 라벨과
   입력칸이 세로로 정렬돼야 훑어 내려가며 채울 수 있습니다. 색·간격·타입은
   전부 기존 토큰이고 새로 만든 값은 없습니다. */

/* 52em(832px)은 본문 글줄에 맞춘 폭입니다. 이 화면은 읽는 글이 아니라
   채우는 칸이라, 그 폭으로 묶으면 넓은 화면에서도 입력칸이 화면의 절반을
   조금 넘길 뿐입니다. 채우기 좋게 넓힙니다. */
.inquiry-host { width: 100%; max-width: 68em; margin: 0 auto; text-align: left; }

.inq-pick {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-15);
  padding-bottom: var(--s-2);
  border-bottom: 1.5px solid var(--page-fg);
}

.inq-form.is-empty .inq-actions,
.inq-form.is-empty .inq-sendnote { display: none; }

.inq-section { margin-top: var(--s-35); }
.inq-section-title {
  padding-bottom: 0.5em;
  border-bottom: 1px solid var(--veil-dark-10);
  margin-bottom: var(--s-15);
}
.inq-note { margin: -0.8em 0 var(--s-15); opacity: 0.55; line-height: 1.5; }

.inq-field { margin-bottom: var(--s-25); }
.inq-label {
  display: block;
  margin-bottom: 0.5em;
  /* 굵기 해제. 라벨은 무게가 아니라 위치와 여백으로 구분됩니다. */
  font-weight: var(--w-regular);
  text-transform: none;
  letter-spacing: 0;
}
.inq-req {
  display: inline-block;
  margin-left: 0.5em;
  padding: 0.1em 0.45em;
  border-radius: 1em;
  background: var(--yellow);
  color: var(--black);
  font-size: var(--t-xsmall);
}
.inq-hint {
  margin: -0.2em 0 0.5em;
  opacity: 0.5;
  text-transform: none;
  line-height: 1.5;
}
.inq-field .field { width: 100%; }
.inq-field textarea.field { resize: vertical; min-height: 4.5em; }

.inq-check { display: flex; align-items: center; gap: 0.6em; cursor: pointer; }
.inq-check input { width: 1.1em; height: 1.1em; accent-color: var(--page-fg); }

.inq-notice { border-left: 2px solid var(--page-fg); padding-left: var(--s-1); }
.inq-notice li {
  margin-bottom: 0.5em;
  opacity: 0.7;
  text-transform: none;
  line-height: 1.6;
}

/* list / photos — 줄을 늘려 가며 적는 항목 */
.inq-list { display: grid; gap: 0.7em; }
/* 줄을 담는 상자에도 간격이 필요합니다. .inq-list 의 gap 은 머리글과 본문
   사이만 벌리고, 정작 줄과 줄 사이는 0 이라 입력 상자 테두리가 맞닿아
   표가 한 덩어리로 뭉쳐 보였습니다. */
.inq-list-body { display: grid; gap: 0.5em; }
.inq-list-row {
  display: grid;
  /* 1fr 은 minmax(auto,1fr) 이라 긴 머리글이 그 칸만 넓힙니다. */
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  /* 0.55em 은 입력 칸이 서로 붙어 보입니다. 칸끼리도, 줄끼리도 띄웁니다. */
  gap: 0.5em 0.9em;
  align-items: center;
}
.inq-list-row.is-head {
  opacity: 0.5;
  margin-bottom: 0.1em;
  font-weight: var(--w-regular);
  text-transform: none;
  letter-spacing: 0;
}
.inq-list-row:has(.inq-list-num) { grid-template-columns: 2.2em repeat(calc(var(--cols) - 1), minmax(0, 1fr)); }
.inq-list-num { text-align: center; opacity: 0.45; }
.inq-add {
  margin-top: 0.6em;
  padding: 0.4em 0.9em;
  border: 1px solid var(--veil-dark-10);
  border-radius: 1em;
  transition: background-color var(--d-micro) var(--e-ease);
}
.inq-add:hover { background-color: var(--veil-dark-10); }

.inq-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-15);
  margin-top: var(--s-35);
  padding-top: var(--s-2);
  border-top: 1.5px solid var(--page-fg);
}
.inq-mail { opacity: 0.6; transition: opacity var(--d-micro) var(--e-ease); }
.inq-mail:hover { opacity: 1; }

.inq-sendnote {
  margin-top: var(--s-15);
  opacity: 0.5;
  text-transform: none;
  line-height: 1.6;
}
.inq-status {
  margin-top: 0.6em;
  min-height: 1.4em;
  opacity: 0;
  text-transform: none;
  transition: opacity var(--d-micro) var(--e-ease);
}
.inq-status.is-on, .inq-status:not(:empty) { opacity: 0.75; }

@media screen and (max-width: 767px) {
  .inq-pick { grid-template-columns: 1fr; }
  .inq-list-row { grid-template-columns: 1fr; }
  .inq-list-row:has(.inq-list-num) { grid-template-columns: 1fr; }
  .inq-list-row.is-head { display: none; }
  .inq-actions { flex-direction: column; align-items: flex-start; }
}

/* --------------------------------------------------- 12. contact floating */
/* 어느 라우트에서든 떠 있는 작은 연락 버튼. 반전(검정 지면 · 아이보리 글자)은
   이 시스템의 유일한 색 장치이고, 그림자는 쓰지 않습니다. 평소엔 CONTACT,
   올리면 번호로 굴러갑니다 — 네비 워드롤과 같은 어휘입니다. */
.contact-fab {
  position: fixed;
  right: var(--gutter);
  bottom: calc(var(--strip-block) + 2.4em);
  z-index: 620;
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  padding: 0.62em 1.05em;
  border-radius: 2em;
  background: var(--invert-bg);
  color: var(--invert-fg);
  transition:
    background-color var(--d-micro) var(--e-ease),
    color var(--d-micro) var(--e-ease),
    bottom var(--d-hero) var(--e-quart),
    right var(--d-hero) var(--e-quart);
}
.contact-fab:hover { background: var(--yellow); color: var(--black); }

.fab-dot {
  width: 0.42em;
  height: 0.42em;
  border-radius: var(--radius-round);
  background: var(--yellow);
  flex: none;
}
.contact-fab:hover .fab-dot { background: var(--black); }

.fab-roll {
  position: relative;
  display: block;
  overflow: hidden;
  height: 1.15em;
  line-height: 1.15em;
}
.fab-face {
  display: block;
  white-space: nowrap;
  transform: translateY(0);
  transition: transform var(--d-roll) var(--e-quad-io);
}
.fab-number { position: absolute; inset: 0; transform: translateY(100%); }
/* 평소에는 "CONTACT" 폭 그대로, 굴러 올라오면 번호 폭으로 벌어집니다.
   넓은 쪽으로 미리 잡아 두면 평소에 좌우가 늘 비어 보입니다. 두 값은
   app.js 의 sizeContactFab() 이 글자를 직접 재서 넘깁니다. */
.fab-roll {
  width: var(--fab-w-label, auto);
  transition: width var(--d-roll) var(--e-quad-io);
}
.contact-fab:hover .fab-roll,
.contact-fab:focus-visible .fab-roll { width: var(--fab-w-number, auto); }
.contact-fab:hover .fab-label,
.contact-fab:focus-visible .fab-label { transform: translateY(-100%); }
.contact-fab:hover .fab-number,
.contact-fab:focus-visible .fab-number { transform: translateY(0); }

/* 패널 라우트에는 타임라인이 없으니 바닥에 붙습니다. */
body.route-away .contact-fab { bottom: var(--s-2); }
/* 분할 상태에서는 왼쪽 절반 안에 머뭅니다 — 오른쪽은 고객 사이트 자리입니다. */
body.is-split .contact-fab { right: calc(var(--split) + var(--gutter)); }

@media screen and (max-width: 767px) {
  .contact-fab { right: var(--s-1); bottom: calc(var(--strip-block) + 1.2em); padding: 0.55em 0.9em; }
  body.is-split .contact-fab { right: var(--s-1); }
}

/* ------------------------------------------------- 13. touch targets (last)
   맨 뒤에 둡니다. 미디어쿼리는 우선순위를 더하지 않으므로, 앞 절에 있는
   기본 규칙을 이기려면 파일에서 뒤에 와야 합니다. (체크박스 크기가 10절
   미디어 블록에서 먹지 않던 이유가 이것이었습니다.) */
@media screen and (max-width: 767px) {
  /* 칩은 줄바꿈되므로 보이지 않는 확장 영역을 쓰면 윗줄 칩을 가로챕니다.
     실제 높이를 키웁니다. */
  /* inline-flex + align-items:center 라 min-height 만으로 정확히 44 가 됩니다. */
  .filter-btn { padding: 0.7em 1em; min-height: 44px; flex: none; }

  /* 칩 4개는 390px 에 들어가지 않습니다. 줄을 바꾸면 마지막 칩만 혼자
     가운데 남아 어색해지므로, 한 줄로 두고 옆으로 미는 방식으로 바꿉니다.
     좌우로 화면 끝까지 흘려 보내 "더 있다"는 것이 보이게 합니다. */
  .filters-form {
    gap: 0.4em;
    flex-wrap: nowrap;
    align-self: stretch;
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    padding-inline: var(--gutter);
    margin-inline: calc(var(--gutter) * -1);
    scroll-padding-inline: var(--gutter);
    -webkit-overflow-scrolling: touch;
  }
  .filters-form::-webkit-scrollbar { display: none; }

  .inq-check { min-height: 44px; }
  .inq-check input { width: 1.4em; height: 1.4em; }
  .inq-list-row .field { min-height: 44px; }

  /* 오버레이 컨트롤 — 닫기는 모바일에서 유일한 탈출구라 특히 중요합니다.
     .cs-close 는 display 로 숨는 요소라, 활성 상태에만 손댑니다. 여기에
     그냥 display:inline-flex 를 주면 기본값 display:none 을 덮어써서
     닫기 버튼이 항상 떠 있고 햄버거를 가립니다. */
  .cs-open,
  .inq-mail,
  .close-content,
  .muted-btn,
  .fullscreen-btn {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .cs-close.is-active {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-inline: 1em;
  }
  /* 「새 탭에서 열기」와 떠 있는 CONTACT 가 둘 다 우측 하단이라 겹칩니다.
     상세 패널 쪽을 왼쪽으로 보냅니다. */
  .cs-open { right: auto; left: var(--s-1); }
}

/* ============================================================== 14. 세로 모드
   한 바퀴를 다 밀면 카드가 화면 폭까지 펼쳐지고(is-expanding), 다 펼쳐지면
   프로젝트를 위아래로 훑는 보통의 문서가 됩니다(is-vertical).
   좁은 화면(<=767px)에서는 처음부터 이 모드입니다. */

/* --- 미는 만큼 카드가 커진다 ---
   --expand 는 filmstrip.js 가 "직접 민 거리 / 한 바퀴" 로 넣어 주는 0~1 값입니다.
   전환(transition)을 걸지 않습니다 — 스크롤에 붙어 따라와야 하는 값이라,
   시간 기반 전환을 얹으면 손보다 늦게 도착합니다. */
/* 펼친 폭의 상한은 1440px 입니다. 원본을 2880px 로 찍었으므로, 카드가
   1440 CSS px 를 넘지 않는 한 dpr 2 화면에서도 확대가 일어나지 않습니다.
   아주 넓은 모니터에서 카드만 끝없이 늘어나 흐려지는 것보다, 가운데에
   1440 으로 서는 편이 낫습니다. */
.timeline-coll-item { width: var(--card-w); }
/* 커지는 동안에는 카드 안의 스크롤도 멈추고 맨 위 — 그 사이트의 히어로 — 를
   보여 줍니다. 자라면서 내용까지 흐르면 무엇을 보는지 알 수 없습니다. */
body.is-growing .seq-reel {
  animation: none;
  transform: translate3d(0, 0, 0);
}

/* --- 세로 목록 --- */
/* 이 사이트는 원래 세로 스크롤이 없는 구조라, 이 모드에서만 문서 흐름을 켭니다. */
body.is-vertical { overflow-y: auto; height: auto; }
body.is-vertical .wrap-all,
body.is-vertical .wrap-sticky,
body.is-vertical .page-wrapper,
body.is-vertical .page-content { display: block; height: auto; }

body.is-vertical .section.is-hero {
  height: 100svh;
  scroll-snap-align: start;
  /* 히어로에서도 한 번에 여러 장을 건너뛰지 않도록. */
  scroll-snap-stop: always;
  transform: none !important;
  opacity: 1;
}

body.is-vertical .timeline--wrapper {
  position: static;
  display: block;
  height: auto;
  overflow: visible;
  padding: 0;
}
body.is-vertical .timeline-progress-top,
body.is-vertical .timeline-bottom-frames { display: none; }

body.is-vertical .timeline-coll-wrapper {
  position: static;
  height: auto;
  overflow: visible;
  cursor: default;
  touch-action: auto;
}
/* 한 화면에 한 프로젝트 + 스냅은 모바일에서만입니다. 마우스 휠은 한 틱이
   120px 인데 스냅 간격이 한 화면(900px)이라, mandatory 는 매 틱을 제자리로
   되돌립니다 — 데스크톱에서 펼친 뒤 위아래 어느 쪽으로도 움직이지 않던
   원인입니다. 손가락은 관성이 붙어 한 번에 넘어가므로 모바일은 그대로. */

body.is-vertical .timeline-coll-list {
  position: static;
  display: flex;
  flex-direction: column;
  gap: 0;
  width: 100% !important;
  transform: none !important;
  will-change: auto;
}
body.is-vertical .timeline-coll-item {
  /* 원본이 2880px 이므로 카드가 1440 CSS px 를 넘으면 dpr 2 에서 확대됩니다.
     넓은 모니터에서는 가운데 1440 으로 섭니다. */
  width: min(100%, 1440px);
  margin: 0 auto;
  height: 100svh;
  padding: 0;
  transition: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
body.is-vertical .timeline-thumbnail-wrap { flex: 1 1 auto; min-height: 0; }
/* 세로 목록에서 되풀이는 의미가 없습니다 — 복제본은 내립니다. */
body.is-vertical .timeline-coll-item[aria-hidden="true"] { display: none; }

body.is-vertical .timeline-thumbnail-wrap { padding: 0; }
body.is-vertical .thumb-plate { aspect-ratio: auto; height: 100%; opacity: 1; }
/* 라벨은 호버를 기다리지 않고 늘 보입니다 — 만질 수 없는 화면도 있으니까요. */
body.is-vertical .timeline-item-meta {
  position: absolute;
  left: var(--gutter);
  top: 5.2em;
  z-index: 3;
  display: flex;
  align-items: baseline;
  gap: 0.9em;
  height: auto;
  overflow: visible;
  padding: 0;
}
body.is-vertical .timeline-coll-item { position: relative; }
body.is-vertical .p-small.is-number,
body.is-vertical .p-small.is-title { position: static; left: auto; right: auto; transition: none; }
body.is-vertical .p-small.is-number { opacity: 0.45; }
body.is-vertical .p-small.is-title { opacity: 1; font-size: var(--t-big); }
body.is-vertical .video-embed { display: none; }

/* 문서가 스크롤되므로 떠 있는 것들은 자기 자리를 다시 잡습니다. */
body.is-vertical .contact-fab { bottom: var(--s-2); }
body.is-vertical .site-footer { position: static; opacity: 0.45; padding: 0 var(--gutter) var(--s-4); }

@media screen and (max-width: 767px) {

}

/* ===================================================== 15. 모바일 세로 리듬
   휴대폰은 세로로 길고 캡처는 16:9 로 넓습니다. 카드를 화면 높이에 맞춰
   늘리면 cover 가 가로의 4분의 1만 남겨 무엇을 찍은 것인지 알 수 없습니다.
   그래서 화면 하나에 "라벨 + 제 비율의 이미지" 를 가운데 두고, 히어로도
   같은 리듬으로 세웁니다. */
@media screen and (max-width: 767px) {
  body.is-vertical .timeline-thumbnail-wrap {
    flex: 0 0 auto;
    min-height: 0;
    padding: 0;
  }

  /* 히어로도 같은 리듬 — 가운데 한 덩어리, 아래에 계속 내리라는 신호. */
  body.is-vertical .section.is-hero { position: relative; }
  /* 워드마크를 두 줄로 세웁니다. 한 줄로 밀어 넣으면 45px 에 그쳐, 화면의
     76%를 채우는 카드 옆에서 히어로만 텅 빈 화면이 됩니다. 실제 크기는
     app.js 가 "가장 긴 낱말" 을 폭에 맞춰 정합니다. */
  .big-title.is-home-title {
    white-space: normal;
    line-height: 0.84;
    padding: 0;
  }
  body.is-vertical .section.is-hero::after {
    content: "↓";
    position: absolute;
    left: 50%;
    bottom: 2.6em;
    transform: translateX(-50%);
    font-size: var(--t-h4);
    opacity: 0.35;
  }

  /* 칩이 한 줄에 다 안 들어갑니다. 예전에는 넘치는 쪽을 흐려 "옆으로 더
     있다"고 알렸는데, 옆으로 밀어야 보이는 것은 결국 한눈에 안 보이는
     것입니다. 줄을 바꿔 전부 내보냅니다. 글자 바닥은 그대로 둡니다. */
  .filters-form {
    flex-wrap: wrap;
    justify-content: center;
    overflow: visible;
    row-gap: 0.4em;
    -webkit-mask-image: none;
    mask-image: none;
  }
  /* 글자는 키우고 좌우는 좁힙니다 — 칩은 글자를 읽는 물건이지
     여백을 보는 물건이 아닙니다. 세로 여백은 손가락 타깃(44px)이라 유지. */
  .filter-btn { padding: 0.7em 0.5em; }
  .filters-form .filter-txt { font-size: 16px; }
}

/* ==================================================== 16. 펼친 뒤: 4열 갤러리
   하단바에서 위로 자라 4열 그리드가 됩니다. 예전에는 한 화면에 카드 한 장
   이었는데, 그 구조는 스냅 간격이 한 화면이라 마우스 휠과 맞지 않았습니다.
   4열은 문서처럼 굴러가므로 휠 한 틱이 그대로 먹힙니다. */
@media screen and (min-width: 768px) {
  body.is-vertical .timeline-coll-list {
    display: grid;
    /* 1fr 은 minmax(auto,1fr) 이라 긴 라벨이 그 열만 넓힙니다. */
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--s-4) var(--grid-gap);
    align-items: start;
    width: min(100vw - var(--gutter) * 2, 1440px) !important;
    margin: 0 auto;
    padding-bottom: var(--s-8);
  }
  body.is-vertical .timeline-coll-item {
    width: auto;
    max-width: none;
    height: auto;
    margin: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    gap: 0.8em;
  }
  /* 확장이 끝난 자리 그대로 — 계산식이 아니라 도착값을 못박습니다. */
  body.is-vertical .thumb-plate {
    aspect-ratio: 2 / 3;
    height: auto;
    opacity: 1;
  }
  /* 그리드에서는 라벨을 카드 위에 얹지 않고 바로 아래 둡니다. */
  body.is-vertical .timeline-item-meta {
    position: static;
    left: auto;
    top: auto;
    padding: 0;
  }
}

/* ====================================================== 17. 모바일: 2열 격자
   데스크톱이 4열이면 휴대폰은 2열입니다. 한 화면에 한 장이던 구조를
   대신하므로, 그때 쓰던 장치들(화면 높이 고정, 강제 스냅, 전면 출혈)을
   같이 걷어냅니다 — 격자에서는 스냅할 단위가 없고, 매 스크롤이 제자리로
   되돌려지기만 합니다.
   파일 끝에 두는 이유: 위쪽 15절도 같은 미디어 쿼리라 특이도가 같습니다.
   나중에 오는 쪽이 이깁니다. */
@media screen and (max-width: 767px) {
  body.is-vertical .timeline-coll-list {
    display: grid;
    /* 1fr 은 minmax(auto,1fr) 이라 긴 라벨(할스톤디벨롭먼트)이 그 열을
       넓혀 137/164 로 어긋났습니다. minmax(0,1fr) 이라야 반반입니다. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* 스트립의 flex-end 가 여기까지 따라와 행 안에서 47px 어긋났습니다. */
    align-items: start;
    gap: var(--s-25) var(--s-15);
    padding: 0 var(--gutter) var(--s-6);
    width: 100% !important;
  }
  /* 좁은 칸에서 라벨이 열을 밀지 못하게. */
  body.is-vertical .timeline-item-meta { min-width: 0; }
  /* 번호는 줄지 않습니다. 긴 제목이 밀면 "06" 이 "0.." 으로 잘립니다. */
  body.is-vertical .p-small.is-number { flex: none; }
  body.is-vertical .p-small.is-title {
    min-width: 0;
    overflow-wrap: anywhere;
  }
  body.is-vertical .timeline-coll-item {
    width: auto;
    max-width: none;
    height: auto;
    margin: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    gap: 0.55em;
    scroll-snap-align: none;
  }
  /* 높이를 정하는 쪽은 판입니다. flex:1 로 두면 부모가 auto 라 0 으로
     주저앉습니다. */
  body.is-vertical .timeline-thumbnail-wrap {
    flex: 0 0 auto;
    min-height: 0;
    padding: 0;
  }
  /* 모바일 판은 9:16 으로 찍어 두었습니다. 타일을 원본 비율에 맞춰야
     cover 가 좌우를 잘라 내지 않습니다. */
  body.is-vertical .thumb-plate {
    aspect-ratio: 9 / 16;
    height: auto;
    max-height: none;
    border-radius: var(--radius);
    opacity: 1;
  }
  body.is-vertical .timeline-item-meta {
    position: static;
    left: auto;
    top: auto;
    padding: 0;
  }
}

/* ================================================ 18. 모바일: 스크롤바 감춤
   휴대폰에서는 스크롤바가 내용 위를 덮고 지나갑니다. 격자로 바뀌면서
   가로로 넘칠 일도 없어졌으니 자리만 차지합니다. 스크롤 자체는 그대로. */
@media screen and (max-width: 767px) {
  html {
    scrollbar-width: none;          /* Firefox */
    -ms-overflow-style: none;       /* 구형 Edge */
  }
  html::-webkit-scrollbar,
  body::-webkit-scrollbar,
  .filters-form::-webkit-scrollbar,
  .page-panel::-webkit-scrollbar,
  .cs-inner::-webkit-scrollbar {
    width: 0;
    height: 0;
    background: transparent;
  }
  .filters-form,
  .page-panel,
  .cs-inner { scrollbar-width: none; }
}

/* ============================================== 19. 홈: 하단바를 커튼으로 연다
   처음 들어오면 히어로만 있습니다. 아래로 굴리면 쇼릴과 같은 커튼 —
   가운데에서 위아래로 벌어지며 — 하단바가 나타나고, 그때부터 카드가
   자랍니다. 프로젝트를 처음부터 깔아 두지 않는 이유는, 그 화면이 무엇을
   하는 곳인지 먼저 읽히고 나서 작업이 나오는 편이 순서에 맞기 때문입니다. */
@media screen and (min-width: 768px) {
  .timeline--wrapper { clip-path: inset(50% 0 50% 0); }
  body.strip-open .timeline--wrapper,
  body.is-vertical .timeline--wrapper { clip-path: inset(0 0 0 0); }

  /* 하단바가 닫혀 있는 동안에는 그 자리를 비워 둘 이유가 없습니다.
     연락 버튼과 저작권 표시를 같은 높이로 화면 아래에 붙입니다 — 둘이
     다른 높이에 뜨면 한쪽이 허공에 떠 보입니다. */
  body:not(.strip-open):not(.is-vertical) .contact-fab,
  body:not(.strip-open):not(.is-vertical) .site-footer { bottom: var(--s-25); }
  /* 저작권 표시는 글자 기준선이 버튼 가운데와 맞아야 한 줄로 읽힙니다. */
  body:not(.strip-open):not(.is-vertical) .site-footer {
    bottom: calc(var(--s-25) + 0.7em);
  }
}
@media (prefers-reduced-motion: reduce) {
  .timeline--wrapper { transition-duration: 0.01s; }
}

/* 좁은 화면에서는 최상단 바가 프로젝트 화면 위를 덮습니다. 머리를 걷어내며
   그 자리도 같이 사라져 라이브 화면이 y=0 에서 시작하고, 그 사이트의 자기
   네비가 우리 바 뒤로 숨었습니다. 바 높이(패딩 둘 + 44px 탭 타깃)만큼만
   비웁니다 — 데스크톱은 바가 왼쪽 절반에 있으므로 그대로 꽉 채웁니다. */
@media screen and (max-width: 767px) {
  .cs-inner { padding-top: var(--nav-h); }
}

/* 자동 제출을 거르려고 둔 미끼 칸. 사람 눈과 화면 낭독기 양쪽에서 빼되
   display:none 은 쓰지 않습니다 — 일부 봇이 그걸 보고 건너뜁니다. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* ================================================ 20. 홈도 커튼으로 열린다
   최상단 메뉴에서 About·Services·Contact·Inquiry 는 패널 커튼으로,
   Showreel 은 제 마스크로 열립니다. Work 만 그냥 나타나 혼자 다른 동작으로
   읽혔습니다. 홈 레이어(히어로 + 하단바)에도 같은 커튼을 겁니다.
   페이드는 뺐습니다 — 커튼과 겹치면 둘 다 흐려 보입니다.
   19절 뒤에 두어야 합니다: strip-open 규칙과 특이도가 같아 나중에 오는
   쪽이 이깁니다. */
.section.is-hero { clip-path: inset(0 0 0 0); }
body.route-away .section.is-hero,
body.route-away .timeline--wrapper { clip-path: inset(50% 0 50% 0); }

/* ============================================ 21. 화면을 바꾸는 막(sweep)
   패널은 clip-path 로 열리고 있었지만, 아이보리 패널이 아이보리 화면 위로
   열리니 경계가 보이지 않았습니다. 화면 픽셀로 재 보면 쇼릴은 전환 내내
   6~27%가 계속 달라지는데 패널은 13%까지 갔다가 1~2%로 떨어졌습니다.
   그래서 쇼릴과 같은 장치를 씁니다: 검은 막이 가운데에서 벌어져 화면을
   덮고, 그 뒤에서 화면을 바꾼 다음, 다시 걷힙니다. */
.route-curtain {
  position: fixed;
  inset: 0;
  z-index: 700;              /* 네비(600) 위, 쇼릴(999) 아래 */
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.route-curtain-mask {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 0;
  overflow: hidden;
  background: var(--invert-bg);
  transition: height var(--d-sweep) var(--e-cubic-io);
}
body.is-sweeping .route-curtain-mask { height: 100%; }

/* 막 한가운데의 이름. 막과 같은 자리에 고정되어 있고 막이 벌어지는 만큼만
   드러나므로, 커튼이 열리면서 글자가 가운데에서 함께 나타납니다.
   flex:none 이 필요합니다 — 높이 0 인 상자 안의 flex 자식은 눌립니다. */
.route-curtain-word {
  flex: none;
  font-family: var(--font-body);
  font-weight: 900;
  font-size: clamp(2.6rem, 9vw, 9rem);
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
  color: var(--invert-fg);
}

@media (prefers-reduced-motion: reduce) {
  .route-curtain-mask { transition: none; }
}

/* 목록형 표의 칸 이름표.
   넓은 화면에서는 머리글(.is-head)이 이미 이름을 보여 주므로 감춥니다.
   좁은 화면에서는 표가 한 줄로 접혀 머리글이 숨으므로, 이름표가 그 자리를
   대신합니다 — 없으면 정체를 알 수 없는 입력 상자만 쌓입니다. */
.inq-list-cell { display: block; min-width: 0; }
.inq-list-cell .field { width: 100%; }
.inq-cell-name { display: none; }

@media screen and (max-width: 767px) {
  .inq-list-row.is-head { display: none; }
  .inq-cell-name {
    display: block;
    margin-bottom: 0.3em;
    opacity: 0.55;
  }
  /* 번호는 칸 위에 가운데로 뜨지 말고 왼쪽에 붙습니다. */
  .inq-list-num { text-align: left; }
  /* 줄과 줄 사이를 벌려 어디까지가 한 줄인지 보이게 합니다. */
  .inq-list { row-gap: var(--s-15); }
}

/* ============================================ 22. 직접 그린 선택 목록
   네이티브 select 의 펼쳐진 목록은 운영체제가 그립니다. CSS 가 닿지 않아
   이 사이트의 글꼴·색·모서리를 따르게 할 방법이 없어 목록만 직접 그립니다.
   닫힌 모습은 .field 를 그대로 쓰므로 다른 입력칸과 한 줄로 섭니다. */
.dd { position: relative; }

.dd-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-1);
  width: 100%;
  text-align: left;
  cursor: pointer;
}
.dd.is-empty .dd-value { opacity: 0.45; }
.dd-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* 화살표. 네이티브가 그려 주던 자리라 없으면 그냥 글상자로 보입니다. */
.dd-arrow {
  flex: none;
  width: 0.62em;
  height: 0.62em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-0.16em) rotate(45deg);
  transition: transform var(--d-micro) var(--e-ease);
}
.dd.is-open .dd-arrow { transform: translateY(0.1em) rotate(-135deg); }

.dd-list {
  position: absolute;
  z-index: 20;
  top: calc(100% + 0.35em);
  right: 0;
  left: 0;
  max-height: 17em;
  margin: 0;
  padding: 0.35em;
  overflow-y: auto;
  overscroll-behavior: contain;
  list-style: none;
  background: var(--page-bg);
  border: 1px solid var(--page-fg);
  border-radius: var(--radius);
}
/* 목록은 초점을 프로그램이 옮겨 줍니다(열자마자). 사용자가 탭으로 간 것이
   아니므로 초점 링을 그리면 사고처럼 보입니다. 어디를 고르는 중인지는
   .is-active 표시가 이미 알려 줍니다. */
.dd-list:focus,
.dd-list:focus-visible { outline: none; }

.dd-opt {
  padding: 0.62em 0.8em;
  border-radius: calc(var(--radius) - 0.2em);
  cursor: pointer;
}
/* 마우스로 훑는 것과 키보드로 옮기는 것을 같은 표시로 보여 줍니다 —
   둘이 다르면 어디를 고르는 중인지 헷갈립니다. */
.dd-opt:hover,
.dd-opt.is-active { background: var(--veil-dark-10); }
.dd-opt[aria-selected="true"] {
  background: var(--invert-bg);
  color: var(--invert-fg);
}

@media screen and (max-width: 767px) {
  /* 손가락 타깃. */
  .dd-opt { min-height: 44px; display: flex; align-items: center; }
  .dd-list { max-height: 60svh; }
}

/* ================================================== 23. 사진 올리기
   예전에는 파일명을 적어 두고 메일에 직접 첨부하는 방식이었습니다. 이제
   이 화면에서 바로 보내므로 파일을 여기서 받습니다. 무엇을 골랐는지
   보이도록 미리보기를 함께 둡니다 — 이름만 나열하면 잘못 고른 것을
   알아채지 못합니다. */
.inq-photos { display: grid; gap: var(--s-1); }
.inq-photo-add { justify-self: start; cursor: pointer; }
.inq-photo-status { opacity: 0.55; }

.inq-photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5em, 1fr));
  gap: var(--s-15);
}
.inq-photo {
  position: relative;
  display: grid;
  gap: 0.4em;
  margin: 0;
}
.inq-photo img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--veil-dark-10);
}
.inq-photo-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.6;
}
.inq-photo-note { width: 100%; }

/* 빼기 단추는 사진 위에 올립니다. 아래에 두면 칸마다 높이가 달라져
   격자가 어긋납니다. */
.inq-photo-del {
  position: absolute;
  top: 0.4em;
  right: 0.4em;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.9em;
  height: 1.9em;
  border-radius: var(--radius-round);
  background: var(--invert-bg);
  color: var(--invert-fg);
  font-size: 1em;
  line-height: 1;
  cursor: pointer;
}
.inq-photo-del:hover { background: var(--yellow); color: var(--black); }

@media screen and (max-width: 767px) {
  .inq-photo-grid { grid-template-columns: repeat(auto-fill, minmax(8em, 1fr)); }
  .inq-photo-del { width: 44px; height: 44px; }
}

/* ============================================ 24. 보내기 전 확인 화면
   사진은 고르는 순간 전송되지 않습니다. 무엇이 갈지 여기서 전부 보여 드리고,
   누르셔야 나갑니다 — 잘못 고른 사진이나 오타를 되돌릴 자리가 필요합니다. */
.inq-review {
  position: fixed;
  inset: 0;
  z-index: 980;               /* 쇼릴(999) 아래, 그 밖의 모든 것 위 */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-25) var(--gutter);
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(var(--blur-near));
  -webkit-backdrop-filter: blur(var(--blur-near));
}
.inq-review-box {
  width: 100%;
  max-width: 46em;
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-25);
  background: var(--page-bg);
  border-radius: var(--radius);
  text-align: left;
}
/* 상자에 초점을 주지만 링은 그리지 않습니다 — 사용자가 탭으로 간 것이
   아니라 창이 열린 것이고, 창 테두리가 이미 경계를 보여 줍니다. */
.inq-review-box:focus,
.inq-review-box:focus-visible { outline: none; }
.inq-review-title { margin-bottom: 0.3em; }
.inq-review-lede { margin-bottom: var(--s-2); opacity: 0.65; }
.inq-review-sec {
  margin: var(--s-2) 0 0.6em;
  padding-top: 0.6em;
  border-top: 1px solid var(--veil-dark-10);
  opacity: 0.55;
}

/* 물음과 답을 두 칸으로. 한 칸으로 흘리면 어디까지가 답인지 흐려집니다. */
.inq-review-meta {
  display: grid;
  grid-template-columns: minmax(0, 11em) minmax(0, 1fr);
  gap: 0.45em 1.1em;
  margin: 0;
}
.inq-review-meta dt { opacity: 0.55; }
/* 여러 줄로 답한 항목(도메인 다섯 순위 같은)은 줄 사이가 필요합니다.
   .text-p-allcaps 의 촘촘한 줄높이를 그대로 두면 줄이 서로 겹쳐 보입니다. */
.inq-review-meta dd { margin: 0; white-space: pre-line; line-height: 1.55; }

.inq-review-photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7.5em, 1fr));
  gap: var(--s-1);
}
.inq-review-photo { margin: 0; display: grid; gap: 0.3em; }
.inq-review-photo img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius);
}
.inq-review-photo figcaption {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.55;
}
.inq-review-files { margin-top: var(--s-15); opacity: 0.55; }
.inq-review-actions {
  display: flex;
  align-items: center;
  gap: var(--s-15);
  margin-top: var(--s-2);
  flex-wrap: wrap;
}
.inq-review-status { margin-top: 0.8em; min-height: 1.4em; }

@media screen and (max-width: 767px) {
  .inq-review { padding: var(--s-1); align-items: flex-end; }
  .inq-review-box { padding: var(--s-15); max-height: 92svh; }
  .inq-review-meta { grid-template-columns: 1fr; gap: 0.15em 0; }
  .inq-review-meta dd { margin-bottom: 0.7em; }
  .inq-review-actions > * { flex: 1 1 100%; }
}

/* ======================================== 25. 모바일 카드가 훑는 움직임
   데스크톱은 조각 18장을 이어 붙여 굴립니다. 휴대폰에서 그대로 쓰면 카드마다
   2880px 이미지를 전부 디코딩해 심하게 버벅였습니다. 두 화면 높이(9:32)를
   담은 한 장을 9:16 창으로 천천히 훑습니다 — 이미지 하나, 움직임은 GPU
   transform 하나입니다. */
.thumb-seq.is-mobile-reel {
  position: absolute;
  inset: 0;
  display: block;
  overflow: hidden;
}
.mreel-shot {
  display: block;
  width: 100%;
  /* .thumb-plate img 가 height:100% 를 걸어 두어 창 높이에 맞춰집니다.
     그러면 훑을 여백이 없어 움직여도 같은 자리입니다. 제 비율대로
     늘어나야 합니다 — 9:32 이므로 창(9:16)의 두 배. */
  height: auto !important;
  object-fit: fill;
  will-change: transform;
  animation: mreelPan 26s ease-in-out infinite alternate;
}
@keyframes mreelPan {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -50%, 0); }
}
/* 눌러 보는 동안에는 멈춥니다 — 읽으려고 멈춘 것을 계속 흘리면 방해입니다. */
.timeline-coll-item:hover .mreel-shot,
.timeline-coll-item:focus-within .mreel-shot { animation-play-state: paused; }
/* 화면 밖 카드는 굴리지 않습니다. app.js 가 붙였다 뗍니다. */
.timeline-coll-item.is-offscreen .mreel-shot { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .mreel-shot { animation: none; }
}
