:root {
  --bg: #08090a;
  --fg: #f5f4f0;
  --fg-dim: rgba(245, 244, 240, 0.6);
  --fg-faint: rgba(245, 244, 240, 0.24);
  --line: rgba(245, 244, 240, 0.09);
  --accent: #c9a355;
  --accent-soft: rgba(201, 163, 85, 0.4);
  --font-sans: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo",
    "Malgun Gothic", "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
  /* Latin brand mark (logo / hero wordmark) — always this face, regardless
     of page language, so "Ben — AI Crossborder" reads the same everywhere. */
  --font-latin: "Space Grotesk", -apple-system, "Segoe UI", sans-serif;
  --ease: cubic-bezier(0.16, 0.84, 0.44, 1);
}

/* per-language body/headline font. Korean and Chinese get a proper native
   webfont instead of relying on the system default — Pretendard / Noto Sans
   SC read noticeably more considered on a landing page than the OS default. */
html[lang="ko"] {
  --font-title: "Pretendard Variable", Pretendard, -apple-system,
    "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
  --font-body: "Pretendard Variable", Pretendard, -apple-system,
    "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
  /* word-break is an inherited property, so setting it once here covers
     every Korean text node on the page — headlines, form legends, button
     labels, checkbox text — without listing each element individually.
     The per-class rules further down (.statement, .scene-desc, etc.) predate
     this and are now redundant with it, but harmless to leave: same value,
     so there's nothing to conflict. */
  word-break: keep-all;
}
html[lang="zh-Hans"] {
  --font-title: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
  --font-body: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
}
html[lang="en"] {
  --font-title: "Space Grotesk", -apple-system, "Segoe UI", sans-serif;
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

* {
  box-sizing: border-box;
  /* kills the blue/gray flash Android/iOS paint on tap for every link,
     button, label, and input — purely a touch-feedback artifact, not a
     focus indicator, so removing it doesn't touch keyboard accessibility */
  -webkit-tap-highlight-color: transparent;
}

/* :focus{outline:none} on its own would strand keyboard users with no
   visible focus indicator anywhere on the site. Pairing it with
   :focus-visible (which only matches keyboard/programmatic focus, not
   mouse or touch activation) removes the outline exactly where it was
   never wanted — the momentary ring after a tap or click — while keeping
   it for the case it exists for. */
:focus {
  outline: none;
}
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

html {
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
  /* ⛔ 2026-08-20 — 헤더(.chrome)가 position:fixed; right:0이고 문의
     플로팅(.ask)도 fixed다. 어딘가 자식 요소가 뷰포트보다 넓어지면
     모바일 브라우저가 그 넓어진 레이아웃 뷰포트에 맞춰 배율을 낮춰
     보여주는데, fixed 요소는 배율이 아니라 레이아웃 뷰포트 기준으로
     자리 잡아 우측 끝(언어 토글·문의 버튼)이 실제 화면 밖으로 밀려
     잘려 보이고, 사용자가 확대·축소하면 그 어긋남이 여백·덜컹거림으로
     드러난다(04편에서 스크린샷으로 실측 확인). html과 body 둘 다 막는다
     — 브라우저마다 어느 쪽 overflow가 실제로 레이아웃 뷰포트를
     제한하는지 갈려서, 하나만 걸면 빠지는 사례가 있다. */
  overflow-x: hidden;
}

body {
  margin: 0;
  min-height: 100svh;
  max-width: 100%;
  overflow-x: hidden;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body, var(--font-sans));
  opacity: 1;
  transition: opacity 200ms ease;
}

html.is-leaving body {
  opacity: 0;
}

body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 48px 48px;
  -webkit-mask-image: radial-gradient(
    ellipse at 50% 0%,
    #000 0%,
    transparent 78%
  );
  mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 78%);
}

/* corner registration marks */
.corner {
  position: fixed;
  width: 14px;
  height: 14px;
  z-index: 45;
  pointer-events: none;
  opacity: 0.4;
}
.corner--tl {
  top: 10px;
  left: 10px;
  border-top: 1px solid var(--fg-faint);
  border-left: 1px solid var(--fg-faint);
}
.corner--tr {
  top: 10px;
  right: 10px;
  border-top: 1px solid var(--fg-faint);
  border-right: 1px solid var(--fg-faint);
}
.corner--bl {
  bottom: 10px;
  left: 10px;
  border-bottom: 1px solid var(--fg-faint);
  border-left: 1px solid var(--fg-faint);
}
.corner--br {
  bottom: 10px;
  right: 10px;
  border-bottom: 1px solid var(--fg-faint);
  border-right: 1px solid var(--fg-faint);
}

