/* 영상(TEST) — 노드 캔버스.
   ⚠️ index.html 에서 button.css **뒤에** 로드한다(특이도 동률이라 순서로만 이긴다).
   ⚠️ 숨김은 전부 hidden 속성으로 — glass.css 의 [hidden]{display:none!important} 가 항상 이긴다. */

/* ── 화면 두 장(목록 · 보드) ────────────────────────────────────────────
   캔버스 앞에 프로젝트 목록이 선다. 둘 다 DOM 에 살아 있고 hidden 으로만 가른다
   (캔버스 리스너는 마운트 때 한 번만 붙는다 — videoLabView.render 주석 참고). */
.vlab-screen { display: block; }

/* ── 프로젝트 목록 ─────────────────────────────────────────────────────── */
.vlab-projects {
  max-width: 1080px;
  margin: 0 auto;
  padding: 28px 24px 48px;
}
/* ★ 스텝퍼가 있는 화면에서는 **머리가 이미 위에서 시작한다** — 옛 28px 을 그대로 두면 크럼·칩과 제목
   사이가 두 배로 벌어진다(간격은 언제나 한 곳이 정한다 · 2026-08-27 codex 지적 #1).
   ⚠️ 0 도 답이 아니다 — 그러면 제목이 칩에 달라붙어 스텝퍼가 제목의 일부처럼 읽힌다(육안 확인). */
.vlab-screen-sub > .vlab-projects { padding-top: var(--sp-lg, 24px); }
/* 머리(맥락+행동)와 본문(결과)을 **선으로 가른다** — 안 그러면 제목·버튼·카드가 한 덩어리로 흐른다(#4). */
.vlab-screen-sub .vlab-projects-head {
  padding-bottom: var(--sp-md);
  border-bottom: 1px solid var(--hairline);
  margin-bottom: var(--sp-lg, 24px);
}
/* ⚠️ **행동은 제목 줄에 선다**(2026-08-27 codex 2차): `flex-end` 는 버튼을 부제 높이로 내려 제목과의
   관계를 끊었다 — 머리가 여전히 조각으로 읽히던 이유다. 위 맞춤으로 제목과 같은 줄에 세운다. */
.vlab-projects-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}
.vlab-projects-title { margin: 0; font-size: 20px; font-weight: 700; }
/* 머리말 오른쪽 행동 묶음([프로젝트 설정] · [새 캔버스]) — 좁은 폭에서 둘이 따로 접히지 않게 한 줄로 묶는다. */
.vlab-projects-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* ⚠️ 보조 글의 대비를 올렸다(2026-08-27 codex 지적 #2: *"화면이 비활성처럼 보인다"*). 세 층으로 쓴다 —
   본문 1.0 · 보조 0.72 · 메타 0.55. 이보다 흐린 값은 **정말 못 쓰는 컨트롤**에만 남긴다. */
.vlab-projects-sub {
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.55;
  opacity: 0.72;
}
/* 로딩·빈 상태·오류가 같은 자리를 쓴다 — 화면이 상태마다 다른 곳으로 튀지 않게. */
/* ★ 빈 화면은 **상자에 담는다**(2026-08-27 codex 지적 #6): 옛 판은 문장과 버튼이 화면 위쪽에 떠 있고
   그 아래가 통째로 빈 채라 페이지가 '미완성' 으로 읽혔다. 테두리와 최소 높이로 자리를 만든다. */
.vlab-projects-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 220px;
  padding: var(--sp-lg, 24px);
  border: 1px dashed var(--hairline);
  border-radius: var(--r-md, 10px);
  gap: 12px;
  padding: 64px 16px;
  opacity: 0.9;
}
.vlab-projects-empty-text { font-size: 14px; line-height: 1.6; opacity: 0.82; }
/* ⚠️ 옛 `.vlab-projects-empty-hint`(권유 한 줄)는 2026-08-27 에 걷혔다 — 빈 화면의 안내가 두 줄이고 버튼이
   둘이라, 머리 오른쪽의 같은 버튼 둘까지 합쳐 **같은 화면에 같은 길이 네 번** 있었다. 지금은 문장 하나 +
   권하는 길 하나다(나머지는 머리에 있다). */

/* ── 소유자 구획(2026-08-27) ────────────────────────────────────────────
   오너 지시: *"여러 프로젝트와 여러사람께 섞여서 보기가힘들어"*. 관리자 전체 보기에서 카드가 한 격자에
   평평하게 쏟아져, 누구 것인지는 카드마다 붙은 이메일 한 줄로만 갈렸다 — 그건 구별이 아니라 반복이다.
   ⚠️ 구획이 하나면 머리글 자체를 안 그린다(렌더러 판단) — 그때 이 규칙들은 아무것도 하지 않는다. */
