/* 레이아웃 + 파트 스타일. 편집기 전용 규칙은 걷어냈습니다. */
@font-face{font-family:'Space Grotesk';font-style:normal;font-weight:400;font-display:swap;src:url(./fonts/grotesk-400.woff2) format('woff2');}
@font-face{font-family:'Space Grotesk';font-style:normal;font-weight:700;font-display:swap;src:url(./fonts/grotesk-700.woff2) format('woff2');}

/**
 * _core/core.css
 * 모든 효과가 공유하는 최소한의 기반 스타일.
 * 이 파일 하나만 먼저 불러오고, 효과별 style.css를 뒤에 붙이면 됩니다.
 */

/* ------------------------------------------------------------------ *
 * 공용 토큰
 * 포트폴리오 테마에 맞춰 :root에서 덮어쓰면 8개 효과가 한꺼번에 따라옵니다.
 * ------------------------------------------------------------------ */
:root {
  --fx-ease: cubic-bezier(0.22, 1, 0.36, 1);      /* 감속 위주, 고급스러운 느낌 */
  --fx-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --fx-duration: 420ms;
  --fx-duration-fast: 220ms;

  --fx-radius: 16px;
  --fx-surface: #ffffff;
  --fx-surface-2: #f4f5f7;
  --fx-border: rgba(0, 0, 0, 0.08);
  --fx-text: #16181d;
  --fx-text-dim: #6b7280;
  --fx-accent: #5b5bd6;
  --fx-glare: rgba(255, 255, 255, 0.55);
  --fx-shadow: 0 10px 30px -12px rgba(16, 18, 29, 0.28);
}
@media (prefers-color-scheme: dark) {
  :root {
    --fx-surface: #16181d;
    --fx-surface-2: #1e2027;
    --fx-border: rgba(255, 255, 255, 0.1);
    --fx-text: #f2f3f5;
    --fx-text-dim: #9ca3af;
    --fx-glare: rgba(255, 255, 255, 0.22);
    --fx-shadow: 0 12px 34px -14px rgba(0, 0, 0, 0.7);
  }
}
/* 사용자가 테마 토글로 강제 지정하는 경우 (html[data-theme]) */
:root[data-theme='dark'] {
  --fx-surface: #16181d;
  --fx-surface-2: #1e2027;
  --fx-border: rgba(255, 255, 255, 0.1);
  --fx-text: #f2f3f5;
  --fx-text-dim: #9ca3af;
  --fx-glare: rgba(255, 255, 255, 0.22);
  --fx-shadow: 0 12px 34px -14px rgba(0, 0, 0, 0.7);
}
:root[data-theme='light'] {
  --fx-surface: #ffffff;
  --fx-surface-2: #f4f5f7;
  --fx-border: rgba(0, 0, 0, 0.08);
  --fx-text: #16181d;
  --fx-text-dim: #6b7280;
  --fx-glare: rgba(255, 255, 255, 0.55);
  --fx-shadow: 0 10px 30px -12px rgba(16, 18, 29, 0.28);
}
/* ------------------------------------------------------------------ *
 * 마운트 상태 클래스
 * ------------------------------------------------------------------ */

/* JS가 붙었다는 표시. CSS에서 "JS 없을 때"와 분기할 때 씁니다. */
.fx {
  /* 3D 변환 시 텍스트가 흐려지는 것을 줄입니다. */
  -webkit-font-smoothing: antialiased;
}
/*
 * 가드에 걸려 꺼진 상태.
 * 왜 꺼졌는지는 DOM의 data-fx-skipped 속성에 남습니다.
 * (devtools에서 요소를 보면 reduced-motion / no-webgl / device-tier 등이 보입니다)
 */
.fx-inert {
  /* 효과가 없어도 레이아웃이 깨지지 않도록 변환만 중립화합니다. */
  transform: none !important;
}
/* ------------------------------------------------------------------ *
 * 안전망
 * 개별 효과는 각자 가드를 갖지만, CSS 트랜지션까지 남는 경우를 대비합니다.
 * .fx 하위로만 좁게 적용해 페이지 전체 애니메이션을 건드리지 않습니다.
 * ------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .fx,
  .fx * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
/* ------------------------------------------------------------------ *
 * 접근성: 키보드 포커스는 어떤 3D 효과 위에서도 보여야 합니다.
 * ------------------------------------------------------------------ */
.fx :focus-visible,
.fx:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 3px;
  border-radius: 6px;
}
/* 스크린 리더 전용 텍스트(효과 설명 등에 사용) */
.fx-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* 캔버스(iframe) 안에서만 쓰이는 스타일. 에디터 UI로 새지 않습니다. */

* { box-sizing: border-box; }
html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  -webkit-font-smoothing: antialiased;
}
button { font: inherit; color: inherit; }
/* 와이어프레임 안의 모든 글자에 적용되는 줄바꿈 규칙 */
.wf-page, .wf-page * {
  word-break: keep-all;       /* 한글: 어절 단위로 끊기 */
  overflow-wrap: anywhere;    /* 그래도 안 들어가면 강제로 */
}
/* ── 페이지 골격 ───────────────────────────────────────────── */

.wf-page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  padding: 10px;
  /* 그리드 가이드(.wf-guide)의 기준 상자 */
  position: relative;
}
/*
 * 그리드 가이드 — 컬럼(연한 칠) + 거터(빈 칸)를 페이지 위에 겹쳐 그립니다.
 * 콘텐츠 최대 폭(--wf-max)에 맞춰 가운데 정렬되므로 본문 컬럼과 일치합니다.
 * 편집 화면 전용: 결과물 HTML에는 이 요소 자체가 나가지 않습니다.
 */
.wf-guide {
  position: absolute;
  top: 10px; bottom: 10px;
  max-width: var(--wf-max);
  margin: 0 auto;
  pointer-events: none;
  z-index: 30;
  background: repeating-linear-gradient(
    90deg,
    rgba(79, 107, 255, 0.09) 0,
    rgba(79, 107, 255, 0.09) var(--gcw),
    transparent var(--gcw),
    transparent calc(var(--gcw) + var(--ggap))
  );
  outline: 1px dashed rgba(79, 107, 255, 0.35);
  outline-offset: -1px;
}
.wf { display: grid; align-content: start; }
/* 뼈대 격자만 남는 높이를 채웁니다 — 추가 행이 위에 와도 흔들리지 않게
   :first-child가 아니라 '추가 행이 아닌 격자'로 가립니다. */
.wf-page > .wf:not(.wf--row) { flex: 1; }
.wf--row { position: relative; }
/* '행을 콘텐츠 폭에 맞춤' — 와이드 화면에서 전폭 행(본문만 가운데 열)과
 * 다열 행(카드가 화면 끝까지)이 섞여 열이 어긋나 보이는 것을 막습니다.
 * 켠 문서만 적용 — 풀블리드 밴드를 쓰는 기존 문서는 그대로입니다. */
.wf-page.is-rowmax > .wf {
  width: 100%;
  max-width: var(--wf-max);
  margin-left: auto;
  margin-right: auto;
}
/* ── 영역 ──────────────────────────────────────────────────── */

.wf__region {
  position: relative;
  display: flex; flex-direction: column;
  gap: 10px;
  min-width: 0;   /* grid 아이템 기본값(auto)이면 콘텐츠가 칸을 밀어냅니다 */
  min-height: 0;
  overflow: hidden;
  border-style: solid;
  border-color: transparent;
  transition: border-color 0.14s, background 0.14s;
}
.wf__region.is-filled {
  border-color: var(--wf-line, currentColor);
  cursor: pointer;
}
/*
 * 스크롤에 고정되는 영역.
 *
 * 두 가지가 빠져 있어서 **내려갈 때 아래 내용이 그대로 비쳐 보였습니다.**
 *
 *   z-index  없으면 `auto`입니다. sticky라도 나중에 오는 형제가 위로 올라오므로
 *            고정된 헤더 위로 본문이 지나갑니다.
 *   배경     영역 기본 배경은 투명입니다. 고정 헤더 뒤로 글자가 흘러가는 것이
 *            그대로 보입니다.
 *
 * 배경은 `--wf-bg` 변수로 받습니다. `inherit`으로 해 봤더니 투명이 그대로
 * 나왔습니다 — 상속은 **직접 부모**(.wf--row, 투명)에서 받고 조부모인
 * `.wf-page`까지 올라가지 않기 때문입니다. CSS 변수는 손자까지 상속되므로
 * 이 방법만 동작합니다. 테마가 바뀌면 따라오고, 사용자가 이 영역에 색을 정하면
 * 인라인이 이겨서 그 색을 씁니다.
 *
 * 값을 4로 둔 것은 편집 표식(.wf__tag는 3)보다는 위, 모달(40)보다는 아래라는 뜻입니다.
 * 더 세밀하게 겹치는 순서를 정하려면 영역 설정의 '레이어 순서'가 이 값을 덮습니다.
 */
.wf__region.is-sticky {
  position: sticky;
  top: 10px;
  align-self: start;
  z-index: 4;
  background-color: var(--wf-bg, transparent);
}
.wf__region.has-divider { border-bottom-color: var(--wf-line); }
/*
 * 내릴 땐 숨기고 올리면 보이는 고정 바(hideOnScroll).
 * 방향 감지는 캔버스(Canvas)와 결과물(init.js)의 몇 줄짜리 리스너가 하고,
 * 여기서는 미끄러져 들어오고 나가는 움직임만 맡습니다.
 */
[data-hide-scroll],
.wf-headbar:has([data-hide-scroll]) { transition: transform 0.28s ease; }
.wf-nav-hidden { transform: translateY(-130%) !important; }
/* ── 메뉴 ──────────────────────────────────────────────────── */

/*
 * gap 기본값이 CSS에 있어야 합니다.
 *
 * 캔버스의 Menu는 `gap`을 인라인으로 주는데(기본 26px), 내보내기의 `menuHTML`은
 * 파트가 gap을 정해 두지 않으면 인라인을 아예 안 붙였습니다. Top Header·Header는
 * gap 옵션이 없어서 **결과물에서만 메뉴가 다 붙어 버렸습니다.**
 * 기본값을 CSS로 내려 두면 인라인이 있든 없든 같은 모양이 됩니다.
 */
.wf-nav { display: flex; align-items: center; flex-wrap: wrap; gap: 26px; }
.wf-nav--v { flex-direction: column; align-items: flex-start; gap: 10px; }
.wf-nav__item {
  background: none; border: 0; padding: 3px 0;
  font-size: 0.9333em; cursor: pointer;
  opacity: 0.62;
  border-bottom: 1.5px solid transparent;
}
.wf-nav__item:hover { opacity: 1; }
.wf-nav__item.is-current { opacity: 1; font-weight: 600; border-bottom-color: var(--wf-accent); }
/* ── 다크/라이트 토글 버튼 ─────────────────────────────────────
 * 라이트에서는 달(☾), 다크에서는 해(☀)를 보여줍니다 — 누르면 무엇이
 * 되는지를 아이콘이 말해 줍니다. 색 전환은 .wf-page의 토큰 교체가 맡고,
 * 여기서는 부드럽게 넘어가는 transition만 둡니다. */
.wf-mode {
  flex: none;
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--wf-line, currentColor);
  /* 원이 아니라 살짝 둥근 사각형 — 요즘 툴바 버튼의 관습입니다. */
  border-radius: 8px;
  background: var(--wf-surface, transparent);
  color: inherit;
  line-height: 1;
  cursor: pointer;
  opacity: 0.85;
  transition: border-color 0.18s, background 0.18s, opacity 0.18s;
}
.wf-mode:hover { border-color: var(--wf-accent); opacity: 1; }
.wf-mode__sun { display: none; }
.wf-page.is-dark .wf-mode__sun { display: inline; }
.wf-page.is-dark .wf-mode__moon { display: none; }
/* 토큰이 갈아끼워질 때 화면 전체가 부드럽게 넘어갑니다. */
.wf-page { transition: background 0.25s ease, color 0.25s ease; }
/* ── 파트 ──────────────────────────────────────────────────── */

.wf-bar {
  display: flex; align-items: center; gap: 16px;
  width: 100%; min-width: 0;
  flex-wrap: wrap;
}
.wf-bar > * { min-width: 0; }
.wf-bar__logo {
  font-size: 1.1333em; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  flex: 0 1 auto;
}
.wf-bar--foot { font-size: 0.8667em; opacity: 0.85; }
.wf-foot__mail { opacity: 0.7; }
.wf-foot__time { font-variant-numeric: tabular-nums; opacity: 0.7; }
.wf-btn {
  display: inline-block;
  padding: 11px 24px;
  background: var(--wf-accent);
  color: #fff;
  font-size: 0.9333em; font-weight: 600;
  border-radius: inherit;
  /* 결과물에서는 <a>라 밑줄이 붙습니다 — 캔버스의 <button>과 같게 지웁니다. */
  text-decoration: none;
}
.wf-btn--sm { padding: 7px 15px; font-size: 0.8667em; }
.wf-hero {
  min-width: 0;
  display: flex; flex-direction: column; gap: 16px;
  justify-content: center; flex: 1;
  width: 100%; max-width: var(--wf-max); margin: 0 auto;
}
.wf-hero__title { margin: 0; line-height: 1.15; white-space: pre-line; letter-spacing: -0.02em; }
.wf-hero__sub { margin: 0; font-size: 1.1333em; color: var(--wf-muted); white-space: pre-line; }
.wf-aside {
  min-width: 0; display: flex; flex-direction: column; gap: 10px; width: 100%; }
.wf-aside__name { margin: 0; font-size: 2.2667em; line-height: 1.15; letter-spacing: -0.02em; }
.wf-aside__role { margin: 0; font-size: 1.0667em; }
.wf-aside__bio { margin: 0 0 8px; font-size: 0.9333em; color: var(--wf-muted); white-space: pre-line; }
.wf-sec {
  min-width: 0; display: flex; flex-direction: column; gap: 14px; width: 100%; max-width: var(--wf-max); margin: 0 auto; }
.wf-sec__title { margin: 0; font-size: 1.7333em; letter-spacing: -0.01em; }
.wf-sec__cols { display: grid; gap: 22px; width: 100%; }
.wf-sec__cols > * { min-width: 0; }
.wf-sec__body { margin: 0; font-size: 0.9667em; line-height: 1.75; color: var(--wf-muted); white-space: pre-line; }
.wf-gal {
  min-width: 0; display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: var(--wf-max); margin: 0 auto; }
.wf-gal__grid { display: grid; gap: 16px; width: 100%; }
.wf-gal__grid > * { min-width: 0; }
.wf-card {
  display: flex; flex-direction: column;
  min-width: 0;
  background: var(--wf-surface);
  border: 1px solid var(--wf-line);
  border-radius: inherit;
  overflow: hidden;
}
.wf-card__thumb {
  position: relative; display: block; width: 100%;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--wf-accent) 14%, transparent),
    color-mix(in srgb, var(--wf-accent) 4%, transparent));
  border-bottom: 1px solid var(--wf-line);
}
.wf-card__thumb em {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-style: normal; font-size: 0.8667em; color: var(--wf-muted);
}
.wf-card__body {
  display: flex; flex-direction: column; gap: 5px;
  padding: 14px 16px 16px;
  min-width: 0;
}
.wf-card__title { font-size: 1.0em; font-weight: 700; line-height: 1.35; }
.wf-card__stack { font-size: 0.8em; color: var(--wf-accent); }
/* 기술 스택을 리드미 배지로 — 배지 모양·색 규칙은 Section의 badges와 공용입니다.
 * 카드는 좁으니 한 단계 작게 두고, 넘치면 다음 줄로 접힙니다. */
.wf-card__stack--badges {
  display: flex; flex-wrap: wrap; gap: 5px;
  margin: 1px 0 2px;
}
.wf-card__stack--badges .wf-tag--badge { font-size: 0.7em; padding: 4px 9px; }
/* 진행 상태 칩 — 썸네일 왼쪽 아래에 얹습니다(참고한 포트폴리오들의 공통 자리).
 * 썸네일을 끄면 글 위에 인라인으로 서도록 아래에서 되돌립니다. */