/* scroll progress, css-driven, degrades to invisible (not broken) if unsupported.
   Neutral, not accent-gold — it's a passive readout, not something you
   click or tap (see the color-system note on .lang below). */
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 100%;
  background: var(--fg-dim);
  transform-origin: 0 50%;
  transform: scaleX(0);
  z-index: 60;
  pointer-events: none;
}
@supports (animation-timeline: scroll()) {
  .progress-bar {
    animation: progressGrow auto linear;
    animation-timeline: scroll(root);
  }
  @keyframes progressGrow {
    from {
      transform: scaleX(0);
    }
    to {
      transform: scaleX(1);
    }
  }
}

/* chrome: logo + language switch. align-items:flex-start (not center) so
   the lang toggle sits on the logo's first line ("Ben"), not centered
   against the full two-line lockup including the descriptor caption. */
.chrome {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  display: flex;
  /* 좌측 로고 락업(2행)과 우측 캡슐 묶음(1행)의 무게중심을 맞춘다.
     flex-start였을 때는 로고만 아래로 흘러 좌우가 비대칭으로 보였다. */
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: max(18px, env(safe-area-inset-top)) clamp(1rem, 4vw, 2rem) 18px;
  background: linear-gradient(to bottom, rgba(8, 9, 10, 0.78), transparent);
}

/* logo — "Ben" (name) stacked over "AI Crossborder" (descriptor, not
   part of the name). Weight + size contrast carries the hierarchy —
   the name reads first, the descriptor is a quiet caption underneath.
   text-align is pinned to left explicitly: this component gets reused
   in a right-aligned footer container (.site-footer__brand), and without
   an explicit value here it inherits text-align:right from there — which
   flips which edge the two lines share, so the header and footer lockups
   end up looking like different components. Pinning it keeps the lockup
   identical everywhere regardless of the surrounding container. */
/* display:grid (not flex) is what lets the "Ben" row match the
   descriptor row's width below without a hardcoded number: a single-
   column grid sizes that column to its widest row (the descriptor,
   uppercase + wide letter-spacing), then stretches every row — including
   .logo__name-row — to fill it. 마크가 들어갈 자리는 이 구조가 만들어
   준다 — 지금은 비어 있지만 폭은 확보되어 있다. */
.logo {
  display: inline-grid;
  gap: 0.2em;
  text-decoration: none;
  color: var(--fg);
  font-family: var(--font-latin);
  line-height: 1;
  text-align: left;
  white-space: nowrap;
}
/* font-size lives here (not on .logo__name) so a future mark would scale
   in lockstep with "Ben" as one unit — .logo--footer below scales
   this single value and everything inside follows, instead of the name
   text and the accent graphic drifting out of proportion at different
   sizes. */
.logo__name-row {
  display: flex;
  align-items: center;
  gap: 0.6em;
  min-width: 0;
  font-size: clamp(1rem, 2.6vw, 1.2rem);
}
.logo__name {
  font-family: var(--font-latin);
  font-weight: 700;
  font-size: 1em;
  letter-spacing: 0.005em;
}
/* accent group = hairline + X, welded together as one unit that fills
   whatever space is left in the row (flex:1) and is pinned flush to the
   row's own right edge. Because .logo__name-row is itself stretched by
   the parent grid to the width of the widest row (the descriptor, see
   .logo below), "flush to the row's right edge" is structurally the
   same edge as the descriptor's right edge — so the X's right edge and
   AI CROSSBORDER's right edge line up by construction, not by a hand-
   tuned letter-spacing number that would drift if the copy, font, or
   viewport changed. A small translateY nudge corrects for flex
   align-items:center landing on the text's geometric mid-line rather
   than its optical cap-height center (the empty space below the
   baseline for descenders pulls the geometric center down a bit). */
/* 마크는 제거된 상태다 (2026-08-10). 「Ben」 오른쪽은 여백으로 둔다 —
   선분+X, 스캔 눈금 두 안 모두 만족스럽지 않아 나중에 제대로 설계해서 넣기로
   했다. 그때까지 워드마크는 문자만으로 선다.
   ⚠️ 아래 .logo의 grid 배치는 그대로 두었다. 이름 행이 디스크립터 폭만큼
   늘어나는 구조라, 마크를 다시 넣을 자리가 이미 확보되어 있다. */