.vlab-proj-section + .vlab-proj-section { margin-top: var(--sp-lg, 24px); }
.vlab-proj-section-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 10px;
  padding-bottom: 6px;
  /* 선이 구획의 경계다 — 카드 사이 여백만으로는 "다음 사람"이 시작된 것을 못 읽는다. */
  border-bottom: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
}
.vlab-proj-section-name {
  /* 14px — 카드 제목(15px)과 한 급 차이다. 13px 이던 옛 값은 **구획이 카드보다 아래로 읽혔다**:
     구획은 위계상 카드를 담는 쪽인데 시각적으로 더 작고 흐렸다(2026-08-27 육안). */
  font-size: 14px;
  font-weight: 700;
  color: var(--ink-dim, #9aa3b2);
  overflow-wrap: anywhere;
}
/* 내 구획만 또렷하다 — 관리자에게도 주된 일은 자기 작업이고 남의 것은 살펴보러 오는 것이다. */
.vlab-proj-section-head.is-mine .vlab-proj-section-name { color: var(--ink, #e8ecf4); }
.vlab-proj-section-count { font-size: 11px; color: var(--ink-dim, #9aa3b2); opacity: 0.7; }

.vlab-proj-grid {
  display: grid;
  /* 280px — 단계 줄(`① 캐릭터 4명 · ② 배경 3곳 · ③ 회차 캔버스 2개`)이 한 줄에 들어가는 폭이다.
     240px 이던 옛 값에서는 그 줄이 두 줄로 접혀 카드 높이가 들쭉날쭉했다. */
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
}
.vlab-proj {
  position: relative;
  display: flex;
  /* 🔴 **위로 붙인다**(2026-08-27 오너 지적 + 코덱스 #1). `center` 였을 때, 소유자 줄이 있는 카드(4줄)와
     없는 카드(3줄)가 한 행에 서면 짧은 쪽 내용이 가운데로 떠서 **제목 높이가 서로 안 맞았다**.
     격자 행은 가장 높은 카드에 맞춰 늘어나므로 이 값이 곧 "제목이 한 줄에 서느냐"를 정한다. */
  align-items: flex-start;
  gap: 8px;
  /* 76/12 — 세 줄(제목·단계·메타)이 들어가는 최소치다. 84/14 는 단계 줄이 없던 시절 값이라 지금은
     빈 여백만 만든다(오너 지적 *"뚱뚱하다"*). */
  min-height: 76px;
  padding: 12px 14px;
  /* 별·삭제는 **제목과 같은 행**이다 — 카드 모서리에 절대배치로 매달면 제목의 첫 줄 높이가 카드마다
     다를 때 아이콘만 제자리에 남는다(코덱스 #3). 자리는 격자가 만들고 눈대중 여백은 쓰지 않는다. */
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
  border-radius: 14px;
  background: var(--glass-bg, rgba(20, 22, 30, 0.92));
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.vlab-proj:hover,
.vlab-proj:focus-visible { border-color: var(--accent, #5b8cff); outline: none; transform: translateY(-1px); }
.vlab-proj.is-opening { opacity: 0.6; pointer-events: none; }
/* 본문은 한 벌의 격자다 — 줄 간격을 한 곳에서 정한다(옛 판은 요소마다 margin-top 이 4·5·2px 로 제각각
   이라 줄 사이 리듬이 없었다). */
.vlab-proj-main {
  min-width: 0;
  flex: 1 1 auto;
  display: grid;
  gap: 4px;
}
/* 제목 행 — 제목이 남는 폭을 다 쓰고, 아이콘은 오른쪽 끝에 자기 크기만큼. */
.vlab-proj-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  /* 제목과 아이콘 사이 숨 — 8px 이면 긴 제목의 말줄임이 별에 붙어 읽힌다. */
  gap: 12px;
  /* 아이콘 버튼(24px)이 제목(15px·line-height 1.4 ≈ 21px)보다 높아 행 높이를 정한다 — 아이콘이 없는
     카드와 있는 카드의 제목 높이를 맞추려면 행 높이를 못박아야 한다. */
  min-height: 24px;
}
.vlab-proj-acts { display: flex; align-items: center; gap: 2px; }
/* ⛔️ 옛 `min-width: 66px` 는 걷혔다(2026-08-27). 별의 x 를 고정하려던 값인데, 삭제가 없는 카드에
   **오른쪽 빈칸**을 남겨 별이 허공에 뜬 것처럼 보였다(오너: *"별 위치가 애매해"*). 지금은 별이
   끝자리라 폭을 못박지 않아도 x 가 같다 — 자리를 비우는 대신 **순서**로 푼 것이다. */
.vlab-proj-title {
  font-size: 15px;
  font-weight: 600;
  /* 긴 제목이 카드를 늘려 격자를 깨지 않게 — 한 줄로 자른다. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ⚠️ `word-break: keep-all` — 한글은 **어절 안에서 끊지 않는다**(2026-08-20 오너 신고 "글자가 깨지는 것
   같다"의 유력한 정체). 기본값이면 브라우저가 음절 단위로 끊어 `…디지털 드로 / 잉` 처럼 마지막 한 글자만
   다음 줄로 떨어지고, 그게 글자가 깨진 것처럼 읽힌다. 영문·숫자는 `overflow-wrap: anywhere` 가 받아
   준다(긴 id 하나가 카드를 밀어내지 않게). */
/* 단계 줄 — `① 캐릭터 4명 · ② 배경 3곳 · ③ 회차 캔버스 2개`(2026-08-27). **메타보다 진하다**: 이름이 같은
   프로젝트를 구별하는 것이 이 줄이고, 기획안·비율·시각은 그다음 사실이다.
   ⚠️ 가운뎃점 대신 **칩 사이 간격**으로 가른다 — 카드 17장에 같은 낱말이 51번 나오는 줄이라, 구분자까지
      글자면 눈이 붙잡을 것이 없다(2026-08-27 육안). */
.vlab-proj-steps {
  display: flex;
  flex-wrap: wrap;
  /* 세로 간격은 0 이다 — 접히지 않는 것이 정상이고(버튼이 흐름 밖이라 폭이 넉넉하다), 좁은 폭에서 접힐
     때만 4px 가 필요하다. `gap: 0 14px` 로 두면 접힌 줄이 붙어 버린다. */
  gap: 3px 14px;
  font-size: 12px;
  word-break: keep-all;
}
/* ⚠️⚠️ **알약(배경·테두리)을 걷었다**(2026-08-27 오너 지적 *"ui가 너무 뚱뚱해지고 비효율적이야"*).
   칩 셋에 배경을 깔면 카드 한 장에 작은 판이 세 개 생겨 잉크가 확 늘고, 패딩만큼 줄 높이도 커진다.
   목적은 **수가 눈에 걸리는 것**이었고 그건 색 대비 하나로 충분하다 — 낱말은 흐리게, 수는 또렷하게.
   구분자는 글자(`·`)가 아니라 **간격**이다(그 줄에 같은 낱말이 이미 셋이라 글자를 더하지 않는다). */
.vlab-proj-step { display: inline-flex; align-items: baseline; gap: 4px; }
.vlab-proj-step-name { color: var(--ink-dim, #9aa3b2); font-size: 11px; }
/* `tabular-nums` — `4명`·`32명` 처럼 자릿수가 달라도 글자폭이 같아 줄이 흔들리지 않는다. */
.vlab-proj-step-num { color: var(--ink, #e8ecf4); font-weight: 600; font-variant-numeric: tabular-nums; }
/* 0 인 단계는 흐리다 — 준비가 안 된 자리가 한눈에 보여야 카드가 "여기서 뭘 해야 하나"에 답한다.
   ⚠️ 빨강·경고색을 쓰지 않는다: 0 은 고장이 아니라 **아직 안 한 것**이다.
   ⚠️ 수의 굵기까지 함께 죽인다 — 색만 바꾸면 굵은 `0` 이 여전히 눈을 끈다. */
.vlab-proj-step.is-empty { opacity: 0.55; }
.vlab-proj-step.is-empty .vlab-proj-step-num { color: var(--ink-dim, #9aa3b2); font-weight: 400; }

/* ── 검색(2026-08-27) — 목록이 SEARCH_MIN 이상일 때만 선다 ───────────────── */
.vlab-proj-search {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  padding: 0 12px;
  height: 34px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
  border-radius: 10px;
  background: var(--glass-bg, rgba(20, 22, 30, 0.92));
}
.vlab-proj-search:focus-within { border-color: var(--accent, #5b8cff); }
.vlab-proj-search-icon { color: var(--ink-dim, #9aa3b2); flex: 0 0 auto; }
.vlab-proj-search-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  outline: none;
  background: transparent;
  color: var(--ink, #e8ecf4);
  font: inherit;
  font-size: 13px;
}
.vlab-proj-search-input::placeholder { color: var(--ink-dim, #9aa3b2); opacity: 0.8; }
/* 🔴 **링이 두 겹으로 보였다**(2026-08-27 육안): 디자인 시스템이 모든 input 의 포커스에 3px box-shadow 를
   거는데, 여기서는 그 위에 컨테이너의 `:focus-within` 테두리가 또 그려진다. 신호는 하나면 되고, 바깥
   테두리가 **입력칸 전체**를 감싸므로 그쪽을 남긴다. `outline:none` 만으로는 안 걷힌다(그림자는 별개다). */
.vlab-proj-search-input:focus,
.vlab-proj-search-input:focus-visible { box-shadow: none; outline: none; }
/* 검색이 하나도 못 찾았을 때 — 빈 화면으로 두면 "목록이 사라졌다"로 읽힌다.
   ⚠️ `.vlab-projects-state` 의 220px 상자를 물려받으면 **한 줄짜리 알림이 거대한 빈 판**이 된다(그 크기는
      큰 CTA 버튼을 안는 '아직 없음' 화면을 위한 값이다). 검색 실패는 되돌리기 쉬운 상태라 작게 말한다. */
.vlab-proj-nohit { font-size: 13px; min-height: 0; padding: 28px 16px; }

.vlab-proj-meta { font-size: 12px; opacity: 0.6; word-break: keep-all; overflow-wrap: anywhere; }
/* 소유자 라벨(관리자 전체 보기) — 메타보다 한 단 더 흐리게. 기획안 목록의 `.pl-row-owner` 와 같은 언어다:
   이메일은 정체를 말하는 조각이고 카드의 주 정보(제목)를 가리면 안 된다. 값이 없으면 요소 자체가 없다. */
.vlab-proj-owner { font-size: 11px; color: var(--ink-dim, #9aa3b2); overflow-wrap: anywhere; }
/* 삭제는 평소엔 흐리게 — 카드의 주 동작(열기)을 가리지 않게. */
.vlab-proj-del { opacity: 0.45; }
.vlab-proj:hover .vlab-proj-del,
.vlab-proj-del:focus-visible { opacity: 1; }
/* 즐겨찾기 별(2026-08-27) — 삭제와 같은 규율로 평소엔 흐리다.
   ⚠️ **켜진 별은 예외다**: 흐리게 두면 "이게 즐겨찾긴가"를 마우스를 올려야 알게 된다. 켜짐은 상태이고
      상태는 늘 보여야 한다(꺼진 별만 행동이라 물러나 있어도 된다). */
/* 별은 삭제보다 진하다 — **파괴적이지 않은 데다 이 화면의 새 기능**이라, 안 보이면 없는 기능이다
   (2026-08-27 `[수정]` 버튼에서 같은 값을 치렀다: 눈에 안 띄는 문은 문이 아니다).
   켜진 별은 상태이므로 언제나 또렷하다. */
.vlab-proj-fav { opacity: 0.62; }
.vlab-proj:hover .vlab-proj-fav,
.vlab-proj-fav:focus-visible,
.vlab-proj-fav.pl-bm--on { opacity: 1; }

/* 즐겨찾기 구획 머리글 — 내 구획과 같이 또렷하다(맨 위에 서는 구획이 흐릴 이유가 없다). */
.vlab-proj-section-head.is-fav .vlab-proj-section-name { color: var(--ink, #e8ecf4); }

/* ── 툴바 ─────────────────────────────────────────────────────────────── */
.vlab-toolbar {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  position: relative;
  z-index: 2;
}
.vlab-title {
  flex: 1 1 auto;
  min-width: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 6px 10px;
  color: inherit;
  font-size: 15px;
  font-weight: 600;
}
.vlab-title:hover { border-color: var(--glass-border, rgba(255, 255, 255, 0.08)); }
.vlab-title:focus { outline: none; border-color: var(--accent, #5b8cff); }
.vlab-add { display: flex; gap: 4px; }

/* ── 캔버스 · 월드 ─────────────────────────────────────────────────────── */
.vlab-canvas {
  position: relative;
  height: calc(100vh - 58px);
  overflow: hidden;           /* 팬/줌은 transform 이 한다 — 스크롤바가 끼면 좌표가 두 벌이 된다 */
  cursor: default;
  /* 점무늬: 팬·줌이 실제로 일어나는지 눈으로 알게 하는 질감.
     ⚠️ 노드는 여기에 **붙지 않는다** — 정렬은 격자가 아니라 다른 노드와의 줄맞춤이 담당한다
     (graphAlignSnap). 그래서 이 간격은 스냅 값과 무관하며 맞출 필요도 없다. */
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
  background-size: 28px 28px;
  touch-action: none;         /* 포인터 제스처를 브라우저 스크롤에 뺏기지 않게 */
}
.vlab-canvas.is-pan-mode { cursor: grab; }
.vlab-canvas.is-panning { cursor: grabbing; }

/* 콘티(board) 노드 — 칸 격자. 열 수는 칸 수에서 도출한다(카드가 inline style 로 준다). */
.vlab-board { display: grid; gap: 3px; margin: 8px 0; }
.vlab-board-cell {
  position: relative; aspect-ratio: 10 / 16; border-radius: 4px; overflow: hidden; cursor: pointer;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: rgba(0, 0, 0, 0.25);
}
.vlab-board-cell:hover { border-color: var(--accent, #5b8cff); }
.vlab-board-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
.vlab-board-cell.is-empty { display: flex; align-items: center; justify-content: center; border-style: dashed; }
.vlab-board-cell-empty { font-size: 16px; opacity: 0.35; }
.vlab-board-cell.is-running { border-color: var(--accent, #5b8cff); }
.vlab-board-cell.is-failed { border-color: var(--danger, #ff6b6b); }
.vlab-board-cell-state {
  position: absolute; inset: auto 0 0 0; text-align: center;
  font-size: 9px; padding: 1px 0; background: rgba(0, 0, 0, 0.7); color: #fff;
}
.vlab-board-cell.is-failed .vlab-board-cell-state { background: var(--danger, #ff6b6b); }
/* 팝업의 t2i 절 — 칸은 격자라 프롬프트 칸을 넣을 자리가 없어 여기서 만든다. */
.vlab-img-prompt {
  width: 100%; padding: 8px 10px; border-radius: 8px; font-size: 13px; resize: vertical;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: rgba(0, 0, 0, 0.16); color: inherit; font-family: inherit;
}
.vlab-img-prompt:focus { outline: none; border-color: var(--accent, #5b8cff); }
.vlab-img-genrow { display: flex; align-items: center; gap: var(--sp-xs); margin-top: var(--sp-xs); }
/* 칸 편집 팝업의 모델 셀렉트 — 한 줄에서 남는 폭을 먹는다([만들기] 버튼은 자기 크기를 지킨다). */
.vlab-img-genrow .vlab-model--md { flex: 1 1 0; min-width: 0; }
.vlab-board-cell-no {
  position: absolute; top: 2px; left: 3px; font-size: 9px; line-height: 1.2;
  padding: 0 3px; border-radius: 3px; background: rgba(0, 0, 0, 0.55); color: #fff;
}
/* 칸 지우기 — hover·포커스에서만. 격자가 작아 항상 보이면 그림을 가린다. */
.vlab-board-cell-del {
  position: absolute; top: 2px; right: 2px; width: 16px; height: 16px; padding: 0;
  display: none; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(0, 0, 0, 0.7); color: #fff; font-size: 12px; line-height: 1;
}
.vlab-board-cell:hover .vlab-board-cell-del,
.vlab-board-cell-del:focus-visible { display: flex; }
.vlab-board-cell-del:hover { background: var(--danger, #ff6b6b); }
.vlab-board-foot { align-items: center; gap: 6px; }
.vlab-board-count { font-size: 11px; opacity: 0.6; margin-right: auto; }
.vlab-board-cap { font-size: 11px; opacity: 0.55; line-height: 1.5; margin-top: 6px; }

/* ── 대사(2026-08-21) ─────────────────────────────────────────────────────
   보드 카드는 **고칠 수 있는 입력**(정본이 칸이다), 클립 카드는 **읽기 전용 사본**이다.
   ⚠️ 같은 클래스를 쓰되 읽기 전용 쪽만 `--ro` 로 갈라 칠한다 — 테두리가 있으면 사람이 고칠 수 있다고
      읽는다(어포던스가 곧 계약이라는 이 탭의 규율: 참조물에 [생성] 버튼을 안 다는 것과 같은 이유). */
.vlab-dlg { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; }
.vlab-dlg-row { display: flex; align-items: center; gap: 5px; min-width: 0; }
.vlab-dlg-no {
  flex: 0 0 auto; min-width: 14px; text-align: center;
  font-size: 9px; line-height: 1.4; padding: 0 3px; border-radius: 3px;
  background: rgba(0, 0, 0, 0.4); opacity: 0.7;
}
.vlab-dlg-text {
  flex: 1 1 0; min-width: 0;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  border-radius: 7px; padding: 3px 6px;
  color: inherit; font: inherit; font-size: 11px; line-height: 1.4;
}
.vlab-dlg-text:focus { outline: none; border-color: var(--accent, #5b8cff); }

/* ── 클립 카드의 대사 편집기(2026-08-21) ──────────────────────────────────────
   ⚠️ 오너 신고: *"대사 치는 영역이 너무 작고 ui가 별로야"*. 그래서 읽기 전용 줄(11px)과 **다른 치수**를
      쓴다 — 입력은 13px·높이 30px 이고 카드도 그만큼 커진다(`NODE_BOX_H.clip`).
   ⚠️ 손잡이는 넉넉히 잡히게 22px 폭을 준다. 작은 손잡이는 "잡았는데 카드가 움직인다"의 절반이다. */
.vlab-dlg--edit { gap: 5px; }
.vlab-dlg-list { display: flex; flex-direction: column; gap: 4px; }
/* ⚠️ **한 행이 한 화자**다(2026-08-24 오너: *"연속된 화자는 … 엔터치면 늘어나는 인풋으로 만들고 2줄이
   써있어야 할거같은데"*). 2026-08-21 판의 "한 줄이 한 대사"를 대체한다 — 저장은 여전히 줄 단위이고
   (`serializeDialogueLines` 가 행을 줄로 편다) 화면만 화자 블록으로 묶는다.
   ⚠️ `align-items` 가 center 가 아니라 **flex-start** 다: 대사 칸이 늘어나면 화자 칸과 [×] 가 가운데로
      끌려 내려가 첫 줄과 어긋난다. 사람이 보는 기준선은 **행의 첫 줄**이다. */
.vlab-dlg--edit .vlab-dlg-row { gap: 4px; align-items: flex-start; }
/* ⚠️ 손잡이·드롭 미리보기 CSS 는 없다 — **순서 바꾸기(드래그)를 걷었다**(2026-08-21 오너: *"드래그하는
   거 없애자 그냥"*). 순서는 쓰는 순서다. */
.vlab-dlg-who, .vlab-dlg-say {
  min-width: 0;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  border-radius: 8px; padding: 6px 8px;
  color: inherit; font: inherit; font-size: 13px; line-height: 1.4;
}
.vlab-dlg-who { flex: 0 0 86px; box-sizing: border-box; min-height: 30px; }
/* ★ 여러 줄 대사 칸 — 쓴 만큼 늘어난다(`autoGrowTextarea` 가 `scrollHeight` 로 높이를 잡는다).
   ⚠️ 네 값이 한 몸이다: `resize:none`(카드 자체 크기 손잡이와 싸우지 않게 — `.vlab-text` 와 같은 근거) ·
      `box-sizing:border-box`(padding 을 뺀 높이를 쓰면 매 입력마다 2px 씩 자란다) ·
      `max-height`+`overflow-y:auto`(**높이를 유계로 만든다** — 인물·배경 카드에 180px 캡을 걸어
      `NODE_BOX_H` 가 지킬 수 있는 약속이 되게 한 2026-08-24 판단과 같다. 캡이 없으면 대사 한 줄이
      길어질수록 카드가 자라 배치 표의 값이 다시 거짓이 된다).
   ⚠️ 캡을 풀면 `NODE_BOX_H.clip` 이 거짓이 된다 — 둘은 한 몸이다. */
.vlab-dlg-say {
  flex: 1 1 0; display: block; box-sizing: border-box;
  min-height: 30px; max-height: 96px; resize: none; overflow-y: auto;
}
.vlab-dlg-who:focus, .vlab-dlg-say:focus { outline: none; border-color: var(--accent, #5b8cff); }
.vlab-dlg-del {
  flex: 0 0 26px; min-height: 30px; display: flex; align-items: center; justify-content: center;
  background: none; border: 0; border-radius: 6px; color: inherit; opacity: 0.45; cursor: pointer;
}
.vlab-dlg-del:hover { opacity: 1; background: rgba(255, 138, 138, 0.14); color: #ff8a8a; }
.vlab-dlg-del:disabled { opacity: 0.2; cursor: default; }
/* 상한을 넘겨 **못 실리는** 줄 — 지울 수 있게 남겨 두되 실리지 않는다는 것을 색으로 말한다. */
.vlab-dlg-row.is-over-cap .vlab-dlg-say,
.vlab-dlg-row.is-over-cap .vlab-dlg-who { opacity: 0.45; border-style: dashed; }
/* 한 줄이 200자를 넘어 **뒤가 잘리는** 행 — 위와 다른 사고라 다르게 말한다(저쪽은 통째로 안 실린다).
   흐리게 두면 안 된다: 이 줄은 실리되 잘린다. */
.vlab-dlg-row.is-too-long .vlab-dlg-say { border-color: rgba(255, 196, 106, 0.65); }
.vlab-dlg-empty { font-size: var(--fs-fine); opacity: 0.5; padding: 2px 0; }
.vlab-dlg-foot { display: flex; justify-content: flex-start; margin-top: 2px; }
/* 읽기 전용 사본 — 입력처럼 보이지 않게 테두리·배경을 걷고 한 줄로 줄인다(카드가 길어지지 않게). */
/* 초안 팝업 2단의 **접힌 근거**(회차 원문·참고한 설정). 승인 판단에는 필요하고 늘 보일 필요는 없다. */
.vlab-draft-fold { margin-top: 10px; }
.vlab-draft-fold > summary { cursor: pointer; list-style: revert; }
.vlab-draft-fold[open] > summary { margin-bottom: 6px; }
.vlab-draft-fold textarea[readonly] { opacity: 0.72; }

.vlab-dlg-text--ro {
  background: none; border-color: transparent; padding: 1px 0;
  opacity: 0.75; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* 빈 캔버스 시작 안내 — 노드 0일 때만. 캔버스(화면 고정)에 붙으므로 줌·팬을 따라가지 않는다.
   ⚠️ 배경(팬/마퀴/우클릭)에 이벤트가 닿아야 하므로 껍데기는 pointer-events:none 이고
      카드만 되살린다. 안 그러면 빈 캔버스에서 우클릭 메뉴가 안 열린다. */
.vlab-guide {
  position: absolute; inset: 0; pointer-events: none;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; padding: 24px;
}
.vlab-guide-title { font-size: 15px; font-weight: 600; opacity: 0.9; margin: 0 0 4px; }
.vlab-guide-card {
  pointer-events: auto;
  width: min(460px, 92%); padding: 16px 18px; border-radius: 12px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: var(--glass-bg, rgba(255, 255, 255, 0.03));
  backdrop-filter: blur(8px);
}
.vlab-guide-card-title { font-size: 13px; font-weight: 600; margin-bottom: 6px; }
.vlab-guide-card-desc { font-size: 12px; line-height: 1.6; opacity: 0.65; margin: 0 0 12px; }
.vlab-guide-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.vlab-guide-note { font-size: 11px; opacity: 0.45; margin: 8px 0 0; }
/* 재료 준비 카드(2026-08-26) — 콘티보다 **먼저** 서는 카드다. 경고색 테두리로 나머지 카드와 갈라
   놓는다: 순서만 바꾸면 세 카드가 똑같이 생겨 무엇이 먼저인지 모양으로는 읽히지 않는다. */
.vlab-guide-card--warn { border-color: var(--warn, #ffb454); background: rgba(255, 180, 84, 0.06); }
/* ⚠️ 옛 글머리표 목록(`.vlab-guide-need`)은 2026-08-27 에 걷혔다 — 안내도 초안 팝업과 **같은 행**
   (`.vlab-prep-row`)을 쓴다. 같은 사실을 두 화면이 다르게 그리면 사용자가 두 번 배운다.
   안내 카드 안에서는 행 배경이 카드보다 밝아야 층이 읽힌다(카드가 이미 경고색 바탕이다). */
.vlab-guide-card--warn .vlab-prep-rows { margin: 0 0 var(--sp-sm); }
.vlab-guide-card--warn .vlab-prep-row { background: rgba(255, 255, 255, 0.06); }

/* 조작법 패널(`?` · 툴바 '조작법') — 오너 신고 2026-08-26 "단축키 설명이 있으면 좋겠습니다".
   ⚠️ `.vlab-guide` 와 달리 껍데기가 곧 카드다(전면을 덮지 않는다) — 패널을 열어 둔 채로 캔버스를
      계속 만질 수 있어야 조작법을 **보면서 따라할 수 있다**. 그래서 우측 상단에 붙는다.
   ⚠️ 캔버스(화면 고정)에 붙으므로 줌·팬을 따라가지 않는다. 월드에 붙이면 흘러간다. */
.vlab-keyhelp {
  position: absolute; top: 12px; right: 12px; z-index: 6;
  width: min(340px, calc(100% - 24px)); max-height: calc(100% - 24px); overflow-y: auto;
  padding: 12px 14px 14px; border-radius: 12px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: var(--glass-bg-strong, rgba(20, 20, 24, 0.82));
  backdrop-filter: blur(10px);
  font-size: 12px;
}
.vlab-keyhelp-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.vlab-keyhelp-title { font-size: 13px; font-weight: 600; margin: 0; }
.vlab-keyhelp-close {
  background: none; border: 0; cursor: pointer; color: inherit;
  opacity: 0.5; font-size: 13px; line-height: 1; padding: 2px 4px;
}
.vlab-keyhelp-close:hover { opacity: 0.9; }
.vlab-keyhelp-group { margin: 12px 0 6px; font-size: 11px; font-weight: 600; opacity: 0.55; }
/* 키 배지 열 + 설명 열. dt/dd 를 격자로 세워 배지 폭이 들쭉날쭉해도 설명이 한 줄에서 시작한다. */
.vlab-keyhelp-list {
  display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; margin: 0; align-items: baseline;
}
.vlab-keyhelp-keys { margin: 0; }
.vlab-keyhelp-kbd {
  display: inline-block; white-space: nowrap;
  padding: 1px 6px; border-radius: 5px; font-size: 11px; font-family: inherit;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12));
  background: rgba(255, 255, 255, 0.06);
}
.vlab-keyhelp-desc { margin: 0; opacity: 0.72; line-height: 1.5; }
.vlab-keyhelp-note { font-size: 11px; opacity: 0.4; margin: 12px 0 0; }

/* 툴바의 읽기전용 안내 — 관리자가 팀원 캔버스를 열었을 때. 편집 버튼이 사라진 이유를 화면이 대답한다.
   ⚠️ 톤은 **중립**이다(경고 아님) — 관리자는 이 줄을 매번 보게 되고, 주황으로 두면 늘 고장 난
      화면으로 읽힌다(`videoLabProjectList` 의 notice 와 같은 판단). */
.vlab-viewonly-note {
  font-size: 11px; opacity: 0.55; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
}

.vlab-world {
  position: absolute;
  left: 0;
  top: 0;
  /* ⚠️ 크기를 준다(캔버스 전체). 자식이 전부 absolute 라 auto 로 두면 0×0 이 되고,
     그러면 그 안의 엣지 svg 도 0×0 이라 **선이 한 줄도 안 그려진다**(크롬 실측). */
  width: 100%;
  height: 100%;
  /* ⚠️ transform-origin 0 0 — graphViewport 의 좌표 수식이 이걸 전제한다. 기본값(50% 50%)이면 전부 어긋난다. */
  transform-origin: 0 0;
  will-change: transform;
}

/* ── 엣지 ──────────────────────────────────────────────────────────────── */
/* ⚠️ 크기 100%(= 월드 = 캔버스) + overflow:visible. 둘 다 필요하다:
   - 크기가 0 이면 브라우저가 내용을 아예 안 그린다(path 좌표는 맞는데 화면에 안 보인다 — 크롬 실측).
   - overflow 가 hidden 이면 캔버스 밖·음수 좌표로 나간 엣지가 잘린다. */
.vlab-edges {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  /* ⚠️ 레이어 자체는 포인터를 먹지 않는다 — 먹으면 elementFromPoint 로 드롭 대상을 못 찾고,
     빈 캔버스 클릭·마퀴도 전부 죽는다. 개별 히트 path 만 auto 로 되살린다. */
  pointer-events: none;
}
.vlab-edge {
  stroke: rgba(255, 255, 255, 0.28);
  stroke-width: 2;
}
.vlab-edge.is-selected { stroke: var(--accent, #5b8cff); stroke-width: 3; }
/* ★ 선택한 카드에 닿는 선만 또렷하게(2026-08-24 오너 신고: *"노드도 너무 꼬여있어서 어디에 뭐가
   포함된건지 알기 어렵고"*). 공용 카드 하나가 아홉 씬으로 뻗는 선은 배치로 줄일 수 없다 —
   **묻는 대로 답하는 것**이 답이다: 카드를 고르면 그 카드의 관계만 남는다.
   ⚠️ 지우지 않고 흐리게 한다(지우면 "연결이 사라졌나" 가 된다). 클릭 판정은 그대로다. */
.vlab-edge.is-near { stroke: rgba(255, 255, 255, 0.72); stroke-width: 2.5; }
.vlab-edge.is-far { stroke: rgba(255, 255, 255, 0.07); }
/* ★ **자식의 자식까지 이어진 사슬**(2026-08-24 오너 지시). 직접 관계(`is-near`)는 흰색으로 또렷하고,
   그 아래로 흐르는 선은 **다른 색**으로 한 단 낮게 — 색이 다르므로 "닿은 것"과 "흘러가는 것"을 갈라
   읽을 수 있다(밝기만 다르면 두 층이 한 다발로 보인다).
   ⚠️⚠️ **색이 보라인 것은 남은 자리가 그것뿐이라서다.** 이 캔버스의 색은 이미 뜻을 갖는다 —
      파랑(#8ab4ff) = 선택·생성 중 · 초록(#6ee7a8) = 완료 · 빨강(#ff8a8a) = 실패 · 주황(#ffb454) = 경고·비용.
      흐름을 파랑으로 두면 **선택한 카드와 같은 색**이 되어 "고른 것"과 "흘러간 것"이 섞인다(첫 판이 그랬다). */
.vlab-edge.is-flow { stroke: rgba(192, 140, 255, 0.6); stroke-width: 2; }
/* ★ 공용 카드에서 갈라져 나가는 선 — **아무것도 안 고른 기본 화면**에서만 옅다(2026-08-24 후속).
   9씬 캔버스 실측에서 선의 65%가 공용 카드 넷에서 나간 것이었고, 그 다발이 격자를 가로질러 씬 안의
   구조선(재료→콘티→클립)을 덮고 있었다. 카드를 고르면 `.is-near` 가 이 규칙을 이겨 또렷해진다.
   ⚠️ 굵기도 함께 내린다 — 색만 내리면 선이 여전히 격자를 '긋는다'(같은 두께의 회색 다발). */
.vlab-edge.is-fan { stroke: rgba(255, 255, 255, 0.10); stroke-width: 1.5; }
.vlab-edge--draft {
  stroke: var(--accent, #5b8cff);
  stroke-width: 2;
  stroke-dasharray: 5 4;
}
.vlab-edge-hit { pointer-events: stroke; cursor: pointer; }

/* 정렬 가이드 — 다른 노드와 줄이 맞는 순간에만 잠깐 뜬다. 엣지와 헷갈리지 않게 색·점선을 다르게. */
.vlab-guide {
  stroke: #ff5fa2;
  stroke-width: 1;
  stroke-dasharray: 4 3;
  /* ⚠️ vector-effect — 확대해도 선이 굵어지지 않는다(가이드는 안내선이지 내용이 아니다). */
  vector-effect: non-scaling-stroke;
}


/* ── 노드 ──────────────────────────────────────────────────────────────── */
.vlab-node {
  position: absolute;
  width: 280px;
  background: var(--glass-bg, rgba(20, 22, 30, 0.92));
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
  border-radius: 14px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  cursor: grab;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}
.vlab-node.is-selected {
  border-color: var(--accent, #5b8cff);
  box-shadow: 0 0 0 1px var(--accent, #5b8cff), 0 8px 28px rgba(0, 0, 0, 0.45);
}
/* ★ 관계의 층을 **카드에도** 입힌다(2026-08-24 — `graphFocusTiers`). 선만 갈라 놓으면 사람은 선을 눈으로
   따라가 카드를 찾아야 한다 — 카드가 자기 층을 말하면 그 걸음이 사라진다.
     `is-lit`  직접 닿은 카드   — 흰 테두리(선 `is-near` 와 같은 흰색)
     `is-flow` 아래로 흐른 카드 — 보라 테두리(선 `is-flow` 와 같은 색 · 색을 고른 이유는 그 규칙에)
     `is-mute` 관계 밖         — 옅게. ⚠️ 지우지 않는다(구조는 계속 보여야 한다) */
.vlab-node.is-lit { border-color: rgba(255, 255, 255, 0.55); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35); }
.vlab-node.is-flow { border-color: rgba(192, 140, 255, 0.55); box-shadow: 0 0 0 1px rgba(192, 140, 255, 0.3); }
/* ⚠️ 0.5 보다 더 낮추면 카드 안 글씨가 안 읽힌다(선택은 잠깐이지만 그 사이 캔버스를 못 읽으면 안 된다). */
.vlab-node.is-mute { opacity: 0.5; }
.vlab-node--text { width: 260px; }
/* ★ 클립은 넓다(2026-08-21) — 대사 편집기가 `[화자][대사][×]` 를 **한 줄**에 놓는다. 280px 에서는 대사
   칸이 100px 짜리 홈이 된다(오너: *"가로 사이즈가 문제면 클립 노드의 크기를 키워도 돼"*).
   ⚠️ 이 값은 `videoLabBoardCells.NODE_BOX_W` 의 거울이다 — 한쪽만 고치면 배치가 겹침을 못 본다. */
.vlab-node--clip, .vlab-node--cut { width: 420px; }
/* ★ 콘티도 넓다(2026-08-24) — 연출 지시 + 평면도 + 소품이 카드 안으로 들어왔다(오너 결정: 재료 텍스트
   카드를 없애고 *"콘티 카드를 크게"*). 280px 에서는 그 글이 25자짜리 홈에서 접히고, 3열 격자의 칸이
   80px 짜리 우표가 된다.
   ⚠️ 이 값은 `videoLabBoardCells.NODE_BOX_W.board` 의 거울이다 — **둘이 어긋나면 배치표가 거짓이 된다**
      (카드는 화면에서 이웃을 덮는데 모델은 그것을 모른다). 2026-08-24 에 실제로 그 사고를 겪었다. */
.vlab-node--board { width: 420px; }

/* ── 크기 조절 손잡이 여덟 — 변 넷 + 모서리 넷 ────────────────────────────────
   ⚠️ **잡을 수 있어야 손잡이다**(오너 지적 2026-08-14). 변은 띠(두께 10px), 모서리는 18×18 로 준다.
      캔버스가 0.4배로 축소돼 있으면 화면에서 그 절반도 안 되므로 이보다 얇게 두면 못 잡는다.
   ⚠️ 모서리가 변보다 **위에** 있어야 한다(z-index) — 겹치는 자리에서 모서리를 잡으려는데 변이 먹으면
      "비율 유지가 안 된다"가 된다. 그래서 변은 모서리 자리를 비우고(inset 14px) 모서리를 위에 얹는다.
   ⚠️ 커서가 곧 설명이다: 변은 그 축 화살표, 모서리는 대각선. */
.vlab-node-resize {
  position: absolute;
  opacity: 0;
  transition: opacity 120ms ease;
  touch-action: none;   /* 모바일에서 스크롤로 새지 않게 */
}
/* 변 — 모서리 자리(14px)는 비운다. */
.vlab-node-resize--n { top: -3px; left: 14px; right: 14px; height: 10px; cursor: ns-resize; border-top: 2px solid var(--accent, #5b8cff); }
.vlab-node-resize--s { bottom: -3px; left: 14px; right: 14px; height: 10px; cursor: ns-resize; border-bottom: 2px solid var(--accent, #5b8cff); }
.vlab-node-resize--w { left: -3px; top: 14px; bottom: 14px; width: 10px; cursor: ew-resize; border-left: 2px solid var(--accent, #5b8cff); }
.vlab-node-resize--e { right: -3px; top: 14px; bottom: 14px; width: 10px; cursor: ew-resize; border-right: 2px solid var(--accent, #5b8cff); }
/* 모서리 — 변 위에 얹는다. */
.vlab-node-resize--nw,
.vlab-node-resize--ne,
.vlab-node-resize--sw,
.vlab-node-resize--se { width: 18px; height: 18px; z-index: 2; }
.vlab-node-resize--nw { top: -4px; left: -4px; cursor: nwse-resize; border-top: 2px solid var(--accent, #5b8cff); border-left: 2px solid var(--accent, #5b8cff); border-radius: var(--r-md) 0 0 0; }
.vlab-node-resize--ne { top: -4px; right: -4px; cursor: nesw-resize; border-top: 2px solid var(--accent, #5b8cff); border-right: 2px solid var(--accent, #5b8cff); border-radius: 0 var(--r-md) 0 0; }
.vlab-node-resize--sw { bottom: -4px; left: -4px; cursor: nesw-resize; border-bottom: 2px solid var(--accent, #5b8cff); border-left: 2px solid var(--accent, #5b8cff); border-radius: 0 0 0 var(--r-md); }
.vlab-node-resize--se { bottom: -4px; right: -4px; cursor: nwse-resize; border-bottom: 2px solid var(--accent, #5b8cff); border-right: 2px solid var(--accent, #5b8cff); border-radius: 0 0 var(--r-md) 0; }

.vlab-node:hover .vlab-node-resize,
.vlab-node.is-selected .vlab-node-resize { opacity: 0.85; }
.vlab-node-resize:hover { opacity: 1; }
@media (hover: none) { .vlab-node-resize { opacity: 0.85; } }

/* 높이를 정한 카드 — 안쪽이 남는 자리를 나눠 갖는다.
   ⚠️ 이게 없으면 카드만 커지고 **그림은 그대로**라 아래가 텅 빈다(크기 조절의 뜻이 사라진다).
   ⚠️ `min-height: 0` 이 있어야 flex 자식이 줄어들 수 있다(안 그러면 내용이 카드를 밀어낸다). */
.vlab-node.is-sized { overflow: hidden; }
.vlab-node.is-sized .vlab-media { flex: 1 1 auto; min-height: 0; max-height: none; }
.vlab-node.is-sized .vlab-thumb { height: 100%; max-height: none; object-fit: contain; }
/* ⚠️ 클래스가 틀리면 아무 일도 안 일어난다 — 처음에 `.vlab-node-text` 라고 적었는데 실제 텍스트영역은
   `.vlab-text` 다(`videoLabNodeView` 의 `el('textarea', 'vlab-text')`). 그래서 카드만 커지고
   **텍스트 상자는 가로로만** 늘어났다(오너 신고 2026-08-14).
   ⚠️ `min-height: 0` 이 함께 있어야 flex 자식이 **줄어들 수도** 있다(기본 min-height:auto 는 내용 높이라
      카드를 밀어낸다). `resize: vertical` 도 끈다 — 카드 크기는 손잡이가 정하는데 브라우저 기본
      리사이저까지 있으면 두 주인이 생긴다. */
.vlab-node.is-sized .vlab-text {
  flex: 1 1 auto;
  min-height: 0;
  resize: none;
}

.vlab-node-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
/* ⚠️ **줄바꿈 금지**(오너 신고 2026-08-24: *"이 텍스트가 텍스하고 트로 두줄로 떨어지는 문제"*).
   머리는 flex 행이고 옆에 붙는 라벨(재료 카드의 `평면도(GEO) · …`)이 길어지면 플렉스가 이 종류
   이름부터 줄인다 — 두 글자에서 접히면 카드가 무엇인지 읽을 수 없다. 종류 이름은 **안 줄인다**. */
.vlab-node-kind { font-size: 12px; font-weight: 600; opacity: 0.7; white-space: nowrap; flex: 0 0 auto; }

/* 셀렉트 + [생성] 한 줄. 셀렉트가 남는 폭을 먹고 버튼은 오른쪽 끝에 붙는다(버튼 위치가 곧 의미다). */
.vlab-node-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 6px; }
/* 무엇을 사는지 — [생성] 위 한 줄. **카드에 새 요소를 더하지 않고** foot 안에서 줄만 바꾼다
   (카드가 길어지면 캔버스에서 한눈에 보이는 노드 수가 줄고, 그 손해는 모든 노드가 항상 치른다).
   ⚠️ `order:-1` 로 DOM 순서와 무관하게 맨 앞에 세운다 — 버튼 배선을 건드리지 않으려는 선택이다. */
.vlab-node-cost {
  flex: 1 0 100%;
  order: -1;
  font-size: 11px;
  opacity: 0.5;
  text-align: left;
  line-height: 1.3;
}
/* ── 셀렉트 정본 ─────────────────────────────────────────────────────────
   ★ 이 탭의 **모든** 셀렉트가 `.vlab-model` 이다: 노드 카드 모델 · 카메라 무빙 · 캐릭터 팝업 모델 ·
     마법사 칸 수. 클래스는 원래부터 하나였는데 **스타일이 네이티브 그대로**여서 다크 카드 안에서
     밝은 배경·기본 화살표·다른 폰트로 튀었다(오너 지적 2026-08-13).
   ⚠️ **옵션 목록은 네이티브를 유지한다** — OS 팝업이다. 커스텀 드롭다운을 만들면 키보드·스크롤·모바일
      동작을 전부 다시 구현해야 하고, 이 탭에는 그럴 이유가 없다(검색이 필요한 자리는 `.vlab-cb` 가 있다).
   ⚠️ `color-scheme: dark` 가 그 네이티브 팝업까지 어둡게 만든다(Chrome/Edge). `option` 배경을 손으로
      칠하는 것보다 이게 정본에 가깝다 — 플랫폼이 자기 위젯을 자기 방식으로 어둡게 그린다. */
.vlab-model {
  /* ⚠️ basis 를 0 으로 둔다. `1 1 auto` 는 basis 가 **콘텐츠 폭**이라 긴 옵션 라벨만큼 자리를 요구하고,
     그 압력이 shrink 가능한 이웃([생성] 버튼)으로 가서 **버튼이 찌그러진다**(실측 2026-08-10 오너 신고).
     basis 0 + min-width 0 이면 셀렉트가 '남는 폭'만 먹는다. 버튼 쪽 flex-shrink: 0 과 한 쌍이다. */
  flex: 1 1 0;
  min-width: 0;
  text-overflow: ellipsis; /* 닫힌 상태에서 긴 라벨을 …로 — 펼치면 전문이 보인다 */
  font: inherit;
  font-size: 11px;
  /* 오른쪽 여백은 화살표 자리다(padding 을 안 주면 라벨이 화살표를 지나간다). */
  padding: 3px 22px 3px 8px;
  border-radius: 8px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.14));
  background-color: rgba(0, 0, 0, 0.3);
  color: inherit;
  cursor: pointer;
  color-scheme: dark;
  -webkit-appearance: none;
  appearance: none;
  /* 화살표 — 인라인 SVG(외부 요청 0 · 파일 0). currentColor 를 쓸 수 없으므로 흐린 흰색으로 굽는다. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23ffffff' stroke-opacity='0.55' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 7px center;
  background-size: 10px 10px;
}
/* ★ **노드 카드 푸터의 셀렉트에는 바닥을 준다**(오너 신고 2026-08-26: *"모델선택하는 셀렉트가 작아서
   안보여"* — 화면에 `: ⌄` 만 남았다). 원인은 이 라운드에 셀렉트가 **넷**이 된 것이다(모델·길이·화질
   ·비트레이트) — `flex: 1 1 0; min-width: 0` 은 남는 폭을 나눠 먹는 규칙이라 넷이 되면 각자 0 까지
   줄어든다. 위 주석이 basis 0 을 고른 근거([생성] 버튼이 찌그러지는 것을 막는다)는 그대로 유효하므로
   **버튼을 지키는 방식을 바꾸지 않고 바닥만** 준다 — 푸터는 이미 `flex-wrap: wrap` 이라, 폭이 모자라면
   찌그러지는 대신 **줄을 바꾼다**(라벨이 읽히는 것이 한 줄에 넣는 것보다 중요하다). */
/* 92px 은 `Seedanc…` 까지만 보였다 — 모델을 알아보는 것이 목적이므로 112px 로 올린다(실측: 11px 폰트
   + 좌우 패딩 30px → 글자 자리 ~82px ≈ `Seedance 2.0`). 카드 폭 414px 에 셀렉트 넷이므로 어차피 두 줄이
   되고, 두 줄이 되는 것보다 라벨을 못 읽는 것이 나쁘다. */
.vlab-node-foot .vlab-model { min-width: 112px; }

/* 팝업의 '또는 — 가진 그림을 그대로 쓰기' 블록(오너 지시 2026-08-26: *"이미지 직접 올리기가 너무
   붙어있으니까 띄우고 생성하는 영역 설명이랑 합쳐져있어서 헷갈리니까"*).
   ⚠️ 구분선 + 캡션이 있는 이유: 위아래 두 버튼이 하는 일이 **반대**다(하나는 사서 만들고 하나는 가진
      것을 쓴다). 설명 글줄이 위 문단과 이어져 보이면 그 반대가 읽히지 않는다. */
.vlab-char-orblock {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
}
.vlab-char-orlabel { font-size: 11px; font-weight: 600; opacity: 0.5; margin-bottom: 7px; }

.vlab-model:hover:not(:disabled) { border-color: var(--accent, #5b8cff); }
.vlab-model:focus-visible { outline: none; border-color: var(--accent, #5b8cff); box-shadow: 0 0 0 2px rgba(91, 140, 255, 0.28); }
.vlab-model:disabled { opacity: 0.5; cursor: default; }
/* 팝업 안(캐릭터 모달 · 마법사) — 그 안의 입력들과 크기를 맞춘다(`.vlab-cb-trigger` 와 같은 치수).
   ⚠️ flex 부모가 아닌 자리에도 서므로 폭을 직접 정한다(basis 0 은 흐름 안에서 아무 일도 하지 않는다). */
.vlab-model--md {
  flex: 0 0 auto;
  width: auto;
  min-width: 180px;
  max-width: 100%;
  font-size: 13px;
  padding: 9px 28px 9px 11px;
  background-position: right 10px center;
  background-size: 12px 12px;
}
/* 팝업의 한 줄(`.vlab-img-row`·`.vlab-char-row`)에서는 남는 폭을 먹는 쪽이 낫다. */
.vlab-img-row .vlab-model--md { flex: 1 1 0; min-width: 0; }
.vlab-node-run {
  display: inline-flex;
  flex: 0 0 auto; /* ⚠️ 셀렉트가 길어질 때 **버튼이 줄어들지 않게**. 이게 없으면 '생성'이 찌그러진다. */
  align-items: center;
  gap: 3px;
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 8px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.14));
  background: transparent;
  color: inherit;
  cursor: pointer;
}
.vlab-node-run:hover:not(:disabled) { border-color: var(--accent, #5b8cff); }
.vlab-node-run:disabled { opacity: 0.35; cursor: default; }

/* ── 포트 ──────────────────────────────────────────────────────────────── */
/* 연결점은 좌·우 두 개. 왼쪽=입력, 오른쪽=출력 — 방향이 고정돼 있어야 그래프의 흐름이 읽힌다. */
.vlab-port {
  position: absolute;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: #1a1d26;
  border: 2px solid rgba(255, 255, 255, 0.35);
  cursor: crosshair;
  z-index: 1;
  /* 평소엔 흐리게 두고 카드에 손이 오면 또렷해진다 — 점 네 개가 늘 도드라지면 카드가 시끄럽다. */
  opacity: 0.45;
  transition: opacity 120ms ease;
}
.vlab-node:hover .vlab-port { opacity: 1; }
/* 좌→우 한 방향 흐름 — 왼쪽이 입력, 오른쪽이 출력. top/bottom 규칙은 사방 포트 시절의 것으로 제거했다. */
.vlab-port--left { left: -7px; top: 50%; margin-top: -6.5px; }
.vlab-port--right { right: -7px; top: 50%; margin-top: -6.5px; }
.vlab-port:hover, .vlab-port.is-target {
  border-color: var(--accent, #5b8cff);
  background: var(--accent, #5b8cff);
  opacity: 1;
  /* ⚠️ 면마다 margin 으로 중심을 잡으므로 scale 은 transform 을 덮지 않게 단독으로 쓴다. */
  box-shadow: 0 0 0 4px rgba(91, 140, 255, 0.25);
}
/* 스냅 반경 안이지만 연결이 안 되는 포트 — 붙는 시늉만 하고 거절하면 고장으로 읽힌다. */
.vlab-port.is-invalid {
  border-color: #ff8a8a;
  background: #ff8a8a;
  opacity: 1;
  box-shadow: 0 0 0 4px rgba(255, 138, 138, 0.25);
}
/* 입력 포트가 없는 노드(텍스트)를 겨냥했을 때 — 표시할 포트가 없으므로 카드를 물들인다. */
.vlab-node.is-target { border-color: var(--accent, #5b8cff); box-shadow: 0 0 0 2px var(--accent, #5b8cff); }
.vlab-node.is-invalid { border-color: #ff8a8a; box-shadow: 0 0 0 2px rgba(255, 138, 138, 0.7); }

/* ── 노드 내부 ─────────────────────────────────────────────────────────── */
.vlab-badge {
  font-size: 11px;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid transparent;
}
.vlab-badge--idle { opacity: 0.5; border-color: var(--glass-border, rgba(255, 255, 255, 0.12)); }
.vlab-badge--running { color: #8ab4ff; border-color: rgba(138, 180, 255, 0.4); }
.vlab-badge--done { color: #6ee7a8; border-color: rgba(110, 231, 168, 0.4); }
.vlab-badge--failed { color: #ff8a8a; border-color: rgba(255, 138, 138, 0.4); }

.vlab-text {
  width: 100%;
  min-height: 120px;
  resize: vertical;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  border-radius: 10px;
  padding: 9px;
  color: inherit;
  font: inherit;
  font-size: 13px;
  line-height: 1.5;
  cursor: text;
}
.vlab-text:focus { outline: none; border-color: var(--accent, #5b8cff); }
/* 노드 자체 프롬프트 — 텍스트 노드보다 낮게(카드 안에서 미디어와 공간을 나눠 쓴다). */
.vlab-text--own { min-height: 62px; font-size: 12px; }

/* 카메라 무빙 셀렉트 — 프롬프트 바로 아래 한 줄(누르면 그 텍스트가 바뀐다). 라벨은 첫 옵션 문구가 진다. */
.vlab-cam-row { display: flex; margin-top: 6px; }
.vlab-cam { flex: 1 1 auto; }
/* 프롬프트 경고 — **차단이 아니라 안내**라서 색만 경고이고 테두리·아이콘으로 겁주지 않는다.
   경고가 없으면 요소가 hidden 이라 자리를 먹지 않는다(카드 높이는 모든 노드가 함께 치르는 비용).
   ⚠️ **한 톤이다**(오너 지적 2026-08-13 — 두 줄이 주황·노랑으로 다닥다닥 붙어 보였다): 색은 블록이
      한 번 정하고, ⚠ 표식은 **첫 줄에만** 붙는다(줄마다 붙이면 두 줄이 두 개의 경고 덩어리로 읽힌다).
      뒤 줄은 그 표식 폭만큼 들여써서 글이 한 문단으로 읽힌다. */
.vlab-lint {
  margin-top: 6px;
  display: grid;
  gap: 4px;
  font-size: 10.5px;
  line-height: 1.45;
  color: var(--warn, #ffb454);
  opacity: 0.85;
}
/* 표식 폭만큼 들여쓴다 — 첫 줄에는 ⚠ 가 그 자리에 앉고, 뒤 줄은 그 줄과 글머리가 맞는다. */
.vlab-lint-line { padding-left: 14px; text-align: left; }
.vlab-lint-line:first-child { position: relative; }
.vlab-lint-line:first-child::before {
  content: '⚠';
  position: absolute;
  left: 0;
  opacity: 0.75;
}
/* `+N건` — **누를 수 있는 것**이다(접힌 경고를 열지 못하면 그 N건은 영영 안 읽힌다).
   버튼처럼 보이지 않게 두되(안내 줄의 일부다) 밑줄로 누를 수 있음을 말한다. */
.vlab-lint-more {
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  padding: 0 0 0 14px;
  cursor: pointer;
  opacity: 0.75;
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.vlab-lint-more:hover { opacity: 1; }
/* 텍스트 노드의 placeholder 는 두 줄이다 — 줄바꿈이 보이게(기본값 pre-line 아님). */
.vlab-node--text .vlab-text::placeholder { white-space: pre-line; }

/* "또는 직접 올리기" — 두 갈래가 있다는 것을 한 줄로 알린다. */
.vlab-or {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  opacity: 0.4;
}
.vlab-or::before, .vlab-or::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--glass-border, rgba(255, 255, 255, 0.12));
}

/* ⚠️ **미디어 상자에 비율을 고정하지 않는다.** 예전에는 `aspect-ratio: 9/16`(세로) 을 박아 두고
   `object-fit: contain` 으로 맞췄는데, 그러면 **가로 이미지가 위아래 검은 여백에 갇힌다.**
   숏폼 산출물이 세로(9:16)라 세로를 가정했지만, 기획안에서 가져온 컨셉아트는 **가로 시트**다
   (오너 신고 2026-08-11 "이미지도 실제 크기에 비해 빈 공간이 있어서 영역을 과하게 먹는다").
   지금은 상자가 **내용의 비율을 따라간다** — 여백이 생기지 않는다.
   ⚠️ 비어 있을 때만 자리를 지킨다(`--empty`): 그마저 없으면 '아직 없음' 문구만 남아 카드가 접히고,
      생성이 끝나 그림이 들어오는 순간 카드 높이가 튀어 노드가 서로 겹쳐 보인다. */
.vlab-media {
  max-height: 300px;
  border-radius: 10px;
  overflow: hidden;
  background: rgba(0, 0, 0, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
}
.vlab-media--empty { aspect-ratio: 9 / 16; max-height: 220px; }
/* ★ 배경 카드는 **가로**다(2026-08-25 — 서버 `locationPrompts.LOCATION_ASPECT` 가 16:9 로 고정했다).
   그림이 들어오면 `.vlab-media` 가 내용 비율을 따라가므로 상관없지만, **비어 있는 동안** 예약하는
   자리는 여기서 정해진다 — 세로 자리를 예약해 두면 그림이 오는 순간 카드 높이가 절반으로 접히며 튄다
   (`--empty` 가 애초에 존재하는 이유가 그 튐을 막는 것이다). */
.vlab-media--wide.vlab-media--empty { aspect-ratio: 16 / 9; max-height: 180px; }
/* height:auto 라야 실제 비율대로 앉는다(100% 면 부모가 준 높이에 다시 늘어난다). */
.vlab-thumb { width: 100%; height: auto; max-height: 300px; object-fit: contain; display: block; }
.vlab-media-empty { font-size: 12px; opacity: 0.35; }
/* ★★ **인물·배경 카드의 그림은 180px 로 캡한다**(2026-08-24 — 오너 신고 *"배경과 캐릭터 노드가 겹치는게
   있어"*). 이 두 종류는 **참조 카드**다: 캔버스에서 할 일은 "누구/어디인지 알아보는 것"이고, 크게 보는
   자리는 카드를 눌러 여는 팝업이다.
   ⚠️ **캡이 없으면 카드 높이가 그림 비율이 정한다** — 세로 그림이 붙은 배경 카드는 실측 **504px** 였고,
      배치가 예약하는 높이(`videoLabBoardCells.NODE_BOX_H` 340)를 164px 넘겨 아래 인물 카드를 파고들었다.
      단위 테스트는 그 표만 보므로 전부 초록인 채로 화면에서만 겹친다.
   ⚠️ 그래서 이 캡과 `NODE_BOX_H.character`·`.location`(400)은 **한 몸이다** — 캡을 풀거나 값을 올릴 때
      둘을 함께 고친다. `.vlab-thumb` 까지 캡해야 한다: 상자만 줄이면 그림이 잘린다(`object-fit: contain`
      은 상자 크기를 따르므로 그림 쪽 상한도 같아야 비율대로 축소된다).
   ⚠️ 크기 손잡이로 키운 카드(`is-sized`)에는 적용되지 않는다 — 그때는 사람이 정한 높이가 진실이고
      `.vlab-node.is-sized .vlab-thumb { max-height: none }` 가 이미 그 규칙을 들고 있다. */
.vlab-node--character .vlab-media,
.vlab-node--location .vlab-media,
.vlab-node--character .vlab-thumb,
.vlab-node--location .vlab-thumb { max-height: 180px; }

/* 실패했지만 이전 산출물이 남아 있는 상태 — 보이는 것이 **이번** 결과가 아님을 못 오해하게.
   ⚠️ 산출물을 흐리게/흑백으로 깔지 않는다. 그건 **멀쩡한 작업물**이고(마지막 시도만 실패한 것이다)
      죽은 것처럼 보이게 하면 "왜 내 결과가 망가졌지"라는 새 오해를 만든다. 라벨로만 말한다. */
.vlab-media { position: relative; }
.vlab-stale-tag {
  position: absolute;
  top: 6px;
  left: 6px;
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.78);
  border: 1px solid rgba(255, 138, 138, 0.45);
  color: #ff8a8a;
}

.vlab-progress { height: 3px; border-radius: 999px; background: rgba(255, 255, 255, 0.08); overflow: hidden; }
.vlab-progress-fill { height: 100%; width: 0; background: var(--accent, #5b8cff); transition: width 240ms ease; }

/* 비확정 진행 바 — 프록시가 중간 진행값을 안 주므로 이게 **기본**이다(videoLabRunStatus 머리주석).
   width 를 애니메이션하지 않고 transform 으로 민다(레이아웃 리플로 없음 — 노드가 수십 개 뜬다). */
.vlab-progress--indet .vlab-progress-fill {
  width: 40%;
  transition: none;
  animation: vlab-indet 1.4s ease-in-out infinite;
}
@keyframes vlab-indet {
  0% { transform: translateX(-105%); }
  100% { transform: translateX(255%); }
}

/* ── 생성 중 표시 ────────────────────────────────────────────────────────
   ⚠️ 배지만으로는 부족했다 — 미디어 영역이 대기와 같은 '아직 없음' 이라 2~5분 걸리는 작업이
      멈춘 것처럼 보였다(오너 신고 2026-08-10). 결과 자리를 차지해서 움직임·경과·기준선을 보여 준다. */
.vlab-media-running {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  /* 높이를 주지 않는다 — 부모 .vlab-media 가 이미 flex 로 가운데 정렬한다. */
}
.vlab-spinner {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.14);
  border-top-color: var(--accent, #5b8cff);
  animation: vlab-spin 900ms linear infinite;
}
@keyframes vlab-spin { to { transform: rotate(360deg); } }
.vlab-running-label { font-size: 12px; font-variant-numeric: tabular-nums; color: #8ab4ff; }
.vlab-running-hint { font-size: 11px; opacity: 0.55; }

/* 카드 테두리도 함께 숨쉰다 — 캔버스를 축소하면 11px 배지는 안 보인다. */
.vlab-node.is-running { border-color: rgba(138, 180, 255, 0.45); animation: vlab-breathe 2s ease-in-out infinite; }
@keyframes vlab-breathe {
  0%, 100% { box-shadow: 0 0 0 0 rgba(138, 180, 255, 0); }
  50% { box-shadow: 0 0 0 3px rgba(138, 180, 255, 0.12); }
}

/* 움직임을 줄여 달라는 설정은 지킨다 — 정보는 문구·경과 시간으로 이미 다 있다. */
@media (prefers-reduced-motion: reduce) {
  .vlab-spinner,
  .vlab-node.is-running,
  .vlab-progress--indet .vlab-progress-fill { animation: none; }
  .vlab-progress--indet .vlab-progress-fill { width: 100%; opacity: 0.35; }
}

.vlab-file { font-size: 11px; color: inherit; opacity: 0.7; }
.vlab-file::file-selector-button {
  margin-right: 8px;
  padding: 4px 10px;
  border-radius: 8px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12));
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}

.vlab-caption {
  width: 100%;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  border-radius: 10px;
  padding: 7px 9px;
  color: inherit;
  font: inherit;
  font-size: 12px;
  cursor: text;
}
.vlab-caption:focus { outline: none; border-color: var(--accent, #5b8cff); }
/* 콘티 캡션은 `씬 제목 — 연출 지시` 라 한 줄에 안 들어간다(실측 약 200자 — `videoLabNodeView.captionRow`).
   ⚠️ 세로만 늘린다: 가로를 늘리면 카드 폭이 격자를 벗어나고 자동 배치가 예약한 폭과 어긋난다. */
.vlab-caption--multi {
  display: block;
  min-height: 54px;
  max-height: 180px;
  resize: vertical;
  line-height: 1.5;
  font-family: inherit;
  overflow-y: auto;
}

.vlab-hint { font-size: var(--fs-fine); opacity: 0.42; line-height: 1.4; }
.vlab-error { font-size: 12px; color: #ff8a8a; line-height: 1.4; }

/* ── 우클릭 메뉴 ───────────────────────────────────────────────────────── */
/* body 자식 + fixed — 변환된 캔버스 안에 두면 스케일에 딸려 찌그러진다.
   ⚠️ .vlab-menu 의 width 는 graphContextMenu.MENU_W 와 **수동 동기화**다(뷰포트 클램프 계산에 쓴다). */
.vlab-menu-backdrop { position: fixed; inset: 0; z-index: 60; }
.vlab-menu {
  position: fixed;
  z-index: 61;
  width: 176px;
  padding: 4px;
  border-radius: 10px;
  background: rgba(24, 26, 34, 0.98);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12));
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
}
.vlab-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 7px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.vlab-menu-item:hover { background: rgba(255, 255, 255, 0.08); }
.vlab-menu-item--danger { color: #ff8a8a; }

/* ── 마퀴 ──────────────────────────────────────────────────────────────── */
/* body 자식이라 fixed — 변환된 캔버스에 잘리지 않게(audioMarqueeSelect 와 같은 이유). */
.vlab-marquee {
  position: fixed;
  z-index: 40;
  pointer-events: none;
  border: 1px solid var(--accent, #5b8cff);
  background: rgba(91, 140, 255, 0.12);
  border-radius: 3px;
}

/* (M0 의 .vlab-empty 자리표시자는 캔버스로 교체되며 소비자가 0 이 되어 제거했다.
   근거 없는 보존은 데드코드를 영구화한다 — FRAME 이 "죽은 것을 위해 죽은 것을 보존"했던 사례.) */

/* ── 이미지 소스 모달 ────────────────────────────────────────────────────
   생성·업로드·보드에서 가져오기·레퍼런스 선택을 한 자리에. sm-* 오버레이(safetyModal) 위에 얹는다. */
.vlab-img-panel { max-width: 560px; width: min(560px, 92vw); max-height: 86vh; overflow-y: auto; }
/* 노드 모달의 절 — 캐릭터 팝업과 **같은 계약**을 쓴다(묶음 사이 17 · 라벨↔컨트롤 4).
   ⚠️ 14·6·12px 은 스케일 밖 값이라 같은 위계가 팝업마다 다르게 보였다(오너 지적 2026-08-14 — 전 팝업 점검). */
.vlab-img-sec { margin-top: var(--sp-md); }
.vlab-img-sec-title { font-size: var(--fs-fine); font-weight: 600; opacity: 0.85; margin: 0 0 var(--sp-xxs); }
.vlab-img-hint { font-size: 11px; opacity: 0.55; margin: 4px 0 6px; }
.vlab-img-prompt {
  width: 100%;
  min-height: 76px;
  resize: vertical;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  border-radius: 10px;
  color: inherit;
  font: inherit;
  font-size: 12px;
  padding: 8px 10px;
}
.vlab-img-row { display: flex; align-items: center; gap: var(--sp-xs); margin-top: var(--sp-sm); }
.vlab-img-row .vlab-model { flex: 1 1 0; min-width: 0; }

/* 썸네일 격자 — 보드 이미지 고르기·레퍼런스 토글에 같은 격자를 쓴다. */
.vlab-img-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--sp-xs); }
.vlab-img-tile {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xxs);
  padding: var(--sp-xxs);
  border-radius: var(--r-md);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12));
  background: rgba(0, 0, 0, 0.25);
  color: inherit;
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.vlab-img-tile img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: var(--r-sm); display: block; }
/* 캡션은 그림의 **설명**이라 붙어 있어야 하지만, 타일 안쪽에서 테두리와는 떨어져야 한다.
   ⚠️ 예전엔 타일 padding 4px 뿐이라 글자가 테두리에 닿아 보였다(오너 지적 2026-08-14). */
.vlab-img-tile-cap {
  font-size: 11px; opacity: 0.7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 0 var(--sp-xxs) var(--sp-xxs);
}
/* 타일의 **첫 번째 사실은 "누구인가"** 다 — 이름은 굵고, 상태는 흐리다(타이포 위계, 오너 지적 2026-08-13). */
.vlab-img-tile-cap--name { font-size: 12px; font-weight: 600; opacity: 1; }
/* 이력 스트립의 캡션(`생성 중…` · `확정` · `실패` · `지시 있음`)은 **가운데 정렬**이다(오너 지적 2026-08-14).
   ⚠️ 이유가 자리마다 다르다: 여기 캡션은 그림 아래 **상태 배지**라 그림의 중심축을 따라야 하고,
      보드·레퍼런스 격자의 캡션은 **이름**이라 왼쪽 정렬이 맞다(글줄은 왼쪽에서 시작한다).
      그래서 전역이 아니라 이력 스트립 안에서만 바꾼다 — 한 클래스를 통째로 가운데로 돌리면
      이름이 가운데 뜨는 목록이 함께 생긴다. */
.vlab-char-hist .vlab-img-tile-cap { text-align: center; }

/* 레퍼런스로 고른 타일 — 테두리로만 말한다(체크 아이콘을 얹으면 썸네일을 가린다). */
.vlab-img-tile.is-on { border-color: var(--accent, #5b8cff); box-shadow: 0 0 0 2px rgba(91, 140, 255, 0.28) inset; }
/* 피커 타일도 목록 카드와 **같은 규율**이다(위 `.vlab-char-thumb` 주석) — 상자가 시트 모양이면 시트는
   `cover`, 세로 그림은 `contain`. 여기서는 **캐릭터 피커에서만** 상자를 16:9 로 돌린다(`.vlab-img-grid`
   는 이미지 노드 모달·화풍 격자와 공용이라 전역으로 바꾸면 그쪽 1:1 타일이 함께 눕는다). */
.vlab-img-tile.is-sheet img { object-fit: contain; }
.vlab-charpick-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
.vlab-charpick-grid .vlab-img-tile img { aspect-ratio: 16 / 9; object-fit: contain; }
.vlab-charpick-grid .vlab-img-tile.is-sheet img { object-fit: cover; }

/* ── 생성 이력 스트립(2026-08-18) ──────────────────────────────────────────
   ★ 구조가 바뀐 자리다. 옛 판은 **한 줄에 4장짜리 후보 격자**였다(한 번에 4장을 사서 비교하는 화면).
     지금은 한 장씩 사고 지나간 장이 전부 남으므로, 화면이 답해야 하는 질문이 달라졌다:
       옛 질문 = "이 4장 중 어느 것이 제일 나은가"  → 큰 타일 4개를 나란히
       새 질문 = "지금 이 장이 쓸 만한가, 아니면 지나간 것 중에 더 나은 게 있었나"
     → **큰 미리보기 한 장 + 작은 이력 줄**. 비교는 큰 자리에서 하고(선택하면 그 장이 크게 온다),
       이력은 좌표만 제공한다. 24장을 같은 크기로 벌이면 그건 비교가 아니라 창고다.
   ⚠️ 최신이 **왼쪽**이다(모델이 뒤집어 준다) — 방금 산 장이 스크롤 밖에 태어나면 안 된다. */
.vlab-char-hist-head { display: flex; align-items: baseline; gap: var(--sp-xs); flex-wrap: wrap; }
.vlab-char-hist-title { font-size: 12px; font-weight: 600; opacity: 0.85; }
.vlab-char-hist-cap { font-size: 11px; opacity: 0.7; color: var(--warn, #e0b34a); }
/* 가로 한 줄 + 넘치면 가로 스크롤. 격자로 접지 않는 이유: 이력은 **시간 순서**라 한 줄이 그 자체로 축이다. */
.vlab-char-hist {
  display: flex;
  gap: var(--sp-xs);
  overflow-x: auto;
  padding-bottom: var(--sp-xxs);
  scrollbar-width: thin;
}
.vlab-img-tile { position: relative; }
.vlab-char-hist-tile {
  flex: 0 0 auto;
  width: 96px;
  min-height: 128px;
  justify-content: center;
}
/* 시트는 **가로 3분할**이라 세로 썸네일에 넣으면 패널이 손톱만 해진다 — 가로로 넓게 준다. */
.vlab-char-hist--wide .vlab-char-hist-tile { width: 168px; min-height: 108px; }
.vlab-char-hist-tile img {
  aspect-ratio: auto;
  object-fit: contain;
  max-height: 110px;
  background: rgba(0, 0, 0, 0.35);   /* contain 여백이 카드 배경과 갈리지 않게 */
}
/* 번호는 항상 보인다 — 이력이 길어지면 "몇 번째였지"가 유일한 좌표다. */
.vlab-char-hist-no {
  position: absolute;
  top: 4px; left: 4px;
  padding: 1px 5px;
  border-radius: var(--r-sm, 4px);
  background: rgba(0, 0, 0, 0.6);
  font-size: 10px; font-weight: 700; line-height: 1.4;
}
/* 지금 쓰는 장은 테두리로 말한다(선택 표시 `is-on` 과 **다른 색**이어야 둘이 겹쳐도 읽힌다). */
.vlab-char-hist-tile.is-current { border-color: rgba(76, 207, 143, 0.65); }
.vlab-char-hist-tile.is-current .vlab-char-hist-no { background: rgba(76, 207, 143, 0.85); color: #06170f; }
/* 추가 지시 입력칸 — **행동 바로 위**에 앉는다(적고 → 누른다). 주인공은 여전히 그림이라 조용하게. */
.vlab-char-extra { display: grid; gap: var(--sp-xxs); }
.vlab-char-extra .vlab-char-step-hint { margin: 0; }
.vlab-char-extra .vlab-img-prompt { margin: 0; }

/* 확대 버튼 — 그림 위 오른쪽 위. 타일 클릭('고르기')과 **다른 의도**라 별도 손잡이로 둔다. */
.vlab-img-zoom {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.55);
  font-size: 13px;
  line-height: 1;
  cursor: zoom-in;
  opacity: 0;                         /* 평소엔 그림을 가리지 않는다 */
  transition: opacity 120ms ease;
}
.vlab-img-tile:hover .vlab-img-zoom,
.vlab-img-tile:focus-within .vlab-img-zoom,
.vlab-img-zoom:focus-visible { opacity: 1; }
/* 터치는 hover 가 없다 — 항상 보인다(안 그러면 확대에 닿을 길이 없다). */
@media (hover: none) {
  .vlab-img-zoom { opacity: 1; }
}
/* 확정본·시트는 그림 자체가 확대 손잡이다. */
.vlab-char-shot.is-zoomable { cursor: zoom-in; }

/* ⚠️ **셀렉트와 버튼의 높이가 달랐다.** `.vlab-model--md` 는 세로 패딩 9px + 13px 글자라 ~38px 인데
   같은 줄의 버튼은 `--btn-h: 28px` 다 — 10px 차이가 한 줄 안에서 그대로 보였다(오너 지적 2026-08-14).
   팝업 안에서만 버튼 높이에 맞춘다(마법사 등 다른 자리의 치수는 건드리지 않는다). */
.vlab-char-panel-modal .vlab-char-row .vlab-model--md {
  height: 28px;
  padding: 0 28px 0 11px;
  font-size: var(--fs-fine);
  min-width: 150px;
}

/* 채우기·수정 버튼은 폭을 다 쓴다(카드 안에서 유일한 행동이라 찾기 쉬워야 한다). */
.vlab-node-fill { flex: 1 1 auto; justify-content: center; }

/* ⚠️ **`.vlab-node-err` 는 어느 CSS 에도 없던 클래스였다**(2026-08-18 배경 라운드에서 발견).
   `videoLabNodeView` 가 캐릭터 카드의 "이 캐릭터를 찾을 수 없습니다"를 이 클래스로 그려 왔는데 규칙이
   없어서 **경고가 평범한 본문 글자로** 떴다 — 참조가 끊긴 노드는 그 사실이 눈에 띄어야 하고, 안 그러면
   사용자는 참조 없이 생성이 돌아간 뒤에야 "왜 얼굴이/배경이 다르지"로 알게 된다.
   Phase 7 의 `vlab-node-model` 과 **같은 형태**의 결함이다(클래스가 있으니 스타일이 있다고 가정하면 안 된다).
   ⚠️ `.vlab-hint`(정보)와 **색으로 갈린다** — 둘이 같은 톤이면 "아직 없음"과 "찾을 수 없음"이 구별되지 않는다. */
.vlab-node-err {
  font-size: 11px;
  line-height: 1.45;
  color: var(--danger, #ff8a8a);
  opacity: 0.95;
}

/* 캡션 묶음 — 카드의 flex gap(9px)이 이 래퍼 하나에만 걸리므로, 안쪽 간격은 여기서 준다.
   ⚠️ 래퍼에 CSS 를 안 주면 입력과 힌트가 붙고 래퍼 위아래로만 여백이 생겨 **간격이 뒤집혀 보인다**
      (오너 신고 2026-08-10). display:grid + gap 이 그 어긋남을 없앤다. */
.vlab-caption-wrap { display: grid; gap: var(--sp-xxs); }
.vlab-caption-wrap .vlab-hint { margin: 0; }

/* ── 모달 레이아웃 ────────────────────────────────────────────────────────
   보드 이미지 목록이 주인공이고 업로드는 한 줄이면 된다.
   ⚠️ 예전에는 파일 입력이 큰 블록을 차지해 목록이 눌렸다(오너 신고 2026-08-10). */
.vlab-img-grid {
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  max-height: 46vh;
  overflow-y: auto;
  padding-right: 2px;   /* 스크롤바가 타일 테두리를 덮지 않게 */
}
.vlab-img-empty { font-size: 12px; opacity: 0.55; padding: 10px 0; }
/* ★ 구분선은 **설명 절**이 소유한다(2026-08-24) — 예전에는 border-top 이 여기 붙어 같은 or 집합의 두
   멤버(보드 이미지 ↔ 파일)를 갈라 놓고, 성질이 다른 설명 절은 경계 없이 붙어 있었다. */
.vlab-img-sec--cap { padding-bottom: var(--sp-sm); border-bottom: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08)); }
.vlab-img-sec--upload { margin-top: var(--sp-md); padding-top: var(--sp-sm); }

.vlab-img-sec--upload .vlab-img-sec-title { display: inline; margin-right: 8px; }
.vlab-img-sec--upload .vlab-file { display: inline-block; }
.vlab-img-sec--upload .vlab-img-hint { display: block; margin: 6px 0 0; }

/* ── 채우기 팝업 — **두 틀을 두 열로**(2026-08-24) ───────────────────────────────
   오너: *"지금 ui가 너무 난잡하고 이미지들도 겹쳐있고 ui가 너무 별로야 모달을 키워도 되니까 전반적으로
   이 모달UI정리해"*. 고친 것 넷:
     ① **겹침의 원인** — 이력 묶음을 `.vlab-char-hist`(flex) 로 그리면서 그 자식에 폭을 정하는 클래스
        (`.vlab-char-hist-tile`)를 안 붙였다. flex 아이템이 폭 없이 `aspect-ratio` 이미지를 들어 서로 겹쳤다.
        → 이력도 **같은 격자**로 그린다(스트립을 안 쓴다). 시간축은 정렬(최신 먼저)이 이미 표현한다.
     ② **타일 위의 ＋ 배지 철거** — 썸네일 위에 버튼을 얹은 것이 난잡함의 절반이었다. 이제 **열이 뜻을
        정한다**(왼쪽=그대로, 오른쪽=재료)이고 선택 표시는 테두리 + 캡션 앞 `✓` 다.
     ③ **모달을 키웠다** — 560px 한 단에 두 틀을 쌓으니 타일이 눌렸다. 1040px 두 단(좁으면 세로로 쌓임).
        ⚠️ 공용 `.vlab-img-panel` 을 넓히지 않는다(캐릭터·배경 피커가 같이 커진다) — 자기 클래스를 뒀다.
     ④ **열마다 자기 스크롤** — 팝업 하나가 통째로 길어지지 않게. 유료 버튼이 화면 밖으로 밀리지 않는다. */
.vlab-fill-panel {
  max-width: 1040px;
  width: min(1040px, 96vw);
  max-height: 88vh;
  overflow-y: auto;
}
.vlab-fill-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-md); align-items: start; }
@media (max-width: 900px) {
  /* 두 열을 억지로 나란히 두면 타일이 60px 로 줄어 무엇인지 못 고른다 — 좁으면 쌓는다. */
  .vlab-fill-cols { grid-template-columns: 1fr; }
}
.vlab-img-box {
  padding: var(--sp-sm);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  border-radius: var(--r-md, 12px);
  background: rgba(255, 255, 255, 0.02);
  min-width: 0;   /* 격자 열 안에서 내용이 열을 밀어내지 않게 */
}
.vlab-img-box-title { display: flex; align-items: center; gap: var(--sp-xs); }
.vlab-img-box-title .vlab-img-sec-title { margin: 0; font-size: var(--fs-sm, 13px); opacity: 1; }
/* 값 배지 — 무료/유료. 두 틀의 차이가 값에서 가장 크게 갈린다. */
.vlab-img-box-tag {
  font-size: 10px; line-height: 1; padding: 3px 6px; border-radius: 999px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12)); opacity: 0.7;
}
.vlab-img-box-tag.is-paid { border-color: rgba(255, 176, 32, 0.5); color: #ffb020; opacity: 1; }
/* 목록 — 스크롤은 **묶음 전체**가 함께 받는다(격자마다 스크롤이 생기면 어디를 끄는지 모른다). */
.vlab-img-list { max-height: 40vh; overflow-y: auto; padding-right: 2px; }
.vlab-img-list .vlab-img-grid { max-height: none; overflow: visible; }
.vlab-img-filter { margin: var(--sp-xxs) 0 var(--sp-xs); }
/* 묶음 머리 — `<button>` 이라 키보드로도 접힌다(접힌 `+N건` 을 누를 수 있게 한 것과 같은 규율). */
.vlab-img-grp {
  display: block; width: 100%; text-align: left; padding: 6px 2px; margin: 0;
  background: none; border: 0; color: inherit; font-size: 11px; opacity: 0.7; cursor: pointer;
}
.vlab-img-grp:hover { opacity: 1; }
.vlab-img-grp.is-open { opacity: 0.85; }
/* 콘티 칸·내역은 **세로 프레임**(9:16)이다 — 1:1 `cover` 로 자르면 인물이 잘려 무엇인지 못 고른다.
   ⚠️ 이 팝업 안에서만 돌린다. `.vlab-img-grid` 는 캐릭터·배경 피커·화풍 격자와 공용이다. */
.vlab-fill-panel .vlab-img-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); }
.vlab-fill-panel .vlab-img-tile img { aspect-ratio: 3 / 4; object-fit: contain; background: rgba(0, 0, 0, 0.35); }
/* 재료 칩 — 지금 이 칸에 더해 둔 것. 타일의 `✓` 와 같은 상태를 가리킨다. */
.vlab-img-chips { display: flex; flex-wrap: wrap; gap: var(--sp-xxs); margin: var(--sp-xs) 0; }
.vlab-img-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 6px 3px 3px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12)); border-radius: 999px;
  font-size: 11px; max-width: 100%;
}
.vlab-img-chip img { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; aspect-ratio: auto; }
.vlab-img-chip-cap { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 140px; }
.vlab-img-chip-x {
  width: 16px; height: 16px; display: grid; place-items: center; padding: 0;
  background: none; border: 0; color: inherit; opacity: 0.55; cursor: pointer; font-size: 13px; line-height: 1;
}
.vlab-img-chip-x:hover { opacity: 1; }

/* ── 기획안에서 가져오기 팝업 ─────────────────────────────────────────────
   기획안 → 회차·컷 → 패널 3단. `sm-*`(safetyModal) 오버레이를 재사용하고 안쪽만 정의한다.
   ⚠️ 여기서 아무것도 생성되지 않는다 — 담기만 한다. 그래서 주 동작 버튼은 [가져오기] 하나다. */
/* ⚠️⚠️ **스크롤은 본문에만 준다**(2026-08-24 오너 신고: 초안이 끝난 뒤 하단 [담기]를 못 봤다).
   옛 판은 **패널 전체**가 스크롤해서(`overflow-y:auto`) 콘티 9장·칸 76개짜리 목록 아래 끝에 있는 승인
   버튼까지 사람이 내려가야 만났다 — 화면에 안 보이는 버튼은 없는 버튼이다.
   이제 패널은 자기 높이 안에서 안 밀리고(`overflow:hidden`) 본문만 스크롤하므로, 제목·완성 문구·승인 줄이
   **제자리에 남는다**(sticky 흉내가 아니라 레이아웃으로 — 안 스크롤하는 것은 밀리지 않는다).
   ⚠️ `min-height: 0` 이 필요하다: 플렉스 자식은 기본 `min-height:auto` 라 내용만큼 늘어나 스크롤이 안 생긴다. */
.vlab-imp-panel { max-width: 760px; width: min(760px, 94vw); max-height: 88vh; overflow: hidden; }
.vlab-imp-panel > .vlab-imp-body { overflow-y: auto; min-height: 0; }
/* 2단 승인 줄 — 완성 문구(왼쪽) + 버튼(오른쪽). 본문과 선으로 갈라 "여기까지가 머리" 를 말한다. */
.vlab-imp-topbar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-sm);
  padding-bottom: var(--sp-sm);
  border-bottom: 1px solid var(--hairline);
}
.vlab-imp-topacts { display: flex; align-items: center; gap: var(--sp-sm); }
.vlab-imp-done { display: flex; align-items: center; gap: var(--sp-xs); font-size: var(--fs-fine); font-weight: 600; }
/* 완료는 이 캔버스에서 초록이다(`vlab-badge--done` 과 같은 색 — 색의 뜻을 한 곳에 모아 둔다). */
.vlab-imp-done-mark { color: #6ee7a8; }
@media (max-width: 640px) {
  /* 좁은 화면에서는 문구가 줄바꿈되며 버튼을 밀지 않게 위아래로 쌓는다. */
  .vlab-imp-topbar { flex-direction: column; align-items: stretch; }
  .vlab-imp-topacts { justify-content: flex-end; }
}
/* ⚠️ 마진을 주지 않는다 — `.sm-panel` 이 이미 `gap: var(--sp-md)` 로 제목과 본문을 벌린다.
   둘을 더하면 29px 이 되어(실측 2026-08-14) 이 팝업만 다른 리듬을 쓰게 된다. 간격은 한 곳이 정한다. */
.vlab-imp-body { margin-top: 0; }
.vlab-imp-crumb { display: flex; align-items: center; gap: var(--sp-xs); margin-bottom: var(--sp-sm); }
.vlab-imp-crumb-text { font-size: var(--fs-fine); opacity: 0.75; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-imp-hint { font-size: var(--fs-fine); opacity: 0.7; margin-bottom: var(--sp-sm); line-height: 1.5; }
.vlab-imp-empty { font-size: var(--fs-fine); opacity: 0.55; padding: var(--sp-sm) 0; }
/* 장애를 빈 목록으로 그리지 않는다 — 색으로도 구분한다(있는 것을 못 본 채 새로 만들기 시작하지 않게). */
.vlab-imp-error { font-size: var(--fs-fine); padding: var(--sp-sm) 0; color: var(--danger, #ff6b6b); }

.vlab-imp-list { display: grid; gap: var(--sp-xxs); max-height: 56vh; overflow-y: auto; }
.vlab-imp-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-sm);
  padding: var(--sp-sm); border-radius: var(--r-sm); cursor: pointer;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: var(--glass-bg, rgba(255, 255, 255, 0.03));
}
.vlab-imp-row:hover { border-color: var(--accent, #5b8cff); }
.vlab-imp-row-title { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-imp-row-meta { font-size: var(--fs-fine); opacity: 0.55; flex: none; }

.vlab-imp-ep { font-size: var(--fs-fine); font-weight: 600; opacity: 0.85; margin: var(--sp-sm) 0 var(--sp-xxs); }
.vlab-imp-cuts { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: var(--sp-xs); }
.vlab-imp-cut {
  display: grid; gap: 4px; padding: 6px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: var(--glass-bg, rgba(255, 255, 255, 0.03));
}
.vlab-imp-cut:hover { border-color: var(--accent, #5b8cff); }
.vlab-imp-cut img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; border-radius: 6px; display: block; }
.vlab-imp-cut-cap { font-size: 10px; opacity: 0.7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 고른 컷 — ⚠️ 이 규칙이 **아예 없었다**(2026-08-12). `is-on` 클래스는 붙는데 스타일이 없어 골라도 화면이
   그대로였다. hover 와 구분되게 링·배경·체크 배지 셋으로 말한다(테두리 하나로는 hover 와 같아 보인다). */
.vlab-imp-cut.is-on {
  position: relative;
  border-color: var(--accent, #5b8cff);
  box-shadow: 0 0 0 2px var(--accent, #5b8cff) inset;
  background: rgba(91, 140, 255, 0.12);
}
.vlab-imp-cut.is-on .vlab-imp-cut-cap { opacity: 1; color: var(--accent, #5b8cff); font-weight: 600; }
.vlab-imp-cut-check {
  position: absolute; top: 10px; right: 10px;
  width: 20px; height: 20px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; line-height: 1;
  background: var(--accent, #5b8cff); color: #fff;
}
/* 패널 정보가 없는 구 컷 — 고를 것이 없다는 사실을 흐림으로도 말한다(클릭은 위에서 막는다). */
.vlab-imp-cut.is-disabled { opacity: 0.4; cursor: default; }
.vlab-imp-cut.is-disabled:hover { border-color: var(--glass-border, rgba(255, 255, 255, 0.08)); }

/* 3×3 시트 위에 칸을 얹는다 — 이미지에 글자를 그리지 않는다(스토리보드 번호 오버레이와 같은 방식). */
.vlab-imp-sheet { position: relative; border-radius: 8px; overflow: hidden; }
.vlab-imp-sheet img { width: 100%; display: block; }
.vlab-imp-cells {
  position: absolute; inset: 0;
  display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 2px;
}
.vlab-imp-cell {
  position: relative; cursor: pointer; padding: 4px;
  display: flex; flex-direction: column; justify-content: flex-end; gap: 2px;
  border: 2px solid transparent; border-radius: 4px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0) 55%);
  transition: border-color 120ms ease, background 120ms ease;
}
.vlab-imp-cell:hover { border-color: rgba(255, 255, 255, 0.35); }
.vlab-imp-cell.is-on { border-color: var(--accent, #5b8cff); background: rgba(91, 140, 255, 0.22); }
.vlab-imp-cell-no {
  position: absolute; top: 4px; left: 4px;
  font-size: 10px; font-weight: 700; line-height: 1;
  padding: 3px 5px; border-radius: 4px; background: rgba(0, 0, 0, 0.6);
}
.vlab-imp-cell.is-on .vlab-imp-cell-no { background: var(--accent, #5b8cff); }
.vlab-imp-cell-shot,
.vlab-imp-cell-line { font-size: 9px; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-imp-cell-shot { opacity: 0.8; }
.vlab-imp-cell-line { opacity: 0.95; font-style: italic; }

.vlab-imp-sum { display: flex; flex-wrap: wrap; gap: var(--sp-sm); align-items: center; font-size: var(--fs-fine); opacity: 0.75; margin-top: var(--sp-sm); }
.vlab-imp-sum-ok { color: var(--accent, #5b8cff); opacity: 1; }
/* 상한 초과는 경고로 말한다 — 서버는 초과분을 말없이 버리므로 여기서 안 말하면 노드가 조용히 사라진다. */
.vlab-imp-sum-warn { color: var(--warn, #ffb454); opacity: 1; }
.vlab-imp-foot { margin-top: var(--sp-sm); }
/*
 * ⚠️ 이 푸터는 버튼이 **셋까지** 선다: 취소 · (클립 갈래의 무료) `← 클립 목록으로` · 긴 유료 버튼
 *    (`클립 목록 다시 만들기 (유료 · 텍스트 1회 · 약 $0.05)`). `.sm-actions` 는 감싸지 않는 한 줄 flex 이고
 *    모달 폭은 좁은 화면에서 `94vw` 까지 줄어든다(2026-08-29 코워크 CLI 검토가 지목).
 *    좁은 폭에서 라벨이 잘리면 **잘리는 것이 유료 문구**이므로 감싼다 — 이 탭의 규율은 "유료는 라벨이 말한다".
 */
.vlab-imp-foot { flex-wrap: wrap; row-gap: var(--sp-xs); }

@media (max-width: 640px) {
  .vlab-imp-cuts { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); }
  .vlab-imp-cell-shot, .vlab-imp-cell-line { display: none; }
}

/* ── 만들기 팝업(프로젝트·캔버스) ─────────────────────────────────────────
   `sm-*`(safetyModal) 오버레이를 재사용하고 안쪽만 정의한다. */
.vlab-new-panel { max-width: 520px; width: min(520px, 92vw); max-height: 86vh; overflow-y: auto; }
/* ── 라벨의 간격 — **쓰이는 방식이 두 가지라 규칙도 두 가지다** ────────────────
   ① `.vlab-new-row` 안(그리드): 간격은 **gap 하나가** 정한다. 라벨이 제 마진을 들고 있으면 gap 과 더해져
      라벨↔입력 12px · 행↔행 20px 이 되는 **이중 리듬**이 생겼다(2026-08-14 1차 수정의 대상).
   ② **평평한 형제 나열**(콘티 초안·새 프로젝트 팝업): 라벨·힌트·컨트롤이 그냥 줄줄이 놓인다. 여기서는
      gap 이 없으므로 **라벨이 스스로 위 여백을 들어야** 새 묶음의 머리로 읽힌다.
   ⚠️ 1차 수정에서 ①만 보고 마진을 0으로 밀었더니 ②가 깨졌다 — `칸 수` 라벨이 바로 위 입력칸에 달라붙어
      "이 라벨이 위 것의 설명인지 아래 것의 이름인지" 알 수 없게 됐다(오너 지적 2026-08-14, 스크린샷).
      한 클래스를 두 문맥이 쓰면 **문맥별로** 규칙을 줘야 한다. */
.vlab-new-row { display: grid; gap: var(--sp-xxs); margin-bottom: var(--sp-sm); }
/* ⚠️ 부모가 이미 gap 을 가진 컨테이너면 **자기 마진을 더하지 않는다.** `.sm-panel` 은 `gap: var(--sp-md)`
   라, 여기에 행의 margin-bottom 12 가 더해져 묶음 사이가 29px 이 됐다(브라우저 실측 2026-08-14).
   ★ 이 파일에서 같은 형태의 어긋남이 **세 번** 나왔다(라벨↔그리드 gap · Foot↔행 margin · 여기).
     규칙으로 굳힌다: **간격은 언제나 한 곳이 정한다 — 부모에 gap 이 있으면 자식은 마진 0.** */
.sm-panel > .vlab-new-row { margin-bottom: 0; }
.vlab-new-label {
  font-size: var(--fs-fine); font-weight: 600; opacity: 0.85;
  margin: var(--sp-md) 0 var(--sp-xxs);   /* 위=묶음 사이(17) · 아래=자기 컨트롤(4) */
}
/* 첫 줄은 위 여백이 필요 없다(패널이 이미 벌려 놨다). */
.vlab-new-label:first-child { margin-top: 0; }
/* 그리드 안에서는 gap 이 정한다 — 마진을 더하면 다시 이중 리듬이다. */
.vlab-new-row .vlab-new-label { margin: 0; }
/* 힌트는 **바로 위 라벨의 것**이다 — 라벨에 붙고(0), 자기 컨트롤과는 4px 떨어진다. */
.vlab-new-label + .vlab-new-hint { margin-top: 0; }
.vlab-new-hint { font-size: var(--fs-fine); opacity: 0.6; line-height: 1.5; margin: 0 0 var(--sp-xxs); }
/* ★ 팝업 안 세그먼트 컨트롤(기본 화질 — 2026-08-25). 화면 상단 탭과 **같은 모양을 재사용**하되,
   이 자리에서만 두 가지가 달라야 한다:
     ① `justify-self: start` — `.vlab-new-row` 가 grid 라서 아이템이 기본으로 트랙을 **다 늘어난다**.
        그러면 여섯 칩이 왼쪽에 몰린 채 테두리만 폭을 다 먹어, 걷어내려던 그 "폭 다 먹는 셀렉트" 모양이
        된다(오너 지적 2026-08-25). 내용만큼만 서게 한다.
     ② `flex-wrap` — 원래 세그먼트는 안 접힌다(탭 3개짜리라 접힐 일이 없다). 여기는 칩이 여섯이라
        좁은 창에서 팝업 밖으로 밀려난다 — 접히는 편이 잘리는 것보다 낫다. */
.vlab-new-row > .vlab-tabs { justify-self: start; max-width: 100%; flex-wrap: wrap; }
.vlab-new-plans { display: grid; gap: var(--sp-xxs); max-height: 34vh; overflow-y: auto; padding-right: var(--sp-xxs); }
/* ── 매체·화풍 피커(`videoLabLookPicker`) — 세 화면이 나눠 쓰는 **묶음** ────────
   ⚠️ 이 래퍼가 생기면서 `.sm-panel > .vlab-new-row { margin-bottom: 0 }` 가 **더 이상 안 닿는다**(직계
      자식이 아니게 됐다). 그래서 묶음 **안**은 행의 margin-bottom 이, 묶음 **밖**은 부모 gap 이 정한다 —
      의미상으로도 맞다(두 행은 한 덩어리다).
   ★ 그리고 **마지막 요소의 아래 마진을 지운다**: 안 지우면 부모 gap 과 더해져 묶음 아래만 벌어지는
     이중 리듬이 된다. 이 파일에서 같은 형태의 어긋남이 이제 **네 번**이다 — 규칙은 하나다:
     **간격은 언제나 한 곳이 정한다.** */
.vlab-look > :last-child { margin-bottom: 0; }


/* 프로젝트 설정 — 화면 톤 문구(L1). 13축 전문이 들어가므로 팝업 안에서 스크롤되는 큰 칸이다. */
.vlab-set-state { font-size: 11px; opacity: 0.6; margin-bottom: 6px; }
/* 직접 고친 상태는 **눈에 보여야** 한다 — 기본값과 구별되지 않으면 "왜 내 톤이 안 먹지"의 원인이 숨는다. */
.vlab-set-state.is-custom { color: var(--accent, #5b8cff); opacity: 1; }
.vlab-set-style {
  width: 100%;
  min-height: 220px;
  max-height: 40vh;
  resize: vertical;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  border-radius: 10px;
  padding: 10px;
  color: inherit;
  font: inherit;
  font-size: 12px;
  line-height: 1.55;
}
.vlab-set-style:focus { outline: none; border-color: var(--accent, #5b8cff); }
/* 대상 피커 — ① 자유/기획안 토글 → ② 기획안일 때만 열리는 검색 셀렉트. */
.vlab-new-slot { margin-top: var(--sp-xs); }
.vlab-new-err { font-size: var(--fs-fine); margin-top: var(--sp-xxs); color: var(--warn, #ffb454); }

/* 검색 내장 셀렉트(콤보박스). 팝업은 절대배치가 아니라 흐름 안에서 펼친다 —
   부모 패널이 overflow-y:auto 라 absolute 면 경계에서 잘린다(videoLabSearchSelect 머리주석). */
.vlab-cb { display: grid; gap: var(--sp-xxs); }
.vlab-cb-trigger {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
  padding: 9px 11px; border-radius: 8px; font-size: 13px; cursor: pointer; text-align: left;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: var(--glass-bg, rgba(255, 255, 255, 0.03));
  color: inherit; font-family: inherit;
}
.vlab-cb-trigger:hover:not(:disabled) { border-color: var(--accent, #5b8cff); }
.vlab-cb-trigger:focus-visible { outline: none; border-color: var(--accent, #5b8cff); box-shadow: 0 0 0 2px rgba(91, 140, 255, 0.28); }
.vlab-cb-trigger:disabled { cursor: default; opacity: 0.6; }
.vlab-cb-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-cb-value.is-empty { opacity: 0.5; }
.vlab-cb-caret { flex: none; opacity: 0.5; font-size: 11px; }
.vlab-cb-pop {
  border-radius: 8px; padding: 8px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: var(--glass-bg, rgba(255, 255, 255, 0.03));
}
.vlab-cb-search {
  width: 100%; padding: 8px 11px; border-radius: 8px; font-size: 13px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: rgba(0, 0, 0, 0.16); color: inherit;
}
.vlab-cb-search:focus { outline: none; border-color: var(--accent, #5b8cff); }
.vlab-cb-count { font-size: 11px; opacity: 0.55; margin: 6px 2px; }
.vlab-cb-list { display: grid; gap: 4px; max-height: 30vh; overflow-y: auto; padding-right: 2px; }
.vlab-cb-opt {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 10px; border-radius: 7px; cursor: pointer; font-size: 13px;
  border: 1px solid transparent;
}
.vlab-cb-opt.is-active { background: rgba(91, 140, 255, 0.14); }
.vlab-cb-opt.is-on { border-color: var(--accent, #5b8cff); }
.vlab-new-plan {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-sm);
  padding: var(--sp-sm); border-radius: var(--r-sm); cursor: pointer; font-size: 13px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: var(--glass-bg, rgba(255, 255, 255, 0.03));
}
.vlab-new-plan:hover { border-color: var(--accent, #5b8cff); }
.vlab-new-plan.is-on { border-color: var(--accent, #5b8cff); box-shadow: 0 0 0 2px rgba(91, 140, 255, 0.28) inset; }
.vlab-new-plan-meta { font-size: var(--fs-fine); opacity: 0.55; flex: none; }
.vlab-new-aspect { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-xs); }
/* 기획안이 사라진 프로젝트 등 — 사실을 말해 주는 줄. */
.vlab-projects-notice {
  font-size: 12px; line-height: 1.5; padding: 10px 12px; border-radius: 8px; margin-bottom: 12px;
  border: 1px solid var(--warn, #ffb454); color: var(--warn, #ffb454);
  background: rgba(255, 180, 84, 0.08);
}

/* 읽기전용·관리자 보기 안내 — 같은 자리, **다른 톤**이다. 기획안 결손 같은 경고는 주황(위)이지만
   "남의 것이라 못 고친다" 는 사실 전달이라 중립색으로 둔다: 관리자는 이 줄을 매번 보게 되는데,
   경고색으로 두면 늘 무언가 고장 난 화면처럼 읽힌다. 결은 `.ca-readonly-note`(기획안 읽기전용)와 같다. */
.vlab-projects-notice--info {
  border-color: var(--hairline, #2a2a36); color: var(--ink-80, #c8c8d4);
  background: rgba(255, 255, 255, 0.05);
}

/* 실측 길이 배지 — 벤더가 실제로 몇 초를 줬는지. 모르면 아예 안 그린다(0 초라고 말하지 않는다). */
.vlab-dur-tag {
  position: absolute;
  bottom: 6px;
  right: 6px;
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.78);
}

/* ── 캐릭터 라이브러리 ────────────────────────────────────────────────────
   회차 캔버스와 **같은 층**이라 화면을 새로 두지 않고 세그먼트 탭으로 가른다
   (videoLabCharacterPanel 머리주석 — 화면이 늘면 뒤로가기 경로와 키보드 가드 판정도 늘어난다). */
/* 프로젝트 안 화면의 **머리**: 크럼(밖으로 나가는 문) → 단계 칩(안에서의 이동) 순이다(2026-08-27).
   ⚠️ 순서가 뜻이다 — 밖이 위, 안이 아래. 옛 판은 패널마다 크럼을 그려 스텝퍼 **아래**에 놓였다. */
.vlab-tabs-wrap { max-width: 1080px; margin: 0 auto; padding: var(--sp-md) 24px 0; display: grid; gap: var(--sp-xs); }
.vlab-tabs-wrap .vlab-imp-crumb { margin-bottom: 0; }
.vlab-tabs {
  display: inline-flex; gap: 2px; padding: 3px; border-radius: 10px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: rgba(0, 0, 0, 0.2);
}
.vlab-tab {
  padding: 6px 14px; border-radius: 8px; border: 1px solid transparent;
  background: transparent; color: inherit; font: inherit; font-size: 13px; cursor: pointer;
}
.vlab-tab:hover { color: var(--accent, #5b8cff); }
.vlab-tab.is-on { background: var(--glass-bg, rgba(255, 255, 255, 0.06)); border-color: var(--accent, #5b8cff); font-weight: 600; }
/* ── 단계 스텝퍼(2026-08-26 오너 지시) — `① 캐릭터 ▸ ② 배경 ▸ ③ 회차 캔버스` ──
   (넷째 'AI 콘티 초안' 칩과 그 점선(`.is-action`)은 같은 날 오너 지시로 걷혔다 — 캔버스 기능이다)
   ⚠️⚠️ **`.vlab-tabs`/`.vlab-tab` 을 재활용하지 않는다.** 그 둘은 이 화면 전용이 아니라 **공용 세그먼트
      어법**이고 네 곳이 쓴다(`videoLabCreateModal`·`videoLabLookPicker`·`videoLabLocationPicker`).
      여기서 모양을 바꾸면 매체·화풍 피커와 배경 뷰 줄이 함께 변형된다 — 그래서 클래스를 새로 세웠다.
   ⚠️ 좁은 폭에서는 **줄을 바꾼다**(`flex-wrap`). 칩을 찌그러뜨리면 detail("4명 · 시트 3")이 먼저 사라지고
      그 수가 이 스텝퍼의 존재 이유다. */
.vlab-steps { display: flex; align-items: stretch; gap: 4px; flex-wrap: wrap; }
.vlab-step {
  display: grid; gap: 2px; text-align: left;
  min-width: 116px; padding: 7px 12px; border-radius: 10px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  background: rgba(0, 0, 0, 0.2); color: inherit; font: inherit; cursor: pointer;
}
.vlab-step:hover { border-color: var(--accent, #5b8cff); }
/* ★ **지금 단계는 채워서** 말한다(2026-08-27 codex 지적 #3: 옛 판은 테두리만 달라 세 칩이 '필터 칩' 으로
   읽혔다). 테두리 하나로는 흐름의 현재 위치가 안 보인다 — 배경·글자 무게를 함께 바꾼다. */
.vlab-step.is-on {
  /* ⚠️ 0.14 는 너무 어두워 여전히 '파란 테두리' 로만 읽혔다(codex 2차) — 채움이 결정적이어야 한다. */
  background: rgba(91, 140, 255, 0.26);
  border-color: var(--accent, #5b8cff);
  box-shadow: inset 0 0 0 1px rgba(91, 140, 255, 0.35);
}
.vlab-step.is-on .vlab-step-no, .vlab-step.is-on .vlab-step-detail { opacity: 0.9; }
.vlab-step-head { display: flex; align-items: center; gap: 5px; font-size: 13px; }
.vlab-step-no { opacity: 0.75; }
.vlab-step.is-on .vlab-step-name { font-weight: 600; }
.vlab-step-detail { font-size: 11px; opacity: 0.72; }
/* 상태 점 — 색이 뜻을 든다. `unknown`(확인 중)은 색을 쓰지 않는다: 모르는 것을 경고색으로 두면
   목록을 받는 몇 백 ms 동안 화면이 늘 무언가 잘못된 것처럼 읽힌다. */
.vlab-step-dot { font-size: 10px; line-height: 1; }
.vlab-step-dot--done { color: var(--ok, #4ade80); }
.vlab-step-dot--partial { color: var(--warn, #ffb454); }
.vlab-step-dot--todo { opacity: 0.4; }
.vlab-step-dot--unknown { opacity: 0.35; }
.vlab-step-arrow { align-self: center; font-size: 11px; opacity: 0.45; }

/* 단계 발 — "이 단계를 마쳤으면 다음으로". 목록 아래에 붙는다(패널이 자기 host 만 비우므로 안전하다). */
/* 발은 목록과 **선으로 갈라** 둔다 — 안 그러면 마지막 카드에 붙어 그 카드의 일부처럼 읽힌다(2026-08-27). */
.vlab-steps-foot {
  max-width: 1080px; margin: var(--sp-md) auto 0; padding: var(--sp-md) 24px 24px;
  border-top: 1px solid var(--hairline);
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-sm); flex-wrap: wrap;
}
.vlab-steps-foot-gap { flex: 0 0 auto; }
.vlab-steps-foot-next { display: flex; align-items: center; gap: var(--sp-xs); margin-left: auto; }
/* ⚠️ 안내 문구가 너무 흐려 **비활성 도움말**처럼 보였다(codex 2차) — 지침은 읽히라고 있는 글이다. */
.vlab-steps-foot-hint { font-size: 12px; opacity: 0.72; }
/* ★ 마지막 단계에는 오른쪽 버튼이 없다 — 그때는 문장을 **왼쪽 문 옆에** 붙인다(codex 2차: 멀리 떨어진
   두 조각이 서로 무관해 보인다). 판정은 렌더러가 클래스로 준다(`:has()` 로 모양을 추측하지 않는다). */
.vlab-steps-foot--end .vlab-steps-foot-next { margin-left: var(--sp-md); }

/* 탭 아래 본문 — 목록 화면들이 자기 패딩을 들고 있어 여기서는 자리만 만든다. */
.vlab-screen-sub { display: block; }

.vlab-char-actions { display: flex; gap: var(--sp-xs); align-items: center; flex-wrap: wrap; }
/* ★ **그림이 카드의 주인공이다**(오너 지적 2026-08-14: "목록 이미지가 너무 작다").
   예전엔 52px 썸네일이 이름 옆에 붙은 아이콘이었다 — 얼굴을 만들어 놓고도 목록에서 **누가 누구인지
   그림으로 알 수 없었다.** 세로 카드로 바꿔 그림을 위에 크게, 글자는 아래로.

   ★ **2026-08-20: 카드가 가로로 통일됐다**(오너: *"캐릭터 이제 시트만 만드니까 캐릭터 목록에서 정렬이랑
     캐릭터 목록리스트 사이즈 그에맞춰서 변경해도 될듯?"*). 포트레이트 단계를 걷어서 **새 캐릭터의 산출은
     시트(16:9) 하나**다 — 세로 3:4 를 기본으로 두고 시트만 `span 2` 로 넓히던 옛 판은 규칙과 예외가
     뒤집힌 상태였다.
     ⚠️ 그 뒤집힘의 대가가 **정렬**이었다: `auto-fill` 격자에서 두 칸을 먹는 카드는 남은 한 칸에 못 들어가
        다음 줄로 밀리고 **그 자리에 구멍이 남는다**. 카드가 대부분 시트가 된 지금은 줄마다 구멍이 생겨
        문서 순서와 보이는 순서가 어긋난다. 폭을 하나로 만들면 그 어긋남이 원인째 사라진다.
     ⚠️ 그래서 트랙이 180 → 320px 이다. 옛 값은 **세로 카드의 폭**이고(시트는 두 칸 = 약 376px),
        지금은 **한 칸이 곧 카드**다 — 넓은 화면에서 줄당 카드 수는 사실상 그대로다.
   ⚠️⚠️ **넓히는 것은 `--wide` 를 붙인 격자뿐이다.** 이 클래스는 **배경 목록이 함께 쓴다**
        (`videoLabLocationPanel` — "CSS 는 캐릭터 라이브러리의 것을 그대로 쓴다"). 배경 스프레드는
        프로젝트 비율(기본 9:16 **세로**)로 나오므로, 여기서 상자를 16:9 로 돌리면 그쪽 목록이 좌우
        여백만 남은 카드가 된다. 시트가 규칙이 된 것은 **캐릭터**뿐이다. */
.vlab-char-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--sp-sm);
}
.vlab-char-grid--wide { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
.vlab-char-card {
  position: relative;
  /* 세로 카드 — 그림(위) · 이름과 상태(아래). 높이는 그림 비율이 정하므로 min-height 를 두지 않는다
     (예전 84px 은 가로 카드에서 격자를 고르게 하려던 값이고, 지금은 aspect-ratio 가 그 일을 한다). */
  display: flex; flex-direction: column; align-items: stretch; gap: var(--sp-xs);
  padding: var(--sp-xs); border-radius: var(--r-md); cursor: pointer;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
  background: var(--glass-bg, rgba(20, 22, 30, 0.92));
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.vlab-char-card:hover,
.vlab-char-card:focus-visible { border-color: var(--accent, #5b8cff); outline: none; transform: translateY(-1px); }
/* 썸네일 — 없으면 이니셜 판(빈 상자를 그리지 않는다). */
.vlab-char-thumb {
  flex: none; width: 100%; aspect-ratio: 3 / 4; border-radius: var(--r-sm); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.3);
}
/* 캐릭터 목록만 시트 모양이다(위 `--wide` 주석 — 배경 스프레드는 세로다). */
.vlab-char-grid--wide .vlab-char-thumb { aspect-ratio: 16 / 9; }
/* 이름·상태는 그림 아래 한 묶음 — 글자끼리는 붙고(4px), 그림과는 카드 gap(8px)만큼 떨어진다. */
.vlab-char-card .vlab-char-main { padding: 0 var(--sp-xxs) var(--sp-xxs); }
/* 그림이 커진 만큼 이니셜도 커져야 빈 카드가 초라해 보이지 않는다. */
.vlab-char-thumb .vlab-char-initial { font-size: 34px; }
/* ★ **자르기 규칙이 뒤집혔다**(2026-08-20 — 상자가 16:9 가 되면서).
     · 시트 = `cover`. 산출이 **1920×1088**(≈16:9)이라 잘리는 곳이 사실상 없다(1.765 vs 1.778).
     · 그 밖(레거시 포트레이트·후보) = `contain`. 세로 3:4 를 16:9 상자에 `cover` 로 넣으면 **위아래를
       잘라 머리가 날아간다** — 목록에서 얼굴을 못 보게 되는 것이 이 자리의 가장 나쁜 결과다.
       좌우에 여백이 생기지만 **격자 높이는 상자가 정하므로 흐트러지지 않는다.**
   ⚠️ 그래서 기본이 `contain` 이고 시트만 `cover` 다 — 옛 판은 상자가 3:4 여서 정확히 그 반대였다.
      판정은 `characterThumbKind` 한 곳이고, 클래스(`.is-sheet`)와 순서가 한 몸이다. */
.vlab-char-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 세로 상자(배경 목록)에서는 **가로 그림을 자르지 않는다** — 3/4 상자에 `cover` 로 넣으면 가운데만 남는다. */
.vlab-char-thumb.is-sheet img { object-fit: contain; }
/* 시트 모양 상자에서는 규칙이 뒤집힌다(위 주석) — 시트가 `cover`, 세로 그림이 `contain`. */
.vlab-char-grid--wide .vlab-char-thumb img { object-fit: contain; }
.vlab-char-grid--wide .vlab-char-thumb.is-sheet img { object-fit: cover; }
/* 후보만 있는 상태(확정 전) 배지 — 다음 행동이 [이 얼굴로 확정]이라 **주의색**으로 말한다.
   ⚠️ `--running`(파랑)과 갈라야 한다: 파랑은 "기다리면 된다"이고 이 상태는 "네가 눌러야 한다"다. */
.vlab-char-badge--candidate { border-color: var(--warn, #e0b34a); color: var(--warn, #e0b34a); opacity: 1; }
.vlab-char-thumb.is-empty { border: 1px dashed var(--glass-border, rgba(255, 255, 255, 0.18)); }
.vlab-char-initial { font-size: 20px; font-weight: 700; opacity: 0.55; }
.vlab-char-main { min-width: 0; flex: 1 1 auto; display: grid; gap: 4px; }
/* 이름이 카드의 **첫 번째 사실**이다 — 굵게, 상태는 흐리게(타이포 위계). */
.vlab-char-name {
  font-size: 14px; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 상태 배지 — 어디까지 왔는지가 카드의 두 번째 사실이다(정본은 videoLabCharacterModel.CHAR_STATUS_KO).
   ⚠️ **10px 한글은 획이 뭉갠다**(오너 신고 2024/2026-08-20: *"후보고르기 프로필만 나오는게 깨지는거같은데?
      ui가 뱃지로 만드려고한거같은데"*). 기하는 정상이었다(알약 20.7px · 글자 상하 여백 4.5/4.7 — 실측)
      — 문제는 **크기와 대비**였다: 10px + 불투명도 0.62 에서 한글은 가로획이 서브픽셀에 먹혀 흐려진다.
      영문 대문자라면 버티는 크기이지만 한글은 못 버틴다. 그래서 11px · 0.78 · `line-height` 고정으로
      올렸다(알약이라는 **의도**가 보이게 — 배지는 상태를 말하는 장치인데 읽히지 않으면 장식이 된다).
   ⚠️ `align-self: start` 를 못 박는다 — 부모가 그리드라 기본값(stretch)이면 행 높이에 따라 알약이
      세로로 늘어나 "찌그러진 알약"이 된다(지금은 아니지만 메타 줄이 두 줄이 되면 그렇게 된다). */
.vlab-char-badge {
  justify-self: start; align-self: start;
  font-size: 11px; line-height: 1.45; padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.18)); opacity: 0.78;
  white-space: nowrap;
}
.vlab-char-badge--running { border-color: var(--accent, #5b8cff); color: var(--accent, #5b8cff); opacity: 1; }
/* ★ 대기 중은 **생성 중보다 옅다**(2026-08-28) — 같은 목록에 둘이 섞이므로 세기로 갈라야 "지금 도는 것"이
   먼저 읽힌다. 색은 같은 계열을 쓴다(다른 색을 주면 무관한 상태로 읽힌다).
   ⚠️ 이 배지는 문장이 길다(`대기 중 · 앞선 생성이 끝나면 시작`) — 알약이 카드 폭을 넘지 않게
      `white-space` 를 여기서만 푼다(공용 규칙은 nowrap 그대로). */
/* 요청 중 — 대기보다 진하고 생성 중보다 옅다(세기가 곧 "얼마나 확실한가"다: 대기 < 요청 중 < 생성 중). */
.vlab-char-badge--sending {
  border-color: var(--accent, #5b8cff); color: var(--accent, #5b8cff); opacity: 0.8;
  white-space: normal;
}
/* ⚠️ `--cooldown` 은 `--queued` 와 **같은 색**이다(다른 것은 문장뿐 — 둘 다 "아직 안 나갔다"). */
.vlab-char-badge--cooldown,
.vlab-char-badge--queued {
  border-color: var(--accent, #5b8cff); color: var(--accent, #5b8cff); opacity: 0.6;
  white-space: normal;
}
.vlab-char-badge--sheet { border-color: var(--ok, #4ccf8f); color: var(--ok, #4ccf8f); opacity: 1; }
.vlab-char-del { position: absolute; top: 6px; right: 6px; opacity: 0.45; }
.vlab-char-card:hover .vlab-char-del,
.vlab-char-del:focus-visible { opacity: 1; }

/* ── 캐릭터 팝업(단계형) ──────────────────────────────────────────────────
   생성과 상세가 같은 것이다 — 채워진 단계는 결과를 보여 주고 그 자리에 남는다.

   ★ **간격 계약**(오너 지적 2026-08-14: "글자와 버튼, 박스와 글자 간격이 엉성하다").
     간격은 장식이 아니라 **묶음을 드러내는 수단**이다. 값은 `tokens.css` 스케일만 쓰고, 자리마다
     의미를 하나씩 고정한다 — 같은 관계에 다른 값이 붙는 순간 화면이 "정렬이 안 맞아" 보인다.

       라벨 ↔ 컨트롤          `--sp-xxs`(4)   가장 붙어 있어야 하는 관계(둘이 한 덩어리다)
       컨트롤 ↔ 보조설명      `--sp-xxs`(4)   설명은 그 컨트롤의 것이다
       묶음 ↔ 묶음            `--sp-sm`(12)   덩어리 사이
       절 ↔ 절                `--sp-lg`(24)   + 구분선
       버튼 ↔ 버튼            `--sp-xs`(8)
       박스 안쪽 패딩         `--sp-sm`(12)   글자가 테두리에 붙지 않게(9·10·11px 혼용을 걷어냈다)

     ⚠️ 위계는 **차이**로 만든다. 4·12·24 처럼 배수로 벌어져야 눈이 묶음을 읽는다 — 6·8·10px 처럼
        비슷한 값이 섞이면 전부 "그냥 떨어져 있는 것"으로 보인다(그게 엉성함의 정체다). */
/* ★ **이 모달의 주인공은 그림이다**(오너 지시 2026-08-13: "이미지영역이 중심이 되고 나머지는 서브").
   여기서 사용자가 하는 유일한 결정은 **얼굴 고르기**인데, 620px 폭에서 후보 4장은 한 장이 140px 도 안 됐다
   — 결정에 필요한 것(이목구비·반사광·피부 질감)이 화면에 아예 없었던 셈이다.
   그래서 폭을 화면에 맞춰 키우고(최대 1160px), 텍스트 입력·모델 선택은 **폭을 제한해 서브로 물린다.**
   ⚠️ 넓힌 것은 폭뿐이다 — 세로는 88vh 그대로다(모달이 화면을 넘어가면 확정 버튼이 스크롤 밖으로 나간다). */
.vlab-char-panel-modal { max-width: 620px; width: min(620px, 94vw); max-height: 88vh; overflow-y: auto; }
/* ⚠️ 넓히기는 **opt-in 이다**(`--wide`). 이 껍데기를 회차 고르기 팝업이 함께 쓰는데, 거기는 목록 한 줄짜리라
   같이 커지면 빈 공간만 늘어난다(오너 지적 2026-08-14). 넓혀야 하는 것은 **그림이 주인공인 팝업뿐**이다. */
/* ⚠️ 폭이 곧 후보 크기다. 2단이라 **후보 한 장 = (전체폭 − 왼쪽단 380 − 여백 48) ÷ 4** 로 정해진다 —
   1160px 이면 한 장이 겨우 170px 라 넓힌 값이 안 나온다. 1440px 이면 ~240px 로 실제로 얼굴이 보인다
   (오너: "모달이 더 커져도 좋으니"). 화면이 좁으면 96vw 가 받아 준다. */
.vlab-char-panel-modal--wide {
  max-width: 1440px;
  width: min(1440px, 96vw);
  /* ★ **패널 자체는 스크롤하지 않는다**(오너 지시 2026-08-14: "우측 이미지 길어진 영역만 스크롤").
     그림이 길어질 때 패널이 통째로 흐르면 제목과 [닫기]가 화면 밖으로 나가고, 왼쪽 프로필도 함께
     밀려 올라가 **고치는 값과 그 결과를 나란히 보는** 2단의 이유가 사라진다.
     ⚠️ `min-height: 0` 이 핵심이다 — flex 자식의 기본 min-height 는 auto 라, 안쪽에 overflow 를 줘도
        자식이 내용만큼 늘어나 스크롤이 **바깥으로 새어 나간다**(전형적인 "왜 안 잡히지"의 원인). */
  overflow: hidden;
  height: 88vh;
}
.vlab-char-panel-modal--wide .vlab-char-cols { flex: 1 1 auto; min-height: 0; }
.vlab-char-panel-modal--wide .vlab-char-col {
  min-height: 0; max-height: 100%; overflow-y: auto;
  /* ⚠️ `min-width: 0` — grid 자식의 기본 `min-width:auto` 는 **긴 낱말의 최소 폭**을 유지하려 해서,
     안에 긴 토큰(영문 프롬프트·금지어)이 있으면 단이 제 폭보다 벌어지고 그 초과분이 잘려 보인다.
     `minmax(0, …)` 이 트랙에는 걸려 있지만 자식에도 있어야 안쪽 flex 줄까지 줄어든다. */
  min-width: 0;
  /* ⚠️ 스크롤바가 생기고 사라질 때 `clientWidth` 가 흔들리면 안쪽 요소가 그때마다 잘린 것처럼 보인다.
     자리를 미리 비워 폭을 고정한다(오너가 본 "보더가 깨지는" 증상의 유력한 자리). */
  scrollbar-gutter: stable;
  box-sizing: border-box;
}
/* 스크롤바가 구분선·타일 테두리를 덮지 않게 안쪽 여백을 조금 준다. */
.vlab-char-panel-modal--wide .vlab-char-col--art { padding-right: var(--sp-xxs); }
/* 좁은 화면(1단)에서는 되돌린다 — 단이 하나면 나눠 스크롤할 것이 없다. */
/* ⚠️ 왼쪽 단을 520 으로 키웠으므로 **접히는 폭도 함께 올린다**(900 → 1120): 안 올리면 1000px 화면에서
   520 + 그림 단이 억지로 끼어 그림이 오히려 좁아진다(두 값은 한 몸이다). */
@media (max-width: 1120px) {
  .vlab-char-panel-modal--wide { height: auto; max-height: 88vh; overflow-y: auto; }
  .vlab-char-panel-modal--wide .vlab-char-col { max-height: none; overflow: visible; }
}
/* ── 2단 배치 — 왼쪽 근거(텍스트) · 오른쪽 결과(그림) ────────────────────────
   비율이 곧 위계다: 텍스트는 **고정 폭**(읽기 좋은 만큼만), 남는 폭은 전부 그림이 가져간다.
   `minmax(0, …)` 이 필요한 이유 — grid 자식의 기본 min-width 는 auto 라, 안의 이미지 격자가
   줄어들지 못해 오른쪽 단이 모달 밖으로 삐져나간다(가로 스크롤이 생기는 전형적인 원인). */
.vlab-char-panel-modal--wide .vlab-char-cols {
  display: grid;
  /* 🔴 **380 → 520**(2026-08-29 4차 · 코워크 CLI 진단). 이 단은 이제 이름·옵션 폼이 아니라
     **3,600자 프롬프트 편집기**를 담는다 — 380px 에서 내부 여백·스크롤바를 빼면 실제 글 폭이 320~340px
     이고, 등폭 12px 이면 한 줄에 한글 약 27자다. 그러면 3,600자가 시각적으로 100줄이 넘어가고
     `rows=14` 안에서 또 스크롤해야 한다(단 스크롤과 칸 스크롤이 겹친다).
     ⚠️ 오른쪽(그림)은 여전히 더 넓다 — 1440px 에서 520/약 850 이다. */
  grid-template-columns: minmax(0, 520px) minmax(0, 1fr);
  gap: var(--sp-lg);
  /* ⚠️ `start` 가 아니라 `stretch` 다 — start 면 각 단이 **내용 높이**로 줄어들어 `max-height: 100%` 가
     걸릴 곳이 없고, 그러면 안쪽 overflow 가 아무 일도 안 한다(스크롤이 패널로 새어 나간다). */
  align-items: stretch;
}
/* 두 단 사이에 선 하나 — 단이 나뉘었다는 사실을 여백만으로 말하면 넓은 화면에서 흩어져 보인다.
   stretch 라 선이 단 전체 높이로 선다(내용 높이에서 끊기지 않는다). */
.vlab-char-panel-modal--wide .vlab-char-col--art {
  padding-left: var(--sp-lg);
  border-left: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
}
/* ⚠️ 단 안에서는 **첫 절의 위쪽 구분선을 지운다** — `.vlab-char-step` 이 세로 스택 전제로 위에 선을 긋는데,
   2단에서는 그 선이 각 단의 머리에 떠서 제목 위에 줄이 하나 얹힌 것처럼 보인다. */
.vlab-char-panel-modal--wide .vlab-char-col > .vlab-char-step:first-child {
  border-top: 0;
  padding-top: 0;
  margin-top: 0;
}
/* 좁은 화면에서는 1단으로 되돌린다 — 그때는 위에서 아래가 곧 ①②③ 단계 순서다. */
@media (max-width: 1120px) {
  .vlab-char-panel-modal--wide .vlab-char-cols { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .vlab-char-panel-modal--wide .vlab-char-col--art { padding-left: 0; border-left: 0; }
}
/* 본문(이름·설명·고급)은 읽기 좋은 폭에서 멈춘다 — 폭을 키운 값이 **그림에만** 가게 한다.
   ⚠️ 이력 스트립(`.vlab-char-hist`)·큰 미리보기(`.vlab-char-shot`)에는 걸지 않는다. 그 둘이 넓어지라고 키운 폭이다. */
.vlab-char-panel-modal--wide .vlab-title,
.vlab-char-panel-modal--wide .vlab-img-prompt,
.vlab-char-panel-modal--wide .vlab-char-adv { max-width: 100%; }
/* ⚠️ **720px 상한은 1단 시절의 잔재였다** — 2단에서는 단 자체가 520px 라 이 값이 아무 일도 안 하면서
   "폭을 재는 규칙이 둘" 이라는 오해만 남긴다. 단 폭이 곧 본문 폭이다. */
/* 단계 사이 — 구분선 하나 + 넉넉한 여백. 예전에는 위아래 여백이 비슷해 **선이 어느 단계에 속한 것인지**
   읽히지 않았다(오너 지적 2026-08-13 정렬 폴리시). 선 위를 넓게, 선 아래를 좁게 둬서 선이 '다음 단계의
   머리'로 읽힌다.
   ⚠️ **간격은 토큰 스케일(4/8/12/17/24)만 쓴다**(`tokens.css`). 예전 값은 22·16·14·10·6·5px 처럼
      스케일 밖 숫자여서, 같은 위계의 두 요소가 2~4px 씩 어긋나 "정렬이 안 맞아 보이는" 상태였다
      (오너 지적 2026-08-14). 눈에 띄는 한 곳이 아니라 **리듬 전체**가 원인이라 값을 한꺼번에 맞춘다. */
.vlab-char-step {
  margin-top: var(--sp-lg);
  padding-top: var(--sp-md);
  border-top: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
}
.vlab-char-step:first-of-type { border-top: 0; padding-top: 0; margin-top: var(--sp-sm); }
.vlab-char-step-title { margin: 0 0 var(--sp-xxs); font-size: 13px; font-weight: 700; }
/* 힌트는 12px(`--fs-fine`) 하나로 통일 — 11px·12px 두 벌이 섞여 있어 같은 위계가 다른 크기로 보였다. */
.vlab-char-step-hint { margin: 0 0 var(--sp-sm); font-size: var(--fs-fine); opacity: 0.62; line-height: 1.5; }
.vlab-char-body { display: grid; gap: var(--sp-sm); }
.vlab-char-row { display: flex; gap: var(--sp-xs); align-items: center; flex-wrap: wrap; margin-top: var(--sp-sm); }
/*
 * ★★ 외형 설정 12칸(2026-08-29 오너 지시 — 프롬프트 패널을 설명 절로 합치면서 값을 **고칠 수 있게** 했다).
 *   ⚠️ 라벨을 왼쪽 고정 폭으로 두는 이유: 12줄이 라벨 길이에 따라 들쭉날쭉하면 어느 칸이 비었는지가
 *      한눈에 안 보인다(빈 칸을 찾는 것이 이 화면의 주된 동작이다).
 *   ⚠️ 좁은 폭에서는 위아래로 쌓는다 — 라벨이 잘리면 무슨 값을 적는 칸인지 알 수 없다.
 */
.vlab-char-look { display: flex; flex-direction: column; gap: var(--sp-xs); }
.vlab-char-look-row { display: flex; gap: var(--sp-xs); align-items: flex-start; }
.vlab-char-look-label {
  flex: 0 0 84px; padding-top: 4px;
  font-size: var(--fs-fine); opacity: 0.75; line-height: 1.4;
}
/* ⚠️ 테두리·배경은 `.vlab-img-prompt` 와 같은 근거를 쓴다 — 같은 절 안에서 입력칸이 두 모양이면
 *    사용자가 "이건 입력이 아닌가?" 를 되묻는다(고칠 수 있다는 것이 이 개편의 요점이다). */
.vlab-char-look-input {
  flex: 1 1 auto; min-width: 0; resize: vertical;
  padding: 6px 8px; border-radius: 8px;
  border: 1px solid var(--line, #2a2f3a); background: var(--surface-2, #12151c); color: inherit;
  font: inherit; font-size: var(--fs-fine); line-height: 1.5;
}
.vlab-char-look-input:focus { outline: none; border-color: var(--accent, #5b8cff); }
@media (max-width: 640px) {
  .vlab-char-look-row { flex-direction: column; gap: 2px; }
  .vlab-char-look-label { flex: none; padding-top: 0; }
}
/* 프롬프트 전문 편집칸(2026-08-29 2차) — 이 글이 그대로 벤더로 간다.
 * ⚠️ 등폭 글꼴이어야 `[Image #1]` 같은 기계 라벨과 줄 구조가 눈에 들어온다(읽기 전용 `<pre>` 였을 때의
 *    그 성질을 편집칸에서도 유지한다 — 사람이 라벨을 지우면 시트가 시트가 아니게 되므로 보여야 한다). */
/* 프롬프트 절은 ① 의 **본체**다(2026-08-29 4차 — `<details>` 를 걷었다).
 * ⚠️ 접힘이 아니므로 여백을 접힘 요소(`.vlab-char-adv`)와 같은 값으로 맞춘다 — 안 맞추면 이 절만
 *    붕 떠 보이고, 그러면 다시 "따로 있는 패널" 처럼 읽힌다(걷어낸 이유가 그것이었다). */
.vlab-char-prompt-box { margin-top: var(--sp-sm); }
/* 절 머리 — 제목(왼쪽)과 [다시 불러오기](오른쪽)가 **한 줄**이다(2026-08-29 4차).
 * ⚠️ 버튼을 본문 끝에 두면 금지어·영어 변환본 뒤에 홀로 떠서 어디에 속한 것인지 안 읽힌다(오너 지적).
 * ⚠️ 좁은 폭에서는 `.vlab-char-row` 의 `flex-wrap` 이 받아 아래로 내려간다 — 잘리지 않는다. */
/* 한 줄짜리 입력이 `.vlab-img-prompt`(textarea 전제)를 함께 쓸 때 — 세로 리사이즈 손잡이가 없고
 * 높이가 한 줄이어야 한다. ⚠️ 폭은 `.vlab-img-prompt` 의 `width:100%` 를 그대로 쓴다(둘이 같은 폭이어야
 * 어긋나 보이지 않는다 — 그것이 이 클래스를 만든 이유다). */
.vlab-char-oneline { resize: none; height: auto; }
.vlab-char-prompt-head { justify-content: space-between; margin-top: 0; margin-bottom: var(--sp-xs); }
.vlab-char-prompt-head > .vlab-new-label { margin: 0; }
.vlab-char-prompt-edit {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px; line-height: 1.6; white-space: pre-wrap;
}
.vlab-char-adv { margin-top: var(--sp-xs); }
.vlab-char-adv summary { font-size: var(--fs-fine); opacity: 0.7; cursor: pointer; margin-bottom: var(--sp-xs); }
.vlab-char-adv .vlab-title,
.vlab-char-adv .vlab-img-prompt { margin-bottom: var(--sp-xs); }

/* ── 행동 묶음(추가 지시 + 모델 + 생성 버튼) ─────────────────────────────
   결과(`.vlab-char-body`)와 행동을 갈라 둔다 — 입력칸이 매 렌더 다시 만들어지면 타이핑이 날아가서
   Foot 은 통째로 안 지우고 버튼 줄만 비운다(모달 JS 주석). 시각적으로도 "여기부터 조작"이 읽힌다.

   ⚠️ **간격은 Foot 이 grid gap 하나로 정한다.** 자식들의 margin 을 섞으면 반드시 어긋난다 — 실제로
      `:first-child { margin-top: 0 }` 로 겹침을 막았더니 **힌트와 버튼 사이가 0px** 이 됐고(브라우저 실측
      2026-08-14), 그 순간 "이번 생성에만 적용됩니다" 가 위(입력칸)가 아니라 **아래(버튼)** 의 설명처럼
      읽혔다(오너 지적 — "저 문구가 어딜 해당하는건지 모르겠다"). 지금은 4px(입력↔설명) vs 12px(묶음↔묶음)
      으로 **3배 차이**가 나서 눈이 소속을 읽는다. */
.vlab-char-foot { display: grid; gap: var(--sp-sm); margin-top: var(--sp-md); }
.vlab-char-foot .vlab-char-row { margin-top: 0; }

/* ★ **성격이 다르면 간격도 다르다**(오너 지시 2026-08-14: "성격별로 마진을 줘서 묶음이 드러나게").
   입력 필드끼리는 12px 로 나란히 서지만, **조작(버튼 줄)은 그보다 멀어야** "여기부터는 누르는 것"이 읽힌다.
   ⚠️ 전부 12px 로 균일하면 리듬은 맞아도 **위계가 없다** — 실측에서 레퍼런스·고급·버튼이 모두 같은
      12px 라 세 묶음이 한 덩어리로 보였다(브라우저 실측 2026-08-14). 17px(--sp-md)로 한 단 벌린다. */
.vlab-char-step > .vlab-char-row { margin-top: var(--sp-md); }
/* 레퍼런스 칩 아래 줄은 예외 — 그 버튼은 바로 위 칩 묶음의 일부다(멀어지면 소속을 잃는다). */
.vlab-char-refs .vlab-char-row { margin-top: var(--sp-xxs); }
/* 인라인 안내(충돌·유효성·저장 결과) — 토스트로 내보내면 팝업 안의 맥락을 잃는다. 비어 있으면 자리도 없다.
   ⚠️ **버튼 줄과 같은 왼쪽 끝에서 시작하는 한 줄 배너**다: 예전에는 색만 있는 글줄이라 버튼과 어긋나
      떠 보였다(오너 지적 2026-08-13). 배경·테두리가 그 문구를 버튼 줄에 묶어 준다.
   ⚠️ **성공을 경고색으로 말하지 않는다**(`is-ok`) — "저장했습니다"가 주황이면 사용자는 무엇이 잘못됐는지
      찾는다. 색이 곧 뜻이다. */
.vlab-char-note {
  font-size: var(--fs-fine); line-height: 1.5; margin-top: var(--sp-sm);
  padding: var(--sp-xs) var(--sp-sm); border-radius: var(--r-sm);
  color: var(--warn, #ffb454);
  border: 1px solid rgba(255, 180, 84, 0.35);
  background: rgba(255, 180, 84, 0.08);
}
.vlab-char-note.is-ok {
  color: var(--ok, #4ccf8f);
  border-color: rgba(76, 207, 143, 0.35);
  background: rgba(76, 207, 143, 0.08);
}
.vlab-char-note:empty { display: none; }
.vlab-char-shot { position: relative; border-radius: var(--r-md); overflow: hidden; background: rgba(0, 0, 0, 0.25); }
/* 큰 미리보기가 **무엇인지** 말하는 배지 — "확정" 과 "3번째 생성"을 구분하지 못하면 사용자는 자기가 지금
   확정본을 보는지 시험작을 보는지 모른 채 판단한다(그 상태에서 [확정] 버튼의 뜻도 흐려진다). */
.vlab-char-shot-badge {
  position: absolute;
  top: var(--sp-xs); left: var(--sp-xs);
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.6);
  font-size: 11px; font-weight: 600; line-height: 1.5;
}
/* ⚠️ 42vh/30vh 는 **620px 폭 모달 시절의 값**이었다 — 폭을 1160px 로 키운 지금 그대로 두면
   그림만 작게 남는다(폭을 키운 이유가 사라진다). 확정본은 세로 인물사진, 시트는 가로 3분할이라
   상한이 갈린다. */
.vlab-char-shot img { width: 100%; max-height: 58vh; object-fit: contain; display: block; }
.vlab-char-shot--wide img { max-height: 44vh; }
.vlab-char-draft-main { min-width: 0; display: grid; gap: 2px; }

/* ── 업로드 레퍼런스 칩(오너 지시 ④) ──────────────────────────────────────
   ① 프로필 안에 산다 — 정본은 설명이고 이건 분위기 자료다. 그래서 후보 격자(`.vlab-img-grid`)처럼 크게
   벌리지 않고 **작은 칩 두 개**로 둔다(최대 2장이라 격자가 필요하지 않다).
   ⚠️ 삭제 버튼은 칩 **위에** 얹는다 — 옆에 두면 칩 두 개가 한 줄을 다 먹어 ① 이 세로로 길어진다. */
.vlab-char-refs { display: grid; gap: var(--sp-xxs); }
.vlab-char-ref-chips { display: flex; gap: var(--sp-xs); flex-wrap: wrap; }
.vlab-char-ref-chip {
  position: relative; width: 72px;
  border-radius: 8px; overflow: hidden;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12));
  background: rgba(0, 0, 0, 0.25);
}
.vlab-char-ref-chip img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; }
.vlab-char-ref-chip .vlab-img-empty { padding: var(--sp-xxs); font-size: 10px; text-align: center; }
.vlab-char-ref-del {
  position: absolute; top: 2px; right: 2px;
  width: 18px; height: 18px; line-height: 1; padding: 0;
  border-radius: 50%; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(0, 0, 0, 0.6); color: #fff;
  font-size: 12px;
}
.vlab-char-ref-del:hover:not(:disabled) { background: rgba(220, 68, 68, 0.85); }
.vlab-char-ref-del:disabled { opacity: 0.4; cursor: default; }

/* 이력 타일의 **지우기**(오너 지시 2026-08-18 2차 — 지나간 생성 삭제).
   ⚠️ ⤢(원본 크게 보기)는 **오른쪽 위**라 겹치지 않게 **왼쪽 위**에 둔다. 두 버튼이 같은 자리에 있으면
      지우려다 확대가 열린다(그리고 그 반대도 — 되돌릴 수 없는 쪽이 더 비싸다).
   ⚠️ 평소엔 옅게 두고 hover·focus 에서 또렷해진다: 타일 격자에서 빨간 ×가 상시로 보이면 화면이
      "지우라"고 재촉하는 것처럼 읽힌다. */
.vlab-char-hist-del {
  /* ⚠️ **왼쪽 위는 번호 배지 자리다**(`.vlab-char-hist-no`) — 거기 두면 ×가 배지 아래로 깔려 안 보인다
     (2026-08-18 스테이징에서 실제로 그랬다: DOM 에는 있는데 화면에서 못 찾는다). 아래 오른쪽으로 옮긴다.
     ⤢(원본 보기)는 오른쪽 **위**라 여전히 서로 안 겹친다. */
  position: absolute; bottom: 4px; right: 4px;
  width: 20px; height: 20px; line-height: 1; padding: 0;
  border-radius: 50%; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(0, 0, 0, 0.55); color: #fff;
  font-size: 12px; opacity: 0.55; transition: opacity 120ms ease, background 120ms ease;
}
.vlab-char-hist-tile:hover .vlab-char-hist-del,
.vlab-char-hist-del:focus-visible { opacity: 1; }
.vlab-char-hist-del:hover:not(:disabled) { background: rgba(220, 68, 68, 0.9); }
.vlab-char-hist-del:disabled { opacity: 0.25; cursor: default; }
/* 이 줄만 위 여백을 줄인다 — `.vlab-char-row` 기본 12px 은 칩 바로 아래에서 과하다. */
.vlab-char-refs .vlab-char-row { margin-top: var(--sp-xxs); }
.vlab-char-refs .vlab-char-step-hint { margin-bottom: 0; }

/* ── 매체 · 화풍 피커(2026-08-18) ──────────────────────────────────────────
   신규 클래스를 최소로 둔다 — 세그먼트는 캔버스 탭(`.vlab-tabs`/`.vlab-tab`), 격자는 이미지 고르기
   (`.vlab-img-grid`/`.vlab-img-tile`)의 것을 그대로 쓴다. 같은 동작(하나 고르기)에 두 번째 어법을
   만들지 않는다. 여기 있는 것은 **그 둘이 팝업 안에서 어긋나는 자리**뿐이다. */
/* 세그먼트는 `.vlab-new-row` 그리드의 한 칸이라 기본값이면 폭 전체로 늘어난다(탭 두 개가 팝업을 가로지른다). */
.vlab-char-media { justify-self: start; }
/* 칸이 6개인데 폭은 왼쪽 단(≈360px)뿐이다 — 96px 격자로는 두 줄에 2·2·2 로 흩어진다. 84px 이면 3열이 서고
   썸네일이 화풍을 알아볼 만큼은 남는다.
   ⚠️ **그 판단이 틀렸다**(오너 지적 2026-08-20: *"화풍 예시가 너무 작아서 확인하기가 힘들어"*). 이 칸이
      답해야 하는 질문은 "무엇이 있나"가 아니라 **"선의 질·채색이 어떻게 다른가"** 인데, 84px 정사각에서는
      그 차이가 화면에 아예 없다. 두 가지를 함께 고친다:
        ① **정사각 crop 을 없앤다** — 예시는 2:3 인물화(341×512)라 `.vlab-img-tile img` 의 `1/1` +
           `cover` 가 **머리와 상체를 잘라냈다**. 화풍의 근거(머리 가닥·눈 광택·옷 주름)가 잘리는 쪽에 있었다.
        ② **칸을 키운다**(84 → 104px, 3열 유지). 정사각 84px(7,056px²) → 2:3 114×171(≈19,500px²) ≈ 2.8배.
      그래도 96px 칸에서 붓질을 판정할 수는 없으므로 **원본 확대(⤢)** 를 함께 붙였다(격자는 좌표, 판정은
      라이트박스에서 — 이력 스트립과 같은 규약이다). 3열을 2열로 벌리지 않은 이유: 일곱 칸이 네 줄로
      흩어지면 **서로 견주는 화면**이 아니게 된다(그 비교를 이미 ⤢ 가 맡는다). */
.vlab-char-style-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); }
/* 예시 파일은 전부 2:3 세로다 — 비율을 원본에 맞추면 잘리는 곳이 없다(`object-fit: cover` 는 그대로 둔다:
   비율이 맞으면 crop 이 일어나지 않고, 나중에 다른 비율이 섞여 들어와도 칸 격자는 흐트러지지 않는다). */
.vlab-char-style-tile img { aspect-ratio: 2 / 3; }
/* 잠긴 칸은 **남아 있어야 한다**(숨기지 않는다 — 왜 못 쓰는지가 보여야 한다). 흐리게 + 커서로 말한다. */
.vlab-char-style-tile:disabled { opacity: 0.42; cursor: not-allowed; }
/* 격자 바로 아래 붙는 안내 — 이 줄은 그 격자의 설명이라 절 하단 여백(12px)까지 벌 이유가 없다. */
.vlab-char-style-hint { margin-bottom: 0; }
.vlab-char-style-hint.is-warn { color: var(--warn, #e0b34a); opacity: 0.9; }
/* 카드의 매체·화풍 줄 — 메타 줄 클래스를 재사용하되, 여기서는 간격을 `.vlab-char-main` 의 gap 이 정한다
   (자기 margin 을 들고 오면 4px + 4px 로 이름·배지·메타의 간격이 어긋난다 — `.vlab-new-row` 와 같은 규율). */
.vlab-char-main .vlab-proj-meta { margin-top: 0; }

/* ── 깨진 이미지 자리표시자 ────────────────────────────────────────────────
   URL 이 죽으면(목 모드 · 서명 URL 1시간 만료 · 파일 삭제) 브라우저 기본 깨진 아이콘 + alt 텍스트가
   그대로 노출됐다(오너 신고 2026-08-13). 그림이 **없는** 상태(`.vlab-char-thumb.is-empty` + 이니셜)와
   **같은 언어**로 말한다 — 사용자가 화면을 두 번 배우지 않게. 컴포넌트는 `videoLabImageFallback.js` 하나.
   ⚠️ 크기는 부모가 정한다: 자리표시자는 100%를 채우고, 부모가 높이를 안 주는 자리에만 비율을 쓴다
      (그러지 않으면 격자 칸이 접히거나 팝업의 큰 그림 자리가 한 줄로 눌린다). */
.vlab-imgfb {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border-radius: 8px;
  border: 1px dashed var(--glass-border, rgba(255, 255, 255, 0.18));
  background: rgba(0, 0, 0, 0.3);
  color: inherit;
  text-align: center;
  padding: 8px;
  box-sizing: border-box;
}
.vlab-imgfb-mark { display: flex; align-items: center; justify-content: center; opacity: 0.5; }
.vlab-imgfb-text { font-size: 10.5px; line-height: 1.35; opacity: 0.55; }
/* 52px 고정 썸네일(캐릭터 카드) · 콘티 칸 — 글자를 넣을 자리가 없어 이니셜/아이콘만 든다(문구는 title). */
.vlab-imgfb--thumb {
  height: 100%;
  gap: 0;
  padding: 2px;
  border-radius: inherit;
}
.vlab-imgfb--tile { aspect-ratio: 1 / 1; }
.vlab-imgfb--shot { min-height: 140px; }
/* 노드 카드의 미디어 상자 — 비어 있을 때의 상자(`.vlab-media--empty`)와 같은 비율·높이를 쓴다. */
.vlab-imgfb--media { aspect-ratio: 9 / 16; max-height: 220px; }
.vlab-imgfb .vlab-char-initial { font-size: 18px; }

/* ── 캐릭터 노드 카드 ─────────────────────────────────────────────────────
   참조물이라 [생성]·모델 셀렉트가 없다 — 카드가 말하는 것은 "누구인가" 하나다. */
.vlab-char-node-name { font-size: 13px; font-weight: 700; }
/* ★ 자산 노드(인물·배경)의 이름 줄 — 이름 왼쪽, [수정] 버튼 오른쪽(2026-08-27 오너 지시).
   🔴 첫 판은 **이름 자체를 버튼**으로 만들고 툴팁만 달았다 — 오너가 못 찾았다. 글처럼 보이는 문은
      문이 아니다. 지금은 늘 보이는 버튼이 그 일을 한다.
   ⚠️ 카드 전체가 아니라 이 버튼만 문이다: 카드는 끌고 고르는 물건이라 놓는 순간 팝업이 뜨면 옮길 수 없다
      (`graphNodeDrag` 가 `button` 을 끌기에서 제외한다). */
/* 자산 노드 머리 — 이름 한 줄 + 행동 한 줄(2026-08-27). 옛 판은 한 줄에 나란히였는데, 버튼이 둘이
   되며 라벨을 온전히 적으면 이름이 100px 로 짜부라졌다(카드 폭 280px · 콘텐츠 256px). */
.vlab-asset-head { display: flex; flex-direction: column; gap: 6px; }
.vlab-asset-head .vlab-char-node-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 행동은 오른쪽 정렬 — 이름이 왼쪽 기준선을 갖고, 행동은 카드 끝에 모인다. 좁으면 접힌다. */
.vlab-asset-acts { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px; }
.vlab-asset-edit, .vlab-asset-swap { flex: 0 0 auto; }
.vlab-char-node-stage { font-size: 10px; opacity: 0.55; }

/* ── AI 콘티 초안 팝업 ────────────────────────────────────────────────────
   2단(재료 → 검토). 오버레이·패널·요약줄은 `sm-*`·`vlab-imp-*` 를 그대로 쓴다 — 여기 있는 것은
   **칸 카드와 인물 칩**뿐이다(신규 오버레이 CSS 0 규약). */
.vlab-draft-source {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  font: inherit;
  font-size: 12px;
  line-height: 1.6;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12));
  background: rgba(255, 255, 255, 0.04);
  color: inherit;
}
.vlab-draft-source:focus { outline: none; border-color: var(--accent, #5b8cff); }
.vlab-draft-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 2px; }
/* 선택 요약 — 접힘과 무관하게 "몇 명 골랐나" 를 든다(2026-08-27 codex 지적 #10). */
.vlab-draft-castcount { font-size: var(--fs-fine); opacity: 0.72; }
/* 긴 명단은 접는다(2026-08-27) — 32명 프로젝트에서 칩이 팝업 절반을 덮어 그 아래의 재료 경고와 유료
   버튼이 화면 밖으로 밀렸다. ⚠️ 줄이는 것은 **보이는 높이**뿐이다: 칩은 전부 DOM 에 남아야 제안이
   제자리에서 켤 수 있다(`applyCastSuggestion` 이 노드를 직접 만진다).
   아래를 흐리게 해 "더 있다" 를 모양으로도 말한다 — 갑자기 잘리면 그건 결함처럼 보인다. */
/* ⚠️ **정확히 두 줄에서 끊는다**(2026-08-27 codex 3차 #2). 옛 84px 은 세 번째 줄을 반쯤 남겨
   "잘린 것" 처럼 보였다 — 페이드도 마지막 12px 안에서만 진다(윗줄까지 흐리면 읽히던 이름이 사라진다).
   칩 한 줄 = 패딩 5+5 + 글줄 ≈ 27px, 줄 간격 6px → 두 줄 60px. */
.vlab-draft-chips.is-folded {
  max-height: 60px; overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 12px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 12px), transparent);
}
/* 요약과 토글은 **한 줄**이다 — 흐린 두 줄이 쌓이면 둘 다 안 읽힌다(같은 지적). */
.vlab-draft-castfoot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-sm); margin-top: var(--sp-xs);
}
.vlab-draft-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 5px 10px;
  border-radius: 999px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12));
  background: transparent;
  color: inherit;
  font-size: 11px;
  cursor: pointer;
}
.vlab-draft-chip:hover { border-color: var(--accent, #5b8cff); }
/* 고른 상태를 **테두리 하나로만** 말하지 않는다(배경 틴트 + 색 — 컷 타일에서 겪은 그 결함). */
.vlab-draft-chip.is-on {
  border-color: var(--accent, #5b8cff);
  background: rgba(91, 140, 255, 0.18);
  color: var(--accent, #5b8cff);
  font-weight: 600;
}
/* ⚠️ **이름이 주인공이다**(2026-08-27 codex 3차 #6): 서른 개가 깔리면 이름과 상태가 같은 무게로 섞여
   누가 고를 수 있는 사람인지 안 읽힌다. 상태는 뒤따르는 작은 글씨로 물린다. */
.vlab-draft-chip-meta { font-size: 10px; opacity: 0.5; }
/* 시트 없는 인물의 칩 메타(2026-08-26) — 이 한 낱말이 "그 얼굴은 참조 없이 그려진다" 는 뜻이라
   흐린 회색으로 두면 옆 낱말과 구별되지 않는다(그게 옛 판이었다). 경고색이지만 칩 자체는 고를 수
   있다 — 막지 않는 것이 이 자리의 결정이다. */
.vlab-draft-chip-meta.is-missing { color: var(--warn, #ffb454); opacity: 0.95; }
/* 1단 재료 경고 슬롯 — 비어 있을 때는 자리를 먹지 않는다(`:empty`). 슬롯을 미리 만들어 두는 것은
   칩 토글에서 **제자리 갱신**을 하기 위해서다(본문 재렌더는 캐럿을 죽인다). */
.vlab-draft-castwarn:not(:empty) { margin-top: var(--sp-sm); }

/* ── 재료 준비 카드(2026-08-27 오너 지적으로 다시 짰다) ─────────────────────────
   *"가독성이 안좋은거같아 영역분리나 ui등 고민좀 해서 … 문구들도 너무 나열해서 막 써둔 느낌"*
   옛 판은 **글머리표 목록 + 설명 문단 + 버튼 줄**을 세로로 쌓았다 — 사실·설명·다음 행동이 서로
   다른 층에 흩어져, "무엇이 없나 → 그럼 어디로 가나" 가 문장을 다 읽어야 나왔다.
   지금은 **행 하나가 그 질문에 통째로 답한다**: 왼쪽 뱃지=어느 단계 · 가운데=사실과 설명 ·
   오른쪽=그 단계로 가는 문.
   ⚠️⚠️ **아래 여백을 반드시 남긴다** — 팝업 본문(`.vlab-imp-body`)은 `overflow-y: auto` 라 한 축이
      visible 이 아니면 다른 축도 잘린다. 카드가 바닥에 붙으면 **버튼 테두리가 잘린다**(오너 스크린샷). */
.vlab-prep {
  display: grid; gap: var(--sp-sm);
  margin: 0 0 var(--sp-xs);
  padding: var(--sp-sm) var(--sp-md) var(--sp-md);
  border-radius: var(--r-md, 10px);
  border: 1px solid var(--warn, #ffb454);
  background: rgba(255, 180, 84, 0.06);
}
.vlab-prep-head { display: flex; align-items: baseline; gap: var(--sp-xs); flex-wrap: wrap; }
.vlab-prep-icon { color: var(--warn, #ffb454); }
.vlab-prep-title { font-size: var(--fs-fine); font-weight: 600; color: var(--warn, #ffb454); }
/* 부제는 **행동 지침**이라 경고색을 쓰지 않는다 — 경고는 제목 하나면 된다(두 줄이 다 빨가면 둘 다 안 읽힌다). */
.vlab-prep-sub { font-size: var(--fs-fine); opacity: 0.75; }
.vlab-prep-rows { display: grid; gap: var(--sp-xxs); margin: 0; padding: 0; list-style: none; }
.vlab-prep-row {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--sp-sm);
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--r-sm, 8px);
  background: rgba(255, 255, 255, 0.04);
}
/* 단계 뱃지 — 스텝퍼와 같은 낱말(`캐릭터`·`배경`)을 쓴다. 사용자가 두 어법을 배우지 않게. */
.vlab-prep-step {
  font-size: var(--fs-fine); opacity: 0.8; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.12));
}
.vlab-prep-text { display: grid; gap: 2px; min-width: 0; }
.vlab-prep-fact { font-size: var(--fs-fine); }
.vlab-prep-note { font-size: var(--fs-fine); opacity: 0.72; line-height: 1.55; }
/* 좁으면 문을 아래로 내린다 — 셋을 한 줄에 욱여넣으면 사실 줄이 먼저 잘린다(그 줄이 이 카드의 이유다). */
@media (max-width: 620px) {
  .vlab-prep-row { grid-template-columns: auto 1fr; }
  .vlab-prep-row > .btn { grid-column: 2; justify-self: start; }
}
/* ★ 1단의 **절 사이**를 벌린다(2026-08-27 codex 지적 #8: *"연속된 폼 덤프라 창작 입력 · 설정 · 검증 ·
   결제가 구분되지 않는다"*). 라벨이 절의 머리이므로 라벨 위 여백이 곧 절 사이의 리듬이다. */
.vlab-imp-body > .vlab-new-label { margin-top: var(--sp-lg, 24px); }
.vlab-imp-body > .vlab-new-label:first-child { margin-top: 0; }
/* ⚠️ **보충 지시는 주 입력이 아니다**(#9) — 옛 판은 둘이 거의 같은 크기라 첫 결정이 묽어졌다.
   원문 칸은 그대로 두고 이 칸만 낮춘다. */
.vlab-draft-source--extra { min-height: 56px; }
.vlab-draft-cells { display: grid; gap: 10px; margin-top: 10px; }
.vlab-draft-cell {
  display: grid;
  /* ⚠️ 6px 이던 것을 넓혔다 — 경고 줄이 뜨면 프롬프트·경고·캡션 입력 셋이 다닥다닥 붙었다
     (오너 지적 2026-08-13). 경고는 **위의 프롬프트에 속한 것**이라 그쪽에 붙여 둔다(아래 규칙). */
  gap: 9px;
  padding: 10px;
  border-radius: 10px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
  background: rgba(255, 255, 255, 0.02);
}
.vlab-draft-cell-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.vlab-draft-cell-no { font-size: 12px; font-weight: 700; opacity: 0.8; }
.vlab-draft-line { font-size: 11px; opacity: 0.7; line-height: 1.5; }
/* 칸 카드의 경고 줄 — 프롬프트 **바로 아래**에 붙고(그 프롬프트의 경고다), 캡션 입력과는 떨어진다. */
.vlab-draft-cell .vlab-lint { margin-top: -4px; }
.vlab-draft-add { margin-top: 10px; }
/* 자동 조립본 배지 — 임포트 모달이 문구로 말하던 것을 배지로 못박는다(사람이 다듬어야 할 결과다). */
.vlab-draft-badge {
  font-size: 10px;
  padding: 2px 8px;
  border-radius: 999px;
  color: var(--warn, #ffb454);
  border: 1px solid rgba(255, 180, 84, 0.4);
}

/* ── 초안 갈래 선택 · 클립 목록(2026-08-28 오너 지시 ⑥⑦) ─────────────────────────
   ⚠️⚠️ 세그먼트 자체는 **공용 `.vlab-tabs`/`.vlab-tab` 을 그대로 쓴다** — 그 둘은 네 곳(매체·화풍 피커 ·
      배경 뷰 줄 · 캔버스 만들기)이 함께 쓰는 어법이라 **모양을 여기서 바꾸면 그쪽이 함께 변형된다**.
      아래 규칙은 (a) 이 자리에만 붙는 여백과 (b) 다른 세그먼트에는 존재하지 않는 `NEW` 배지뿐이다. */
.vlab-draft-mode { margin-bottom: var(--sp-sm); }
/* `NEW` — 낱말이 아니라 **표식**이다. 라벨을 밀어내지 않게 작고, 갈래 이름보다 약하게 둔다. */
.vlab-new-tag {
  margin-left: 6px;
  padding: 1px 5px;
  border-radius: 999px;
  font-size: 9px; font-weight: 700; letter-spacing: 0.04em;
  color: var(--accent, #5b8cff);
  border: 1px solid rgba(91, 140, 255, 0.45);
  background: rgba(91, 140, 255, 0.12);
  vertical-align: middle;
}
/* 클립 목록 — 행 하나가 "무엇을 · 얼마나 길게 · 가져올까" 에 통째로 답한다(재료 준비 카드와 같은 어법). */
.vlab-clip-rows { display: grid; gap: 8px; margin-top: 10px; }
/* 🔴 **열이 넷이다**(2026-08-31 · 체크박스 추가). 옛 값은 `auto 1fr auto`(체크 · 번호 · 본문) 셋이었고,
   앞에 체크박스를 끼우자 **[가져오기]가 다음 줄로 밀렸다**(크롬 육안이 그 자리에서 잡았다).
   ⚠️ 자식 수를 늘리면 이 줄도 함께 늘린다 — 그리드는 남는 자식을 조용히 다음 행으로 내린다. */
.vlab-clip-row {
  display: grid; grid-template-columns: auto auto 1fr auto; align-items: center; gap: var(--sp-sm);
  padding: 9px 10px;
  border-radius: 10px;
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
  background: rgba(255, 255, 255, 0.02);
}
.vlab-clip-no { font-size: 12px; font-weight: 700; opacity: 0.8; white-space: nowrap; }
.vlab-clip-mid { display: grid; gap: 2px; min-width: 0; }
.vlab-clip-title { font-size: var(--fs-fine); overflow: hidden; text-overflow: ellipsis; }
.vlab-clip-meta { font-size: 11px; opacity: 0.6; }
/* 결과 한 줄 — 성공은 강조색으로 짧게, 못 세운 경우는 경고색으로(둘 다 **그 행에** 남는다). */
.vlab-clip-note { font-size: 11px; color: var(--accent, #5b8cff); }
.vlab-clip-note.is-warn { color: var(--warn, #ffb454); }
.vlab-clip-busy { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.vlab-clip-done { font-size: 11px; opacity: 0.7; white-space: nowrap; }
/* 좁으면 행동을 아래로 내린다 — 셋을 한 줄에 욱여넣으면 제목이 먼저 잘린다(그 제목이 고르는 근거다). */
@media (max-width: 620px) {
  .vlab-clip-row { grid-template-columns: auto auto 1fr; }
  .vlab-clip-row > .btn,
  .vlab-clip-row > .vlab-clip-busy,
  .vlab-clip-row > .vlab-clip-done { grid-column: 3; justify-self: start; }
}

/* ── 카드의 직전 생성 실패 한 줄(2026-08-31 · 캐릭터·배경 목록이 함께 쓴다) ────────
   ★ 배지 **아래**에 제 줄로 선다 — 배지는 "지금 무엇이 있나", 이 줄은 "만들려던 것이 왜 없나"다
     (두 사실이 한 알약에 들어가면 사용자가 할 일이 뭉개진다). 간격은 `.vlab-char-main` 의 gap 이
     정한다(마진 0 — 이 파일이 네 번 밟은 "간격은 언제나 한 곳이 정한다" 규칙).
   ⚠️ **말줄임을 걸지 않는다.** 문장 뒤쪽에 행동이 있어서(*"…잠시 뒤 다시 만들어 주세요"*) 잘리면
      사용자가 무엇을 해야 하는지가 사라진다 — 카드가 길어지는 편이 낫다(실패는 드물다).
      `keep-all` + `anywhere` 는 메타 줄(`.vlab-proj-meta`)과 같은 규약이다(한글이 낱말 중간에서
      끊기지 않게 하되, 긴 영문·URL 토큰은 카드 밖으로 삐져나가지 않게). */
.vlab-char-fail {
  font-size: 11px; line-height: 1.5;
  color: var(--danger, #ff6b6b);
  word-break: keep-all; overflow-wrap: anywhere;
}

/* ⚠️ **`.vlab-char-oneline` 이 제 주석과 다르게 굴고 있었다**(2026-08-31 발견): "높이가 한 줄이어야
   한다"고 적어 두고 `min-height` 를 안 풀어서, 함께 쓰는 `.vlab-img-prompt` 의 `min-height: 76px`
   가 그대로 이겼다 — 한 줄짜리 입력이 76px 짜리 상자로 섰다(배경 팝업의 [앵커] 칸을 이 클래스로
   맞추면서 드러났고, 캐릭터 팝업의 [목소리] 칸이 같은 상태였다). 값을 새로 정하지 않고 상속받은
   바닥만 푼다 — 높이는 `height: auto` 가, 폭·테두리는 `.vlab-img-prompt` 가 계속 소유한다. */
.vlab-char-oneline { min-height: 0; }

/* 클립 목록 일괄 실행(2026-08-31) — 행의 체크박스와 머리줄이 **같은 폭**을 차지해야 글자가 안 어긋난다. */
.vlab-clip-bulk {
  display: flex; align-items: center; gap: var(--sp-sm);
  padding: var(--sp-sm) 0; flex-wrap: wrap;
}
.vlab-clip-bulk-label { font-size: var(--fs-fine); opacity: 0.8; margin-right: auto; }
.vlab-clip-pick { width: 16px; height: 16px; flex: 0 0 16px; accent-color: var(--accent, #5b8cff); }
/* 고를 수 없는 행의 자리지킴 — 보이지 않지만 폭은 그대로다(격자가 흔들리지 않게). */
.vlab-clip-pick--off { visibility: hidden; }

/* ── AI 로 프롬프트 고치기(2026-09-01 오너 지시) — 기존·지시·결과 세 영역 ─────────────────────
   ★ 팝업 틀·두 열은 [채우기] 팝업 것을 **그대로 쓴다**(`.vlab-fill-panel`·`.vlab-fill-cols`) — 넓은 폭에
     두 벌을 나란히 놓는다는 사정이 같다. 여기서 폭을 다시 정하면 같은 모양이 두 값으로 갈린다.
   ⚠️ 색은 토큰이다(`--danger`·`--positive`) — 이 둘은 tokens.css 가 대비(WCAG AA)까지 재어 둔 값이고,
      여기에 새 빨강·초록을 적으면 그 검증 밖의 색이 된다. */
.vlab-refine-ask { display: grid; gap: var(--sp-sm); }
/* 지시 칸은 짧다 — 한두 문장이 대부분이고, 크게 두면 아래 대조가 화면 밖으로 밀린다. */
.vlab-refine-input { min-height: 64px; }
.vlab-refine-askfoot { display: flex; align-items: center; gap: var(--sp-sm); flex-wrap: wrap; }

/* 대조 상자 — 줄 구조가 곧 계약인 글이라(컷 헤더·대사 줄) 접어 내리되 가로로는 밀지 않는다. */
.vlab-diff {
  margin: var(--sp-xs) 0 0;
  padding: var(--sp-sm);
  max-height: 44vh;
  overflow-y: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;   /* 긴 영문 프레이밍 토큰이 열을 밀어내지 않게 */
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--fs-fine, 12px);
  line-height: 1.55;
  border-radius: var(--r-sm, 8px);
  background: rgba(255, 255, 255, 0.03);
}
.vlab-diff-empty { opacity: 0.55; }

/* ⚠️ 색만으로 가르지 않는다 — 없어질 곳은 **취소선**, 새로 생긴 곳은 밑줄을 함께 준다.
      색각 이상에서 빨강·초록은 이 조합이 가장 안 갈리는 쌍이고, 이 화면은 그 둘만 쓴다. */
.vlab-diff-del {
  color: var(--danger, #e06a6a);
  background: var(--danger-soft, rgba(224, 106, 106, 0.14));
  text-decoration: line-through;
  border-radius: 3px;
}
.vlab-diff-add {
  color: var(--positive, #3fae6d);
  background: rgba(63, 174, 109, 0.14);
  text-decoration: underline;
  text-underline-offset: 2px;
  border-radius: 3px;
}
.vlab-diff-sum { margin-top: var(--sp-xs); font-size: var(--fs-fine); opacity: 0.7; }
.vlab-diff-tag { margin-left: auto; font-size: var(--fs-fine); font-variant-numeric: tabular-nums; }
.vlab-diff-tag--del { color: var(--danger, #e06a6a); }
.vlab-diff-tag--add { color: var(--positive, #3fae6d); }