.wf-card__status {
  position: absolute; left: 10px; bottom: 10px; z-index: 2;
  padding: 4px 10px;
  border-radius: 5px;
  background: var(--st-bg, #6b7280);
  color: var(--st-fg, #fff);
  font-size: 0.7em; font-weight: 700; letter-spacing: 0.01em;
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.18);
}
.wf-card__body > .wf-card__status {
  position: static;
  align-self: flex-start;
  box-shadow: none;
}
.wf-card__desc { font-size: 0.8333em; line-height: 1.65; color: var(--wf-muted); }
.wf-card__link { margin-top: 2px; font-size: 0.7667em; color: var(--wf-accent); }
/* 태그 목록 (스킬 등) */
.wf-tags { display: flex; flex-wrap: wrap; gap: 7px; }
.wf-tag {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 4px 11px;
  border: 1px solid var(--wf-line); border-radius: 999px;
  font-size: 0.8333em;
}
.wf-tag em { font-style: normal; font-size: 0.7em; color: var(--wf-muted); }
/* 깃허브 리드미 배지 — shields.io 모양. 브랜드색(--bdg-*)은 인라인으로 실립니다.
 * 밝은 브랜드색(노랑·하늘)은 techColors.js가 검정 글자를 골라 줍니다. */
.wf-tag--badge {
  border: 0;
  border-radius: 4px;
  padding: 5px 11px;
  background: var(--bdg-bg, #555);
  color: var(--bdg-fg, #fff);
  font-size: 0.7667em;
  font-weight: 600;
  letter-spacing: 0.01em;
  align-items: center; /* 로고와 글자의 세로 중심을 맞춥니다 (기본은 baseline) */
}
.wf-tag__logo { width: 1.05em; height: 1.05em; flex: none; }
.wf-tag--badge em {
  color: inherit;
  opacity: 0.8;
  margin-left: 7px;
  padding-left: 7px;
  border-left: 1px solid color-mix(in srgb, currentColor 35%, transparent);
}
/* ── 스킬 태그 마퀴 ─────────────────────────────────────────────
 * 같은 세트 두 벌을 이어 붙이고 트랙을 -50% 밀면 이음새 없이 무한히 흐릅니다.
 * 세트 안 간격과 세트 끝 여백(padding-right)이 같아야 이음새가 안 보입니다.
 * 양끝은 마스크로 페이드 — 잘려 나가는 태그가 부드럽게 사라집니다.
 */
.wf-marquee {
  width: 100%;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.wf-marquee__track {
  display: flex;
  width: max-content;
  animation: wf-marquee var(--marq-t, 20s) linear infinite;
}
.wf-marquee__set { display: flex; gap: 7px; padding-right: 7px; }
@keyframes wf-marquee { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  /* 흐르지 않고 보통 태그 줄처럼 놓입니다. 두 번째 벌은 치웁니다. */
  .wf-marquee__track { animation: none; flex-wrap: wrap; }
  .wf-marquee__set[aria-hidden] { display: none; }
  .wf-marquee { -webkit-mask-image: none; mask-image: none; }
}
/* ── 배너 ① Cover — 큰 이미지 + 문구 ──────────────────────────
 * 이미지는 절대 배치로 깔고 문구만 위로 띄웁니다. 어둡게(veil)는 필터가
 * 아니라 덮개입니다 — 필터로 하면 그 위 글자까지 함께 어두워집니다.
 */
.wf-cover {
  position: relative;
  display: flex; flex-direction: column;
  width: 100%;
  overflow: hidden;
  border-radius: inherit;
}
.wf-cover__img,
.wf-cover__empty {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* 이미지가 없을 때 — 무엇을 채울 자리인지 보이는 은은한 격자 */
.wf-cover__empty {
  background:
    repeating-linear-gradient(45deg, var(--wf-surface, #8882) 0 10px, transparent 10px 20px),
    var(--wf-surface, #8881);
  opacity: 0.5;
}
.wf-cover__veil {
  position: absolute; inset: 0;
  background: #000;
  pointer-events: none;
}
.wf-cover__body {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; gap: 8px;
  padding: min(40px, 5vw);
  max-width: var(--wf-max);
}
.wf-cover__title {
  font-size: clamp(1.4em, 4.4vw, 2.8em);
  font-weight: 800; line-height: 1.15; letter-spacing: -0.02em;
  color: #fff;
  white-space: pre-line;
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.35);
}
.wf-cover__sub { font-size: 1.0667em; color: rgba(255, 255, 255, 0.86); }
/* 켄번즈 — 아주 느린 확대. '움직임'보다 '숨쉬기'에 가깝습니다. */
.wf-cover.is-kb .wf-cover__img { animation: wf-kenburns 26s ease-in-out infinite alternate; }
@keyframes wf-kenburns {
  from { transform: scale(1) translate(0, 0); }
  to { transform: scale(1.12) translate(-1.5%, -1%); }
}
/*
 * 스크롤 따라 확대 — 배너가 화면을 지나는 동안 커집니다.
 * view() 타임라인이라 스크립트 0. 미지원이면 그냥 정지 이미지입니다.
 */
.wf-cover.is-zoom .wf-cover__img {
  animation: wf-cover-zoom linear both;
  animation-timeline: view();
  animation-range: cover 0% cover 100%;
  animation-duration: 1ms;   /* 뷰 타임라인에선 무시되지만 없으면 시작하지 않습니다 */
}
@keyframes wf-cover-zoom { from { transform: scale(1); } to { transform: scale(1.18); } }
@media (prefers-reduced-motion: reduce) {
  .wf-cover.is-kb .wf-cover__img,
  .wf-cover.is-zoom .wf-cover__img { animation: none; }
}
/* ── 배너 ② Marquee — 큰 글씨가 흐르는 띠 ────────────────────── */
.wf-mq {
  width: 100%;
  overflow: hidden;
  display: flex; align-items: center;
  border-radius: inherit;
}
.wf-mq__track {
  display: flex;
  width: max-content;
  animation: wf-marquee var(--mq-t, 26s) linear infinite;
  animation-direction: var(--mq-dir, normal);
}
.wf-mq__set { display: flex; align-items: center; }
.wf-mq__txt {
  font-size: var(--mq-size, 2.6em);
  font-weight: 800; letter-spacing: -0.02em; line-height: 1.1;
  white-space: nowrap;
}
.wf-mq__sep {
  font-size: calc(var(--mq-size, 2.6em) * 0.62);
  font-style: normal;
  color: var(--wf-accent);
  padding: 0 0.35em;
}
/* 윤곽 글자 — 채움을 비우고 선만 남깁니다. 큰 글씨에서 인상이 강합니다. */
.wf-mq.is-outline .wf-mq__txt {
  color: transparent;
  -webkit-text-stroke: 1.5px currentColor;
}
.wf-mq.is-pausable:hover .wf-mq__track { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  /* 흐르지 않고 한 줄만 남깁니다 — 넘치는 부분은 잘립니다. */
  .wf-mq__track { animation: none; }
  .wf-mq__set[aria-hidden] { display: none; }
}
/* ── 배너 ③ CTA — 행동 유도 + 버튼 ───────────────────────────── */
.wf-cta {
  position: relative;
  display: flex; flex-direction: column; gap: 12px;
  width: 100%; max-width: var(--wf-max); margin: 0 auto;
  justify-content: center;
  border-radius: inherit;
}
.wf-cta__title {
  font-size: clamp(1.2em, 3vw, 1.9em);
  font-weight: 700; line-height: 1.25; letter-spacing: -0.01em;
}
.wf-cta__sub { font-size: 1.0667em; color: var(--wf-muted); }
.wf-cta__btn { margin-top: 6px; }
/*
 * 배경 그라디언트 흐름 — 강조색 주변을 아주 천천히 오갑니다.
 * 영역 배경색을 덮지 않도록 뒤에 깔고, 내용은 z-index로 올립니다.
 */
.wf-cta.is-grad::before {
  content: '';
  position: absolute; inset: calc(-1 * min(44px, 6vw));
  z-index: 0;
  border-radius: inherit;
  background: linear-gradient(
    100deg,
    color-mix(in srgb, var(--wf-accent) 22%, transparent),
    color-mix(in srgb, var(--wf-accent) 6%, transparent),
    color-mix(in srgb, var(--wf-accent) 22%, transparent)
  );
  background-size: 220% 100%;
  animation: wf-cta-grad 14s ease-in-out infinite alternate;
}
.wf-cta.is-grad > * { position: relative; z-index: 1; }
@keyframes wf-cta-grad { from { background-position: 0% 50%; } to { background-position: 100% 50%; } }
@media (prefers-reduced-motion: reduce) {
  .wf-cta.is-grad::before { animation: none; }
}
/* ── 영역 안 자유 배치 ─────────────────────────────────────────
 * 영역은 격자에 그대로 두고, **안의 항목들**(제목·부제·버튼 …)만 원하는
 * 자리에 놓습니다. 자리는 영역이 변수로 실어 보내고(--fiNx/--fiNy),
 * 실제 배치는 아래 nth-child 규칙이 합니다 — 마크업을 건드리지 않으므로
 * 캔버스와 내보낸 결과물이 같은 그림을 그립니다.
 *
 * 본문을 고를 때 편집 전용 클래스 이름(.wf__tag 등)을 쓰면 안 됩니다 — 내보낼 때
 * 그 규칙이 통째로 걷힙니다(stripEditorCss). 그래서 요소 이름으로 가릅니다.
 *
 * `> div`가 아니라 `> :not(span, button)`인 이유: 편집 화면에서는 파트 본문이 늘
 * div지만 **내보낸 결과물에서는 다릅니다.** 푸터 본문은 의미 태그를 살려 `<footer>`로
 * 나가므로 `> div`가 빗나가, 자유 배치를 켠 푸터가 편집 화면과 다르게 그려졌습니다.
 * 편집 표식은 span(칩·손잡이·배지)과 button(테두리 + 버튼)뿐이라 그 둘만 빼면
 * 본문이 정확히 잡힙니다 — button을 빼먹으면 + 버튼이 본문으로 오인되어
 * 끌기가 죽고 + 버튼 모양도 망가집니다(실제 있었던 사고).
 * `:not()`에 목록으로 넣으면 특이도가 태그 하나 값 그대로라 우선순위가 안 변합니다.
 */
.wf__region.is-freeitems > :not(span, button) {
  position: relative;
  height: 100%;
  display: block;
}
/*
 * 건드리지 않는 것들 — `.wf-i` 로 줄여 부릅니다(아래 선택자에 반복해 씁니다).
 *
 * 슬라이더의 라디오·트랙·점, 갤러리 상세 모달, 커버의 배경·가림막, 마퀴 트랙은
 * 자기 규칙(inset: 0 · position: fixed · width: max-content)으로 자리를
 * 잡습니다. 여기서 absolute와 left/top을 덮어씌우면 파트가 통째로 망가집니다 —
 * 트랙이 쪼그라들고, 상세 모달이 화면 밖으로 밀려납니다.
 *
 * :where()는 특이도가 0이라 아래 규칙들의 우선순위는 그대로입니다.
 * Wireframe.jsx의 FIXED_ITEMS와 **같은 목록**이어야 합니다 — 한쪽만 고치면
 * 끌 수는 있는데 화면은 안 따라오는(또는 그 반대) 상태가 됩니다.
 */
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)) {
  position: absolute;
  margin: 0;
  max-width: 92%;
}
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(1) { left: var(--fi1x, auto); top: var(--fi1y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(2) { left: var(--fi2x, auto); top: var(--fi2y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(3) { left: var(--fi3x, auto); top: var(--fi3y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(4) { left: var(--fi4x, auto); top: var(--fi4y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(5) { left: var(--fi5x, auto); top: var(--fi5y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(6) { left: var(--fi6x, auto); top: var(--fi6y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(7) { left: var(--fi7x, auto); top: var(--fi7y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(8) { left: var(--fi8x, auto); top: var(--fi8y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(9) { left: var(--fi9x, auto); top: var(--fi9y, auto); }
.wf__region.is-freeitems > :not(span, button) > *:not(:where(.wf-sld__r, .wf-sld__track, .wf-sld__dots, .wf-detail, .wf-cover__img, .wf-cover__empty, .wf-cover__veil, .wf-mq__track)):nth-child(10) { left: var(--fi10x, auto); top: var(--fi10y, auto); }
/* ── 배너 ④ Slider — 여러 장이 넘어가는 배너 ────────────────────
 * 넘김은 **라디오 + CSS**입니다. 스크립트가 죽어도 점·화살표는 그대로
 * 동작하고, 자동 넘김과 스와이프만 결과물의 init.js가 얹습니다.
 * 지금 몇 번째 장인지는 라디오가 들고 있고, 아래 생성 규칙이 트랙에는
 * `--i`(장 번호), 활성 장·점에는 `--on: 1`을 심습니다. 나머지 연출은
 * 그 두 변수만 읽습니다 — 전환 방식을 늘려도 규칙을 다시 쓸 일이 없습니다.
 */
.wf-sld {
  position: relative;
  width: 100%;
  height: var(--sld-h, 340px);
  overflow: hidden;
  border-radius: inherit;
}
/* 라디오는 상태 보관용입니다 — 화면에서 치우되 접근성 초점은 남깁니다. */
.wf-sld__r {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; pointer-events: none; margin: 0;
}
.wf-sld__track { position: absolute; inset: 0; }
.wf-sld__slide { position: absolute; inset: 0; overflow: hidden; }
.wf-sld__img,
.wf-sld__empty {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.wf-sld__empty {
  background:
    repeating-linear-gradient(45deg, var(--wf-surface, #8882) 0 10px, transparent 10px 20px),
    var(--wf-surface, #8881);
  opacity: 0.5;
}
.wf-sld__veil { position: absolute; inset: 0; background: #000; pointer-events: none; }
.wf-sld__body {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; gap: 8px;
  padding: min(40px, 5vw);
  z-index: 2;
}
.wf-sld__title {
  font-size: clamp(1.3em, 3.8vw, 2.4em);
  font-weight: 800; line-height: 1.15; letter-spacing: -0.02em;
  color: #fff; text-shadow: 0 2px 24px rgba(0, 0, 0, 0.35);
}
.wf-sld__sub { font-size: 1.0667em; color: rgba(255, 255, 255, 0.86); }
.wf-sld__btn { margin-top: 8px; }
/* ── 전환 4종 — 전부 --i / --on 만 읽습니다 ─────────────────── */

/* 밀기: 트랙을 통째로 옆으로 민다 */
.wf-sld--slide .wf-sld__track {
  display: flex; width: 100%; height: 100%;
  transform: translateX(calc(var(--i, 0) * -100%));
  transition: transform var(--sld-t, 600ms) cubic-bezier(0.4, 0, 0.2, 1);
}
.wf-sld--slide .wf-sld__slide { position: relative; inset: auto; flex: 0 0 100%; height: 100%; }
/* 페이드·덮기·확대: 겹쳐 두고 활성 장만 드러낸다 */
.wf-sld--fade .wf-sld__slide,
.wf-sld--cover .wf-sld__slide,
.wf-sld--zoom .wf-sld__slide { z-index: calc(var(--on, 0)); }
.wf-sld--fade .wf-sld__slide {
  opacity: var(--on, 0);
  transition: opacity var(--sld-t, 600ms) ease;
}
.wf-sld--cover .wf-sld__slide {
  transform: translateX(calc((1 - var(--on, 0)) * 100%));
  transition: transform var(--sld-t, 600ms) cubic-bezier(0.4, 0, 0.2, 1);
}
.wf-sld--zoom .wf-sld__slide {
  opacity: var(--on, 0);
  transform: scale(calc(0.94 + 0.06 * var(--on, 0)));
  transition: opacity var(--sld-t, 600ms) ease, transform var(--sld-t, 600ms) ease;
}
/* 자유 스크롤: 자동 넘김 없이 손으로 미는 띠(scroll-snap) */
.wf-sld--scroll .wf-sld__track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.wf-sld--scroll .wf-sld__track::-webkit-scrollbar { display: none; }
.wf-sld--scroll .wf-sld__slide {
  position: relative; inset: auto;
  flex: 0 0 100%; height: 100%;
  scroll-snap-align: start;
}
/* ── 조작 UI ─────────────────────────────────────────────────── */
.wf-sld__dots {
  position: absolute; left: 0; right: 0; bottom: 12px;
  z-index: 3;
  display: flex; justify-content: center; gap: 8px;
}
.wf-sld__dots > label {
  width: 9px; height: 9px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.45);
  cursor: pointer;
  transition: background 0.2s, width 0.2s;
}
.wf-sld__dots > label:hover { background: rgba(255, 255, 255, 0.75); }
/* 활성 점 — 색과 길이로 두 번 알려 줍니다(색만으로는 구분이 약합니다). */
.wf-sld__dots > label {
  background: rgba(255, 255, 255, calc(0.4 + 0.6 * var(--on, 0)));
  width: calc(9px + 13px * var(--on, 0));
}
.wf-sld__nav {
  position: absolute; inset: 0;
  z-index: 3;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 10px;
  pointer-events: none;
}
.wf-sld__arrow {
  pointer-events: auto;
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.32);
  color: #fff; font-size: 20px; line-height: 1;
  cursor: pointer;
  opacity: 0; transition: opacity 0.2s, background 0.2s;
}
.wf-sld:hover .wf-sld__arrow { opacity: 1; }
.wf-sld__arrow:hover { background: rgba(0, 0, 0, 0.55); }
/* 키보드로 라디오를 옮길 때도 화살표가 보여야 어디로 가는지 압니다. */
.wf-sld__r:focus-visible ~ .wf-sld__track .wf-sld__arrow { opacity: 1; }
.wf-sld.no-arrows .wf-sld__nav { display: none; }
/* 켄번즈 — 활성 장의 이미지만 아주 느리게 확대됩니다. */
.wf-sld.is-kb .wf-sld__img {
  transform: scale(calc(1 + 0.08 * var(--on, 0)));
  transition: transform 8s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  /* 넘김 자체는 남기고 움직임만 없앱니다 — 조작은 그대로 됩니다. */
  .wf-sld__slide,
  .wf-sld--slide .wf-sld__track,
  .wf-sld.is-kb .wf-sld__img { transition: none !important; }
}
/* 활성 장·점을 짚는 규칙 — SLIDER_MAX(12)장까지 생성해 둡니다. */
.wf-sld__r:nth-of-type(1):checked ~ .wf-sld__track { --i: 0; }
.wf-sld__r:nth-of-type(1):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(1) { --on: 1; }
.wf-sld__r:nth-of-type(1):checked ~ .wf-sld__dots > label:nth-child(1) { --on: 1; }
.wf-sld__r:nth-of-type(2):checked ~ .wf-sld__track { --i: 1; }
.wf-sld__r:nth-of-type(2):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(2) { --on: 1; }
.wf-sld__r:nth-of-type(2):checked ~ .wf-sld__dots > label:nth-child(2) { --on: 1; }
.wf-sld__r:nth-of-type(3):checked ~ .wf-sld__track { --i: 2; }
.wf-sld__r:nth-of-type(3):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(3) { --on: 1; }
.wf-sld__r:nth-of-type(3):checked ~ .wf-sld__dots > label:nth-child(3) { --on: 1; }
.wf-sld__r:nth-of-type(4):checked ~ .wf-sld__track { --i: 3; }
.wf-sld__r:nth-of-type(4):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(4) { --on: 1; }
.wf-sld__r:nth-of-type(4):checked ~ .wf-sld__dots > label:nth-child(4) { --on: 1; }
.wf-sld__r:nth-of-type(5):checked ~ .wf-sld__track { --i: 4; }
.wf-sld__r:nth-of-type(5):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(5) { --on: 1; }
.wf-sld__r:nth-of-type(5):checked ~ .wf-sld__dots > label:nth-child(5) { --on: 1; }
.wf-sld__r:nth-of-type(6):checked ~ .wf-sld__track { --i: 5; }
.wf-sld__r:nth-of-type(6):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(6) { --on: 1; }
.wf-sld__r:nth-of-type(6):checked ~ .wf-sld__dots > label:nth-child(6) { --on: 1; }
.wf-sld__r:nth-of-type(7):checked ~ .wf-sld__track { --i: 6; }
.wf-sld__r:nth-of-type(7):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(7) { --on: 1; }
.wf-sld__r:nth-of-type(7):checked ~ .wf-sld__dots > label:nth-child(7) { --on: 1; }
.wf-sld__r:nth-of-type(8):checked ~ .wf-sld__track { --i: 7; }
.wf-sld__r:nth-of-type(8):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(8) { --on: 1; }
.wf-sld__r:nth-of-type(8):checked ~ .wf-sld__dots > label:nth-child(8) { --on: 1; }
.wf-sld__r:nth-of-type(9):checked ~ .wf-sld__track { --i: 8; }
.wf-sld__r:nth-of-type(9):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(9) { --on: 1; }
.wf-sld__r:nth-of-type(9):checked ~ .wf-sld__dots > label:nth-child(9) { --on: 1; }
.wf-sld__r:nth-of-type(10):checked ~ .wf-sld__track { --i: 9; }
.wf-sld__r:nth-of-type(10):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(10) { --on: 1; }
.wf-sld__r:nth-of-type(10):checked ~ .wf-sld__dots > label:nth-child(10) { --on: 1; }
.wf-sld__r:nth-of-type(11):checked ~ .wf-sld__track { --i: 10; }
.wf-sld__r:nth-of-type(11):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(11) { --on: 1; }
.wf-sld__r:nth-of-type(11):checked ~ .wf-sld__dots > label:nth-child(11) { --on: 1; }
.wf-sld__r:nth-of-type(12):checked ~ .wf-sld__track { --i: 11; }
.wf-sld__r:nth-of-type(12):checked ~ .wf-sld__track > .wf-sld__slide:nth-child(12) { --on: 1; }
.wf-sld__r:nth-of-type(12):checked ~ .wf-sld__dots > label:nth-child(12) { --on: 1; }
/* ── 숫자 카운트업 ──────────────────────────────────────────────
 * @property로 정수를 등록하면 CSS가 숫자 사이를 보간할 수 있고,
 * counter()가 그 값을 글자로 그립니다. view() 타임라인이라 스크립트 0.
 * 정지 상태(미지원·동작 줄이기)의 값이 목표 숫자라 퇴화해도 안전합니다.
 * counter-reset의 var()를 못 읽는 브라우저에는 @supports로 일반 글자를 냅니다.
 */
@property --wf-n {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}
.wf-stats { display: flex; flex-wrap: wrap; gap: 24px 44px; width: 100%; }
.wf-stat { display: flex; flex-direction: column; gap: 4px; }
.wf-stat__num {
  display: flex; align-items: baseline; gap: 2px;
  font-size: 2.4em; font-weight: 700; line-height: 1.1;
  color: var(--wf-accent);
}
.wf-stat__label { font-size: 0.8333em; color: var(--wf-muted); }
.wf-stat__count {
  font-style: normal;
  --wf-n: var(--wf-n-to, 0);
  counter-reset: wfnum calc(var(--wf-n));
  animation: wf-count linear both;
  animation-duration: 1ms;   /* 스크롤 타임라인에서는 무시되지만 없으면 시작하지 않습니다 */
  animation-timeline: view();
  animation-range: entry 0% entry 90%;
}
.wf-stat__count::after { content: counter(wfnum); }
@keyframes wf-count { from { --wf-n: 0; } }
.wf-stat__plain { display: none; }
@supports not (counter-reset: n calc(var(--x, 0))) {
  .wf-stat__count { display: none; }
  .wf-stat__plain { display: inline; }
}
@media (prefers-reduced-motion: reduce) {
  .wf-stat__count { animation: none; }   /* 목표 숫자로 바로 놓입니다 */
}
/* 보조기기 전용 — 화면에서는 숨기고 낭독기에는 실제 값을 줍니다. */
.wf-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* 타임라인 목록 (경력 등) */
.wf-time { list-style: none; margin: 0; padding: 0 0 0 14px; width: 100%; }
.wf-time__row {
  position: relative;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--wf-line);
}
.wf-time__row::before {
  content: ''; position: absolute; left: -14px; top: 15px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--wf-accent);
}
.wf-time__row b { font-size: 0.9333em; }
.wf-time__row span { font-size: 0.8em; color: var(--wf-muted); }
/* ── Timeline 파트 — 히스토리 라인 ─────────────────────────────
 * 링 마커 + 항목을 잇는 세로 연결선. 전부 CSS라 결과물에 스크립트 0.
 * 색은 테마 변수(--wf-accent 등)만 쓰므로 다크/라이트에 함께 따라갑니다. */
.wf-tl { width: 100%; min-width: 0; }
.wf-tl__eyebrow {
  display: block;
  font-family: ui-monospace, 'Cascadia Code', Consolas, monospace;
  font-size: 0.78em;
  letter-spacing: 0.08em;
  color: var(--wf-accent);
  margin-bottom: 8px;
}
.wf-tl__title { margin: 0 0 28px; font-size: 1.7em; letter-spacing: -0.01em; }
.wf-tl__list { list-style: none; margin: 0; padding: 0; }
.wf-tl__item { position: relative; padding: 0 0 30px 32px; }
.wf-tl__item:last-child { padding-bottom: 0; }
/* 세로 연결선 — 링 아래에서 다음 링 위까지. 마지막 항목에는 없습니다. */
.wf-tl__item::before {
  content: '';
  position: absolute; left: 6px; top: 22px; bottom: 4px;
  width: 1px;
  background: var(--wf-line);
}
.wf-tl__item:last-child::before { display: none; }
/* 링 마커 — 속이 빈 동그라미. 굵기는 폰트 크기와 무관하게 고정입니다. */
.wf-tl__item::after {
  content: '';
  position: absolute; left: 0; top: 3px;
  width: 9px; height: 9px;
  border-radius: 50%;
  border: 2px solid var(--wf-accent);
}
.wf-tl__label {
  display: block;
  font-size: 0.78em;
  letter-spacing: 0.14em;
  color: var(--wf-muted);
}
.wf-tl__head { display: block; margin: 7px 0 5px; font-size: 0.9333em; }
.wf-tl__desc { margin: 0; font-size: 0.8667em; color: var(--wf-muted); line-height: 1.65; }
.wf-main { display: grid; width: 100%; min-width: 0; flex: 1; }
.wf-main > * { min-width: 0; }
.wf-main__slot {
  display: grid; place-items: center;
  border: 1px dashed var(--wf-line);
  border-radius: inherit;
  font-size: 0.8em; color: var(--wf-muted);
  min-height: 140px;
}
/* 3D 전용 파트 상자 (Character / Model) */
.wf-3d {
  position: relative;
  width: 100%;
  display: grid; place-items: center;
  min-height: 160px;
}
/*
 * '영역 전체 채우기' — 뷰어가 영역 상자에 absolute로 깔려 환경광(skybox)이
 * 영역 배경처럼 동작합니다. inset이 크기를 정하므로 .fx-model의 4:3 비율과
 * 인라인 높이는 무시됩니다. 모서리는 영역을 따라갑니다.
 * (내보내기에도 그대로 실립니다 — 선택자에 편집 전용 표식이 없습니다)
 */
.wf__region:has(> .wf-3d.is-fill) { position: relative; }
.wf-3d.is-fill {
  position: absolute;
  inset: 0;
  height: auto !important;
  aspect-ratio: auto;
  border-radius: inherit;
  overflow: hidden;
}
/* flip 카드는 앞뒷면이 겹쳐 쌓입니다 */
.wf-card.is-flip { background: none; border: 0; overflow: visible; }
.wf-card.is-flip .fx-flip-card__face {
  display: flex; flex-direction: column; gap: 6px;
  justify-content: center; padding: 18px;
  background: var(--wf-surface);
  border: 1px solid var(--wf-line);
}
.wf-badge {
  align-self: flex-start;
  padding: 2px 8px;
  border: 1px solid var(--part-color);
  border-radius: 999px;
  font-size: 0.6667em; color: var(--part-color);
}
/* 영역의 기준 글자 크기 — 안쪽 글자는 전부 이 값의 배수(em)입니다.
 * 영역별 '글자 크기'를 바꾸면 제목·본문·카드가 비례해 함께 움직입니다. */
.wf__region { font-size: var(--wf-fs, 15px); }
/* ── 이미지 ────────────────────────────────────────────────── */

.wf-aside__photo {
  display: block; flex: none;
  border: 1px solid var(--wf-line);
  border-radius: 8px;
}
.wf-aside__photo.is-round { border-radius: 50%; }
/* 히어로 프로필 사진 — 원형 증명사진 + 중앙 인사말 구성용 */
.wf-hero__photo {
  display: block; flex: none;
  object-fit: cover;
  border: 1px solid var(--wf-line);
  border-radius: 12px;
  margin-bottom: 6px;
}
.wf-hero__photo.is-round { border-radius: 50%; }
.wf-card__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
}
/*
 * 흑백에서 컬러로 — 갤러리 옵션(grayHover). 옛 포트폴리오들이 가장 자주 쓰던
 * 호버 문법입니다: 쉬고 있는 카드는 톤을 낮추고, 짚은 카드만 색이 살아납니다.
 * 순수 CSS라 결과물에 스크립트가 늘지 않습니다.
 */
.wf-gal.is-gray .wf-card__img {
  filter: saturate(0%);
  transition: filter 0.45s ease, transform 0.45s ease;
}
.wf-gal.is-gray .wf-card:hover .wf-card__img,
.wf-gal.is-gray .wf-card:focus-visible .wf-card__img {
  filter: saturate(100%);
  transform: scale(1.03);
}
/* ── 외부 링크 ─────────────────────────────────────────────── */

.wf-links {
  min-width: 0; display: flex; flex-direction: column; gap: 14px;
  width: 100%; max-width: var(--wf-max); margin: 0 auto;
}
.wf-links__row { display: flex; flex-wrap: wrap; gap: 10px; }
.wf-link {
  display: inline-flex; align-items: baseline; gap: 7px;
  color: inherit; text-decoration: none;
}
.wf-link__url { font-style: normal; font-size: 0.8em; opacity: 0.6; }
/* 칩 버튼 — 가장 흔한 모양 */
.wf-links--buttons .wf-link {
  padding: 9px 16px;
  border: 1px solid var(--wf-line);
  border-radius: 999px;
}
.wf-links--buttons .wf-link:hover { border-color: var(--wf-accent); color: var(--wf-accent); }
/* 밑줄 텍스트 */
.wf-links--text .wf-link { text-decoration: underline; text-underline-offset: 3px; }
/* 세로 목록 — 주소까지 보여줄 때 */
.wf-links--list .wf-links__row { flex-direction: column; gap: 8px; }
.wf-links--list .wf-link {
  justify-content: space-between;
  width: 100%; padding: 10px 2px;
  border-bottom: 1px solid var(--wf-line);
}
/* 카드 — 링크마다 소개 문단이 붙는 Archiving 구성 (참고: 개발자 포폴의 Notion/GitHub 칸) */
.wf-links--cards .wf-links__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  width: 100%;
}
.wf-links--cards .wf-link {
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  padding: 22px;
  border: 1px solid var(--wf-line);
  border-radius: 14px;
  font-weight: 600;
}
/* 결과물에서는 링크 이름이 텍스트 노드라 자식 선택이 안 됩니다 — 카드 전체를 물들이고
 * 설명(.wf-link__desc)은 제 색을 명시해 둬서 흐려진 채 남습니다. */
.wf-links--cards .wf-link:hover { border-color: var(--wf-accent); color: var(--wf-accent); }
.wf-link__desc {
  margin: 0;
  font-size: 0.8333em;
  font-weight: 400;
  line-height: 1.65;
  color: var(--wf-muted);
  white-space: pre-line;
}
/* ── 디지털 명함 ───────────────────────────────────────────────
 * 종이 명함 비율(91×55mm ≈ 1.655)을 그대로 씁니다 — 화면에서도 "명함"으로
 * 읽히고, 스크린샷을 그대로 인쇄해도 비율이 맞습니다.
 *
 * 껍데기(.wf-bcard)와 안쪽(.wf-bcard__in)을 나눈 이유
 *   글자·여백을 카드 폭에 매려면 컨테이너 단위(cqw)가 필요한데, cqw는
 *   **조상** 컨테이너를 잽니다. 껍데기 자신의 padding에 쓰면 조상(없으면 화면)을
 *   재서 1440px 화면에서 패딩이 100px까지 부풀었습니다. %로 바꿔도 부모 폭을
 *   재기는 마찬가지입니다. 그래서 껍데기는 크기만 정하고, 여백과 배치는 한 겹
 *   안에서 잡습니다 — 안쪽에게는 껍데기가 조상이라 cqw가 카드 폭을 가리킵니다.
 */
.wf-bcard {
  container-type: inline-size;
  position: relative;
  width: 100%;
  aspect-ratio: 9 / 5;   /* 90×50mm 국내 표준 (받은 디자인 시스템 기준) */
  margin: 0 auto;
  overflow: hidden;
  border: 1px solid var(--wf-line);
  border-radius: inherit;
  background: var(--wf-surface);
  --bc-accent: var(--wf-accent);

  /*
   * 받은 디자인 시스템(Myeongham)의 토큰입니다. `:root`가 아니라 카드 안에만
   * 푸는 것은 사이트 테마와 섞이지 않게 하기 위해서입니다 — 명함은 제 색으로
   * 서는 물건이고, 페이지 테마를 바꿔도 인쇄물 색이 따라 변하면 곤란합니다.
   *
   * 글꼴은 가이드가 지정한 Archivo·IBM Plex를 **CDN으로 불러오지 않습니다**.
   * 결과물이 외부 요청 없이 혼자 돌아야 한다는 이 제품의 규칙이 우선이라,
   * 같은 자리에 시스템 글꼴 묶음을 놓았습니다(모노는 실제 모노스페이스).
   */
  --bc-ink-900: #0B0C0E; --bc-ink-700: #1C1F24; --bc-ink-500: #4A5058;
  --bc-ink-300: #8A9099; --bc-ink-200: #B9BCC1; --bc-ink-100: #D8D6D0;
  --bc-paper-0: #FFFFFF; --bc-paper-1: #F4F2ED; --bc-paper-2: #EAE7DF;
  --bc-signal: #1F5AE8; --bc-neon: #00E5A0; --bc-warm: #E8462B; --bc-gold: #D9A400;
  /*
   * 코드 배색은 밝은 판·어두운 판 두 벌입니다(-d가 어두운 판). 시안의
   * 신택스 명함은 흰 종이라 밝은 판을 쓰고, 셸은 검은 종이라 어두운 판을
   * 씁니다. 한 벌로 돌려 쓰면 흰 바탕에 연한 파랑이 얹혀 안 읽힙니다.
   */
  --bc-syn-key: #1F5AE8; --bc-syn-str: #0F9D58; --bc-syn-num: #D9A400;
  --bc-syn-com: #8A9099; --bc-syn-punct: #4A5058; --bc-syn-fn: #E8462B;
  --bc-syn-key-d: #7AA2FF; --bc-syn-str-d: #00E5A0; --bc-syn-num-d: #F0C455;
  --bc-syn-com-d: #5A626C; --bc-syn-punct-d: #9AA1AA; --bc-syn-fn-d: #FF8A6B;
  --bc-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}
/*
 * 가이드가 쓰는 네 조각 — 작은 라벨 / 이니셜 마크 / 한 줄 소개 / 기술 스택.
 * 기본은 안 그립니다. 쓰는 디자인에서만 켭니다(옛 명함 넷은 그대로 둡니다).
 */
.wf-bcard__label,
.wf-bcard__mono,
.wf-bcard__tagline,
.wf-bcard__stack { display: none; }
.wf-bcard__label {
  font-family: var(--bc-mono);
  font-size: 2.3cqw; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
}
.wf-bcard__mono {
  font-size: 5.6cqw; font-weight: 800; letter-spacing: -0.03em; line-height: 1;
}
.wf-bcard__tagline {
  margin: 0; font-size: 2.9cqw; line-height: 1.45; font-weight: 500;
  text-wrap: pretty;
}
.wf-bcard__stack {
  font-family: var(--bc-mono);
  font-size: 2.2cqw; color: var(--bc-ink-300);
}
.wf-bcard__in {
  position: relative;
  display: grid;
  height: 100%;
  padding: 7cqw;
  gap: 3cqw;
}
.wf-bcard__lead { display: flex; flex-direction: column; min-width: 0; }
.wf-bcard__role {
  font-size: 7cqw; font-weight: 800; line-height: 1;
  letter-spacing: -0.02em; color: var(--bc-accent);
  /*
   * 전역 규칙이 `overflow-wrap: anywhere`라 'FullStack'이 'FullStac / k'로
   * 잘렸습니다. 명함의 큰 글자는 한 낱말이 곧 그림이라 낱말 중간에서
   * 끊기면 안 됩니다 — 이 자리에서만 되돌립니다.
   */
  overflow-wrap: normal;
}
.wf-bcard__rolesub { font-size: 4.4cqw; font-weight: 700; line-height: 1.2; }
.wf-bcard__info { display: flex; flex-direction: column; justify-content: center; gap: 2.2cqw; min-width: 0; }
.wf-bcard__row { display: flex; align-items: center; gap: 2.6cqw; min-width: 0; }
.wf-bcard__text { display: flex; flex-direction: column; min-width: 0; }
.wf-bcard__name { font-size: 4.8cqw; font-weight: 800; line-height: 1.15; }
.wf-bcard__nameen { font-size: 2.5cqw; color: var(--wf-muted); line-height: 1.3; }
.wf-bcard__title { font-size: 2.5cqw; color: var(--wf-muted); line-height: 1.3; }
.wf-bcard__line { font-size: 2.5cqw; line-height: 1.35; white-space: pre-line; }
.wf-bcard__ico {
  flex: none; display: grid; place-items: center;
  width: 5.4cqw; height: 5.4cqw;
  border-radius: 50%;
  background: var(--bc-accent);
  color: #fff;
}
.wf-bcard__ico svg { width: 60%; height: 60%; display: block; }
.wf-bcard__side { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1.6cqw; }
.wf-bcard__qr { display: block; width: 100%; }
.wf-bcard__qr .wf-qr { display: block; width: 100%; height: auto; color: inherit; }
.wf-bcard__save {
  font-size: 2.3cqw; font-weight: 700;
  color: var(--bc-accent);
  text-decoration: none;
  white-space: nowrap;
}
/* ══ 받은 디자인 시스템(Myeongham)의 명함 ═══════════════════════
 * 공통 골격: 위 줄 = 작은 라벨 ↔ 이니셜 마크, 가운데 = 이름·직함·연락처,
 * 아래 줄 = 한 줄 소개(또는 기술 스택) ↔ QR. 안전영역 4mm(=4.4cqw).
 * 액센트는 가이드 규칙대로 **한 장에 한 색**만 씁니다.
 *
 * 두 묶음으로 나눠 씁니다.
 *   격자 — 골격을 함께 쓰는 모든 장(개발자용 여덟 + 일반용)
 *   개발자용 — 작은 글씨 치수까지 같은 여덟 장. 일반용은 시안 치수가 달라
 *              제 규칙을 따로 답니다.
 *
 * 목록은 :is()로 묶습니다. 예전에는 선택자를 한 줄씩 늘어놓았는데, 장이
 * 하나 늘 때마다 여덟 군데를 똑같이 고쳐야 해서 한 곳을 빠뜨리면 그 장만
 * 격자를 잃고 무너졌습니다(실제로 겪었습니다). :is()는 인자 중 가장 높은
 * 명시도를 취하고 인자가 전부 홑클래스라 계산값은 예전과 같습니다.
 */
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax,
    .wf-bcard--manifest, .wf-bcard--schema,
    .wf-bcard--formal, .wf-bcard--executive,
    .wf-bcard--field, .wf-bcard--hangeul, .wf-bcard--directory) {
  border-radius: 3px;
  --bc-accent: var(--bc-signal);
}
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax,
    .wf-bcard--manifest, .wf-bcard--schema,
    .wf-bcard--formal, .wf-bcard--executive,
    .wf-bcard--field, .wf-bcard--hangeul, .wf-bcard--directory) .wf-bcard__in {
  padding: 4.4cqw;
  gap: 0;
  grid-template-columns: 1fr auto;
  grid-template-rows: auto 1fr auto;
}
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax,
    .wf-bcard--manifest, .wf-bcard--schema,
    .wf-bcard--formal, .wf-bcard--executive,
    .wf-bcard--field, .wf-bcard--hangeul, .wf-bcard--directory) .wf-bcard__label {
  display: block; grid-column: 1; grid-row: 1; color: var(--bc-ink-300);
}
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax,
    .wf-bcard--manifest, .wf-bcard--schema,
    .wf-bcard--formal, .wf-bcard--executive,
    .wf-bcard--field, .wf-bcard--hangeul, .wf-bcard--directory) .wf-bcard__mono {
  display: block; grid-column: 2; grid-row: 1; justify-self: end; color: var(--bc-accent);
}
/* 큰 분야 글자(예전 명함의 주인공)는 이 장들에서 쓰지 않습니다 — 가이드의
   위계는 '이름'이 가장 큽니다. */
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax,
    .wf-bcard--manifest, .wf-bcard--schema,
    .wf-bcard--formal, .wf-bcard--executive,
    .wf-bcard--field, .wf-bcard--hangeul, .wf-bcard--directory) .wf-bcard__lead { display: none; }
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax,
    .wf-bcard--manifest, .wf-bcard--schema,
    .wf-bcard--formal, .wf-bcard--executive,
    .wf-bcard--field, .wf-bcard--hangeul, .wf-bcard--directory) .wf-bcard__info {
  /* QR과 같은 줄에 섭니다 — 아래 줄을 따로 내주면 그 높이만큼 카드를 넘깁니다. */
  grid-column: 1; grid-row: 2; justify-content: center; gap: 1.2cqw;
}
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax,
    .wf-bcard--manifest, .wf-bcard--schema,
    .wf-bcard--formal, .wf-bcard--executive,
    .wf-bcard--field, .wf-bcard--hangeul, .wf-bcard--directory) .wf-bcard__name {
  font-size: 6.2cqw; letter-spacing: -0.03em;
}
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax,
    .wf-bcard--manifest, .wf-bcard--schema,
    .wf-bcard--formal, .wf-bcard--executive,
    .wf-bcard--field, .wf-bcard--hangeul, .wf-bcard--directory) .wf-bcard__line {
  font-family: var(--bc-mono); font-size: 2.4cqw;
}
/* ── 여기서부터는 개발자용 여덟 장만 ── */
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax) .wf-bcard__ico {
  /* 가이드는 명함 아이콘을 11px 고정으로 씁니다(360px 카드에서 약 3cqw). */
  width: 3.4cqw; height: 3.4cqw;
}
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax)
  :is(.wf-bcard__nameen, .wf-bcard__title) { font-size: 2.4cqw; }
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax) .wf-bcard__stack {
  display: block; grid-column: 1; grid-row: 3; align-self: end;
}
:is(.wf-bcard--quiet, .wf-bcard--shell, .wf-bcard--grid, .wf-bcard--night,
    .wf-bcard--signal, .wf-bcard--letter, .wf-bcard--swiss, .wf-bcard--syntax) .wf-bcard__side {
  grid-column: 2; grid-row: 2; align-self: center; width: 13cqw;
}
/* 01 콰이엇 — 흰 종이, 위계만. 이름 위 헤어라인 하나가 전부입니다. */
.wf-bcard--quiet { background: var(--bc-paper-0); color: var(--bc-ink-900); }
.wf-bcard--quiet .wf-bcard__info { border-top: 1px solid var(--bc-ink-100); padding-top: 2.4cqw; }
.wf-bcard--quiet .wf-bcard__nameen, .wf-bcard--quiet .wf-bcard__title { color: var(--bc-ink-500); }
/* 02 셸 — 터미널. 잉크 바탕 · 네온 · 전부 모노스페이스.
   프롬프트 기호는 가이드가 허용한 세 글리프 중 하나입니다(➜ ◀▶ ·). */
.wf-bcard--shell { background: var(--bc-ink-900); color: var(--bc-paper-1); --bc-accent: var(--bc-neon); }
.wf-bcard--shell .wf-bcard__in { font-family: var(--bc-mono); }
.wf-bcard--shell .wf-bcard__name { font-family: var(--bc-mono); font-size: 7.2cqw; color: #fff; }
/*
 * 이름 끝의 깜빡이는 커서. 시안의 6×12px 블록 커서입니다.
 * steps(1)로 끊어 깜빡여야 터미널처럼 보입니다 — 부드럽게 흐리면
 * 커서가 아니라 그냥 빛나는 사각형이 됩니다.
 */
.wf-bcard--shell .wf-bcard__name::after {
  content: ''; display: inline-block;
  width: 1.7cqw; height: 3.3cqw;
  margin-left: 0.8cqw;
  vertical-align: -0.55cqw;
  background: var(--bc-neon);
  animation: wf-bcard-caret 1s steps(1) infinite;
}
@keyframes wf-bcard-caret { 50% { opacity: 0; } }
.wf-bcard--shell .wf-bcard__name::before { content: '\279C\00A0'; color: var(--bc-neon); font-weight: 400; }
.wf-bcard--shell .wf-bcard__nameen, .wf-bcard--shell .wf-bcard__title { color: var(--bc-syn-com-d); font-family: var(--bc-mono); }
.wf-bcard--shell .wf-bcard__label { color: var(--bc-syn-com-d); }
.wf-bcard--shell .wf-bcard__ico { background: transparent; color: var(--bc-neon); }
.wf-bcard--shell .wf-bcard__line { color: var(--bc-paper-1); }
/* 09 매니페스트 — 괘선 공책. 왼쪽 여백선 하나와 22px 줄 간격이 전부입니다.
 *
 * 줄은 배경 그라디언트로 긋습니다(요소를 스물몇 개 만들 일이 아닙니다).
 * 글이 줄 위에 정확히 앉으려면 줄 간격과 행간이 같아야 해서 연락처 줄의
 * 행간을 22px 환산값으로 못 박습니다.
 *
 * 시안 치수: 줄 간격 22px · 여백선 왼쪽 34px · 안쪽 여백 14/16/14/44px
 */
.wf-bcard--manifest {
  background: var(--bc-paper-1); color: var(--bc-ink-900);
  --bc-accent: var(--bc-warm);
}
.wf-bcard--manifest .wf-bcard__in {
  padding: 3.9cqw 4.4cqw 3.9cqw 12.2cqw;
  gap: 0;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: 8.3cqw minmax(0, 1fr) auto;
  /*
   * 괘선의 시작점은 **글이 시작하는 자리**여야 합니다.
   * 배경 위치는 안쪽 여백 상자를 기준으로 재므로 위 여백(3.9)과 머리줄
   * (8.3)을 더한 12.2cqw에서 출발해야 각 줄의 밑선이 글줄 바닥과 맞습니다.
   * 8.3만 주었더니 줄이 글자 한가운데를 가로질렀습니다.
   */
  background-image: repeating-linear-gradient(
    to bottom, transparent 0 5.83cqw, #DED9CE 5.83cqw 6.11cqw);
  background-position: 0 12.2cqw;
}
/* 공책의 빨간 여백선 — 왼쪽 34px 자리에 세로로 한 줄. */
.wf-bcard--manifest .wf-bcard__in::before {
  content: '';
  position: absolute; left: 9.4cqw; top: 0; bottom: 0;
  width: 1px; background: var(--bc-warm); opacity: 0.7;
}
.wf-bcard--manifest .wf-bcard__label { grid-column: 1; grid-row: 1; color: var(--bc-ink-300); }
.wf-bcard--manifest .wf-bcard__mono {
  grid-column: 2; grid-row: 1; justify-self: end;
  font-family: var(--bc-mono); font-size: 2.3cqw; font-weight: 600;
  letter-spacing: 0.18em; color: var(--bc-ink-300);
}
/*
 * QR을 아랫줄에 두었더니 그 줄이 QR 높이(62px)만큼 자라, 가운데 칸이
 * 90px로 눌려 연락처가 51px 잘렸습니다. 옆줄로 옮겨 세로를 비웁니다.
 */
.wf-bcard--manifest .wf-bcard__info {
  grid-column: 1; grid-row: 2;
  justify-content: flex-start; gap: 0;
  /*
   * 여기서는 넘침을 자르지 않습니다. 이름은 글자 크기(7.2cqw)가 줄 간격
   * (6.11cqw)보다 커서 글리프가 제 상자 위로 7px 솟습니다 — 괘선 공책의
   * 큰 글씨는 원래 그렇게 앉습니다. 잘라 버리면 이름의 머리가 날아갑니다.
   * 솟는 자리는 위쪽 머리줄의 빈 공간이라 부딪힐 것이 없습니다.
   */
}
.wf-bcard--manifest .wf-bcard__name { font-size: 7.2cqw; line-height: 6.11cqw; letter-spacing: -0.03em; }
/*
 * 줄 하나가 곧 한 칸입니다 — 아이콘을 라벨 칸처럼 왼쪽에 고정합니다.
 *
 * 높이를 못 박는 것은 **연락처 줄에만** 해당합니다. 이름 줄은 이름·영문
 * 이름·직함 세 줄이 겹쳐 있어 22px에 가두면 글이 칸 밖으로 밀려 나가
 * 괘선과 어긋납니다(그렇게 보였습니다).
 */
.wf-bcard--manifest .wf-bcard__row { gap: 3.3cqw; }
/*
 * 이름 줄의 사람 아이콘은 뺍니다. 다른 판에서는 이름 옆 표식이지만
 * 공책에서는 왼쪽 라벨 칸에 홀로 앉아 무엇을 가리키는지 알 수 없습니다
 * — 이름은 그 자체로 읽힙니다.
 */
.wf-bcard--manifest .wf-bcard__row:has(.wf-bcard__name) .wf-bcard__ico { display: none; }
/* 이름·영문 이름·직함이 각각 줄 하나씩을 차지해야 괘선을 탑니다. */
.wf-bcard--manifest .wf-bcard__nameen,
.wf-bcard--manifest .wf-bcard__title { line-height: 6.11cqw; }
.wf-bcard--manifest .wf-bcard__ico {
  order: -1; width: 3.4cqw; height: 3.4cqw;
  background: transparent; color: var(--bc-warm);
}
/*
 * 줄 높이가 아니라 **글줄의 행간**을 괘선 간격에 맞춥니다. 칸 높이를
 * 못 박으면 글이 칸 가운데에 뜨고(0.2줄쯤) 괘선 위에 앉지 않습니다.
 */
.wf-bcard--manifest .wf-bcard__line { font-family: var(--bc-mono); font-size: 2.8cqw; line-height: 6.11cqw; }
.wf-bcard--manifest .wf-bcard__nameen,
.wf-bcard--manifest .wf-bcard__title { font-size: 2.8cqw; color: var(--bc-ink-500); }
.wf-bcard--manifest .wf-bcard__stack { grid-column: 1; grid-row: 3; }
.wf-bcard--manifest .wf-bcard__side {
  grid-column: 2; grid-row: 2;
  justify-self: end; align-self: center;
  width: 11cqw;
}
/* 10 스키마 — 청사진. 모눈 위에 점선 재단선과 네 귀퉁이 십자 표식.
 *
 * 모눈과 십자 표식은 모두 배경 겹치기로 그립니다. 십자는 귀퉁이마다
 * 가로·세로 두 겹이라 여덟 겹인데, 요소를 넷 더 만드는 것보다 낫습니다
 * — 마크업은 열다섯 장이 함께 쓰는 것이라 한 장 때문에 늘릴 수 없습니다.
 *
 * 시안 치수: 모눈 20px · 십자 11px, 귀퉁이에서 8px · 재단선 점선 1px
 */
.wf-bcard--schema {
  background-color: #0C1526; color: #AFC0DE;
  --bc-accent: #7AA2FF;
}
/*
 * 모눈과 십자는 카드가 아니라 **면**에 그립니다. 앞뒤 뒤집기를 켜면
 * 카드 껍데기의 배경이 통째로 지워지고(3D로 도는 면이 제 배경을 갖습니다)
 * 카드에 그린 무늬는 그때 같이 사라집니다.
 */
.wf-bcard--schema .wf-bcard__in {
  background-image:
    linear-gradient(#16233C 1px, transparent 1px),
    linear-gradient(90deg, #16233C 1px, transparent 1px),
    linear-gradient(#3C5484, #3C5484), linear-gradient(#3C5484, #3C5484),
    linear-gradient(#3C5484, #3C5484), linear-gradient(#3C5484, #3C5484),
    linear-gradient(#3C5484, #3C5484), linear-gradient(#3C5484, #3C5484),
    linear-gradient(#3C5484, #3C5484), linear-gradient(#3C5484, #3C5484);
  background-size:
    5.6cqw 5.6cqw, 5.6cqw 5.6cqw,
    1px 3.05cqw, 3.05cqw 1px,
    1px 3.05cqw, 3.05cqw 1px,
    1px 3.05cqw, 3.05cqw 1px,
    1px 3.05cqw, 3.05cqw 1px;
  background-position:
    0 0, 0 0,
    left 3.63cqw top 2.2cqw, left 2.2cqw top 3.63cqw,
    right 3.63cqw top 2.2cqw, right 2.2cqw top 3.63cqw,
    left 3.63cqw bottom 2.2cqw, left 2.2cqw bottom 3.63cqw,
    right 3.63cqw bottom 2.2cqw, right 2.2cqw bottom 3.63cqw;
  background-repeat: repeat, repeat, no-repeat, no-repeat, no-repeat, no-repeat,
    no-repeat, no-repeat, no-repeat, no-repeat;
}
.wf-bcard--schema .wf-bcard__in {
  padding: 4.4cqw;
  gap: 0;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
/* 재단선 — 안쪽으로 안전영역만큼 들어온 점선 테두리. */
.wf-bcard--schema .wf-bcard__in::before {
  content: '';
  position: absolute; inset: 4.4cqw;
  border: 1px dashed #2A3C60;
  pointer-events: none;
}
.wf-bcard--schema .wf-bcard__label { grid-column: 1; grid-row: 1; color: #5C7099; }
.wf-bcard--schema .wf-bcard__mono {
  grid-column: 2; grid-row: 1; justify-self: end;
  font-family: var(--bc-mono); font-size: 2.3cqw; font-weight: 600;
  letter-spacing: 0.1em; color: #5C7099;
}
.wf-bcard--schema .wf-bcard__info {
  grid-column: 1; grid-row: 2;
  justify-content: center; align-items: center; text-align: center;
  overflow: hidden;
}
.wf-bcard--schema .wf-bcard__row { justify-content: center; }
.wf-bcard--schema .wf-bcard__text { align-items: center; text-align: center; }
.wf-bcard--schema .wf-bcard__name { font-size: 7.2cqw; letter-spacing: -0.03em; color: #EAF1FF; }
.wf-bcard--schema .wf-bcard__nameen,
.wf-bcard--schema .wf-bcard__title {
  font-size: 2.5cqw; text-transform: uppercase;
  letter-spacing: 0.08em; color: #7E93BC;
}
.wf-bcard--schema .wf-bcard__line { font-family: var(--bc-mono); font-size: 2.8cqw; color: #AFC0DE; }
.wf-bcard--schema .wf-bcard__ico { width: 3.4cqw; height: 3.4cqw; background: transparent; color: #7AA2FF; }
.wf-bcard--schema .wf-bcard__stack { grid-column: 1; grid-row: 3; color: #5C7099; }
.wf-bcard--schema .wf-bcard__side {
  grid-column: 2; grid-row: 2;
  justify-self: end; align-self: center;
  width: 11cqw;
}
/* ── 일반(비개발) 명함 ──────────────────────────────────────
 * 개발자용 여덟 장과 달리 **회사명·부서까지** 자리표시자에 들어갑니다.
 * 뼈대는 같은 격자를 그대로 쓰고 색과 괘선만 갈아입힙니다.
 */

/* 11 포멀 — 표준 기업 명함. 로고 아래 전폭 괘선, 이름이 가장 큼.
 *
 * 시안 치수를 그대로 옮깁니다. 시안은 360px 카드에 px로 적혀 있고 우리는
 * 컨테이너 비율(cqw)을 쓰므로 px÷3.6으로 바꿨습니다 — 카드가 커져도
 * 글자와 여백의 비율이 시안과 같게 유지됩니다.
 *   로고 13px·700·자간 .08em / 괘선 위 여백 11px / 이름 26px·행간 1.05
 *   부서·직함 11px / 영문이름 11px·500·자간 .08em
 */
.wf-bcard--formal { background: var(--bc-paper-0); color: var(--bc-ink-900); }
/*
 * 로고는 액센트 한 색. 위 여덟 장의 라벨은 모노·대문자의 작은 머리표지만
 * 여기서는 '회사 이름'으로 읽혀야 해서 그 옷을 벗깁니다.
 *
 * 두 칸을 모두 차지하게 하는 것이 핵심입니다 — 시안의 괘선은 로고 폭이
 * 아니라 카드 폭을 가로지릅니다. 한 칸에 두면 로고 글자 길이에 따라
 * 선 길이가 들쭉날쭉해집니다.
 */
.wf-bcard--formal .wf-bcard__label {
  grid-column: 1 / -1;
  font-family: inherit; text-transform: none;
  font-size: 3.6cqw; font-weight: 700; letter-spacing: 0.08em;
  color: var(--bc-accent);
  padding-bottom: 3.05cqw;
  border-bottom: 1px solid var(--bc-ink-900);
}
/* 시안의 포멀에는 이니셜 마크가 없습니다 — 로고가 그 자리를 씁니다. */
.wf-bcard--formal .wf-bcard__mono { display: none; }
.wf-bcard--formal .wf-bcard__info { justify-content: flex-end; gap: 1.7cqw; }
.wf-bcard--formal .wf-bcard__name {
  font-size: 7.2cqw; font-weight: 700; line-height: 1.05; letter-spacing: -0.015em;
}
/*
 * 시안의 아래 묶음은 '부서·직함 → 이름 → 영문이름' 차례입니다. 마크업은
 * 열세 장이 함께 쓰므로 건드리지 않고, 이 디자인에서만 순서를 올립니다.
 */
.wf-bcard--formal .wf-bcard__title { order: -1; }
.wf-bcard--formal .wf-bcard__nameen { font-weight: 500; letter-spacing: 0.08em; }
.wf-bcard--formal .wf-bcard__nameen,
.wf-bcard--formal .wf-bcard__title { font-size: 3.05cqw; color: var(--bc-ink-500); }
.wf-bcard--formal .wf-bcard__side { grid-column: 2; grid-row: 2; align-self: end; width: 13cqw; }
.wf-bcard--formal .wf-bcard__ico { width: 3.4cqw; height: 3.4cqw; }
/* 12 이그제큐티브 — 검은 종이에 금색 한 줄. 임원·대표용.
 *   로고 11px·600·자간 .2em 금색 / 이니셜 13px 모노·자간 .24em 회색
 *   금색 헤어라인 26×1px, 아래 여백 11px / 이름 26px·600·행간 1.05 흰색
 *   부제 11px 회색 / 연락처 글자 #9AA1AA, 아이콘 금색
 */
.wf-bcard--executive {
  background: var(--bc-ink-900); color: var(--bc-paper-1);
  --bc-accent: var(--bc-gold);
}
.wf-bcard--executive .wf-bcard__label {
  font-family: inherit; text-transform: none;
  font-size: 3.05cqw; font-weight: 600; letter-spacing: 0.2em;
  color: var(--bc-gold);
}
.wf-bcard--executive .wf-bcard__mono {
  font-family: var(--bc-mono);
  font-size: 3.6cqw; font-weight: 600; letter-spacing: 0.24em;
  color: var(--bc-ink-500);
}
/*
 * 금색 헤어라인. 시안은 이름 묶음 위에 짧은 선 하나를 답니다.
 * 절대배치 장식(__deco)을 쓰지 않는 것은 그 자리가 이름 높이에 따라
 * 달라지기 때문입니다 — 묶음의 첫 칸으로 넣으면 저절로 따라옵니다.
 */
.wf-bcard--executive .wf-bcard__info { justify-content: flex-end; gap: 1.9cqw; }
.wf-bcard--executive .wf-bcard__info::before {
  content: '';
  width: 7.2cqw; height: 1px; flex: none;
  background: var(--bc-gold);
  margin-bottom: 1.15cqw;
}
.wf-bcard--executive .wf-bcard__name {
  font-size: 7.2cqw; font-weight: 600; line-height: 1.05;
  letter-spacing: -0.015em; color: #fff;
}
.wf-bcard--executive .wf-bcard__nameen,
.wf-bcard--executive .wf-bcard__title { font-size: 3.05cqw; color: var(--bc-ink-300); }
.wf-bcard--executive .wf-bcard__line { font-family: inherit; font-size: 2.8cqw; color: #9AA1AA; }
.wf-bcard--executive .wf-bcard__side { grid-column: 2; grid-row: 2; align-self: end; width: 13cqw; }
.wf-bcard--executive .wf-bcard__ico { width: 3.4cqw; height: 3.4cqw; }
/* 13 컬러 필드 — 왼쪽 126px 색 띠에 로고, 오른쪽 흰 종이에 이름.
 *
 * 이름은 '좌우 분할'이지만 그 이름은 자체 제작 판이 이미 쓰고 있어
 * 시안의 부제(2분할 컬러 필드)를 따랐습니다. 겹치는 이름을 두면
 * 고르는 사람이 어느 쪽인지 알 수 없습니다.
 *
 * 시안 치수: 띠 126px · 로고 16px · 이름 26px · 연락처 10px/1.6
 */
.wf-bcard--field {
  background: var(--bc-paper-0); color: var(--bc-ink-900);
  --bc-face-bg: var(--bc-paper-0);
}
.wf-bcard--field .wf-bcard__in {
  padding: 0; gap: 0;
  grid-template-columns: 35cqw minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
/* 왼쪽 색 띠 — 이니셜 자리를 띠 전체로 늘려 씁니다. */
.wf-bcard--field .wf-bcard__mono {
  grid-column: 1; grid-row: 1 / -1;
  justify-self: stretch; align-self: stretch;
  display: flex; align-items: flex-start;
  padding: 5cqw;
  background: var(--bc-accent); color: #fff;
  font-size: 4.4cqw; font-weight: 700; letter-spacing: -0.015em;
}
.wf-bcard--field .wf-bcard__label {
  grid-column: 1; grid-row: 3;
  align-self: end; padding: 0 5cqw 5cqw;
  color: rgb(255 255 255 / 0.7);
}
.wf-bcard--field .wf-bcard__info {
  grid-column: 2; grid-row: 1 / -1;
  justify-content: center; gap: 2.5cqw;
  padding: 0 4.4cqw;
}
.wf-bcard--field .wf-bcard__name { font-size: 7.2cqw; letter-spacing: -0.015em; }
.wf-bcard--field .wf-bcard__nameen,
.wf-bcard--field .wf-bcard__title { font-size: 3.05cqw; color: var(--bc-ink-500); }
.wf-bcard--field .wf-bcard__line { font-size: 2.8cqw; line-height: 1.6; }
.wf-bcard--field .wf-bcard__ico { width: 3.4cqw; height: 3.4cqw; }
.wf-bcard--field .wf-bcard__side {
  grid-column: 3; grid-row: 1 / -1;
  align-self: center; width: 13cqw; margin-right: 4.4cqw;
}
.wf-bcard--field .wf-bcard__stack { grid-column: 2; grid-row: 3; padding: 0 4.4cqw 4.4cqw; }
/* 14 한글 — 국문 위주, 넓은 여백. 이름 옆에 직함이 같은 밑선에 섭니다.
 *
 * 시안 치수: 안쪽 여백 22/20px · 이름 34px · 직함 11px · 주황 22×2px
 */
.wf-bcard--hangeul {
  background: var(--bc-paper-1); color: var(--bc-ink-900);
  --bc-accent: var(--bc-warm);
  --bc-face-bg: var(--bc-paper-1);
}
.wf-bcard--hangeul .wf-bcard__in {
  padding: 6.1cqw 5.6cqw;
  gap: 0;
  grid-template-columns: minmax(0, 1fr) auto;
  /* 셋째 줄은 주황 선 전용입니다 — 넷째 줄이 아래 묶음입니다. */
  grid-template-rows: auto minmax(0, 1fr) auto auto;
}
.wf-bcard--hangeul .wf-bcard__label { grid-column: 1; grid-row: 1; color: var(--bc-ink-300); }
.wf-bcard--hangeul .wf-bcard__mono { display: none; }
.wf-bcard--hangeul .wf-bcard__info {
  grid-column: 1; grid-row: 2;
  justify-content: center; gap: 1.4cqw;
}
/*
 * 이름과 직함을 같은 밑선에 세웁니다 — 시안은 둘을 한 줄로 붙이고
 * 직함만 작게 둡니다. 마크업이 세로 묶음이라 여기서만 가로로 눕힙니다.
 */
.wf-bcard--hangeul .wf-bcard__text { flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 2.5cqw; }
.wf-bcard--hangeul .wf-bcard__name { font-size: 9.4cqw; letter-spacing: -0.02em; }
.wf-bcard--hangeul .wf-bcard__nameen,
.wf-bcard--hangeul .wf-bcard__title { font-size: 3.05cqw; color: var(--bc-ink-500); }
/*
 * 아래 묶음 머리의 주황 짧은 선.
 *
 * 처음에는 '기술 스택' 칸에 붙였는데, 그 값을 비우면 칸 자체가 없어져
 * 선까지 사라졌습니다. 이 판의 유일한 색이라 값과 상관없이 서야 합니다.
 */
.wf-bcard--hangeul .wf-bcard__in::after {
  content: '';
  grid-column: 1; grid-row: 3;
  width: 6.1cqw; height: 2px;
  margin-bottom: 2.8cqw;
  background: var(--bc-warm);
}
.wf-bcard--hangeul .wf-bcard__stack { grid-column: 1; grid-row: 4; color: var(--bc-ink-300); }
.wf-bcard--hangeul .wf-bcard__line { font-size: 2.8cqw; line-height: 1.8; color: var(--bc-ink-500); }
.wf-bcard--hangeul .wf-bcard__ico { width: 3.4cqw; height: 3.4cqw; }
.wf-bcard--hangeul .wf-bcard__side { grid-column: 2; grid-row: 3 / -1; justify-self: end; align-self: end; width: 13cqw; }
/* 15 디렉터리 — 정보 밀집. 머리줄 아래 굵은 선, 아래는 3열 표.
 *
 * 시안 치수: 머리줄 여백 11px · 아래 2px 선 · 3열 간격 10px ·
 * 칸마다 위쪽 실선 · 라벨 8px · 값 9px
 */
.wf-bcard--directory {
  background: var(--bc-paper-0); color: var(--bc-ink-900);
  --bc-face-bg: var(--bc-paper-0);
}
.wf-bcard--directory .wf-bcard__in {
  padding: 0; gap: 0;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
.wf-bcard--directory .wf-bcard__label {
  grid-column: 1; grid-row: 1;
  display: flex; align-items: center;
  padding: 3.05cqw 4.4cqw;
  font-family: inherit; text-transform: none;
  font-size: 3.05cqw; font-weight: 700; letter-spacing: 0.16em;
  color: var(--bc-ink-900);
  border-bottom: 2px solid var(--bc-ink-900);
}
.wf-bcard--directory .wf-bcard__mono {
  grid-column: 2; grid-row: 1;
  display: flex; align-items: center;
  padding: 3.05cqw 4.4cqw;
  font-family: inherit; font-size: 2.5cqw; font-weight: 400; letter-spacing: 0;
  color: var(--bc-ink-500);
  border-bottom: 2px solid var(--bc-ink-900);
}
.wf-bcard--directory .wf-bcard__info {
  grid-column: 1 / -1; grid-row: 2;
  justify-content: center; gap: 1.7cqw;
  padding: 3.3cqw 4.4cqw;
}
.wf-bcard--directory .wf-bcard__name { font-size: 7.2cqw; letter-spacing: -0.015em; }
.wf-bcard--directory .wf-bcard__nameen,
.wf-bcard--directory .wf-bcard__title { font-size: 3.05cqw; color: var(--bc-ink-500); }
/*
 * 연락처는 3열 표입니다. 줄 묶음을 격자로 바꾸면 되지만 마크업이
 * 줄 단위라 각 줄을 칸으로 세우고 위에 실선을 하나씩 얹었습니다.
 */
.wf-bcard--directory .wf-bcard__in--back .wf-bcard__info,
.wf-bcard--directory:not(.is-2face) .wf-bcard__info {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2.8cqw; align-content: center;
}
.wf-bcard--directory:not(.is-2face) .wf-bcard__row:has(.wf-bcard__name) { grid-column: 1 / -1; }
.wf-bcard--directory .wf-bcard__row:not(:has(.wf-bcard__name)) {
  flex-direction: column; align-items: flex-start; gap: 0.8cqw;
  border-top: 1px solid var(--bc-ink-100);
  padding-top: 1.7cqw;
}
.wf-bcard--directory .wf-bcard__line { font-size: 2.5cqw; line-height: 1.5; }
.wf-bcard--directory .wf-bcard__ico { order: -1; width: 3.05cqw; height: 3.05cqw; }
.wf-bcard--directory .wf-bcard__stack { grid-column: 1; grid-row: 3; padding: 0 4.4cqw 3.3cqw; }
.wf-bcard--directory .wf-bcard__side {
  grid-column: 2; grid-row: 3;
  justify-self: end; align-self: end;
  width: 11cqw; margin: 0 4.4cqw 3.3cqw 0;
}
/* 03 그리드 — 브루탈리즘. 3px 외곽 안에 **눈에 보이는 칸**을 긋습니다.
 *
 * 시안은 4열 격자에 칸마다 테두리를 두르고, 윗줄 오른쪽 끝에 검은 이니셜
 * 상자, 아랫줄 오른쪽 끝에 주황 칸을 답니다. 예전에는 외곽선만 굵게 하고
 * 칸을 긋지 않아 '그리드'라는 이름만 남아 있었습니다.
 *
 * 시안 치수(360px 카드): 윗줄 28px · 아랫줄 34px · 이름 34px
 */
.wf-bcard--grid {
  background: var(--bc-paper-1); color: var(--bc-ink-900);
  --bc-accent: var(--bc-warm);
  border: 3px solid var(--bc-ink-900);
}
.wf-bcard--grid .wf-bcard__in {
  padding: 0;
  /*
   * 셋째 칸은 이니셜 상자 · QR · 주황 칸이 세로로 이어지는 오른쪽 줄입니다.
   * QR을 아래 띠(34px)에 두려 했더니 '연락처 저장'까지 56px이라 띠를 4px
   * 넘겨 가운데 칸을 파고들었습니다 — 제 칸을 주는 편이 맞습니다.
   */
  grid-template-columns: minmax(0, 1fr) auto 9.4cqw;
  grid-template-rows: 7.8cqw minmax(0, 1fr) 9.4cqw;
}
.wf-bcard--grid .wf-bcard__label {
  grid-column: 1 / 3; grid-row: 1;
  display: flex; align-items: center;
  padding: 0 1.9cqw;
  color: var(--bc-ink-500);
  border-right: 1px solid var(--bc-ink-900);
  border-bottom: 1px solid var(--bc-ink-900);
}
.wf-bcard--grid .wf-bcard__mono {
  grid-column: 3; grid-row: 1;
  justify-self: stretch; align-self: stretch;
  display: grid; place-items: center;
  background: var(--bc-ink-900); color: #fff;
  font-size: 5.6cqw; letter-spacing: 0;
  border-bottom: 1px solid var(--bc-ink-900);
}
.wf-bcard--grid .wf-bcard__info {
  grid-column: 1 / 3; grid-row: 2;
  justify-content: center; overflow: hidden;
  padding: 0 1.9cqw;
  border-bottom: 1px solid var(--bc-ink-900);
}
.wf-bcard--grid .wf-bcard__name { font-size: 9.4cqw; text-transform: uppercase; }
/*
 * 아랫줄 오른쪽 끝의 주황 칸. 마크업에 없는 조각이라 __in의 가상 요소로
 * 만듭니다 — 격자 컨테이너의 ::after는 그 자체가 격자 항목이 됩니다.
 */
.wf-bcard--grid .wf-bcard__in::after {
  content: '';
  grid-column: 3; grid-row: 3;
  background: var(--bc-warm);
  border-left: 1px solid var(--bc-ink-900);
}
.wf-bcard--grid .wf-bcard__stack {
  grid-column: 1; grid-row: 3;
  align-self: center; padding: 0 1.9cqw;
}
/*
 * 한 면만 쓸 때는 연락처와 QR까지 한 장에 들어옵니다. 시안의 34px 이름을
 * 그대로 두면 가운데 칸이 넘치므로 그때만 이름을 줄입니다.
 */
/*
 * 뒷면에서는 연락처가 왼쪽 칸을, QR이 오른쪽 칸을 씁니다. 자리를 안 주면
 * QR이 격자 밖 새 열로 밀려나 카드가 가로로 넘칩니다.
 */
.wf-bcard--grid .wf-bcard__side {
  grid-column: 3; grid-row: 2;
  justify-self: center; align-self: center;
  width: 7cqw;
}
/*
 * 오른쪽 칸은 이니셜 상자 폭(34px)이라 '연락처 저장'이 한 줄로는 안
 * 들어갑니다. 여기서만 줄바꿈을 허용합니다 — 기본값이 nowrap인 것은
 * 넓은 자리에서 어색하게 끊기지 않게 하려는 것이라 좁은 칸에서는
 * 그 이유가 없습니다.
 */
.wf-bcard--grid .wf-bcard__save {
  white-space: normal; text-align: center;
  font-size: 1.9cqw; line-height: 1.25;
}
.wf-bcard--grid:not(.is-2face) .wf-bcard__name { font-size: 6.2cqw; }
/* 한 면만 쓸 때는 가운데 칸에 이름과 연락처가 함께 들어와 빠듯합니다. */
.wf-bcard--grid:not(.is-2face) .wf-bcard__info { gap: 0.8cqw; }
.wf-bcard--grid:not(.is-2face) .wf-bcard__line { font-size: 2.2cqw; }
.wf-bcard--grid .wf-bcard__nameen, .wf-bcard--grid .wf-bcard__title { color: var(--bc-ink-500); }
/* 04 나이트 — 다크 모드 앱 UI. 한 겹 패널에 담긴 느낌. */
/*
 * 시안의 표식은 **왼쪽 34px 레일**입니다. 예전에는 배경색만 어둡게 하고
 * 레일이 없어 04라는 번호만 붙은 다른 카드였습니다.
 *
 * 레일 자체는 __in의 가상 요소로 깔고, 그 위에 이니셜 상자를 얹습니다.
 * 시안 치수: 레일 34px · 이니셜 상자 18px · 본문 여백 14/16px · 이름 26px
 */
.wf-bcard--night { background: var(--bc-ink-900); color: var(--bc-paper-1); }
.wf-bcard--night .wf-bcard__in {
  background: #14161A;
  padding: 0;
  /* 셋째 칸은 QR 자리입니다 — 연락처와 같은 칸에 두면 글 위에 얹힙니다. */
  grid-template-columns: 9.4cqw minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
.wf-bcard--night .wf-bcard__in::before {
  content: '';
  grid-column: 1; grid-row: 1 / -1;
  background: var(--bc-ink-900);
}
/* 레일 위의 액센트 이니셜 상자 — 레일과 같은 칸에 겹쳐 놓습니다. */
.wf-bcard--night .wf-bcard__mono {
  grid-column: 1; grid-row: 1;
  justify-self: center; align-self: start;
  margin-top: 3.3cqw;
  display: grid; place-items: center;
  width: 5cqw; height: 5cqw;
  border-radius: 0.6cqw;
  background: var(--bc-accent); color: #fff;
  font-size: 2.5cqw; font-weight: 700; letter-spacing: 0;
}
.wf-bcard--night .wf-bcard__label {
  grid-column: 2 / -1; grid-row: 1;
  display: flex; align-items: center; justify-content: space-between;
  margin: 3.9cqw 4.4cqw 0;
  padding-bottom: 2.5cqw;
  border-bottom: 1px solid #262A31;
  color: var(--bc-ink-300);
}
/* 오른쪽 끝의 작은 네온 점 — 시안의 '살아 있음' 표시입니다. */
.wf-bcard--night .wf-bcard__label::after {
  content: '';
  width: 1.9cqw; height: 1.9cqw;
  border-radius: 50%;
  background: var(--bc-neon);
}
.wf-bcard--night .wf-bcard__info {
  grid-column: 2; grid-row: 2;
  justify-content: flex-end; overflow: hidden;
  padding: 0 4.4cqw 3.9cqw;
}
.wf-bcard--night .wf-bcard__name { font-size: 7.2cqw; color: #fff; letter-spacing: -0.03em; }
.wf-bcard--night .wf-bcard__nameen, .wf-bcard--night .wf-bcard__title { color: var(--bc-ink-300); }
.wf-bcard--night .wf-bcard__row { border-bottom: 1px solid #262A31; padding: 1.4cqw 0; }
/*
 * 한 면만 쓸 때는 이름과 연락처가 같은 칸에 들어와 7px 모자랐습니다.
 * 넘친 쪽이 위라 이름의 머리가 잘려 나갔습니다 — flex-end로 아래에
 * 붙여 두었기 때문인데, 그 방향의 넘침은 스크롤로도 닿지 않습니다.
 */
.wf-bcard--night:not(.is-2face) .wf-bcard__name { font-size: 6.4cqw; }
.wf-bcard--night:not(.is-2face) .wf-bcard__row { padding: 0.7cqw 0; }
.wf-bcard--night:not(.is-2face) .wf-bcard__info { gap: 0.4cqw; }
.wf-bcard--night .wf-bcard__line { color: #C6CAD1; }
.wf-bcard--night .wf-bcard__stack { grid-column: 2; grid-row: 3; padding: 0 4.4cqw 3.9cqw; }
.wf-bcard--night .wf-bcard__side {
  grid-column: 3; grid-row: 2;
  align-self: center;
  width: 11cqw; margin-right: 4.4cqw;
}
/* 05 시그널 — 네온. 글로우는 가이드가 이 장에만 허용합니다. */
/*
 * 시안의 표식은 오른쪽 위 **네온 막대**와 네온으로 빛나는 이름입니다.
 * 예전에는 연락처 왼쪽에 세로선을 긋고 이름은 흰색이었습니다 — 장치도
 * 자리도 다른 물건이었습니다.
 */
.wf-bcard--signal { background: #07080A; color: #fff; --bc-accent: var(--bc-neon); }
.wf-bcard--signal .wf-bcard__label { color: var(--bc-neon); text-shadow: 0 0 14px rgb(0 229 160 / 0.45); }
/*
 * 이니셜 자리를 막대가 대신합니다(시안의 이 장에는 이니셜이 없습니다).
 * 글자를 숨기고 크기를 직접 주는 방식이라 자동으로 만든 이니셜만
 * 사라지고 사용자가 적은 값은 어디에서도 지워지지 않습니다.
 */
.wf-bcard--signal .wf-bcard__mono {
  font-size: 0; align-self: center;
  width: 11cqw; height: 0.55cqw;
  background: var(--bc-neon);
  box-shadow: 0 0 10px rgb(0 229 160 / 0.6);
}
.wf-bcard--signal .wf-bcard__name {
  font-size: 9.4cqw; color: var(--bc-neon);
  text-shadow: 0 0 14px rgb(0 229 160 / 0.45);
}
.wf-bcard--signal .wf-bcard__nameen,
.wf-bcard--signal .wf-bcard__title {
  color: #8A9099; text-transform: uppercase; letter-spacing: 0.08em;
}
.wf-bcard--signal .wf-bcard__line { color: #9AA1AA; }
/* 06 레터프레스 — 따뜻한 종이에 금박, 가운데 정렬.
   압인은 텍스트 그림자로만 흉내 냅니다(가이드가 정한 유일한 질감 표현). */
/*
 * 시안은 paper-1에 안쪽 그림자 두 겹으로 '눌린 종이'를 냅니다.
 * 바깥 1px 테두리 그림자가 재단면, 넓게 퍼지는 쪽이 가장자리 그늘입니다.
 */
.wf-bcard--letter {
  background: var(--bc-paper-1); color: #6E6A60; --bc-accent: var(--bc-gold);
  box-shadow: inset 0 0 0 1px rgb(11 12 14 / 0.09), inset 0 0 22px rgb(11 12 14 / 0.05);
}
.wf-bcard--letter .wf-bcard__in {
  /* 가운데 정렬 판이라 QR까지 한 줄에 세우면 무게중심이 오른쪽으로 쏠립니다.
   * 세로로 라벨 / 이름·연락처 / 기술·QR 세 줄을 쌓고 전부 가운데로 모읍니다. */
  grid-template-columns: 1fr; grid-template-rows: auto 1fr auto;
  justify-items: center; text-align: center;
}
.wf-bcard--letter .wf-bcard__mono { display: none; }
.wf-bcard--letter .wf-bcard__label { grid-column: 1; color: var(--bc-accent); }
.wf-bcard--letter .wf-bcard__info { grid-column: 1; align-items: center; }
.wf-bcard--letter .wf-bcard__text { align-items: center; text-align: center; }
.wf-bcard--letter .wf-bcard__row { justify-content: center; }
.wf-bcard--letter .wf-bcard__info { grid-row: 2; align-self: center; }
.wf-bcard--letter .wf-bcard__stack { grid-column: 1; grid-row: 3; justify-self: center; align-self: end; }
.wf-bcard--letter .wf-bcard__side { grid-column: 1; grid-row: 2; justify-self: end; align-self: center; }
.wf-bcard--letter .wf-bcard__name {
  color: #6E6A60;
  text-shadow: 0 1px 0 rgb(255 255 255 / 0.85), 0 -1px 0 rgb(11 12 14 / 0.18);
}
/* 이름 아래 28×1px 금선 — 이 장의 유일한 장식입니다. */
.wf-bcard--letter .wf-bcard__name::after {
  content: ''; display: block;
  width: 7.8cqw; height: 1px;
  margin: 2.5cqw auto;
  background: var(--bc-gold);
}
.wf-bcard--letter .wf-bcard__line { color: #6E6A60; letter-spacing: 0.02em; }
.wf-bcard--letter .wf-bcard__nameen, .wf-bcard--letter .wf-bcard__title { color: var(--bc-ink-500); }
/* 07 스위스 — 왼쪽 색 필드에 이니셜, 오른쪽에 정보. 이름 아래 2px 규칙선. */
.wf-bcard--swiss { background: var(--bc-paper-0); color: var(--bc-ink-900); }
.wf-bcard--swiss .wf-bcard__in {
  /*
   * 오른쪽은 세 줄입니다 — 라벨 / 이름·연락처 / 기술·QR.
   * 예전에는 라벨과 이름을 같은 칸에 넣고 하나는 위, 하나는 아래로 붙였는데,
   * 이름 칸이 길어지면 둘이 10px 겹쳤습니다(칸이 하나면 결국 같은 자리입니다).
   */
  padding: 0;
  /* 셋째 칸은 QR 자리 — 없으면 연락처와 같은 칸에 겹쳐 앉습니다. */
  grid-template-columns: 31% minmax(0, 1fr) auto;
  grid-template-rows: auto 1fr auto;
}
.wf-bcard--swiss .wf-bcard__mono {
  grid-column: 1; grid-row: 1 / -1; justify-self: stretch; align-self: stretch;
  background: var(--bc-accent); color: #fff;
  display: flex; align-items: flex-start;
  padding: 4.4cqw; font-size: 8.6cqw;
}
.wf-bcard--swiss .wf-bcard__label { grid-column: 2 / -1; grid-row: 1; align-self: start; padding: 4.4cqw 4.4cqw 0; color: var(--bc-accent); }
.wf-bcard--swiss .wf-bcard__info { grid-column: 2; grid-row: 2; align-self: center; padding: 0 4.4cqw; }
.wf-bcard--swiss .wf-bcard__stack { grid-column: 2; grid-row: 3; align-self: end; padding: 0 4.4cqw 3.4cqw; }
.wf-bcard--swiss .wf-bcard__side { grid-column: 3; grid-row: 2; align-self: center; margin-right: 4.4cqw; }
.wf-bcard--swiss .wf-bcard__name::after {
  content: ''; display: block; width: 34px; height: 2px;
  background: var(--bc-ink-900); margin: 1.6cqw 0 0.8cqw;
}
.wf-bcard--swiss .wf-bcard__nameen, .wf-bcard--swiss .wf-bcard__title { color: var(--bc-ink-500); }
/* 08 신택스 — 코드 편집기. 왼쪽 **줄번호 거터**가 이 카드의 표식입니다.
 *
 * 예전에는 검은 바탕에 네온 글자였는데 시안은 **흰 종이**입니다. 어두운
 * 판은 02 셸이 맡고 있어 둘이 겹쳐 있었던 셈입니다.
 *
 * 줄번호는 마크업에 없어 거터를 __in의 가상 요소로 만들고 번호를
 * content에 줄바꿈(\A)으로 넣습니다 — 결과물에 스크립트를 안 넣는다는
 * 규칙 아래에서 번호를 세는 유일한 방법입니다.
 *
 * 시안 치수: 거터 26px · 번호 9px · 본문 11px/1.75 · 여백 12/14px
 */
.wf-bcard--syntax { background: var(--bc-paper-0); color: var(--bc-syn-punct); }
.wf-bcard--syntax .wf-bcard__in {
  padding: 0;
  font-family: var(--bc-mono);
  /* 셋째 칸은 이니셜과 QR 자리입니다 — 라벨과 같은 칸에 두면 겹칩니다. */
  grid-template-columns: 7.2cqw minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
.wf-bcard--syntax .wf-bcard__in::before {
  content: '1\A 2\A 3\A 4\A 5\A 6\A 7\A 8\A 9';
  grid-column: 1; grid-row: 1 / -1;
  white-space: pre; text-align: right;
  background: var(--bc-paper-1);
  border-right: 1px solid var(--bc-ink-100);
  color: var(--bc-ink-200);
  font-size: 2.5cqw; line-height: 1.75;
  padding: 3.3cqw 1.7cqw 0 0;
}
.wf-bcard--syntax .wf-bcard__label {
  grid-column: 2; grid-row: 1;
  padding: 3.3cqw 3.9cqw 0;
  color: var(--bc-syn-com);
}
.wf-bcard--syntax .wf-bcard__mono {
  grid-column: 3; grid-row: 1;
  justify-self: end; align-self: start;
  padding: 3.3cqw 3.9cqw 0 0;
  font-size: 3.05cqw; color: var(--bc-syn-fn);
}
.wf-bcard--syntax .wf-bcard__info {
  grid-column: 2; grid-row: 2;
  justify-content: center; overflow: hidden;
  padding: 0 3.9cqw;
}
/* 시안의 이름은 코드 속 문자열입니다 — 본문과 같은 크기로 둡니다. */
.wf-bcard--syntax .wf-bcard__name {
  font-family: var(--bc-mono); font-weight: 400;
  font-size: 3.05cqw; letter-spacing: 0;
  color: var(--bc-syn-str);
}
.wf-bcard--syntax .wf-bcard__name::before { content: 'const '; color: var(--bc-syn-key); }
.wf-bcard--syntax .wf-bcard__nameen,
.wf-bcard--syntax .wf-bcard__title { color: var(--bc-syn-com); font-family: var(--bc-mono); }
.wf-bcard--syntax .wf-bcard__line { color: var(--bc-syn-str); }
.wf-bcard--syntax .wf-bcard__ico { width: 3.4cqw; height: 3.4cqw; background: transparent; color: var(--bc-syn-key); }
.wf-bcard--syntax .wf-bcard__stack { grid-column: 2; grid-row: 3; padding: 0 3.9cqw 3.3cqw; color: var(--bc-syn-com); }
.wf-bcard--syntax .wf-bcard__side {
  grid-column: 3; grid-row: 2;
  align-self: center;
  width: 11cqw; margin-right: 3.9cqw;
}
/* 한 줄 소개는 여덟 장 모두에서 기술 스택 자리와 겹칩니다 —
 * 둘 다 적었으면 소개를 접습니다(명함에 문장은 최대 하나라는 가이드 규칙). */
.wf-bcard--quiet .wf-bcard__tagline, .wf-bcard--shell .wf-bcard__tagline,
.wf-bcard--grid .wf-bcard__tagline, .wf-bcard--night .wf-bcard__tagline,
.wf-bcard--signal .wf-bcard__tagline, .wf-bcard--letter .wf-bcard__tagline,
.wf-bcard--swiss .wf-bcard__tagline, .wf-bcard--syntax .wf-bcard__tagline { display: none; }
/* 아이콘 모양 — 우리가 그린 도형이라 상업적으로 써도 됩니다.
 * 바깥 아이콘 팩을 담지 않는 것은 팩마다 표시 의무·재배포 조건이 달라
 * 결과물을 받은 사람이 그 조건을 떠안게 되기 때문입니다. */
.wf-bcard--ico-line .wf-bcard__ico {
  background: transparent;
  color: var(--bc-accent);
  border: 0.35cqw solid var(--bc-accent);
}
.wf-bcard--ico-bare .wf-bcard__ico {
  background: transparent;
  color: var(--bc-accent);
  width: 4.6cqw; height: 4.6cqw;
}
.wf-bcard--ico-bare .wf-bcard__ico svg { width: 100%; height: 100%; }
.wf-bcard--ico-none .wf-bcard__ico { display: none; }
/* 아이콘을 빼면 잇던 점선도 뺍니다 — 이을 것이 없습니다. */
.wf-bcard--ico-none .wf-bcard__info::before { display: none; }
/* QR 자리 — '자동'이면 디자인이 정한 자리를 그대로 씁니다.
 * 나머지는 모서리에 띄우므로 세로 흐름에서 빠집니다(높이를 안 먹습니다). */
.wf-bcard.is-qr-right .wf-bcard__side,
.wf-bcard.is-qr-bl .wf-bcard__side,
.wf-bcard.is-qr-br .wf-bcard__side {
  position: absolute;
  width: 17cqw;
  margin: 0;
  /*
   * 그리드 자리를 반드시 풀어야 합니다. 자리를 정해 둔 격자 칸의 자식을
   * absolute로 띄우면 기준(컨테이너 블록)이 **격자 칸**이 되어, left를 5cqw로
   * 줘도 카드 왼쪽이 아니라 그 칸 왼쪽에서 잽니다(왼쪽 아래가 163px에 붙던
   * 이유). auto로 되돌리면 기준이 카드 안쪽 상자가 됩니다.
   */
  grid-column: auto;
  grid-row: auto;
  /* 디자인이 미리 잡아 둔 좌표를 먼저 지웁니다 — 가운데 정렬의 top:50%가
   * 남아 있으면 '아래'로 옮겨도 위에 붙은 채로 있습니다. */
  inset: auto;
  translate: none;
}
.wf-bcard.is-qr-right .wf-bcard__side { right: 5cqw; top: 50%; translate: 0 -50%; }
.wf-bcard.is-qr-bl .wf-bcard__side { left: 5cqw; bottom: 5cqw; }
.wf-bcard.is-qr-br .wf-bcard__side { right: 5cqw; bottom: 5cqw; }
/* 띄운 QR과 글이 겹치지 않게 그쪽 여백을 넓힙니다. */
.wf-bcard.is-qr-right .wf-bcard__in { padding-right: 22cqw; }
.wf-bcard.is-qr-br .wf-bcard__in { padding-right: 24cqw; }
.wf-bcard.is-qr-bl .wf-bcard__in { padding-left: 24cqw; }
/* 장식 곡선 — 기본은 안 그립니다. '사이드 라인'만 켭니다. */
.wf-bcard__deco { display: none; }
/* 사이드 라인 — 왼쪽에 큰 분야, 오른쪽에 아이콘 줄. 참고 이미지의 배치입니다.
 * 아이콘을 잇는 세로 점선은 아이콘 칸 뒤에 깔아 그립니다(요소를 안 늘립니다). */
/*
 * 두 칸입니다. 예전엔 큰 글자 / 연락처 / QR 세 칸이었는데, 폭을 나눠 보면
 * 셋을 한 줄에 세우는 순간 연락처 칸이 카드 폭의 15%까지 눌려 이메일이
 * 두 줄로 접혔습니다(글자를 줄여도 다 같이 줄어들어 비율은 그대로입니다).
 * QR은 큰 글자 아래의 빈 자리로 내려보냅니다 — 세로로는 남으니까요.
 */
.wf-bcard--sideline .wf-bcard__in {
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: 1fr auto;
  align-items: center;
}
.wf-bcard--sideline .wf-bcard__lead { grid-column: 1; grid-row: 1; align-self: end; }
.wf-bcard--sideline .wf-bcard__info { grid-column: 2; grid-row: 1 / -1; }
.wf-bcard--sideline .wf-bcard__side { grid-column: 1; grid-row: 2; align-self: start; align-items: flex-start; }
/*
 * 아이콘을 꿰는 세로 점선.
 *
 * 위치는 아이콘 반지름에서 옵니다 — 아이콘이 줄 끝에 붙으므로 그 중심은
 * 오른쪽 끝에서 정확히 아이콘 폭의 절반(5.4cqw ÷ 2)만큼 안쪽입니다.
 * 전에는 3cqw로 어림해 두어 2px 어긋난 채로 지나갔습니다.
 * 선 자체의 두께 1px도 반만큼 물려야 가운데가 맞습니다.
 */
.wf-bcard--sideline .wf-bcard__info::before {
  content: '';
  position: absolute; top: 8%; bottom: 8%;
  right: calc(2.7cqw - 0.5px);
  width: 1px;
  background: repeating-linear-gradient(to bottom,
    var(--bc-accent) 0 3px, transparent 3px 7px);
  opacity: 0.55;
}
.wf-bcard--sideline .wf-bcard__info { position: relative; }
.wf-bcard--sideline .wf-bcard__row { justify-content: flex-end; text-align: right; }
.wf-bcard--sideline .wf-bcard__text { align-items: flex-end; text-align: right; }
.wf-bcard--sideline .wf-bcard__ico { position: relative; z-index: 1; }
.wf-bcard--sideline .wf-bcard__side { width: 20cqw; }
.wf-bcard--sideline .wf-bcard__deco {
  display: block;
  position: absolute; left: -8cqw; top: -18%; bottom: -18%; width: 34cqw;
  border-radius: 50%;
  border: 1.2cqw solid color-mix(in srgb, var(--bc-accent) 26%, transparent);
  border-right-color: transparent;
  transform: rotate(-12deg);
  pointer-events: none;
}
/* 가운데 정렬 — 이름을 앞세우는 가장 조용한 판 */
/*
 * 가운데 정렬은 세로로 쌓는 판이라 담을 높이가 가장 빠듯합니다.
 * 큰 글자 + 이름 세 줄 + 연락처 세 줄 + QR을 그대로 세우면 카드 높이를
 * 17px 넘겼습니다(1.655 비율은 못 늘리므로 넘친 만큼 잘려 나갔습니다).
 * QR을 오른쪽에 띄워 세로 흐름에서 빼고, 글자도 한 단계 조입니다.
 */
.wf-bcard--center .wf-bcard__in {
  grid-template-rows: auto auto;
  justify-items: center; align-content: center;
  text-align: center;
  gap: 1.6cqw;
}
.wf-bcard--center .wf-bcard__info { gap: 1.4cqw; }
.wf-bcard--center .wf-bcard__name { font-size: 4.4cqw; }
.wf-bcard--center .wf-bcard__line { font-size: 2.4cqw; }
.wf-bcard--center .wf-bcard__ico { width: 4.6cqw; height: 4.6cqw; }
.wf-bcard--center .wf-bcard__side {
  position: absolute; right: 5cqw; top: 50%; translate: 0 -50%;
  width: 16cqw;
}
/* QR이 있을 때만 그만큼 오른쪽을 비웁니다 — 없으면 가운데 그대로입니다. */
.wf-bcard--center .wf-bcard__in:has(.wf-bcard__side) { padding-right: 26cqw; }
.wf-bcard--center .wf-bcard__lead { align-items: center; }
.wf-bcard--center .wf-bcard__role { font-size: 7cqw; }
.wf-bcard--center .wf-bcard__rolesub { font-size: 3.4cqw; color: var(--wf-muted); }
.wf-bcard--center .wf-bcard__info { align-items: center; }
.wf-bcard--center .wf-bcard__text { align-items: center; text-align: center; }
/* 좌우 분할 — 왼쪽 색 띠에 분야, 오른쪽에 연락처 */
.wf-bcard--split .wf-bcard__in {
  grid-template-columns: 34% 1fr auto;
  align-items: center;
  padding: 0 6cqw 0 0;
  gap: 4cqw;
}
.wf-bcard--split .wf-bcard__lead {
  align-self: stretch; justify-content: center;
  padding: 0 4cqw;
  background: var(--bc-accent);
  color: #fff;
}
.wf-bcard--split .wf-bcard__role { color: #fff; font-size: 6.6cqw; }
.wf-bcard--split .wf-bcard__rolesub { color: rgb(255 255 255 / 0.85); font-size: 3.6cqw; }
.wf-bcard--split .wf-bcard__side { width: 15cqw; }
/* 다크 카드 — 어두운 바탕에 포인트 색만 남깁니다 */
.wf-bcard--dark {
  background: #111114;
  border-color: rgb(255 255 255 / 0.14);
  color: #f4f4f5;
}
.wf-bcard--dark .wf-bcard__in {
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: 1fr auto;
  align-items: center;
}
.wf-bcard--dark .wf-bcard__lead { grid-column: 1; grid-row: 1; align-self: end; }
.wf-bcard--dark .wf-bcard__info { grid-column: 2; grid-row: 1 / -1; }
.wf-bcard--dark .wf-bcard__side { grid-column: 1; grid-row: 2; align-self: start; align-items: flex-start; }
.wf-bcard--dark .wf-bcard__nameen,
.wf-bcard--dark .wf-bcard__title { color: rgb(255 255 255 / 0.6); }
.wf-bcard--dark .wf-bcard__row { justify-content: flex-end; text-align: right; }
.wf-bcard--dark .wf-bcard__text { align-items: flex-end; text-align: right; }
.wf-bcard--dark .wf-bcard__side { width: 20cqw; }
.wf-bcard--dark .wf-bcard__qr .wf-qr { color: #f4f4f5; }
/*
 * 좁을 때 — 기준은 **화면 폭이 아니라 명함 폭**입니다(@container).
 * 화면으로 재면 넓은 모니터에서 좁은 칸에 넣은 명함이 안 줄고, 반대로 폰에서
 * 화면을 꽉 채운 명함의 QR이 이유 없이 사라집니다. 명함은 제 폭만 압니다.
 * 안쪽 글자는 cqw라 저절로 줄어드니 QR만 자리를 양보합니다.
 */
@container (max-width: 300px) {
  .wf-bcard__side { display: none; }
  .wf-bcard--split .wf-bcard__in { grid-template-columns: 32% 1fr; }
}
/* ── 앞·뒤 명함 ────────────────────────────────────────────────
 * 누르면 Y축으로 돌아 뒷면이 나옵니다. 상태를 담는 것은 체크박스 하나이고
 * 자바스크립트는 없습니다 — 결과물에 스크립트를 넣지 않는다는 규칙 때문인데,
 * 덤으로 키보드(Tab → Space)로도 넘어갑니다. 체크박스는 원래 포커스를 받습니다.
 */
.wf-bcard.is-2face {
  perspective: 1400px;
  /* 3D로 도는 면이 잘리지 않게 껍데기의 자르기를 풉니다(면마다 따로 자릅니다). */
  overflow: visible;
  background: none;
  border-color: transparent;
}
.wf-bcard__toggle {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  margin: 0; opacity: 0; z-index: 2;
  cursor: pointer;
  appearance: none; -webkit-appearance: none;
}
.wf-bcard__flip {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.wf-bcard__toggle:checked ~ .wf-bcard__flip { transform: rotateY(180deg); }
/* 포커스가 갔을 때 어디에 있는지 보이게 — 키보드로 넘기는 사람을 위한 표시입니다. */
.wf-bcard__toggle:focus-visible ~ .wf-bcard__flip { outline: 2px solid var(--bc-accent); outline-offset: 3px; }
.wf-bcard__face {
  position: absolute; inset: 0;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  border-radius: inherit;
  overflow: hidden;
  /* 껍데기가 배경을 비웠으므로 색은 면이 가집니다 — 디자인 클래스의 값을 그대로 받습니다. */
  background: inherit;
}
.wf-bcard.is-2face .wf-bcard__face { background: var(--bc-face-bg, var(--wf-surface)); }
.wf-bcard__face--back { transform: rotateY(180deg); }
/* 뒷면은 한 줄 소개가 맨 위에 섭니다(앞면의 라벨 자리). */
.wf-bcard__face--back .wf-bcard__tagline {
  display: block;
  grid-column: 1 / -1; grid-row: 1;
  align-self: start;
}
.wf-bcard__in--back .wf-bcard__info { grid-row: 2; }
.wf-bcard__flipbtn {
  position: absolute; right: 3cqw; bottom: 2.6cqw; z-index: 3;
  padding: 1.2cqw 2.4cqw;
  border-radius: 999px;
  background: color-mix(in srgb, var(--bc-accent) 92%, transparent);
  color: #fff;
  font-size: 2.2cqw; font-weight: 700;
  cursor: pointer;
  user-select: none;
}
.wf-bcard__flipbtn:hover { background: var(--bc-accent); }
/*
 * PNG 저장.
 *
 * 자리는 짐작이 아니라 재서 골랐습니다. 서른여덟 칸에서 네 모서리에
 * 30px 상자를 놓아 보고 무엇과 부딪히는지 셌습니다.
 *     왼쪽 위 24/38 · 오른쪽 위 25/38 · 왼쪽 아래 8/38 · 오른쪽 아래 4/38
 * 위쪽 두 모서리는 로고와 이니셜이 늘 차지하고 있어 쓸 수 없습니다.
 *
 * 그래서 기본은 오른쪽 아래입니다. 남는 두 경우만 비켜 세웁니다.
 *   · 앞뒤 판은 그 자리에 뒤집기 버튼이 있으므로 그 왼쪽으로.
 *   · QR을 오른쪽 아래에 두는 네 판(포멀·이그제큐티브·한글·디렉터리)은
 *     왼쪽 아래로. 그 네 판은 왼쪽 아래가 비어 있는 것을 확인했습니다.
 *
 * 카드 **바깥**에 두는 방법은 통하지 않았습니다. 영역이 넘침을 자르기
 * 때문에 카드 위로 6px만 올려도 통째로 잘려 보이지 않습니다
 * (예전에 영역 사이 '+' 버튼이 잘렸던 것과 같은 자리입니다).
 *
 * 평소에는 흐리게, 카드에 마우스를 올리거나 초점이 닿으면 또렷하게.
 * 손가락으로 쓰는 화면에는 호버가 없으므로 처음부터 또렷합니다.
 */
.wf-bcard__png {
  position: absolute; right: 3cqw; bottom: 2.6cqw; z-index: 4;
  display: inline-flex; align-items: center; justify-content: center;
  width: 5.4cqw; height: 5.4cqw; min-width: 22px; min-height: 22px;
  padding: 0;
  border: 0; border-radius: 999px;
  background: color-mix(in srgb, var(--bc-accent) 92%, transparent);
  color: #fff;
  font-size: 3cqw;
  cursor: pointer;
  opacity: 0.45;
  transition: opacity 0.15s ease;
}
.wf-bcard:hover .wf-bcard__png,
.wf-bcard__png:focus-visible { opacity: 1; }
/* 앞뒤 판 — 뒤집기 버튼 왼쪽으로 비켜섭니다. */
.wf-bcard.is-2face .wf-bcard__png { right: calc(3cqw + 74px); }
/* QR이 오른쪽 아래를 쓰는 판 — 왼쪽 아래로 비켜섭니다. */
:is(.wf-bcard--hangeul, .wf-bcard--directory):not(.is-2face) .wf-bcard__png {
  right: auto; left: 3cqw;
}
/*
 * 포멀·이그제큐티브는 한 면일 때 네 모서리가 전부 찹니다(로고 줄이 위를
 * 가로지르고, 연락처가 왼쪽 아래, QR이 오른쪽 아래). 오른쪽 가장자리
 * 한가운데만 비어 있어 그리로 보냅니다 — 재 보고 고른 자리입니다.
 */
:is(.wf-bcard--formal, .wf-bcard--executive):not(.is-2face) .wf-bcard__png {
  right: 3cqw; left: auto;
  bottom: auto; top: 50%; translate: 0 -50%;
}
@media (hover: none) {
  .wf-bcard__png { opacity: 1; }
}
/*
 * ── 종이 질감 ────────────────────────────────────────────────
 *
 * 명함은 인쇄물이라 완전히 평평한 색면이면 화면 도형처럼 보입니다.
 * 아주 옅은 결을 얹으면 같은 색이라도 '종이'로 읽힙니다.
 *
 * 이미지를 쓰지 않습니다 — 결과물이 외부 요청 없이 혼자 돌아야 한다는
 * 이 제품의 규칙 때문입니다. 어떻게 그리는지는 아래 background-image에
 * 적어 두었습니다.
 *
 * 세기는 밝은 종이와 어두운 카드가 다릅니다. 흰 종이에서는 검은 결이,
 * 검은 카드에서는 흰 결이 보여야 같은 깊이로 읽힙니다.
 *
 * 무늬는 면 위에 덮되 글자 아래에 깔고, 클릭을 가로채지 않게 합니다.
 */
.wf-bcard__paper {
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 0;
  border-radius: inherit;
  /*
   * 세기는 오른쪽 패널의 슬라이더가 정합니다(0이면 요소를 아예 안 그립니다).
   * 최댓값에서 확실히 보여야 슬라이더가 무슨 일을 하는지 알 수 있어서,
   * 결 자체를 눈에 잡히는 세기로 두고 평소 값은 기본 40%로 낮춰 씁니다.
   */
  opacity: var(--bc-paper, 0.4);
  /*
   * 결은 다섯 겹입니다. 빗금 두 겹만 쓰면 아무리 옅게 해도 '줄무늬'로
   * 읽힙니다 — 주기가 눈에 잡히기 때문입니다. 서로 다른 크기의 점 세
   * 겹을 얹으면 규칙이 깨져 섬유처럼 보입니다.
   *
   * 점 격자의 주기(5·8·13px)는 서로 나누어떨어지지 않게 골랐습니다.
   * 배수 관계면 몇 칸마다 같은 자리에서 겹쳐 격자무늬가 드러납니다.
   */
  background-image:
    repeating-linear-gradient(58deg,
      rgb(11 12 14 / 0.055) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(148deg,
      rgb(11 12 14 / 0.04) 0 1px, transparent 1px 9px),
    radial-gradient(circle at 30% 20%, rgb(11 12 14 / 0.06) 0 0.6px, transparent 0.6px),
    radial-gradient(circle at 70% 65%, rgb(11 12 14 / 0.05) 0 0.5px, transparent 0.5px),
    radial-gradient(circle at 15% 80%, rgb(11 12 14 / 0.045) 0 0.7px, transparent 0.7px);
  background-size: auto, auto, 5px 5px, 8px 8px, 13px 13px;
}
/* 어두운 판에서는 결이 빛으로 뒤집힙니다. */
:is(.wf-bcard--shell, .wf-bcard--night, .wf-bcard--signal, .wf-bcard--schema,
    .wf-bcard--executive, .wf-bcard--dark) .wf-bcard__paper {
  background-image:
    repeating-linear-gradient(58deg,
      rgb(255 255 255 / 0.05) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(148deg,
      rgb(255 255 255 / 0.035) 0 1px, transparent 1px 9px),
    radial-gradient(circle at 30% 20%, rgb(255 255 255 / 0.055) 0 0.6px, transparent 0.6px),
    radial-gradient(circle at 70% 65%, rgb(255 255 255 / 0.045) 0 0.5px, transparent 0.5px),
    radial-gradient(circle at 15% 80%, rgb(255 255 255 / 0.04) 0 0.7px, transparent 0.7px);
  background-size: auto, auto, 5px 5px, 8px 8px, 13px 13px;
}
/* 글은 결 위로 올라옵니다. */
.wf-bcard__in { z-index: 1; }
/*
 * 인쇄할 때는 뺍니다. 화면에서 종이처럼 보이라고 넣은 결인데 진짜 종이에
 * 얹으면 잉크만 더 쓰고 결은 이미 그 종이에 있습니다.
 */
@media print {
  .wf-bcard__paper { display: none; }
}
/*
 * ── 뒷면 공통 정리 ───────────────────────────────────────────
 *
 * 앞면 배치는 '큰 분야 글자'나 색 띠를 기준으로 짜여 있는데 **뒷면에는
 * 그 조각이 아예 없습니다**(앞은 이름, 뒤는 연락처로 나누기 때문입니다).
 * 앞면 규칙을 그대로 물려받으면 그 자리가 빈 칸으로 남아, 연락처가 좁은
 * 쪽에 몰리고 QR이 엉뚱한 자리에 섭니다 — 실제로 좌우 분할은 연락처가
 * 카드 폭의 3분의 1에 눌렸고, 사이드 라인·다크는 QR이 왼쪽에 붙었습니다.
 *
 * 뒷면은 어느 판이든 할 일이 같습니다. 연락처를 넓게, QR을 한쪽에,
 * 기술 스택을 아래에. 그래서 판마다 따로 적지 않고 여기서 한 번 정합니다.
 */
:is(.wf-bcard--sideline, .wf-bcard--dark, .wf-bcard--split,
    .wf-bcard--swiss, .wf-bcard--night, .wf-bcard--syntax) .wf-bcard__in--back {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: minmax(0, 1fr) auto;
  align-items: center;
  padding: 4.4cqw;
  gap: 3cqw;
}
:is(.wf-bcard--sideline, .wf-bcard--dark, .wf-bcard--split,
    .wf-bcard--swiss, .wf-bcard--night, .wf-bcard--syntax)
  .wf-bcard__in--back .wf-bcard__info {
  grid-column: 1; grid-row: 1;
  justify-content: center; padding: 0;
}
:is(.wf-bcard--sideline, .wf-bcard--dark, .wf-bcard--split,
    .wf-bcard--swiss, .wf-bcard--night, .wf-bcard--syntax)
  .wf-bcard__in--back .wf-bcard__side {
  grid-column: 2; grid-row: 1;
  justify-self: end; align-self: center;
  width: 15cqw; margin: 0;
}
:is(.wf-bcard--sideline, .wf-bcard--dark, .wf-bcard--split,
    .wf-bcard--swiss, .wf-bcard--night, .wf-bcard--syntax)
  .wf-bcard__in--back .wf-bcard__stack {
  grid-column: 1 / -1; grid-row: 2;
  justify-self: start; align-self: end; padding: 0;
}
/* 앞면에서 오른쪽으로 몰아 둔 판(다크·사이드 라인)도 뒤에서는 왼쪽부터 읽습니다. */
:is(.wf-bcard--sideline, .wf-bcard--dark) .wf-bcard__in--back .wf-bcard__row {
  justify-content: flex-start; text-align: left;
}
:is(.wf-bcard--sideline, .wf-bcard--dark) .wf-bcard__in--back .wf-bcard__text {
  align-items: flex-start; text-align: left;
}
/* 앞면의 장식 곡선·세로 점선은 뒷면에서 설 자리가 없습니다. */
.wf-bcard--sideline .wf-bcard__in--back .wf-bcard__info::before { content: none; }
/* 각 디자인의 바탕색을 면이 쓸 수 있게 변수로 한 번 더 적어 둡니다.
 * (배경 속성은 상속되지 않아 `background: inherit`만으로는 부족합니다) */
.wf-bcard--quiet { --bc-face-bg: var(--bc-paper-0); }
.wf-bcard--shell,
.wf-bcard--night,
.wf-bcard--signal,
.wf-bcard--executive { --bc-face-bg: var(--bc-ink-900); }
.wf-bcard--grid,
.wf-bcard--manifest { --bc-face-bg: var(--bc-paper-1); }
.wf-bcard--schema { --bc-face-bg: #0C1526; }
.wf-bcard--letter { --bc-face-bg: var(--bc-paper-1); }
.wf-bcard--swiss,
.wf-bcard--formal,
/* 신택스는 시안이 흰 종이입니다 — 검은 판은 02 셸이 맡습니다. */
.wf-bcard--syntax { --bc-face-bg: var(--bc-paper-0); }
.wf-bcard--dark { --bc-face-bg: #111114; }
/* 움직임을 불편해하는 사람에게는 회전을 빼고 바로 바뀌게 합니다. */
@media (prefers-reduced-motion: reduce) {
  .wf-bcard__flip { transition: none; }
  .wf-bcard--shell .wf-bcard__name::after { animation: none; }
}
/* 버튼에 파일이 걸렸을 때의 꼬리표 */
.wf-btn__file { font-style: normal; font-size: 0.8em; opacity: 0.72; margin-left: 7px; }
/* 앵커로 내려갈 때 고정 헤더에 글자가 가리지 않게 */
html { scroll-behavior: smooth; }
.wf__region { scroll-margin-top: 76px; }
/* ── 작업물 상세 ───────────────────────────────────────────────
 * 결과물은 자바스크립트 없이 CSS `:target`으로 엽니다.
 * 캔버스는 iframe이 srcdoc이라 해시를 못 써서 `.is-open`으로 엽니다.
 */
.wf-detail { display: none; }
.wf-detail:target {
  display: grid; place-items: center;
  position: fixed; inset: 0; z-index: 40;
  padding: 24px;
}
.wf-detail__scrim {
  position: absolute; inset: 0;
  background: rgb(0 0 0 / 0.55);
  cursor: pointer;
}
.wf-detail__box {
  position: relative;
  width: min(680px, 100%); max-height: 88vh;
  display: flex; flex-direction: column; gap: 10px;
  padding: 24px;
  overflow-y: auto;
  background: var(--wf-surface);
  border: 1px solid var(--wf-line);
  border-radius: 14px;
  box-shadow: 0 24px 64px rgb(0 0 0 / 0.3);
}
.wf-detail__close {
  position: absolute; top: 12px; right: 14px;
  width: 28px; height: 28px;
  display: grid; place-items: center;
  font-size: 0.9em; line-height: 1;
  color: inherit; text-decoration: none;
  border: 1px solid var(--wf-line); border-radius: 50%;
  cursor: pointer;
}
.wf-detail__img {
  display: block; width: 100%; max-height: 38vh;
  object-fit: cover;
  border-radius: 9px;
}
.wf-detail__title { font-size: 1.5em; line-height: 1.25; }
.wf-detail__body { margin: 0; font-size: 0.95em; line-height: 1.75; color: var(--wf-muted); white-space: pre-line; }
/* 상세가 있는 카드는 누를 수 있음을 알려 줍니다 */
.wf-card.has-detail { cursor: pointer; }
.wf-card.has-detail:hover { border-color: var(--wf-accent); }
/* ── 연락처 ────────────────────────────────────────────────── */

.wf-contact {
  min-width: 0; display: flex; flex-direction: column; gap: 12px;
  width: 100%; max-width: var(--wf-max); margin: 0 auto;
}
.wf-contact__mail {
  color: var(--wf-accent);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--wf-accent) 45%, transparent);
  align-self: inherit;
}
.wf-contact__mail.is-big { font-size: 2em; line-height: 1.2; letter-spacing: -0.02em; border-bottom-width: 2px; }
.wf-contact__meta { font-size: 0.85em; color: var(--wf-muted); }
.wf-contact__meta a { color: inherit; text-decoration: none; }
.wf-contact__meta i { font-style: normal; opacity: 0.5; }
/* ── 좁은 화면 ─────────────────────────────────────────────────
 * 여기까지 결과물에 그대로 나갑니다. 포트폴리오는 휴대폰으로 보는 비중이 크고,
 * 지금까지는 3열 그리드가 그대로 뜨고 230px 사이드바가 화면을 짜부라뜨렸습니다.
 *
 * `!important`를 쓰는 이유: 뼈대와 격자의 열 수는 인라인 style로 박혀 있어
 * (템플릿마다 다르므로) 미디어쿼리가 이길 방법이 없습니다.
 */
/*
 * 태블릿 — 아이패드 세로가 정확히 768px이라 아래 760px 규칙에 걸리지 않습니다.
 * 뼈대(사이드바 등)는 그대로 두고 격자만 2열로 줄입니다. 768px에 230px 사이드바
 * + 538px 본문은 아직 읽을 만하지만, 3~4열 카드는 이미 좁습니다.
 */
@media (max-width: 1024px) {
  .wf-gal__grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
  .wf-sec__cols { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
  .wf-main { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
}
@media (max-width: 760px) {
  /*
   * 뼈대를 grid에서 세로 흐름으로 바꿉니다. 그러면 인라인 grid-column·grid-row가
   * 무력화되고 영역이 뼈대에 적힌 순서대로 쌓입니다 — 열마다 다시 계산할 필요가 없습니다.
   */
  .wf,
  .wf--row { display: flex !important; flex-direction: column; }

  .wf-gal__grid { grid-template-columns: 1fr !important; }
  .wf-sec__cols { grid-template-columns: 1fr !important; }
  .wf-main { grid-template-columns: 1fr !important; }

  /*
   * 세로 배치(align-self)는 **격자에서만** 뜻이 있습니다.
   *
   * 위에서 뼈대를 세로 흐름(flex column)으로 바꾸는 순간 align-self는 가로축을
   * 가리킵니다. '가운데'로 둔 영역이 내용 크기로 쪼그라들어 화면 가운데
   * 떠 버립니다(내용이 짧은 푸터·상단바일수록 심합니다). 영역이 이미 위에서
   * 아래로 쌓여 있으니 세로 배치는 의미가 없어졌고, 폭만 되돌리면 됩니다.
   * 인라인으로 실려 오므로 !important라야 이깁니다.
   */
  /*
   * 폭도 함께 되돌립니다. 혼자 있는 행을 가운데·오른쪽으로 두면 정확한
   * 자리를 잡으려고 width와 justify-self를 인라인으로 싣는데(layRows의 fit),
   * 세로로 쌓이는 좁은 화면에서는 그 폭이 그대로 남아 **그 영역만 좁게**
   * 서 있습니다. 여기서는 다 같이 꽉 채우는 것이 맞습니다.
   */
  .wf__region {
    align-self: stretch !important;
    justify-self: stretch !important;
    width: auto !important;
  }

  /* 고정 요소는 좁은 화면에서 화면을 반쯤 먹습니다 — 흐름으로 되돌립니다. */
  .wf__region.is-sticky { position: static !important; }

  /* 메뉴가 한 줄에 안 들어가면 접히게 */
  .wf-bar { flex-wrap: wrap; gap: 10px; }
  .wf-nav { flex-wrap: wrap; }

  .wf-aside { align-items: flex-start !important; text-align: left !important; }
  .wf-detail__box { padding: 18px; }
  .wf-links--list .wf-link { flex-direction: column; align-items: flex-start; gap: 2px; }
}
@media (max-width: 460px) {
  .wf-links--buttons .wf-link { padding: 8px 13px; }
  .wf-contact__mail.is-big { font-size: 1.35em; }
}
/* ── 사이트 전체 배경 ──────────────────────────────────────────
 * 효과는 전부 CSS입니다 — 결과물에 자바스크립트가 들어가지 않습니다.
 */
/*
 * z-index를 -1로 두면 안 됩니다.
 *
 * `.wf-page`는 테마 배경색을 **인라인**으로 갖고 있어 불투명합니다. 자식이
 * `z-index: -1`이면 부모 배경보다 뒤로 가므로 배경 이미지가 완전히 가려집니다
 * (이미지는 제대로 로드되는데 화면에는 아무 변화가 없어 보입니다).
 *
 * 그래서 배경은 0, 내용은 1로 쌓습니다. 인라인 배경색을 CSS로 이길 수는 없으니
 * 순서를 바꾸는 쪽이 확실합니다.
 */
.wf-bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.wf-bg--fixed { position: fixed; }
/*
 * 배경이 깔린 페이지에서는 내용이 그 위로 올라와야 합니다.
 *
 * `.wf__add`(편집용 행 추가 버튼)를 같은 규칙에 묶으면 안 됩니다 —
 * 내보낼 때 편집 전용 셀렉터가 든 규칙은 **통째로** 걷히므로, 결과물에서
 * 내용이 배경 뒤에 남습니다. 캔버스에서는 되는데 받은 파일에서만 깨지는
 * 종류의 사고라 눈에 잘 안 띕니다. 그래서 규칙을 갈라 둡니다.
 */
.wf-page:has(> .wf-bg),
.wf-page:has(> .wf-bgshift) { position: relative; }
/*
 * 스크롤 배경 전환 — 영역 디테일의 bgShift.
 *
 * 색을 정한 영역이 화면을 지나는 동안 페이지 전체가 그 색으로 물듭니다.
 * 영역에 이름 붙은 view-timeline을 선언하고(timeline-scope로 페이지까지 올림),
 * 뒤에 깔린 고정 레이어가 그 타임라인으로 페이드 인/아웃합니다 — 스크립트 0.
 * 미지원 브라우저에서는 opacity 0 그대로라 원래 배경이 남습니다(안전한 퇴화).
 */
.wf-bgshift {
  position: fixed; inset: 0; z-index: 0;
  opacity: 0; pointer-events: none;
  animation: wf-bgshift-fade linear both;
  animation-duration: 1ms; /* 뷰 타임라인에서는 무시되지만 없으면 시작하지 않습니다 */
  animation-range: cover 0% cover 100%;
}
@keyframes wf-bgshift-fade {
  0% { opacity: 0; }
  28% { opacity: 1; }
  72% { opacity: 1; }
  100% { opacity: 0; }
}
.wf-page:has(> .wf-bg) > .wf,
.wf-page:has(> .wf-bg) > .wf--row { position: relative; z-index: 1; }
.wf-bg__media {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.wf-bg--tile .wf-bg__media {
  width: auto; height: auto;
  object-fit: none;
  /* 타일은 <img>로 안 되므로 배경 반복은 부모가 맡습니다 */
  display: none;
}
.wf-bg--tile { background-repeat: repeat; background-size: 220px auto; }
.wf-bg__veil { position: absolute; inset: 0; display: block; }
.wf-bg.has-gradient .wf-bg__veil {
  background-image: linear-gradient(to bottom, transparent 30%, rgba(0, 0, 0, 0.75));
}
/* 느린 확대 — 30초에 3%. 눈에 거슬리지 않는 정도로만. */
@keyframes wf-bg-zoom { from { transform: scale(1); } to { transform: scale(1.03); } }
.wf-bg--zoom .wf-bg__media { animation: wf-bg-zoom 30s ease-in-out infinite alternate; }
/* 움직임을 불편해하는 사람에게는 멈춥니다 */
@media (prefers-reduced-motion: reduce) {
  .wf-bg--zoom .wf-bg__media { animation: none; }
}
/* 배경이 깔리면 영역 배경은 비워 둡니다 — 안 그러면 배경이 하나도 안 보입니다 */
/*
 * 사이트 배경(이미지·영상)이 있어도 영역은 기본적으로 **불투명**합니다.
 * 예전에는 여기서 전부 transparent로 뚫어서 배경이 모든 영역에 비쳐 보였고,
 * 사진 위에 글이 얹혀 읽기 힘들었습니다. 지금은 영역마다
 * '사이트 배경 비침'(bgSee 0~1)을 **인라인 color-mix**로 싣습니다 —
 * CSS 변수 + calc() 퍼센트 조합은 color-mix가 못 읽는 브라우저가 있어
 * 렌더러(Wireframe/toHTML)가 숫자 퍼센트로 계산해 넣습니다.
 */

/* ── 글자 등장 효과 ────────────────────────────────────────────
 * 전부 CSS입니다 — 결과물에 스크립트가 들어가지 않습니다.
 * 대상은 클래스로 고릅니다: `--title`은 제목만, `--all`은 본문까지.
 */

/* 한 글자씩 (타이핑) — 타자기처럼.
 * 글자는 제 차례에 **즉시** 찍히고(스르륵 떠오르면 타자가 아닙니다),
 * 캐럿이 방금 찍힌 글자 뒤를 따라다니다 다 치면 몇 번 깜빡이고 사라집니다.
 * 캐럿은 absolute라 글자 간격(레이아웃)에는 영향이 없습니다.
 *
 * 끝 상태는 fill(forwards)이 아니라 **기본값**이어야 합니다.
 * 예전엔 글자마다 1ms 애니메이션 + forwards로 '보임'을 만들었는데, 로드 직후
 * 프레임이 밀리면(배경 이미지 디코딩·폰트 로딩) 크로미움이 한 프레임보다 짧은
 * 활성 구간을 통째로 건너뛰어 끝 상태가 커밋되지 않았습니다 — 글자는 숨은 채,
 * 캐럿은 켜진 채 화면에 굳는 실제 증상이 있었습니다. 지금은 글자 기본이 '보임',
 * 캐럿 기본이 '숨김'이고 애니메이션은 그 반대 상태를 잠깐 만들 뿐이라,
 * 어떤 애니메이션이 어긋나도 결국 기본값(글자 보임/캐럿 없음)으로 수렴합니다. */
/* 어절 래퍼 — inline-block 글자 span은 아무 사이에서나 줄이 바뀌어 keep-all이
 * 무력화됩니다. 어절째 원자로 묶어 줄바꿈이 어절 사이에서만 일어나게 합니다. */
.wf-tfx--chars .wf-tfx__w { display: inline-block; }
.wf-tfx--chars .wf-tfx__ch {
  display: inline-block;
  position: relative;
  /* 제 차례가 올 때까지의 시간만큼 숨겼다가, 끝나는 순간 기본값 '보임'으로 */
  animation: wf-tfx-hide calc(var(--tfx-delay, 0ms) + var(--i, 0) * var(--tfx-step, 40ms)) steps(1, end);
}
@keyframes wf-tfx-hide { from, to { opacity: 0; } }
/* 캐럿 — 이 글자가 찍혀 있는 한 스텝 동안만 글자 오른쪽에 나타납니다. */
.wf-tfx--chars .wf-tfx__ch::after {
  content: '';
  position: absolute;
  right: -0.14em; top: 6%; bottom: 4%;
  width: 0.07em; min-width: 2px;
  background: currentColor;
  opacity: 0;
  animation: wf-tfx-caret var(--tfx-step, 40ms) steps(1, end);
  /* 글자와 같은 순번 지연 — inherit는 ::after에서 어긋나는 일이 있어 직접 계산합니다 */
  animation-delay: calc(var(--tfx-delay, 0ms) + var(--i, 0) * var(--tfx-step, 40ms));
}
/* 마지막 글자(마지막 어절의 마지막 글자) — 다 쳤으니 캐럿이 세 번 깜빡이고 사라집니다. */
.wf-tfx--chars .wf-tfx__w:last-child > .wf-tfx__ch:last-child::after {
  animation: wf-tfx-caret-end 0.8s steps(1, end) 3;
  animation-delay: calc(var(--tfx-delay, 0ms) + var(--i, 0) * var(--tfx-step, 40ms));
}
@keyframes wf-tfx-caret { from, to { opacity: 1; } }
@keyframes wf-tfx-caret-end { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
/* 아래에서 올라오며 — 요소 단위 (제목은 항상, 본문은 대상이 '제목 + 본문'일 때) */
.wf-tfx--up :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-card__title, .wf-cover__title, .wf-cta__title),
.wf-tfx--up.wf-tfx-on--all :is(.wf-hero__sub, .wf-sec__body, .wf-aside__bio) {
  animation: wf-tfx-up 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: var(--tfx-delay, 0ms);
}
@keyframes wf-tfx-up { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
/* 흐릿함에서 선명하게 */
.wf-tfx--blur :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-card__title, .wf-cover__title, .wf-cta__title),
.wf-tfx--blur.wf-tfx-on--all :is(.wf-hero__sub, .wf-sec__body, .wf-aside__bio) {
  animation: wf-tfx-blur 0.8s ease-out both;
  animation-delay: var(--tfx-delay, 0ms);
}
@keyframes wf-tfx-blur { from { opacity: 0; filter: blur(12px); } to { opacity: 1; filter: none; } }
/* 자간 벌어지며 */
.wf-tfx--spread :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-cover__title, .wf-cta__title) {
  animation: wf-tfx-spread 0.9s cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: var(--tfx-delay, 0ms);
}
@keyframes wf-tfx-spread { from { opacity: 0; letter-spacing: -0.14em; } to { opacity: 1; letter-spacing: inherit; } }
/* 밑줄 좌→우 */
.wf-tfx--underline :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-cover__title, .wf-cta__title) {
  position: relative;
  padding-bottom: 0.24em;
}
.wf-tfx--underline :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-cover__title, .wf-cta__title)::after {
  content: '';
  position: absolute; left: 0; bottom: 0;
  width: 100%; height: 2px;
  background: var(--wf-accent);
  transform-origin: left;
  animation: wf-tfx-line 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: var(--tfx-delay, 0ms);
}
@keyframes wf-tfx-line { from { transform: scaleX(0); } to { transform: scaleX(1); } }
/*
 * 글리치 — 시안/마젠타 색수차 그림자가 간헐적으로 어긋납니다.
 * 주기(3.2s)의 90%는 완전 정지 — 계속 흔들면 읽을 수 없습니다.
 * transform이 아니라 text-shadow만 움직여 레이아웃이 떨리지 않습니다.
 */
.wf-tfx--glitch :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-cover__title, .wf-cta__title),
.wf-tfx--glitch.wf-tfx-on--all :is(.wf-hero__sub, .wf-sec__body) {
  animation: wf-tfx-glitch 3.2s steps(1) infinite;
  animation-delay: var(--tfx-delay, 0ms);
}
@keyframes wf-tfx-glitch {
  0%, 88%, 100% { text-shadow: none; }
  89% { text-shadow: 2px 0 rgba(34, 211, 238, 0.85), -2px 0 rgba(244, 114, 182, 0.85); }
  91% { text-shadow: -3px 0 rgba(34, 211, 238, 0.85), 3px 1px rgba(244, 114, 182, 0.85); }
  93% { text-shadow: 2px -1px rgba(34, 211, 238, 0.85), -2px 1px rgba(244, 114, 182, 0.85); }
  95% { text-shadow: -1px 0 rgba(34, 211, 238, 0.85), 1px 0 rgba(244, 114, 182, 0.85); }
  97% { text-shadow: none; }
}
@media (prefers-reduced-motion: reduce) {
  .wf-tfx--glitch :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-cover__title, .wf-cta__title),
  .wf-tfx--glitch.wf-tfx-on--all :is(.wf-hero__sub, .wf-sec__body) { animation: none; }
}
/* 글자 그라디언트 — 움직임 없이 인상만 */
.wf-tfx--gradient :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-cover__title, .wf-cta__title) {
  background-image: linear-gradient(100deg, var(--wf-accent), color-mix(in srgb, var(--wf-accent) 35%, var(--wf-text, currentColor)));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/*
 * 스크롤 따라 채워지기 — 회색에서 본문색으로 (Apple 소개 페이지 스타일).
 *
 * `animation-timeline: view()`로 요소의 뷰포트 진행률에 묶습니다. 스크립트 0.
 * 정지 상태(애니메이션 미지원·동작 줄이기)의 값이 **채워진 쪽**이도록
 * keyframes는 from만 씁니다 — 퇴화해도 글이 읽힙니다.
 * color: transparent라 currentColor를 못 쓰므로 테마 글자색 변수를 받습니다.
 */
.wf-tfx--fill :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-cover__title, .wf-cta__title),
.wf-tfx--fill.wf-tfx-on--all :is(.wf-hero__sub, .wf-sec__body, .wf-aside__bio) {
  background-image: linear-gradient(to right, var(--wf-text, #1c1917) 50%, color-mix(in srgb, var(--wf-text, #1c1917) 24%, transparent) 50%);
  background-size: 200% 100%;
  background-position: 0% 0;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: wf-tfx-fill linear both;
  animation-duration: 1ms;   /* 스크롤 타임라인에서는 무시되지만 없으면 시작하지 않습니다 */
  animation-timeline: view();
  animation-range: entry 10% cover 55%;
}
@keyframes wf-tfx-fill { from { background-position: 100% 0; } }
/* 움직임을 불편해하는 사람에게는 전부 끕니다 */
@media (prefers-reduced-motion: reduce) {
  .wf-tfx .wf-tfx__ch,
  .wf-tfx :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-card__title, .wf-cover__title, .wf-cta__title, .wf-hero__sub, .wf-sec__body, .wf-aside__bio) {
    animation: none !important;
    opacity: 1 !important;
    filter: none !important;
  }
  .wf-tfx--underline :is(.wf-hero__title, .wf-sec__title, .wf-aside__name, .wf-cover__title, .wf-cta__title)::after { animation: none !important; transform: none; }
  /*
   * 타자기 캐럿도 함께 끕니다 — 글자는 위 규칙이 전부 보이게 하는데 캐럿만
   * 남으면 "다 쓰인 글 끝에서 막대만 깜빡이는" 어리둥절한 화면이 됩니다
   * (동작 줄이기를 켠 Windows에서 실제로 보고된 증상입니다).
   */
  .wf-tfx--chars .wf-tfx__ch::after { animation: none !important; opacity: 0 !important; }
}
/* ── 스크롤 진행 바 ────────────────────────────────────────────
 * `animation-timeline: scroll()` — 스크립트 0. 미지원 브라우저에서는
 * scaleX(0) 그대로라 아예 보이지 않습니다(깨지는 대신 조용히 빠집니다).
 * 동작 줄이기에서도 끄지 않습니다 — 스스로 움직이는 장식이 아니라
 * 스크롤 위치를 알려 주는 표시(스크롤바와 같은 성질)입니다.
 */
.wf-progress {
  position: fixed;
  top: 0; left: 0;
  width: 100%; height: 3px;
  z-index: 80;
  pointer-events: none;
  background: var(--wf-accent);
  transform-origin: 0 50%;
  transform: scaleX(0);
  animation: wf-progress linear both;
  animation-duration: 1ms;   /* 스크롤 타임라인에서는 무시되지만 없으면 시작하지 않습니다 */
  animation-timeline: scroll();
}
@keyframes wf-progress { to { transform: scaleX(1); } }
/* ── 그레인(종이 질감) ─────────────────────────────────────────
 * SVG feTurbulence 한 장을 data URL로 — 이미지 파일도 스크립트도 없습니다.
 * 세기는 인라인 opacity로 옵니다.
 *
 * 질감은 **배경에만** 깔립니다(z-index:0). 예전에는 맨 위(70)에서 화면 전체를
 * 덮는 필름 방식이었는데, 글자와 사진까지 노이즈가 앉아 가독성이 떨어졌습니다.
 * 영역(.wf__region)은 position:relative라 그 위에 또렷하게 그려집니다.
 */
.wf-grain {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23g)'/%3E%3C/svg%3E");
}
/* ── 배경의 스크롤 반응 ────────────────────────────────────────
 * `animation-timeline: scroll()` — 스크롤 진행률에 애니메이션을 묶습니다.
 * **스크립트가 한 줄도 필요 없습니다.** 지원하지 않는 브라우저에서는
 * 애니메이션이 시작되지 않아 정지 배경으로 조용히 퇴화합니다(깨지지 않습니다).
 *
 * 시작 배율을 1보다 크게 두는 이유: 흐림 보정 인라인 transform이 애니메이션에
 * 덮이므로, 가장자리가 비어 보이지 않게 여기서 다시 확보합니다.
 */
.wf-bg--sfx-zoom .wf-bg__media,
.wf-bg--sfx-drift .wf-bg__media,
.wf-bg--sfx-blur .wf-bg__media,
.wf-bg--sfx-dim .wf-bg__veil {
  animation-timeline: scroll();
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-duration: 1ms;   /* 스크롤 타임라인에서는 무시되지만 없으면 시작하지 않습니다 */
}
.wf-bg--sfx-zoom .wf-bg__media { animation-name: wf-bgs-zoom; }
@keyframes wf-bgs-zoom { from { transform: scale(1.04); } to { transform: scale(1.28); } }
.wf-bg--sfx-drift .wf-bg__media { animation-name: wf-bgs-drift; }
@keyframes wf-bgs-drift { from { transform: scale(1.16) translateY(-5%); } to { transform: scale(1.16) translateY(5%); } }
.wf-bg--sfx-blur .wf-bg__media { animation-name: wf-bgs-blur; }
@keyframes wf-bgs-blur { from { filter: blur(0); transform: scale(1.06); } to { filter: blur(11px); transform: scale(1.12); } }
.wf-bg--sfx-dim .wf-bg__veil { animation-name: wf-bgs-dim; }
@keyframes wf-bgs-dim { from { opacity: 0.35; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .wf-bg--sfx-zoom .wf-bg__media,
  .wf-bg--sfx-drift .wf-bg__media,
  .wf-bg--sfx-blur .wf-bg__media,
  .wf-bg--sfx-dim .wf-bg__veil { animation: none !important; }
}
/* ── 링크 기본 스타일 지우기 ────────────────────────────────────
 * 캔버스에서 메뉴·카드는 <button>·<span>이라 상속된 색으로 나오지만,
 * 내보낸 결과물에서는 같은 자리가 <a>입니다. 브라우저 기본값이 그때만 튀어나와
 * **파란 글씨에 밑줄**이 됩니다 — 편집 화면과 받은 파일이 달라 보이던 원인입니다.
 * 한 줄로 전부 막습니다. 밑줄이 필요한 곳(links의 '밑줄 텍스트')은 따로 켭니다.
 */
/*
 * 링크는 글자색을 물려받습니다 — 버튼만 빼고.
 *
 * 예전에는 `.wf-page a`가 예외 없이 color:inherit이었습니다. 캔버스의 CTA는
 * <button>이라 안 걸렸지만 **내보낸 결과물에서는 <a>** 라서 이 규칙이 걸렸고,
 * 선택자 무게(0,2,1)가 .wf-btn(0,1,0)을 이겨 흰 글자가 본문색으로 덮였습니다.
 * 강조색이 거의 검정인 테마에서는 검정 위 검정 — '이력서 보기'가 안 보였습니다.
 * 캔버스는 button, 결과물은 a — 이 프로젝트에서 반복해 문제가 된 모양입니다.
 */
.wf-page a:not(.wf-btn) { color: inherit; text-decoration: none; }
.wf-page a:visited { color: inherit; }
/* ── 세로 스크롤바 숨기기 ──────────────────────────────────────
 * 포트폴리오는 화면을 꽉 채우는 물건이라 회색 막대가 오른쪽에 늘 서 있으면
 * 인상이 깎입니다. 스크롤은 그대로 되고 막대만 감춥니다.
 * 대신 아래에 '내려가기' 안내를 두어 스크롤할 수 있다는 것을 알려 줍니다.
 */
.wf-page { scrollbar-width: none; }
.wf-page::-webkit-scrollbar { display: none; }
/*
 * 스크롤바는 `html`/`body`가 냅니다 — `.wf-page`에만 걸어 둬서 내보낸 결과물에는
 * 세로 스크롤바가 그대로 보였습니다.
 *
 * `overflow-x`는 `hidden`이 아니라 `clip`입니다. body에 `hidden`을 걸면 그 요소가
 * 스크롤 컨테이너가 되어 `position: sticky`와 `animation-timeline: view()`가
 * 기준을 잃습니다(등장 효과·시차 배경이 죽습니다). `clip`은 스크롤 컨테이너를
 * 만들지 않고 넘친 부분만 잘라내므로 둘 다 살아 있습니다.
 *
 * 가로 넘침은 효과에서 나옵니다 — 등장 효과의 translateX(±14%), 시차 레이어,
 * 도트 웨이브 캔버스. 세로로만 읽는 사이트에서 가로 스크롤은 사고입니다.
 */
html, body {
  overflow-x: clip;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
html::-webkit-scrollbar, body::-webkit-scrollbar { display: none; }
/* ── 내려가기 버튼 ─────────────────────────────────────────────
 * 오른쪽 아래 고정. 첫 섹션으로 데려갑니다(앵커가 있을 때만 나옵니다).
 * 통통 튀는 모션으로 "여기 더 있다"를 알려 줍니다.
 */
.wf-down {
  position: fixed; right: 18px; bottom: 18px;
  z-index: 30;
  width: 42px; height: 42px;
  display: grid; place-items: center;
  color: var(--wf-text, currentColor);
  background: color-mix(in srgb, var(--wf-surface) 82%, transparent);
  border: 1px solid var(--wf-line);
  border-radius: 50%;
  backdrop-filter: blur(6px);
  text-decoration: none;
  animation: wf-down-bob 1.9s ease-in-out infinite;
}
.wf-down:hover { border-color: var(--wf-accent); color: var(--wf-accent); }
.wf-down svg { display: block; }
@keyframes wf-down-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(5px); }
}
@media (prefers-reduced-motion: reduce) { .wf-down { animation: none; } }
/*
 * 첫 화면을 지나면 사라집니다.
 *
 * 이 버튼이 하는 말은 "아래에 더 있다" 하나입니다. 그런데 화면에 고정된 채
 * 끝까지 떠 있었고, 목적지가 **첫 섹션 고정**이라 첫 섹션을 지난 뒤에는
 * 아래 화살표를 보여주면서 실제로는 위로 되돌렸습니다. 맨 아래에서 누르면
 * 1,500px 넘게 거슬러 올라갔습니다 — 방문자에게는 고장으로 보입니다.
 *
 * 첫 화면에서만 보이면 그 말이 맞는 자리에서만 눌리고, 되돌아가는 일도
 * 함께 사라집니다. 화살표를 뒤집는 대신 자리를 비우는 쪽을 골랐습니다 —
 * 이 프로젝트가 지켜 온 "스크립트 0"으로 풀 수 있는 방법이기도 합니다.
 *
 * animation-timeline: scroll()이라 스크립트가 없습니다. 미지원 브라우저에서는
 * 이 블록이 통째로 무시돼 **종전처럼 계속 보입니다** — 사라지지 않을 뿐,
 * 버튼이 없어지지는 않습니다(안전한 퇴화).
 */
@keyframes wf-down-away {
  0% { opacity: 1; visibility: visible; }
  70% { opacity: 0; visibility: visible; }
  /* visibility는 단계값이라 두 지점 사이에서 튑니다 — 다 사라진 뒤에 끕니다. */
  71%, 100% { opacity: 0; visibility: hidden; }
}
@supports (animation-timeline: scroll()) {
  .wf-down {
    animation: wf-down-bob 1.9s ease-in-out infinite, wf-down-away linear both;
    animation-timeline: auto, scroll();
    /* 첫 220px 안에 사라집니다. 문서 길이와 무관하게 늘 같은 거리입니다. */
    animation-range: normal, 0 220px;
  }
  /* 동작 줄이기 — 통통 튀는 것만 멈추고, 사라지는 것은 남깁니다(움직임이 아니라 표시 여부). */
  @media (prefers-reduced-motion: reduce) {
    .wf-down {
      animation: wf-down-away linear both;
      animation-timeline: scroll();
      animation-range: 0 220px;
    }
  }
}
/* ── 인트로 로더 ───────────────────────────────────────────────
 * 사이트 제목 + 차오르는 바 + 걷히는 커튼. 결과물 전용(마크업은 내보내기만
 * 만듭니다). 타이밍이 전부 CSS라 스크립트가 죽어도 2.5초 안에 반드시 걷히고,
 * 동작 줄이기에서는 아예 표시하지 않습니다.
 */
.wf-intro {
  position: fixed; inset: 0; z-index: 120;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;
  background: var(--wf-bg, #101014);
  pointer-events: none;
  animation: wf-intro-out 0.6s cubic-bezier(0.7, 0, 0.3, 1) 1.9s both;
}
.wf-intro__name {
  font-size: clamp(20px, 4vw, 44px);
  font-weight: 700; letter-spacing: 0.04em;
  animation: wf-intro-name 0.7s ease both;
}
.wf-intro__bar {
  position: relative; overflow: hidden;
  width: min(260px, 60vw); height: 2px;
  background: color-mix(in srgb, var(--wf-text, currentColor) 16%, transparent);
}
.wf-intro__bar::after {
  content: '';
  position: absolute; inset: 0;
  transform-origin: left;
  background: var(--wf-accent, currentColor);
  animation: wf-intro-fill 1.55s cubic-bezier(0.2, 0.7, 0.2, 1) 0.15s both;
}
@keyframes wf-intro-name { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; } }
@keyframes wf-intro-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes wf-intro-out { to { transform: translateY(-101%); visibility: hidden; } }
@media (prefers-reduced-motion: reduce) { .wf-intro { display: none; } }
/* ── 영역 등장 효과 ────────────────────────────────────────────
 * `animation-timeline: view()` — 요소가 화면에 들어오는 진행률에 애니메이션을
 * 묶습니다. **스크립트가 한 줄도 없습니다.** 미지원 브라우저에서는 애니메이션이
 * 시작되지 않아 그냥 제자리에 보입니다(안전한 퇴화).
 *
 * 어느 칸이 왼쪽인지는 CSS로 알 수 없어 렌더할 때 `data-enter`를 붙입니다 —
 * `:nth-child`로 하면 흐름 배치(빈 영역이 빠진 뒤)와 어긋납니다.
 */
.wf-enter .wf__region[data-enter] {
  animation-timeline: view();
  /*
   * 시작 지점은 변수로 — 꾸미기의 '등장 시작 지점(노출 %)'이 --enter-at을 정합니다.
   * 요소가 화면에 그만큼 드러난 뒤 시작해, 거기서 30%p 더 지나면 끝납니다
   * (기본 2%면 예전의 entry 2% cover 32%와 같습니다).
   */
  animation-range: entry var(--enter-at, 2%) cover calc(30% + var(--enter-at, 2%));
  animation-timing-function: cubic-bezier(0.2, 0.7, 0.2, 1);
  animation-fill-mode: both;
  animation-duration: 1ms;   /* 뷰 타임라인에서는 무시되지만 없으면 시작하지 않습니다 */
}
.wf-enter--sides .wf__region[data-enter='left'] { animation-name: wf-in-left; }
.wf-enter--sides .wf__region[data-enter='right'] { animation-name: wf-in-right; }
.wf-enter--sides .wf__region[data-enter='up'] { animation-name: wf-in-up; }
.wf-enter--up .wf__region[data-enter] { animation-name: wf-in-up; }
.wf-enter--zoom .wf__region[data-enter] { animation-name: wf-in-zoom; }
@keyframes wf-in-left { from { opacity: 0; transform: translateX(-14%); } to { opacity: 1; transform: none; } }
@keyframes wf-in-right { from { opacity: 0; transform: translateX(14%); } to { opacity: 1; transform: none; } }
@keyframes wf-in-up { from { opacity: 0; transform: translateY(34px); } to { opacity: 1; transform: none; } }
@keyframes wf-in-zoom { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .wf-enter .wf__region[data-enter] { animation: none !important; }
}
/* ── 레이아웃 무관 모드 (3D Model) ────────────────────────────
 * 그리드에서 빠져 화면에 고정됩니다. 칸에 맞추지 않는 장식이므로 테두리도
 * 그리지 않습니다.
 *
 * 위치는 인라인 `left/top/width`로 옵니다(끌어 옮길 수 있어야 하므로 값이
 * 문서에 있습니다). 여기서는 고정 방식과 동선만 정합니다.
 */
.wf__region.is-free {
  position: fixed;
  z-index: 6;                  /* 고정 헤더(4)보다 위. 모달(40)보다 아래 */
  border: 0 !important;        /* 떠 있는 장식에 칸 테두리는 어울리지 않습니다 */
  background: none !important;
  padding: 0 !important;
  overflow: visible;           /* 기본 규칙의 hidden을 풀어야 밖으로 나갈 수 있습니다 */
  pointer-events: none;        /* 아래 내용을 가리지 않게. 편집 중에는 아래에서 되살립니다 */
  translate: -50% -50%;        /* left/top을 중심 기준으로 */
}
/*
 * 좁은 화면.
 *
 * 크기는 **키우지 않고 줄입니다.** 처음에는 46%로 키웠는데, 좁은 화면에서 크게
 * 만들면 본문을 더 가릴 뿐입니다. 폭 상한을 32vw로 낮추고 화면 오른쪽 아래로
 * 밀어 둡니다 — 장식이 본문을 비켜서게 하는 편이 낫습니다.
 */
@media (max-width: 760px) {
  .wf__region.is-free {
    width: clamp(88px, 30vw, 32vw) !important;
    left: 78% !important;
    top: 82% !important;
  }
}
/* 3D 파트는 세로가 짧은 화면(가로로 든 휴대폰)에서 더 눌러야 합니다. */
@media (max-height: 520px) {
  .wf-3d { height: min(56vh, 260px) !important; }
}

/* ── 효과 팩 스타일 ── */
/**
 * silk-bg / style.css
 * _core/core.css 를 먼저 불러오세요.
 *
 * ★ 핵심: CSS 그라디언트 폴백이 "항상" 깔려 있습니다.
 *   캔버스는 그 위에 얹힐 뿐입니다.
 *   WebGL이 없든, OGL 로드가 실패하든, 동작 줄이기든 —
 *   어떤 경우에도 빈 화면이 나오지 않습니다.
 */

/* ================================================================== *
 * 1. 컨테이너 — 이 클래스는 사용자가 직접 붙입니다(JS와 무관)
 * ================================================================== */

.fx-silk {
  position: relative;
  isolation: isolate;
  overflow: hidden;

  /* 셰이더와 동일한 색. JS가 옵션으로 색을 받으면 이 변수를 덮어씁니다. */
  --fx-silk-c1: #1b1d3a;
  --fx-silk-c2: #5b5bd6;
  --fx-silk-c3: #a78bfa;

  /* 폴백 그라디언트 — 셰이더 무늬를 대략 흉내 냅니다 */
  background-color: var(--fx-silk-c1);
  background-image:
    radial-gradient(120% 90% at 18% 12%, var(--fx-silk-c2) 0%, transparent 58%),
    radial-gradient(100% 80% at 82% 78%, var(--fx-silk-c3) 0%, transparent 55%),
    radial-gradient(90% 70% at 55% 45%, color-mix(in srgb, var(--fx-silk-c2) 55%, var(--fx-silk-c3)) 0%, transparent 62%);
}

/* ================================================================== *
 * 2. 캔버스
 * ================================================================== */

.fx-silk-bg__canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* 배경 위의 콘텐츠가 클릭 가능해야 합니다 */
  pointer-events: none;

  /* 셰이더 첫 프레임이 그려지기 전 깜빡임을 막습니다 */
  opacity: 0;
  transition: opacity 600ms var(--fx-ease-out);
}

.fx-silk-bg--ready .fx-silk-bg__canvas {
  opacity: 1;
}

/* 컨텍스트 손실 시: 캔버스를 감추면 아래의 CSS 그라디언트가 드러납니다 */
.fx-silk-bg--lost .fx-silk-bg__canvas {
  opacity: 0;
}

/* 배경 위 콘텐츠는 반드시 캔버스보다 위로.
   `.fx-layer`(배경 레이어 공용 표식)는 건너뜁니다 — 같은 영역에 함께 걸린
   다른 팩의 배경까지 콘텐츠로 끌어올리면 서로를 망칩니다. */
.fx-silk > *:not(.fx-layer) {
  position: relative;
  z-index: 1;
}

/* ================================================================== *
 * 3. 선택 사항 — 배경 위 가독성 보정
 *
 * 그라디언트 배경 위의 흰 글씨는 밝은 영역에서 읽히지 않습니다.
 * 어두운 오버레이를 한 겹 깔면 어느 프레임에서도 대비가 유지됩니다.
 * ================================================================== */

.fx-silk--scrim::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.15),
    rgba(0, 0, 0, 0.45)
  );
}

/* 밝은 배경 위 어두운 글씨용 */
.fx-silk--scrim-light::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.25),
    rgba(255, 255, 255, 0.55)
  );
}

/* ================================================================== *
 * 4. 선택 사항 — 히어로 레이아웃
 * ================================================================== */

.fx-silk-hero {
  display: grid;
  place-items: center;
  min-height: min(80vh, 760px);
  border-radius: var(--fx-radius, 16px);
  padding: 40px 24px;
  text-align: center;
  color: #fff;
}

.fx-silk-hero h1 {
  margin: 0 0 14px;
  font-size: clamp(2rem, 6vw, 4.2rem);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1.04;
  /* 밝은 무늬 위에서도 글자가 뜨도록 아주 약한 그림자 */
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.35);
}

.fx-silk-hero p {
  margin: 0;
  font-size: clamp(0.95rem, 2vw, 1.15rem);
  color: rgba(255, 255, 255, 0.82);
}


/**
 * magnetic-button / style.css
 * _core/core.css 를 먼저 불러오세요.
 */

/* ================================================================== *
 * 1. 기능부 (필수)
 * ================================================================== */

.fx-magnetic-button {
  position: relative;
  transform-style: preserve-3d;

  transform:
    perspective(600px)
    translate3d(var(--fx-mag-x, 0px), var(--fx-mag-y, 0px), 0)
    rotateX(var(--fx-mag-rx, 0deg))
    rotateY(var(--fx-mag-ry, 0deg))
    scale(var(--fx-mag-scale, 1));

  /* 글로우가 버튼 밖으로 새어나가지 않게 */
  overflow: hidden;
  isolation: isolate;
}

.fx-magnetic-button[data-fx-magnetic='1'] {
  will-change: transform;
}

/*
 * 라벨은 버튼보다 labelFactor 배만큼 "더" 움직입니다.
 * 이 미세한 어긋남이 버튼에 두께가 있다는 착각을 만듭니다.
 * labelFactor를 음수로 주면 라벨이 반대로 밀려 유리 안에 갇힌 느낌이 납니다.
 */
.fx-magnetic-button [data-fx-label] {
  position: relative;
  z-index: 1;
  display: inline-block;
  transform: translate3d(
    calc(var(--fx-mag-x, 0px) * var(--fx-mag-lf, 0.5)),
    calc(var(--fx-mag-y, 0px) * var(--fx-mag-lf, 0.5)),
    0
  );
  will-change: transform;
}

/* 커서를 따라다니는 빛 번짐 */
.fx-magnetic-button__glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
  opacity: var(--fx-mag-glow, 0);
  background: radial-gradient(
    circle at var(--fx-mag-px, 50%) var(--fx-mag-py, 50%),
    var(--fx-glare) 0%,
    transparent 55%
  );
}

/* 꺼졌을 때 */
.fx-magnetic-button.fx-inert [data-fx-label] {
  transform: none;
}
.fx-magnetic-button.fx-inert .fx-magnetic-button__glow {
  display: none;
}

/* ================================================================== *
 * 2. 선택 사항 — 기본 버튼 스킨
 * ================================================================== */

.fx-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;

  padding: 14px 28px;
  border: 0;
  border-radius: 999px;

  font: inherit;
  font-size: 0.9375rem;
  font-weight: 650;
  letter-spacing: -0.005em;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;

  color: #fff;
  background: var(--fx-accent);
  cursor: pointer;

  box-shadow: 0 8px 22px -10px color-mix(in srgb, var(--fx-accent) 80%, #000);
  transition: box-shadow var(--fx-duration-fast) var(--fx-ease),
              background-color var(--fx-duration-fast) var(--fx-ease);
}

.fx-btn[data-fx-magnetic='1'] {
  box-shadow: 0 16px 34px -12px color-mix(in srgb, var(--fx-accent) 85%, #000);
}

.fx-btn:active {
  /* 누를 때는 자석 이동 위에 살짝 눌리는 느낌만 얹습니다 */
  filter: brightness(0.94);
}

/* 보조 스타일(외곽선) */
.fx-btn--ghost {
  color: var(--fx-text);
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--fx-border);
}

.fx-btn--ghost[data-fx-magnetic='1'] {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-accent) 60%, transparent),
              0 14px 30px -16px rgba(0, 0, 0, 0.5);
}


/**
 * scroll-reveal / style.css
 * _core/core.css 를 먼저 불러오세요.
 *
 * ★ 이 파일에서 가장 중요한 사실
 *   "숨김" 상태는 .fx-scroll-reveal 클래스에만 걸려 있습니다.
 *   이 클래스는 JS가 마운트에 성공했을 때만 붙습니다.
 *   → JS가 죽으면 콘텐츠가 사라지는 게 아니라, 그냥 애니메이션 없이 보입니다.
 *   콘텐츠를 CSS로 미리 숨겨두는 리빌 라이브러리는 JS 로드 실패 시
 *   페이지 전체가 백지가 됩니다. 그 사고를 구조적으로 막았습니다.
 */

/* ================================================================== *
 * 1. 공통
 * ================================================================== */

.fx-scroll-reveal {
  opacity: 0;
  transition:
    opacity var(--fx-reveal-duration, 700ms) var(--fx-ease-out) var(--fx-reveal-delay, 0ms),
    transform var(--fx-reveal-duration, 700ms) var(--fx-ease-out) var(--fx-reveal-delay, 0ms);
  /*
   * will-change를 일부러 쓰지 않습니다.
   * opacity와 transform은 이미 컴포지터에서 처리되고,
   * 리빌 대상은 보통 수십 개라 상시 레이어를 만들면 메모리만 먹습니다.
   */
}

.fx-scroll-reveal.is-revealed {
  opacity: 1;
  transform: none;
}

/* ================================================================== *
 * 2. 프리셋 — 숨김 상태의 transform
 * ================================================================== */

/* rise — 아래에서 위로. 가장 무난하고 어디에나 어울립니다. */
.fx-scroll-reveal[data-fx-preset='rise'] {
  transform: translate3d(0, var(--fx-reveal-distance, 28px), 0);
}

/* depth — 뒤에서 앞으로 다가옵니다. 진짜 Z축 이동입니다. */
.fx-scroll-reveal[data-fx-preset='depth'] {
  transform:
    perspective(var(--fx-reveal-perspective, 1000px))
    translate3d(0, calc(var(--fx-reveal-distance, 28px) * 0.4), var(--fx-reveal-depth, -140px))
    scale(var(--fx-reveal-scale, 0.92));
}

/* flip — 바닥에서 카드가 일어섭니다. 그리드 카드에 잘 어울립니다. */
.fx-scroll-reveal[data-fx-preset='flip'] {
  transform-origin: 50% 100%;
  transform:
    perspective(var(--fx-reveal-perspective, 1000px))
    rotateX(var(--fx-reveal-rotate, 22deg))
    translate3d(0, calc(var(--fx-reveal-distance, 28px) * 0.5), 0);
}

/* swing — 옆에서 문이 열리듯. 타임라인/리스트 항목에 어울립니다. */
.fx-scroll-reveal[data-fx-preset='swing'] {
  transform-origin: 0% 50%;
  transform:
    perspective(var(--fx-reveal-perspective, 1000px))
    rotateY(calc(var(--fx-reveal-rotate, 22deg) * -1))
    translate3d(calc(var(--fx-reveal-distance, 28px) * -0.5), 0, 0);
}

/* zoom — 살짝 커지며 등장. 이미지에 어울립니다. */
.fx-scroll-reveal[data-fx-preset='zoom'] {
  transform: scale(var(--fx-reveal-scale, 0.92));
}

/* ================================================================== *
 * 3. scrub 모드 — CSS 스크롤 기반 애니메이션
 *
 * 메인 스레드 JS가 0줄입니다. 스크롤이 아무리 무거워도 이 애니메이션은
 * 절대 끊기지 않습니다(컴포지터 스레드에서 처리).
 *
 * 2026년 7월 기준: Chrome/Edge 115+, Firefox 132+, Safari 18+
 * 글로벌 지원율 약 84%. Baseline에는 아직 미달입니다.
 * → @supports로 감싸고, 미지원 브라우저는 JS가 once 모드로 자동 강등합니다.
 * ================================================================== */

@supports (animation-timeline: view()) {
  .fx-scroll-reveal[data-fx-mode='scrub'] {
    /* 트랜지션 대신 애니메이션이 상태를 관장합니다. */
    transition: none;
    animation-fill-mode: both;
    animation-timing-function: linear;
    animation-timeline: view();
    animation-range: var(--fx-reveal-start, entry 10%) var(--fx-reveal-end, cover 40%);
  }

  .fx-scroll-reveal[data-fx-mode='scrub'][data-fx-preset='rise']  { animation-name: fx-reveal-rise; }
  .fx-scroll-reveal[data-fx-mode='scrub'][data-fx-preset='depth'] { animation-name: fx-reveal-depth; }
  .fx-scroll-reveal[data-fx-mode='scrub'][data-fx-preset='flip']  { animation-name: fx-reveal-flip; }
  .fx-scroll-reveal[data-fx-mode='scrub'][data-fx-preset='swing'] { animation-name: fx-reveal-swing; }
  .fx-scroll-reveal[data-fx-mode='scrub'][data-fx-preset='zoom']  { animation-name: fx-reveal-zoom; }
}

@keyframes fx-reveal-rise {
  from { opacity: 0; transform: translate3d(0, var(--fx-reveal-distance, 28px), 0); }
  to   { opacity: 1; transform: none; }
}

@keyframes fx-reveal-depth {
  from {
    opacity: 0;
    transform:
      perspective(var(--fx-reveal-perspective, 1000px))
      translate3d(0, calc(var(--fx-reveal-distance, 28px) * 0.4), var(--fx-reveal-depth, -140px))
      scale(var(--fx-reveal-scale, 0.92));
  }
  to { opacity: 1; transform: none; }
}

@keyframes fx-reveal-flip {
  from {
    opacity: 0;
    transform:
      perspective(var(--fx-reveal-perspective, 1000px))
      rotateX(var(--fx-reveal-rotate, 22deg))
      translate3d(0, calc(var(--fx-reveal-distance, 28px) * 0.5), 0);
  }
  to { opacity: 1; transform: none; }
}

@keyframes fx-reveal-swing {
  from {
    opacity: 0;
    transform:
      perspective(var(--fx-reveal-perspective, 1000px))
      rotateY(calc(var(--fx-reveal-rotate, 22deg) * -1))
      translate3d(calc(var(--fx-reveal-distance, 28px) * -0.5), 0, 0);
  }
  to { opacity: 1; transform: none; }
}

@keyframes fx-reveal-zoom {
  from { opacity: 0; transform: scale(var(--fx-reveal-scale, 0.92)); }
  to   { opacity: 1; transform: none; }
}

/* flip/swing은 회전 기준점이 필요하므로 keyframes 밖에서 유지합니다. */
.fx-scroll-reveal[data-fx-mode='scrub'][data-fx-preset='flip']  { transform-origin: 50% 100%; }
.fx-scroll-reveal[data-fx-mode='scrub'][data-fx-preset='swing'] { transform-origin: 0% 50%; }


/**
 * tilt-card / style.css
 * _core/core.css 를 먼저 불러온 뒤 이 파일을 불러오세요.
 */

/* ================================================================== *
 * 1. 기능부 (필수)
 * ================================================================== */

.fx-tilt-card {
  position: relative;
  /* 자식의 translateZ가 살아나려면 반드시 필요합니다. */
  transform-style: preserve-3d;

  transform:
    perspective(var(--fx-tilt-perspective, 900px))
    rotateX(var(--fx-tilt-rx, 0deg))
    rotateY(var(--fx-tilt-ry, 0deg))
    scale3d(var(--fx-tilt-scale, 1), var(--fx-tilt-scale, 1), 1);

  /* 3D 회전 시 뒷면 렌더링 비용을 없앱니다. */
  backface-visibility: hidden;
}

/*
 * will-change는 "상호작용 중에만" 겁니다.
 * 카드 20장에 상시로 걸면 각각 별도 GPU 레이어가 생겨 메모리를 크게 먹습니다.
 * JS가 pointerenter에서 data-fx-tilting="1"을 세팅합니다.
 */
.fx-tilt-card[data-fx-tilting='1'] {
  will-change: transform;
}

/* ------------------------------------------------------------------ *
 * 깊이 레이어 — data-fx-depth="숫자" 를 가진 자식
 *
 * translateZ로 실제 3D 높이를 주고,
 * 포인터 방향(-1~1)에 비례해 살짝 반대로 밀어서 시차(parallax)를 만듭니다.
 * 이 두 개가 겹쳐야 "떠 있다"는 느낌이 납니다.
 * ------------------------------------------------------------------ */
.fx-tilt-card [data-fx-depth] {
  transform: translate3d(
    calc(var(--fx-tilt-nx, 0) * var(--fx-depth, 0) * -0.22px),
    calc(var(--fx-tilt-ny, 0) * var(--fx-depth, 0) * -0.22px),
    calc(var(--fx-depth, 0) * 1px)
  );
  /* 레이어는 부모보다 아주 살짝 늦게 따라오면 더 입체적으로 보입니다. */
  transition: transform 80ms linear;
}

/* ------------------------------------------------------------------ *
 * 글레어 — 포인터 위치를 따라다니는 빛 반사
 * ------------------------------------------------------------------ */
.fx-tilt-card__glare {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  overflow: hidden;

  background: radial-gradient(
    circle at var(--fx-tilt-px, 50%) var(--fx-tilt-py, 50%),
    var(--fx-glare) 0%,
    transparent 60%
  );

  opacity: var(--fx-tilt-glare-o, 0);

  /* glareZ만큼 띄우고, 원근으로 커진 만큼 역보정해 카드를 정확히 덮습니다. */
  transform:
    translateZ(var(--fx-tilt-glare-z, 0px))
    scale(var(--fx-tilt-glare-scale, 1));

  /* 취향에 따라 켜세요. 밝은 배경에서 더 자연스럽습니다.
     mix-blend-mode: soft-light; */
}

/* ------------------------------------------------------------------ *
 * 가드에 걸려 꺼진 상태 (모바일 / 동작 줄이기)
 * 3D는 사라지지만 레이아웃은 그대로 유지되어야 합니다.
 * ------------------------------------------------------------------ */
.fx-inert .fx-tilt-card__glare,
.fx-tilt-card.fx-inert .fx-tilt-card__glare {
  display: none;
}

.fx-tilt-card.fx-inert [data-fx-depth] {
  transform: none;
}

/* ================================================================== *
 * 2. 선택 사항 — 기본 카드 스킨
 *
 * 이미 카드 디자인이 있으면 이 블록은 지우세요.
 * 바로 써보고 싶을 때만 class="fx-card"를 같이 붙이면 됩니다.
 * ================================================================== */

.fx-card {
  display: block;
  border-radius: var(--fx-radius, 16px);
  background: var(--fx-surface);
  border: 1px solid var(--fx-border);
  box-shadow: var(--fx-shadow);
  color: var(--fx-text);
  padding: 20px;
  text-decoration: none;
  transition: box-shadow var(--fx-duration-fast) var(--fx-ease);
}

.fx-card[data-fx-tilting='1'] {
  box-shadow: 0 26px 50px -20px rgba(16, 18, 29, 0.42);
}

.fx-card__media {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: calc(var(--fx-radius, 16px) - 8px);
  background: var(--fx-surface-2);
  margin-bottom: 16px;
}

.fx-card__title {
  margin: 0 0 6px;
  font-size: 1.0625rem;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.fx-card__desc {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--fx-text-dim);
}

.fx-card__tag {
  display: inline-block;
  margin-top: 14px;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--fx-accent);
  background: color-mix(in srgb, var(--fx-accent) 12%, transparent);
}


/* spotlight — 커서를 따라오는 은은한 빛 */

.fx-spotlight { position: relative; }

.fx-spotlight__glow {
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  transition: opacity 0.35s ease;
  background: radial-gradient(
    var(--fx-spot-size, 420px) circle at var(--fx-spot-x, 50%) var(--fx-spot-y, 40%),
    color-mix(in srgb, var(--fx-spot-color, var(--wf-accent, #8b8bff)) calc(var(--fx-spot-alpha, 0.22) * 100%), transparent),
    transparent 70%
  );
}

.fx-spotlight.is-spot-on .fx-spotlight__glow { opacity: 1; }

/* 광원은 뒤, 내용은 앞 — 배경류 팩의 공통 계약입니다.
   `.fx-layer`는 건너뜁니다(같은 영역의 다른 배경 팩까지 끌어올리지 않게). */
.fx-spotlight > *:not(.fx-layer) { position: relative; z-index: 1; }


/**
 * model-showcase / style.css
 * _core/core.css 를 먼저 불러오세요.
 */

/* ================================================================== *
 * 1. 컨테이너 (사용자가 직접 붙이는 클래스)
 * ================================================================== */

.fx-model {
  position: relative;
  display: block;
  width: 100%;
  /* 캔버스는 높이를 만들지 못하므로 비율을 고정합니다. */
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--fx-radius, 16px);
  /*
   * 상자 배경은 **페이지 테마**를 따릅니다.
   *
   * 예전에는 --fx-surface-2만 썼습니다. 이 값은 OS의 다크 모드(prefers-color-scheme)를
   * 따라 #1e2027로 뒤집히는데, 페이지는 밝은 테마 그대로라 상자만 새까매졌습니다.
   * 모델을 올리기 전 안내 글이 그 검정 위에 깔려 읽히지 않았습니다.
   * --wf-surface(페이지 테마 값)를 먼저 보고, 팩을 단독으로 쓸 때만 종전 값으로 물러납니다.
   */
  background: var(--wf-surface, var(--fx-surface-2));
  isolation: isolate;
}

/* ================================================================== *
 * 2. 포스터 — 3단 로딩의 1단계
 *
 * HTML에 미리 존재합니다. JS가 없어도, WebGL이 없어도, 로드가 실패해도
 * 이 이미지는 항상 보입니다.
 * ================================================================== */

.fx-model__poster {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transition: opacity 500ms var(--fx-ease-out);
}

/* 모델이 완전히 뜨면 포스터를 걷어냅니다 */
.fx-model--loaded .fx-model__poster {
  opacity: 0;
  pointer-events: none;
}

/* ================================================================== *
 * 3. model-viewer
 * ================================================================== */

.fx-model__viewer {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  /* 로드 전에는 투명 — 포스터가 그대로 보입니다 */
  opacity: 0;
  transition: opacity 500ms var(--fx-ease-out);

  /* 기본 배경 제거(포스터/컨테이너 배경을 쓰기 위해) */
  background-color: transparent;
  --poster-color: transparent;

  /* 드래그로 회전할 수 있다는 신호 */
  cursor: grab;
}

.fx-model__viewer:active {
  cursor: grabbing;
}

.fx-model--loaded .fx-model__viewer {
  opacity: 1;
}

/* ================================================================== *
 * 4. 로딩 진행 표시
 *
 * --fx-model-progress (0~1)를 JS가 갱신합니다.
 * GLB는 보통 1~10MB라 로딩이 체감됩니다. 진행률이 없으면
 * 방문자는 "고장났나?" 하고 스크롤을 내려버립니다.
 * ================================================================== */

.fx-model::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 3;
  height: 2px;
  width: calc(var(--fx-model-progress, 0) * 100%);
  background: var(--fx-accent);
  opacity: 0;
  transition: opacity 300ms var(--fx-ease);
  pointer-events: none;
}

.fx-model--loading::after {
  opacity: 1;
}

/* ================================================================== *
 * 5. 실패 상태
 *
 * 아무 표시 없이 포스터만 남으면 개발 중에 눈치채기 어렵습니다.
 * 아주 은은한 표시만 남깁니다(방문자에게는 거의 안 보이게).
 * ================================================================== */

.fx-model--failed {
  outline: 1px dashed color-mix(in srgb, var(--fx-text-dim) 40%, transparent);
  outline-offset: -1px;
}

/* ================================================================== *
 * 6. 선택 사항 — 조작 힌트
 * ================================================================== */

.fx-model__hint {
  position: absolute;
  left: 50%;
  bottom: 12px;
  z-index: 4;
  transform: translateX(-50%);

  padding: 6px 14px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;

  color: var(--fx-text);
  background: color-mix(in srgb, var(--fx-surface) 82%, transparent);
  backdrop-filter: blur(8px);
  border: 1px solid var(--fx-border);

  opacity: 0;
  transition: opacity 400ms var(--fx-ease-out) 600ms;
  pointer-events: none;
}

.fx-model--loaded .fx-model__hint {
  opacity: 1;
}

/* 사용자가 한 번 조작하면 힌트를 감춥니다(JS로 클래스 추가) */
.fx-model--touched .fx-model__hint {
  opacity: 0;
  transition-delay: 0ms;
}

/* ================================================================== *
 * 7. 선택 사항 — 비율 변형
 * ================================================================== */

.fx-model--square { aspect-ratio: 1; }
.fx-model--wide   { aspect-ratio: 16 / 9; }
.fx-model--tall   { aspect-ratio: 3 / 4; }


/**
 * cursor-dot / style.css
 */

.fx-cursor__dot,
.fx-cursor__ring {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 90;
  pointer-events: none;
  border-radius: 50%;
  opacity: 0;
  transition: opacity 200ms ease;
}

.fx-cursor__dot.is-on,
.fx-cursor__ring.is-on { opacity: 1; }

.fx-cursor__dot {
  width: 6px;
  height: 6px;
  background: var(--wf-accent, currentColor);
}

.fx-cursor__ring {
  border: 1.5px solid var(--wf-accent, currentColor);
  opacity: 0;
  transition: opacity 200ms ease, scale 0s;
}

.fx-cursor__ring.is-grown { background: color-mix(in srgb, var(--wf-accent, #000) 12%, transparent); }

/* 터치 화면에서는 혹시 떠 있어도 숨깁니다(가드가 있지만 이중 안전). */
@media (pointer: coarse) {
  .fx-cursor__dot, .fx-cursor__ring { display: none; }
}