.logo__descriptor {
  /* explicit, not just inherited from .logo — the zh page briefly had a
     localized descriptor ("AI 跨境") here, and while the breakage that
     round turned out to be the differing glyph width/count itself (not a
     font leak), locking font-family directly on this element too means
     it can never silently start rendering in a CJK face no matter what
     content or inheritance chain surrounds it later. */
  font-family: var(--font-latin);
  font-weight: 500;
  font-size: 0.56em;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.logo--footer .logo__name-row {
  font-size: clamp(1.1rem, 3vw, 1.4rem);
}

/* language switch — segmented pill, 3-way */
/* 헤더 메뉴 — 자료실이 생기면서 갈 곳이 늘었다. 티저의 절제를
   깨지 않도록 언어 전환 옆에 링크 하나만 붙인다. */
.chrome__nav {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  flex-shrink: 0;
}
/* INSIGHT는 원래 맨 텍스트였다. 옆의 언어 토글이 테두리 있는 캡슐이라
   우측 묶음의 무게가 한쪽으로 쏠렸고, 좌측 로고 락업과도 크기가 안 맞았다.
   같은 높이·같은 라운드의 캡슐로 맞춰 두 요소를 한 벌로 만든다.
   높이는 언어 토글과 동일하게 계산한다 — 안쪽 1.9rem + 패딩 3px*2 + 테두리 1px*2. */
/* 높이는 로고 락업(2행) 높이에 맞춘다. 언어 토글과 같은 셈식으로 계산해
   두 캡슐이 항상 같은 높이가 되게 한다 — 안쪽 1.4rem + 패딩 2px*2 + 테두리 1px*2. */
/* ★ 폭이 절대 변하지 않아야 한다. 홈에서는 <a>, 목록에서는 <span aria-current>로
   찍히는데 예전에는 상태에 따라 글자 굵기가 달라져 캡슐 폭이 흔들렸다.
   굵기를 한 값으로 고정하고 폭도 못 박는다. */
.chrome__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 30px;
  width: 78px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: rgba(245, 244, 240, 0.03);
  font-family: var(--font-latin);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-indent: 0.16em; /* 자간 때문에 오른쪽으로 쏠리는 것을 되돌린다 */
  color: var(--fg-dim);
  text-decoration: none;
  white-space: nowrap;
  transition: color 180ms var(--ease), border-color 180ms var(--ease),
    background 180ms var(--ease);
}
.chrome__link:hover,
.chrome__link:focus-visible {
  color: var(--accent);
  border-color: var(--accent-soft);
  background: rgba(201, 163, 85, 0.08);
}
/* INSIGHT 목록에 서 있을 때는 링크가 아니라 span으로 찍힌다.
   지금 보고 있는 곳임을 캡슐 자체로 알린다. */
.chrome__link[aria-current="page"] {
  color: var(--accent);
  border-color: var(--accent-soft);
  background: rgba(201, 163, 85, 0.1);
}
/* 두 캡슐이 나란히 서면 구분선은 오히려 잡음이 된다. */
.chrome__sep {
  display: none;
}
/* 좁은 화면에서 캡슐 둘이 로고를 밀어내지 않도록 여백만 줄인다. */
@media (max-width: 420px) {
  .chrome__nav {
    gap: 0.4rem;
  }
  .chrome__link {
    width: 70px;
    letter-spacing: 0.12em;
    text-indent: 0.12em;
  }
  .lang a,
  .lang__current {
    width: 27px;
  }
}
@media (max-width: 420px) {
  .chrome__nav {
    gap: 0.6rem;
  }
  .chrome__link {
    font-size: 0.76rem;
  }
}

/* ★ 전면 재설계 (2026-08-10) — 예전에는 INSIGHT와 같은 알약 모양이라
   "버튼 하나를 양옆으로 찢어 토글을 끼운" 것처럼 보였다. 각진 탭 그룹으로
   바꿔 성격을 가른다. INSIGHT는 누르면 이동하는 버튼, 이쪽은 현재 상태를
   보여주는 탭이다. 모서리를 각지게 하고 칸 사이를 실선으로 나눈다. */
.lang {
  display: flex;
  gap: 0;
  padding: 0;
  height: 30px;
  border-radius: 7px;
  border: 1px solid var(--line);
  background: rgba(245, 244, 240, 0.03);
  overflow: hidden;
  flex-shrink: 0;
}
.lang > * + * {
  border-left: 1px solid var(--line);
}

/* explicit height + flex centering + a fixed Latin font (not --font-body,
   which differs per page: Pretendard/Noto Sans SC/system) — glyph metrics
   vary enough between those faces that vertical padding alone made the
   pill height visibly shift between language pages. Height is fixed and
   font is language-independent, so it can't move anymore. */
/* ★ 폰트가 페이지마다 달라지던 버그의 원인은 --font-latin(Space Grotesk)에
   「한」·「中」 글리프가 없다는 것이었다. 없으면 브라우저가 그 페이지에 로드된
   다음 폰트로 대체하는데, 그게 ko는 Pretendard, zh는 Noto Sans SC, en은 시스템
   폰트라 같은 글자가 세 가지 얼굴로 렌더됐다. 폭까지 달라져서 언어를 바꾸거나
   페이지를 옮길 때 토글이 흔들렸다.
   → 두 글자만 담은 Noto Sans KR 서브셋을 전 페이지에 로드하고 여기 고정한다.
   EN은 앞의 Space Grotesk가 받고, 한/中은 어느 페이지에서든 같은 얼굴이 된다.
   min-width는 이중 안전장치다 — 폰트가 늦게 뜨더라도 칸 폭이 안 변한다. */
.lang a,
.lang__current {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 28px;
  /* ★ min-width가 아니라 고정 width다. 활성 칸은 font-weight 600이라 같은
     글자라도 폭이 더 넓어지는데, min-width였을 때는 그 차이가 그대로 반영돼
     언어를 바꿀 때마다 캡슐 전체 폭이 흔들렸다. 폭을 못 박아 어떤 칸이
     활성이든 총 폭이 변하지 않게 한다. */
  width: 32px;
  padding: 0;
  border-radius: 0;
  /* ⛔ var(--font-latin)을 쓰면 안 된다 — 그 변수 안에 이미 generic sans-serif가
     들어 있어서, 뒤에 무엇을 붙여도 generic이 먼저 잡혀 도달하지 못한다.
     실제로 그렇게 붙였다가 여전히 시스템 폰트로 떨어지는 것을 실측으로 확인했다.
     그래서 여기서는 스택을 직접 쓴다. Noto Sans KR이 generic보다 앞에 와야
     「한」·「中」이 어느 페이지에서든 같은 얼굴로 렌더된다.
     (generic으로 떨어지면 Chrome이 html lang에 따라 다른 CJK 폰트를 고른다 —
     ko 페이지와 zh 페이지에서 같은 글자가 다르게 보이던 것이 이 때문이다.) */
  font-family: "Space Grotesk", "Noto Sans KR", -apple-system, "Segoe UI",
    sans-serif;
  font-size: 0.72rem;
  line-height: 1;
  letter-spacing: 0.02em;
  text-decoration: none;
  transition: background 250ms var(--ease), color 250ms var(--ease);
}

/* color system (9차 피드백): --accent (gold) is reserved for interactive
   elements only — things a visitor clicks or taps, or a state that
   reflects one (this pill showing the current language, a checked
   checkbox, a focused input, the submit button, hovered links). Non-
   interactive/decorative uses — the logo, the scroll progress bar, the
   rail dots, the scroll-cue, policy-page headings — stay in the neutral
   --fg/--fg-dim family instead. Kept as one comment here rather than
   repeated at every selector; search this file for "var(--accent)" to
   see every current use and confirm each one is genuinely clickable or a
   state of something clickable. */
/* ★ 활성 칸을 금색 통짜로 채우면 그 칸만 앞으로 튀어나와 보여서, 수치상
   높이가 같아도 INSIGHT 버튼과 다른 크기처럼 읽혔다. 채움을 걷고 은은한
   배경 + 하단 밑줄로 바꾼다. 선택된 탭으로 읽히면서 무게는 가벼워진다. */
.lang__current {
  background: rgba(201, 163, 85, 0.14);
  box-shadow: inset 0 -2px 0 var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.lang a {
  color: var(--fg-dim);
}

.lang a:hover {
  color: var(--fg);
}

/* scene rail */
.rail {
  position: fixed;
  right: 22px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 11px;
  z-index: 40;
}

.rail__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: 1px solid var(--fg-faint);
  transition: transform 400ms var(--ease), background 400ms var(--ease),
    border-color 400ms var(--ease);
}

.rail__dot.is-active {
  background: var(--fg);
  border-color: var(--fg);
  transform: scale(1.5);
}

@media (max-height: 480px) {
  .rail {
    display: none;
  }
}

/* scenes */
main {
  position: relative;
  z-index: 1;
}

.scene {
  min-height: 100svh;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  padding: 0 clamp(1.25rem, 6vw, 4rem);
}

/* alignment variation between scenes */
.scene--left {
  justify-content: flex-start;
}
.scene--left .scene__copy,
.scene--left .statement,
.scene--left .scene-desc {
  text-align: left;
}
.scene--right {
  justify-content: flex-end;
}
.scene--right .scene__copy,
.scene--right .statement,
.scene--right .scene-desc {
  text-align: right;
}
.scene--center {
  justify-content: center;
}
.scene--center .scene__copy,
.scene--center .statement,
.scene--center .scene-desc {
  text-align: center;
}
.scene--center .scene__copy {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.scene--right .scene__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

/* ---- background system ----------------------------------------------
   One fixed, full-viewport photo stack shared by the whole page — not a
   per-scene background. Layers crossfade (opacity only) as the active
   scene changes; the stack itself never moves or hard-cuts, which is
   what keeps consecutive scenes from reading as separate slides.
   A CSS gradient is always painted underneath as the base color, so if
   a photo 404s (onerror removes the <img>) the result is a plain dark
   gradient, never a blank/white flash. No blur — the brief was "make it
   read as a real photo," so the images are sharp; legibility comes from
   the scrim below, not from mushing the image. */
/* height:100svh instead of inset:0 on purpose — a fixed element sized
   with inset:0 resolves against the browser's large viewport (the one
   assuming toolbar/URL-bar is hidden), so on mobile, as the toolbar
   shows/hides during scroll the box's actual rendered size changes
   under it. With object-fit:cover images inside, that reads as the
   photo growing/shrinking mid-scroll. 100svh (small viewport height —
   toolbar assumed visible) is the conservative, stable size that
   doesn't move under scroll-driven toolbar collapse. */
.bg-stack {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100svh;
  z-index: 0;
  overflow: hidden;
  background: radial-gradient(120% 70% at 50% 0%, #14120e 0%, var(--bg) 60%);
}

.bg-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 1400ms var(--ease);
  will-change: opacity;
}
.bg-layer.is-active {
  opacity: 1;
}

/* Monotonic top-to-bottom darkening only — no mid-way lighter stop.
   An earlier version had a diagonal 3-stop gradient (dark→lighter→dark)
   that put a visibly lighter diagonal band across the middle of the
   screen, which read as a translucent "plate" sitting behind the copy —
   exactly the boxed-caption look this was supposed to avoid. A gradient
   that only ever gets darker going down has no such band. */
.bg-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(6, 7, 8, 0.38) 0%, rgba(6, 7, 8, 0.58) 45%, rgba(6, 7, 8, 0.82) 100%);
}

/* copy sits straight on the photo — no card/box behind it. A soft drop
   shadow does the legibility work a card used to, without reading as a
   panel. */
.statement,
.scene-desc,
.hero__line,
.contact__intro {
  text-shadow: 0 2px 20px rgba(4, 4, 5, 0.55);
}

/* ---- reveal-on-scroll system --------------------------------------
   Everything below defaults to fully visible with no transform. JS adds
   .js-armed the moment it starts observing an element, which is the only
   thing that switches an element into the hidden "about to reveal" state.
   If reveal.js fails to load, throws, or IntersectionObserver never
   fires for some reason, content simply stays visible — it never
   depends on JS succeeding to be seen at all. */

.hero__mark {
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  opacity: 1;
  transform: none;
}
.hero__mark.js-armed {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 900ms var(--ease), transform 900ms var(--ease);
}
.hero__mark.js-armed.in-view {
  opacity: 1;
  transform: translateY(0);
}

.hero__name {
  margin: 0;
  font-family: var(--font-latin);
  font-weight: 700;
  font-size: clamp(2.6rem, 11vw, 5.5rem);
  letter-spacing: 0.01em;
  line-height: 1;
  user-select: none;
}
.hero__descriptor {
  margin: 0.35rem 0 0;
  font-family: var(--font-latin);
  font-weight: 500;
  font-size: 0.78rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--fg-dim);
  user-select: none;
}

.hero__line {
  font-family: var(--font-body, var(--font-sans));
  font-size: clamp(0.95rem, 2.6vw, 1.15rem);
  line-height: 1.5;
  color: var(--fg-dim);
  max-width: 420px;
  text-align: center;
  margin: 0.4rem 0 0;
  text-wrap: balance;
  user-select: none;
}
html[lang="ko"] .hero__line {
  word-break: keep-all;
}

.scroll-cue {
  position: relative;
  z-index: 3;
  width: 1px;
  height: 42px;
  background: var(--fg-faint);
  overflow: hidden;
  opacity: 1;
}
.scroll-cue.js-armed {
  opacity: 0;
  transition: opacity 900ms var(--ease) 300ms;
}
.scroll-cue.js-armed.in-view {
  opacity: 1;
}
.scroll-cue span {
  position: absolute;
  top: -100%;
  left: 0;
  width: 100%;
  height: 100%;
  background: var(--fg);
  animation: scrollCue 1.9s ease-in-out infinite;
}
@keyframes scrollCue {
  0% {
    top: -100%;
  }
  50% {
    top: 0;
  }
  100% {
    top: 100%;
  }
}

/* statement scenes */
.scene--hero {
  flex-direction: column;
  gap: clamp(1.5rem, 6vh, 3rem);
}

.scene__num {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: clamp(0.5rem, 5vw, 3rem);
  font-family: var(--font-latin);
  font-size: clamp(7rem, 24vw, 16rem);
  font-weight: 600;
  line-height: 1;
  color: transparent;
  -webkit-text-stroke: 1px var(--line);
  z-index: 2;
  pointer-events: none;
  user-select: none;
}

.scene__copy {
  position: relative;
  z-index: 3;
}

.statement {
  font-family: var(--font-title, var(--font-sans));
  font-weight: 700;
  max-width: min(92vw, 640px);
  margin: 0 0 0.9rem;
  font-size: clamp(1.7rem, 4.8vw, 3.4rem);
  line-height: 1.16;
  letter-spacing: -0.015em;
  overflow-wrap: break-word;
  /* balance evens out the two auto-wrapped sub-lines a single authored
     line can still split into on a narrow viewport (each .statement line
     is already hand-broken into two lines via separate .line-mask spans —
     this only matters for further wrapping within one of those). Browsers
     without support simply ignore the declaration and keep the default
     "wrap" behavior — nothing to fall back to by hand. */
  text-wrap: balance;
  user-select: none;
}
html[lang="ko"] .statement {
  word-break: keep-all;
}

.line-mask {
  display: block;
  overflow: hidden;
}

.line-inner {
  display: inline-block;
  opacity: 1;
  transform: none;
}
.line-inner.js-armed {
  opacity: 0;
  transform: translateY(115%);
  transition: transform 900ms var(--ease), opacity 700ms var(--ease);
  transition-delay: calc(var(--i, 0) * 130ms);
}
.line-inner.js-armed.in-view {
  transform: translateY(0);
  opacity: 1;
}

.scene-desc {
  font-family: var(--font-body, var(--font-sans));
  font-weight: 400;
  max-width: min(88vw, 480px);
  margin: 0;
  font-size: clamp(0.92rem, 2.4vw, 1.1rem);
  line-height: 1.55;
  overflow-wrap: break-word;
  /* pretty avoids a lone orphan word stranded on its own last line —
     ignored gracefully by unsupporting browsers (falls back to "wrap") */
  text-wrap: pretty;
  color: var(--fg-dim);
  opacity: 1;
  transform: none;
  user-select: none;
}
html[lang="ko"] .scene-desc {
  word-break: keep-all;
}
.scene-desc.js-armed {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 650ms var(--ease), transform 650ms var(--ease);
  transition-delay: calc(var(--i, 0) * 130ms);
}
.scene-desc.js-armed.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* contact — deliberately NOT part of the scroll narrative. It has no
   data-scene attribute (so sceneIO skips it — no bg-layer crossfade, no
   rail dot) and paints its own opaque background, so none of the fixed
   photo stack shows through. This is meant to read as "the story ends,
   here's a form" rather than one more scene in the sequence — a flat
   surface, no photo, no scroll-cue. min-height is auto instead of the
   100vh every narrative scene uses, since a form should be sized to its
   content, not forced to fill the viewport like a cinematic scene. */
.scene--contact {
  justify-content: center;
  min-height: auto;
  background: var(--bg);
  padding-top: clamp(4.5rem, 14vh, 7rem);
  padding-bottom: clamp(3.5rem, 10vh, 5.5rem);
}

.contact {
  position: relative;
  z-index: 3;
  width: 100%;
  max-width: 440px;
}

.contact__intro {
  font-family: var(--font-title, var(--font-sans));
  font-weight: 700;
  font-size: clamp(1.5rem, 4vw, 2.1rem);
  line-height: 1.2;
  letter-spacing: -0.01em;
  margin: 0 0 0.5rem;
  text-wrap: balance;
  opacity: 1;
  transform: none;
  user-select: none;
}
html[lang="ko"] .contact__intro {
  word-break: keep-all;
}
.contact__intro.js-armed {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 650ms var(--ease), transform 650ms var(--ease);
  transition-delay: calc(var(--i, 0) * 130ms);
}
.contact__intro.js-armed.in-view {
  opacity: 1;
  transform: translateY(0);
}

.contact__sub {
  font-family: var(--font-body, var(--font-sans));
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--fg-dim);
  margin: 0 0 1.8rem;
  max-width: 380px;
  text-wrap: pretty;
  opacity: 1;
  transform: none;
  user-select: none;
}
html[lang="ko"] .contact__sub {
  word-break: keep-all;
}
.contact__sub.js-armed {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 650ms var(--ease), transform 650ms var(--ease);
  transition-delay: calc(var(--i, 0) * 130ms);
}
.contact__sub.js-armed.in-view {
  opacity: 1;
  transform: translateY(0);
}

.contact__form {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}

.field-group {
  opacity: 1;
  transform: none;
}
.field-group.js-armed {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 600ms var(--ease), transform 600ms var(--ease);
  transition-delay: calc(var(--i, 0) * 110ms);
}
.field-group.js-armed.in-view {
  opacity: 1;
  transform: translateY(0);
}

.field {
  width: 100%;
  padding: 0.95rem 1.1rem;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: rgba(245, 244, 240, 0.04);
  color: var(--fg);
  font-family: inherit;
  font-size: 1rem;
  transition: border-color 200ms var(--ease), box-shadow 200ms var(--ease);
}
.field::placeholder {
  color: var(--fg-dim);
}
.field:focus {
  outline: none;
  border-color: var(--accent-soft);
  box-shadow: 0 0 0 3px rgba(201, 163, 85, 0.14);
}
.field:invalid:not(:placeholder-shown):not(:focus) {
  border-color: rgba(217, 119, 87, 0.55);
}

/* select fields reuse .field's box but need the native chrome stripped
   and a custom arrow drawn in, since the default OS select arrow clashes
   with a dark UI. */
select.field {
  appearance: none;
  -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--fg-dim) 50%),
    linear-gradient(135deg, var(--fg-dim) 50%, transparent 50%);
  background-position: calc(100% - 21px) center, calc(100% - 16px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 2.5rem;
  cursor: pointer;
}
select.field:invalid {
  color: var(--fg-dim);
}
select.field option {
  background: #16181c;
  color: var(--fg);
}

/* multi-select interest area — checkboxes styled as toggleable pills,
   matching the visual language of .field without being a single-choice
   control. :has() lets the label itself pick up the checked look. */
.checkbox-group {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  border: none;
}
.checkbox-group legend {
  width: 100%;
  padding: 0;
  margin: 0 0 0.55rem;
  font-size: 0.78rem;
  color: var(--fg-dim);
  text-wrap: pretty;
}

/* wraps a phrase that reads as one semantic unit (a brand name, a tightly
   bound clause) so it only ever wraps as a whole, never mid-phrase —
   applied by hand at specific spots in the copy, not globally, since
   forcing every phrase atomic would risk overflowing narrow viewports */
.nowrap-unit {
  display: inline-block;
}
.checkbox-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.42rem;
  padding: 0.55rem 0.9rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(245, 244, 240, 0.03);
  color: var(--fg-dim);
  font-size: 0.86rem;
  cursor: pointer;
  transition: border-color 200ms var(--ease), color 200ms var(--ease),
    background 200ms var(--ease);
}
.checkbox-pill input {
  width: 14px;
  height: 14px;
  accent-color: var(--accent);
}
.checkbox-pill:has(input:checked) {
  border-color: var(--accent-soft);
  background: rgba(201, 163, 85, 0.1);
  color: var(--accent);
}

.consent {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--fg-dim);
  cursor: pointer;
}
.consent input[type="checkbox"] {
  margin-top: 0.2rem;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  accent-color: var(--accent);
}
.consent a {
  color: var(--accent);
}

.submit {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  padding: 1rem 1.4rem;
  border-radius: 12px;
  border: none;
  background: var(--accent);
  color: #14100a;
  font-family: inherit;
  font-size: 0.98rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 150ms var(--ease), opacity 250ms var(--ease);
}
.submit:active {
  transform: scale(0.97);
}
.submit__check {
  display: none;
}
.submit.is-done {
  opacity: 0.75;
  cursor: default;
}
.submit.is-done .submit__label {
  display: none;
}
.submit.is-done .submit__check {
  display: block;
}

/* validation feedback — novalidate suppresses the browser's own messages, so
   rejections are silent without this */
.field-error {
  margin: 0.5rem 0 0;
  font-size: 0.82rem;
  line-height: 1.5;
  color: #d97757;
}
:lang(ko) .field-error {
  word-break: keep-all;
}
.field.has-error,
.consent.has-error {
  border-color: rgba(217, 119, 87, 0.55);
}
.consent.has-error {
  color: #d97757;
}

/* submission confirmation — replaces the form so the outcome is unmistakable */
.contact__form.is-sent {
  display: none;
}
.contact__done {
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 2.25rem 1.75rem;
  text-align: center;
  opacity: 0;
  transform: translateY(8px);
  animation: contact-done-in 520ms var(--ease) forwards;
}
@keyframes contact-done-in {
  to {
    opacity: 1;
    transform: none;
  }
}
.contact__done-mark {
  color: var(--accent);
  margin-bottom: 1rem;
}
.contact__done-title {
  margin: 0 0 0.65rem;
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--fg);
}
.contact__done-desc {
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.65;
  color: var(--fg-dim);
  text-wrap: pretty;
}
:lang(ko) .contact__done-desc,
:lang(ko) .contact__done-title {
  word-break: keep-all;
}
@media (prefers-reduced-motion: reduce) {
  .contact__done {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

.form-status {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  margin: 0;
}

/* footer — full width, not centered/narrow. Opaque background so it
   reads as a clean stop, not the fixed photo bleeding through underneath. */
.site-footer {
  position: relative;
  z-index: 1;
  background: var(--bg);
  border-top: 1px solid var(--line);
  padding: 2.5rem clamp(1.25rem, 6vw, 4rem) 3rem;
  display: flex;
  flex-direction: column;
  gap: 2rem;
}
@media (min-width: 700px) {
  .site-footer {
    flex-direction: row;
    justify-content: space-between;
    align-items: flex-end;
  }
}
.site-footer__legal {
  max-width: 560px;
}
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  margin: 0 0 0.75rem;
}
.footer-links a {
  font-size: 0.78rem;
  color: var(--fg-dim);
  text-decoration: none;
}
.footer-links a:hover {
  color: var(--accent);
}
.footer-links a:not(:last-child)::after {
  content: "|";
  margin-left: 0.55rem;
  color: var(--fg-faint);
}
.site-footer .legal {
  margin: 0 0 0.6rem;
  font-size: 0.76rem;
  line-height: 1.65;
  color: var(--fg-dim);
  text-wrap: pretty;
}
.site-footer .legal:last-child {
  margin-bottom: 0;
}
.site-footer a {
  color: var(--fg-dim);
}
.site-footer a:hover {
  color: var(--accent);
}
.site-footer__brand {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
  gap: 0.5rem;
}
.copyright {
  margin: 0;
  font-size: 0.72rem;
  color: var(--fg-faint);
}

/* privacy policy page — plain scrolling document, not a scroll-snap scene */
.policy {
  position: relative;
  z-index: 1;
  max-width: 720px;
  margin: 0 auto;
  padding: 4rem clamp(1.25rem, 6vw, 3rem) 5rem;
}
.policy h1 {
  font-family: ui-serif, Georgia, serif;
  font-size: clamp(1.6rem, 4vw, 2.2rem);
  margin: 0 0 0.5rem;
}
.policy h2 {
  font-size: 1.05rem;
  font-weight: 700;
  margin: 2.25rem 0 0.75rem;
  color: var(--fg);
}
.policy p,
.policy li {
  color: var(--fg-dim);
  line-height: 1.7;
  font-size: 0.95rem;
}
.policy ul {
  padding-left: 1.2rem;
}
.policy section + section {
  margin-top: 3.5rem;
  padding-top: 3rem;
  border-top: 1px solid var(--line);
}
.policy .policy-date {
  margin-top: 2.5rem;
  font-size: 0.85rem;
  color: var(--fg-dim);
}
.policy a {
  color: var(--accent);
}
.policy .back-link {
  display: inline-block;
  margin-top: 2rem;
  font-size: 0.85rem;
  color: var(--fg-dim);
  text-decoration: none;
}
.policy .back-link:hover {
  color: var(--fg);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-timeline: none !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ★ 페이지 전환이 뚝딱거리던 것을 잡는다 (2026-08-10).
   나가는 쪽만 페이드가 걸려 있고 들어오는 쪽은 즉시 그려졌다. 그래서 새 페이지의
   웹폰트가 늦게 뜨면 글자가 한 번 바뀌는 순간이 그대로 노출됐다.
   들어올 때도 짧게 페이드를 줘서 그 구간을 덮는다. */
@keyframes pageIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
body {
  animation: pageIn 240ms var(--ease) both;
}
@media (prefers-reduced-motion: reduce) {
  body {
    animation: none;
  }
}

/* 고정 배경 격자는 마스크가 걸린 전체 화면 레이어다. 자기 레이어로 승격해
   두지 않으면 본문이 스크롤될 때마다 같이 다시 그려진다. */
body::before {
  will-change: transform;
  transform: translateZ(0);
}
.chrome {
  will-change: transform;
}

/* 푸터의 문의하기는 다른 링크와 성격이 다르다 — 약관·방침은 참고용이고
   이쪽만 행동을 요청한다. 색으로 가른다.
   `.footer-links a`(0,1,1)가 먼저 잡히므로 컨테이너를 붙여 우선순위를 맞춘다. */
.footer-links .footer-links__cta {
  color: var(--accent);
}
.footer-links .footer-links__cta:hover {
  color: var(--fg);
}
