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

/* ── 화면 두 장(목록 · 보드) ────────────────────────────────────────────
   캔버스 앞에 프로젝트 목록이 선다. 둘 다 DOM 에 살아 있고 hidden 으로만 가른다
   (캔버스 리스너는 마운트 때 한 번만 붙는다 — videoLabView.render 주석 참고). */
.vlab-screen { display: block; }
/* ★ 보드 = 툴바 + 캔버스 세로 쌓기(2026-09-16). 캔버스는 **남은 높이를 전부** 갖는다 — 툴바 높이를 숫자로 빼지 않는다
   (옛 `calc(100vh - 58px)` 는 툴바가 두 줄이 되는 순간 캔버스 아래가 화면 밖으로 밀렸다). ⚠️ 숨김은 `[hidden]` 이 이긴다. */
.vlab-screen--board { display: flex; flex-direction: column; height: 100vh; }
/* ★ 캔버스 직행 중(2026-09-21 오너 "프로젝트 화면이 떴다가 캔버스가 나온다") — 목록을 그리지 않는 동안 서는 자리.
   ⚠️ 빈 화면으로 두지 않는다: 몇 백 ms 라도 아무것도 없으면 고장으로 읽힌다. 카드도 스켈레톤도 없다(그게 요점이다). */
.vlab-screen--opening { display: flex; align-items: center; justify-content: center; min-height: 40vh; padding: 48px 24px; }
.vlab-opening-note { color: var(--text-muted, #8b8b96); font-size: 14px; }
/* ★ 작업면 직행 중(2026-09-26 — `…/video/open/:vid/:tab` 새로고침) — 목록 스켈레톤 대신 **작업면 뼈대**(크럼 자리 · 단계 칩 · 카드 뼈대)가 선다.
   가운데 정렬은 한 줄 안내용이라 여기서는 풀고, 작업면 화면(`.vlab-screen--list`)과 같은 세로 흐름으로 쌓는다. */
.vlab-screen--opening.is-project { display: block; min-height: 0; padding: 0; }
/* 크럼 줄은 아직 비어 있어도 **높이는 같다**(버튼 sm 28px) — 비우면 작업면이 뜰 때 칩이 28px 내려갔다(스테이징 실측).
   제목 자리 뼈는 제목 줄 높이(v2 `--lh-title-1` 30px)를 채운다 — 머리 줄 높이가 같아야 아래 카드 뼈대가 안 움직인다. */
.vlab-screen--opening.is-project .vlab-imp-crumb { min-height: 28px; }
.vlab-opening-title-bone { width: 280px; max-width: 60%; height: 24px; margin: 3px 0; border-radius: var(--r-sm, 8px); }

/* ── 프로젝트 목록 ─────────────────────────────────────────────────────── */
.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 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 rgba(255, 255, 255, 0.1);
  border-radius: 14px;
  background: 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 rgba(255, 255, 255, 0.1);
  border-radius: 10px;
  background: 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-main` 의 gap 이 정한다. */
.vlab-proj-people { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 2px; min-width: 0; }
.vlab-proj-people-owner { font-size: 11px; color: var(--ink-dim, #9aa3b2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* 삭제는 평소엔 흐리게 — 카드의 주 동작(열기)을 가리지 않게. */
.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); }

/* ── 툴바 ─────────────────────────────────────────────────────────────── */
/* ★ 두 줄(2026-09-16 오너 신고 *"노트북에서 가려져"* · `videoLabToolbar.js` 머리주석):
   1줄 = [목록] 제목 [저장] │ [남은 생성 취소] [전체 실행] · 2줄 = [＋ 추가 ▾] [AI 자동 초안] · ↶↷ 전체 보기 조작법 · 정렬 이어서 보기.
   ⚠️ 줄바꿈(wrap)을 켜지 않는다 — 버튼이 제멋대로 다음 줄로 흘러가면 자리가 폭마다 달라진다. 좁으면 **제목이 먼저** 줄어든다.
   ★ 메인 헤더 44 + 메뉴 띠 36(같은 날 오너 *"헤더가 커진 느낌 … 상단이 메인 헤더고 이건 메뉴 같은 느낌을"* · 코덱스 A안):
     두 줄이 같은 바탕·같은 간격이라 한 덩어리 헤더로 읽혔다 → 띠는 **한 톤 밝은 바탕 + 위아래 선 + 오른쪽 정렬 + 네모난 단추**.
   ⚠️ 툴바는 `padding·gap 0` 이고 높이는 두 행이 정한다 — 캔버스는 남은 높이를 채운다(`.vlab-canvas` · 높이 차감식 금지).
   ⚠️ 바탕 두 값은 캔버스 토큰 `--vl-canvas-bg`(#141414)·`--vl-node-head`(#1d1d1d)와 같은 값이다 — 그 토큰은 `.vlab-canvas`
      스코프라 툴바(형제)에서 못 읽어 여기 이름을 따로 둔다. 캔버스 쪽을 바꾸면 같이 바꾼다. */
.vlab-toolbar {
  --vl-toolbar-head-bg: #141414;
  --vl-toolbar-menu-bg: #1d1d1d;
  display: flex;
  flex-direction: column;
  flex: none;
  gap: 0;
  padding: 0;
  position: relative;
  z-index: 2;
}
.vlab-toolbar-row { display: flex; align-items: center; min-width: 0; box-sizing: border-box; padding: 0 16px; }
.vlab-toolbar-row--main { height: 44px; gap: 8px; background: var(--vl-toolbar-head-bg); }
/* 메뉴 띠 — 높이 36 은 위아래 선을 **포함**한다(단추 28 이 가운데). `safe` 는 넘칠 때 왼쪽 끝이 화면 밖으로 잘리지 않게(미지원이면 윗줄). */
.vlab-toolbar-row--menu {
  height: 36px; gap: 0;
  justify-content: flex-end;
  justify-content: safe flex-end;
  background: var(--vl-toolbar-menu-bg);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.vlab-toolbar-group { display: flex; align-items: center; gap: 4px; flex: none; }
/* 그룹 사이 16px — 그 가운데 1×14 선. 선은 JS 가 **세운 그룹 사이에만** 긋는다(읽기전용에서 남는 선이 없다). */
.vlab-toolbar-sep { flex: none; width: 1px; height: 14px; margin: 0 8px; background: rgba(255, 255, 255, 0.08); }
/* 메뉴 단추는 메뉴처럼 — 알약이 아니라 네모(반경 4) · 바탕 없음 · 호버 흰색 6%. 메인 헤더 단추는 건드리지 않는다. */
.vlab-toolbar-row--menu .btn { border-radius: 4px; background: transparent; }
.vlab-toolbar-row--menu .btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.06); }
.vlab-toolbar .vlab-title { text-overflow: ellipsis; white-space: nowrap; }
/* 실행 구획 — 저장(무료)과 세로선으로 가른다. [전체 실행]이 맨 오른쪽(메뉴 띠 마지막 단추와 같은 오른쪽 16px 선)이고
   실행 중에만 서는 [남은 생성 취소]는 그 왼쪽이다 — 자리 예약 없음(2026-09-16 · `videoLabToolbar.js` 머리주석). */
.vlab-toolbar-run {
  display: flex; align-items: center; gap: 8px; flex: none;
  padding-left: 8px; border-left: 1px solid rgba(255, 255, 255, 0.12);
}
.vlab-toolbar-add-caret { margin-left: -2px; opacity: 0.7; }
.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: rgba(255, 255, 255, 0.08); }
.vlab-title:focus { outline: none; border-color: var(--accent, #5b8cff); }

/* ── 캔버스 비주얼 토큰(2026-09-15 · 계약 `.specs/spec_video_lab/canvas-comfy-parity.md` §2) ─────────────
   ★ 오너: *"캔버스·노드·선 색 시인성이 나쁘고 노드 구분이 안 된다 · ComfyUI 느낌이 좋다"*. 값은 ComfyUI dark 팔레트의
     **사실(hex)** 을 참고했다(ComfyUI 는 GPL-3.0 — 코드는 옮기지 않았다).
   ⚠️ 종류색(`--vl-kind-*`)·선 성격색(`--vl-role-*`)은 `web/ui/graphEdgeKinds.EDGE_ROLES` 와 짝이다 — `graphEdgeKinds.test.mjs` 가
      여기서 hex 를 읽어 목록 대조 + 두 계열의 같은 값 + 캔버스(#141414)·몸통(#262626) 대비 3:1 을 잰다. 값을 바꾸면 그 테스트가 다시 잰다.
   ⚠️ 스코프에 `.vlab-menu` 가 함께 있는 이유: 우클릭 메뉴는 body 에 붙는다(캔버스 밖) — 색 견본이 같은 색 쌍을 읽어야 한다. */
.vlab-canvas,
.vlab-menu,
.vlab-import {
  --vl-canvas-bg: #141414;
  --vl-grid-minor: rgba(255, 255, 255, 0.035);
  --vl-grid-major: rgba(255, 255, 255, 0.07);
  --vl-node-body: #262626;
  --vl-node-head: #1d1d1d;
  --vl-node-border: #3a3a3a;
  --vl-node-title: #dddddd;
  --vl-node-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  --vl-node-radius: 10px;
  --vl-selected: #ffffff;
  --vl-error: #ee3333;
  --vl-link-hover: #ffcc00;
  --vl-bypass: #b03fb0;
  /* 타입을 모르는 선·입력 포트(Phase 2 전) — 중립 회색. #262626 위 3.5:1 · #141414 위 5:1. */
  --vl-neutral: #8a8a8a;
  --vl-port-neutral: #7a7a7a;
  /* 실행 중 색 — 배지(`.vlab-badge--running`)와 미니맵(`graphMinimap.readMinimapColors`)이 **같은 값**을 읽는다.
     ⚠️ hex 를 두 곳에 적지 않는다: 미니맵의 "돌고 있는 노드" 얼룩과 카드 배지가 다른 파랑이면 같은 뜻의 색이 둘이 된다. */
  --vl-status-running: #8ab4ff;
  /* 종류색(헤더 띠 · 계약 §2-2). */
  --vl-kind-text: #FFD500;
  --vl-kind-context: #9E9E9E;
  --vl-kind-image: #64B5F6;
  --vl-kind-character: #B39DDB;
  --vl-kind-location: #81C784;
  --vl-kind-board: #FFA931;
  --vl-kind-clip: #FF9CF9;
  --vl-kind-angle: #6EE7B7;
  /* 선 성격색(계약 §3-1 · `graphEdgeKinds.EDGE_ROLES`) — **출발 종류의 헤더색과 같은 값**이다("보라 선 = 인물에서 온 선").
     ⚠️ var() 로 잇지 않고 hex 를 다시 적는 이유: 대비·대조 테스트가 hex 를 읽는다. 둘이 같은지는 그 테스트가 잰다. */
  --vl-role-prompt: #FFD500;
  --vl-role-context: #9E9E9E;
  --vl-role-character: #B39DDB;
  --vl-role-location: #81C784;
  --vl-role-board: #FFA931;
  --vl-role-image: #64B5F6;
  --vl-role-chain: #FF9CF9;
  /* ★ 원본(2026-09-16 · 앵글로 **들어가는** 선) — 출발 종류가 없는 유일한 성격이라 짝지을 헤더색이 없다(도착이 정한다).
     그래서 값은 "이웃과 갈리는가" 로 골랐다: 범례에서 바로 아래 `시작 장면`(#6EE7B7 민트)과 나란히 서고, 원본이 될 수
     있는 카드의 색(이미지 #64B5F6 파랑 · 인물 #B39DDB 보라)도 가까이 있다. 남은 자리는 **따뜻한 쪽**이었다 —
     #FF6F91(장미) 은 그 셋과 색상환에서 130° 이상 떨어져 있고, 실패 외곽선(#ee3333 진한 빨강)·콘티(#FFA931 주황)·
     이어짐(#FF9CF9 마젠타)과도 밝기·채도가 갈린다. 대비는 #141414 위 6.96:1 · #262626 위 5.72:1(테스트가 다시 잰다). */
  --vl-role-source: #FF6F91;
  --vl-role-start: #6EE7B7;
  /* 사용자 노드 색 — 헤더/몸통 쌍(ComfyUI `node_colors` 의 color/bgcolor). 종류색 띠는 칠해도 남는다. */
  --vl-color-red-head: #322;
  --vl-color-red-body: #533;
  --vl-color-brown-head: #332922;
  --vl-color-brown-body: #593930;
  --vl-color-green-head: #232;
  --vl-color-green-body: #353;
  --vl-color-blue-head: #223;
  --vl-color-blue-body: #335;
  --vl-color-pale_blue-head: #2a363b;
  --vl-color-pale_blue-body: #3f5159;
  --vl-color-cyan-head: #233;
  --vl-color-cyan-body: #355;
  --vl-color-purple-head: #323;
  --vl-color-purple-body: #535;
  --vl-color-yellow-head: #432;
  --vl-color-yellow-body: #653;
  --vl-color-black-head: #222;
  --vl-color-black-body: #000;
  /* ★ 그룹색(계약 §4-1 · ComfyUI `groupcolor` 의 사실값) — 이름 집합은 노드 색(`graphModel.NODE_COLORS`)과 **같다**(서버 `LAB_NODE_COLORS`
     하나를 공유). 노드 헤더/몸통 쌍보다 밝은 값이다 — 상자는 채움 12%·테두리 60% 로만 섞어 쓰기 때문(`.vlab-group`).
     부재 = `pale_blue`(`graphGroups.GROUP_DEFAULT_COLOR`). ⚠️ 이 스코프에 `.vlab-menu` 가 있어야 메뉴 견본이 같은 색을 읽는다. */
  --vl-group-red: #A88;
  --vl-group-brown: #b06634;
  --vl-group-green: #8A8;
  --vl-group-blue: #88A;
  --vl-group-pale_blue: #3f789e;
  --vl-group-cyan: #8AA;
  --vl-group-purple: #a1309b;
  --vl-group-yellow: #b58b2a;
  --vl-group-black: #444;
}

/* ── 캔버스 · 월드 ─────────────────────────────────────────────────────── */
.vlab-canvas {
  position: relative;
  /* 높이는 보드(`.vlab-screen--board`)의 남은 몫이다 — `min-height: 0` 이 없으면 flex 자식이 내용 크기 밑으로 안 줄어든다. */
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;           /* 팬/줌은 transform 이 한다 — 스크롤바가 끼면 좌표가 두 벌이 된다 */
  cursor: default;
  background-color: var(--vl-canvas-bg);
  touch-action: none;         /* 포인터 제스처를 브라우저 스크롤에 뺏기지 않게 */
}

/* ── 격자 ──────────────────────────────────────────────────────────────── */
/* 격자 두 단(작은 칸 20 · 큰 칸 100 그래프 px · 2026-09-15 점무늬 대체): 팬·줌이 실제로 일어나는지 눈으로 알게 하는 질감.
   ★ **월드를 따라 움직인다** — 칸 크기와 미는 양은 `graphViewport.applyGridBackground` 가 뷰포트마다 이 요소에
     직접 쓴다(아래 값은 1배·원점 기본값). 레이어 순서(큰 가로·큰 세로·작은 가로·작은 세로)가 그 함수와의 계약이다.
   🔴 **2026-09-18 캔버스 배경에서 떼어 내 자기 레이어로 세웠다**(오너 신고 *"확대축소하거나 이동하면 더"*).
      `.vlab-canvas` 의 background 로 두고 `background-position` 을 프레임마다 쓰면 **뷰포트 전체가 다시 칠해지고**,
      그 캔버스는 카드·선·미니맵의 부모라 그 무효화가 그 층 전체로 간다. 지금은 격자 한 장만 `transform` 으로
      밀고(합성기가 텍스처를 옮긴다 · 재도색 0) 배율이 바뀔 때만 칸 크기를 새로 칠한다.
   ⚠️ 사방 **-256px / +512px** 은 `graphViewport.GRID_INSET`(256)과 **같은 값**이다 — 미는 양의 상한(큰 칸 하나 =
      100 × SCALE_MAX 2.5 = 250px)보다 커야 모서리에 격자 없는 띠가 생기지 않는다. 한쪽만 고치면 격자가
      어긋난 자리에 서고(월드와 안 맞는다) 아무 테스트도 안 깨진다.
   ⚠️ 포인터를 안 받는다 — 빈 곳 클릭·마퀴·우클릭은 캔버스의 몫이다(받으면 `elementFromPoint` 가 이 판을 집는다).
   ⚠️ 노드는 여기에 **붙지 않는다** — 정렬은 격자가 아니라 다른 노드와의 줄맞춤이 담당한다(graphAlignSnap).
      그래서 이 간격은 스냅 값과 무관하며 맞출 필요도 없다. */
.vlab-grid {
  position: absolute;
  left: -256px;
  top: -256px;
  width: calc(100% + 512px);
  height: calc(100% + 512px);
  pointer-events: none;
  transform-origin: 0 0;
  will-change: transform;
  background-image:
    linear-gradient(to right, var(--vl-grid-major) 1px, transparent 1px),
    linear-gradient(to bottom, var(--vl-grid-major) 1px, transparent 1px),
    linear-gradient(to right, var(--vl-grid-minor) 1px, transparent 1px),
    linear-gradient(to bottom, var(--vl-grid-minor) 1px, transparent 1px);
  background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px;
  background-position: 0 0;
}
.vlab-canvas.is-pan-mode { cursor: grab; }
.vlab-canvas.is-panning { cursor: grabbing; }
/* 바깥 이미지 파일을 끌고 캔버스 위에 있는 동안(2026-10-19 · `videoLabFileDrop`) — 여기 놓으면 이미지 카드가 선다는 옅은 안쪽 테두리.
   `overflow:hidden` 이라 바깥 outline 은 잘리고, 캔버스 자신의 안쪽 그림자는 격자·월드 층(absolute 자식)에 덮인다 — 맨 위 가상 요소로 긋는다.
   포인터는 안 받는다(놓기 이벤트의 대상이 바뀌지 않게). */
.vlab-canvas.is-file-over::after {
  content: ''; position: absolute; inset: 0; z-index: 50; pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--accent, #5b8cff);
}

/* 콘티(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 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 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: 5px; left: 5px; font-size: 9px; line-height: 1.2;
  padding: 0 3px; border-radius: 3px; background: rgba(0, 0, 0, 0.55); color: #fff;
}
/* 칸 지우기 — hover·포커스에서만. 격자가 작아 항상 보이면 그림을 가린다. */
.btn.btn--icon-only.vlab-board-cell-del { /* iconBtn(sm) — .btn 의 28px 높이·min-width 와 새 셸 ghost 의 투명 배경을 이긴다(그림 위 × 는 어두운 원이어야 읽힌다) */
  position: absolute; top: 2px; right: 2px; width: 20px; min-width: 20px; height: 20px; 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;
}
.vlab-board-cell:hover .vlab-board-cell-del,
.btn.vlab-board-cell-del:focus-visible { display: flex; }
.btn.vlab-board-cell-del:hover { background: var(--danger, #ff6b6b); color: #fff; }
.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 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 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; }
.vlab-draft-fold[open] > summary { margin-bottom: 6px; }
.vlab-draft-fold > summary { list-style: none; }
.vlab-draft-fold > summary::-webkit-details-marker { display: none; }
.vlab-draft-fold > summary::after {
  content: ''; display: inline-block; vertical-align: middle; width: 14px; height: 14px; margin-left: 4px;
  background: currentColor; transition: transform 0.15s ease;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
}
.vlab-draft-fold[open] > summary::after { transform: rotate(180deg); }
.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 rgba(255, 255, 255, 0.08);
  background: 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` 는 2026-09-22 에 걷혔다 — 오너가 [캐릭터 앉히기]·[직접 만들기] 두 갈래를
   물렀고(`videoLabEmptyGuide.js` 머리주석의 🔴 절), 그 둘을 담던 줄이 이 클래스의 유일한 소비자였다. */
.vlab-guide-note { font-size: 11px; opacity: 0.45; margin: 8px 0 0; }
/* 갈래 칸 둘(2026-10-19) — 옛 셸 최소 모양. 새 셸 모양은 `v2/canvas-contrast.css` ⑥ 이 소유한다. */
.vlab-guide-opt { margin-top: 12px; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.10); }
.vlab-guide-opt.is-recommended { border-color: rgba(79, 145, 245, 0.55); background: rgba(79, 145, 245, 0.08); }
.vlab-guide-opt-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.vlab-guide-opt-name { font-size: 14px; font-weight: 600; }
.vlab-guide-badge { padding: 1px 8px; border-radius: 999px; font-size: 11px; color: #5fa0ff; background: rgba(79, 145, 245, 0.16); }
.vlab-guide-opt-desc { font-size: 12px; line-height: 1.6; opacity: 0.65; margin: 0 0 6px; }
.vlab-guide-points { list-style: none; margin: 0 0 8px; padding: 0; font-size: 12px; line-height: 1.6; }
.vlab-guide-points > li::before { content: '✓ '; color: #5fa0ff; }
.vlab-guide-alt { margin-top: 10px; text-align: center; font-size: 12px; opacity: 0.8; }
/* 재료 준비 카드(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 rgba(255, 255, 255, 0.08);
  background: 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; }
.btn.btn--icon-only.vlab-keyhelp-close {
  width: 24px; min-width: 24px; height: 24px; padding: 0;
  background: none; border: 0; cursor: pointer; color: inherit; opacity: 0.5;
}
.btn.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 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; }

/* ── 대사 모아보기(2026-09-23 오너 지시 · `videoLabDialogueModal.js`) ─────────────────────────────
   ⚠️ 접두는 `vlab-dsheet-` 다 — `vlab-dlg-*` 는 옛 카드 대사 편집기가 이미 쓰는 이름이라(위 `.vlab-dlg-say` 의
      max-height·테두리) 같은 접두를 쓰면 팝업의 대사가 입력칸처럼 그려진다(코덱스 검토에서 잡았다).
   ★ 여는 단추는 캔버스 우상단 뜬 단추가 아니라 **보드 툴바 안**에 산다(2026-09-23 — [클립 이어서 보기] 옆,
      `videoLabToolbar.js` 정리·감상 묶음) — 다른 툴바 단추와 같은 표면이라 여기서 따로 칠하지 않는다. */

/* 팝업 — 폭은 읽기 폭(대본 한 줄이 한눈에), 머리·발은 고정이고 **목록만 구른다**. */
.sm-panel.vlab-dsheet-panel {
  width: min(760px, 94vw); max-width: 760px; max-height: 86vh;
  display: flex; flex-direction: column; gap: 0; padding-bottom: var(--sp-md, 17px);
}
.vlab-dsheet-panel > .sm-title { flex: none; }
.vlab-dsheet-panel:focus { outline: none; }
.vlab-dsheet-meta {
  flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px;
  margin-top: var(--sp-xs, 8px);
  font-size: var(--fs-fine, 12px); color: var(--ink-80, #ccc);
}
.vlab-dsheet-chip {
  display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px;
  border-radius: var(--r-pill, 999px); background: var(--surface-3, #242424);
}
.vlab-dsheet-chip-k { color: var(--ink-48, #999); }
.vlab-dsheet-chip-v { color: var(--ink, #fff); font-weight: 600; }
.vlab-dsheet-count { font-variant-numeric: tabular-nums; }
.vlab-dsheet-count.is-warn { display: inline-flex; align-items: center; gap: 4px; color: var(--ink, #fff); font-weight: 600; }
.vlab-dsheet-hint {
  flex: none; margin: 6px 0 var(--sp-sm, 12px);
  font-size: var(--fs-fine, 12px); line-height: 1.5; color: var(--ink-48, #999);
}
.vlab-dsheet-list {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable;
  display: flex; flex-direction: column; gap: 12px;
  margin: 0 calc(-1 * var(--sp-xs, 8px)); padding: 0 var(--sp-xs, 8px);
}
.vlab-dsheet-empty { padding: var(--sp-lg, 24px) 0; text-align: center; font-size: var(--fs-caption, 14px); color: var(--ink-48, #999); }

/* 클립 한 덩이 — 판(surface-1) 위 한 단 밝은 카드 + 헤어라인. 머리는 누르면 캔버스의 그 카드로 간다(버튼). */
.vlab-dsheet-clip {
  flex: 0 0 auto;
  border: 1px solid var(--hairline, #262626); border-radius: var(--r-md, 11px);
  background: var(--surface-2, #1c1c1c);
}
/* ⚠️ 머리를 목록 안에 고정(sticky)하지 않는다 — 새 셸의 글라스 층(`liquid-glass-overlays.css`)이 패널 안 `--surface-*` 를
   **반투명**으로 갈아 끼워, 고정된 머리 뒤로 대사가 비쳐 겹쳐 보였다(코덱스 2차 검토). 불투명 새 색을 들이는 대신 고정을 뺐다. */
.vlab-dsheet-clip-head {
  display: flex; align-items: center; gap: 10px; width: 100%;
  min-height: 44px; padding: 6px 12px; margin: 0;
  border: 0; border-radius: var(--r-md, 11px); background: none; color: inherit;
  font: inherit; text-align: left; cursor: pointer;
}
.vlab-dsheet-clip-head:hover { background: var(--surface-3, #242424); }
.vlab-dsheet-clip-head:focus-visible { outline: none; box-shadow: var(--ring-focus, 0 0 0 2px #5fa0ff); }
.vlab-dsheet-ord {
  flex: none; padding: 0 6px; border-radius: var(--r-sm, 6px); background: var(--surface-3, #242424);
  font-size: var(--fs-fine, 12px); font-weight: 600; line-height: 1.6;
  color: var(--ink, #fff); font-variant-numeric: tabular-nums;
}
.vlab-dsheet-titles { flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 8px; }
.vlab-dsheet-title {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-caption, 14px); font-weight: 600; color: var(--ink, #fff);
}
.vlab-dsheet-title.is-untitled { color: var(--ink-48, #999); font-weight: 500; }
.vlab-dsheet-hint-scene { flex: none; font-size: var(--fs-fine, 12px); color: var(--ink-48, #999); }
.vlab-dsheet-clip-tail { flex: none; display: inline-flex; align-items: center; gap: 8px; font-size: var(--fs-fine, 12px); color: var(--ink-48, #999); }
.vlab-dsheet-clip-count { font-variant-numeric: tabular-nums; }
.vlab-dsheet-clip-warn {
  display: inline-flex; align-items: center; gap: 3px; height: 20px; padding: 0 7px;
  border-radius: var(--r-pill, 999px); background: var(--surface-3, #242424); color: var(--ink, #fff); font-weight: 600;
}
/* 이동 표식은 **늘 보인다** — 호버 전에 숨으면 머리가 그냥 제목으로 읽히고, 누르면 팝업이 닫히는 것이 놀랍다. */
.vlab-dsheet-go { display: inline-flex; align-items: center; gap: 2px; color: var(--ink-48, #999); }
.vlab-dsheet-clip-head:hover .vlab-dsheet-go { color: var(--ink, #fff); }

/* 컷 한 덩이 — 왼쪽 열에 컷 번호, 오른쪽에 줄들. 컷 사이는 헤어라인.
   ⚠️ 2026-09-23 오너 지시로 구간(시:분 표시)을 뺐다 — "CUT N" 한 줄뿐이라 열이 좁다(56px). */
.vlab-dsheet-cut {
  display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 0 12px;
  margin: 0 12px; padding: 12px 0;
  border-top: 1px solid var(--hairline, #262626);
}
.vlab-dsheet-cutno { padding-top: 3px; }
.vlab-dsheet-cutno-n { font-size: var(--fs-fine, 12px); line-height: 1.5; font-weight: 600; color: var(--ink-80, #ccc); font-variant-numeric: tabular-nums; }
.vlab-dsheet-lines { display: flex; flex-direction: column; gap: 12px; min-width: 0; }

/* 한 줄 — 화자 열 + 말 열. **말이 이 화면의 주인공이다**(가장 크고 밝다 · 제목 14 위 16). */
.vlab-dsheet-line { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 0 12px; align-items: baseline; }
.vlab-dsheet-who { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; min-width: 0; }
.vlab-dsheet-name { font-size: var(--fs-fine, 12px); line-height: 1.5; font-weight: 600; color: var(--ink-80, #ccc); overflow-wrap: anywhere; }
.vlab-dsheet-name.is-unknown { font-weight: 500; color: var(--ink-48, #999); }
/* 종류 태그(V.O·화면 글)는 **테두리만** — 채움은 언어 경고의 몫이다(종류 ≠ 검수 사항). */
.vlab-dsheet-tag {
  display: inline-flex; align-items: center; padding: 0 5px;
  border: 1px solid var(--hairline, #262626); border-radius: var(--r-xs, 5px);
  font-size: 11px; line-height: 18px; font-weight: 600; letter-spacing: 0.02em; color: var(--ink-80, #ccc);
}
.vlab-dsheet-say { min-width: 0; }
.vlab-dsheet-text {
  margin: 0; font-size: 16px; font-weight: 400; line-height: 1.6; color: var(--ink, #fff);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.vlab-dsheet-text.is-empty { color: var(--ink-48, #999); }
.vlab-dsheet-line[data-kind="screen"] .vlab-dsheet-text { font-size: var(--fs-fine, 12px); line-height: 1.5; color: var(--ink-48, #999); font-family: var(--font-mono, monospace); }
/* 언어 경고 — 주황 없이(이 디자인 시스템에 없는 색) 아이콘 + 흰 굵은 글자 + 표면 단차(`canvas-modals.css` 색 규칙). */
.vlab-dsheet-warn {
  display: inline-flex; align-items: flex-start; gap: 6px; max-width: 100%; box-sizing: border-box;
  margin-top: 6px; padding: 4px 8px;
  border-radius: var(--r-xs, 5px); background: var(--surface-3, #242424);
  font-size: var(--fs-fine, 12px); font-weight: 600; line-height: 1.5; color: var(--ink, #fff);
}
.vlab-dsheet-warn svg { flex: none; margin-top: 3px; }
.vlab-dsheet-panel > .sm-actions { flex: none; margin-top: var(--sp-sm, 12px); }

/* 좁은 화면 — 새 셸이 영상탭 모달을 전체 화면 시트로 바꾼다(`canvas-modals.css` ≤810). 그 규칙의 간격(`gap`)과
   닫기(×)의 음수 여백은 **패널이 통째로 구르는** 모달을 전제로 한 값이라, 목록만 구르는 이 팝업에서는 풀어 준다.
   ⚠️ 선택자를 한 단 높인다(`.sm-overlay >`) — 그 파일이 이 파일보다 **뒤에** 로드된다. */
@media (max-width: 810px) {
  body.shell-v2 .sm-overlay > .sm-panel.vlab-dsheet-panel { gap: 0; overflow: hidden; }
  body.shell-v2 .sm-overlay > .sm-panel.vlab-dsheet-panel > .btn.vlab-modal-x { position: absolute; top: 4px; right: 8px; margin: 0; }
  .vlab-dsheet-cut { grid-template-columns: 1fr; gap: 6px; }
}
@media (max-width: 640px) {
  .vlab-dsheet-line { grid-template-columns: 1fr; row-gap: 4px; }
  .vlab-dsheet-titles { flex-direction: column; align-items: stretch; gap: 2px; }
  .vlab-dsheet-hint-scene { white-space: normal; overflow-wrap: anywhere; }
}

/* 툴바의 읽기전용 안내 — 관리자가 팀원 캔버스를 열었을 때. 편집 버튼이 사라진 이유를 화면이 대답한다.
   ⚠️ 톤은 **중립**이다(경고 아님) — 관리자는 이 줄을 매번 보게 되고, 주황으로 두면 늘 고장 난
      화면으로 읽힌다(`videoLabProjectList` 의 notice 와 같은 판단). */
.vlab-viewonly-note {
  /* 12/500 · 색은 본문 흐림(중립) — 옛 11px·투명도 0.55 는 44px 헤더에서 읽기 어려웠다(A안). */
  font-size: 12px; font-weight: 500; color: var(--ink-80, #c8c8d0); white-space: nowrap; flex: none;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid 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` 은 **움직이는 동안만** 붙는다(`graphMotionHint` · 2026-09-28 오너 신고 *"화면 깜빡이는거 아직도있고"*).
   옛 판은 월드에 늘 붙어 있었다 — 크롬은 이 힌트가 붙은 층의 래스터 배율을 올리기만 하고 내리지 않아, 확대했다 축소하면
   축소 화면 전체를 확대 해상도로 칠해 둔다(실측 타일 84MB → 511MB · 한도 512MB). 한도에 붙으면 타일이 버려졌다 다시 칠해지며
   카드 속과 툴바가 한 프레임씩 빈다. 팬·줌 동안 붙이고 멈춘 뒤 떼어 지금 배율로 한 번 다시 칠한다 · 한 손짓 동안은 크게 축소해도 떼고 붙이지 않는다(2026-10-06 — 핀치 ×3.5 에서 손짓마다 3~4번 재래스터). */
.vlab-world.is-moving {
  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;
}
/* ★ 2026-09-15 선 = **출발 포트 타입색 3px + 아래 검은 테 5px + 중간점**(계약 §2-4). 선 하나가 hit→border→line→dot 네 요소다
   (`graphEdgeLayer` — 순서가 아래 호버 인접 선택자의 계약이다).
   ⚠️ 옛 판의 흰 반투명 한 색은 **무엇이 무엇으로 흐르는지**를 말하지 못했다(오너 *"노드 구분이 안 된다"*).
   ⚠️ focus 층은 **투명도로만** 말한다 — 색은 이제 타입의 것이라 층이 색을 쓰면 두 뜻이 섞인다(옛 `is-flow` 보라를 걷은 이유).
      테·점도 같은 층 클래스를 입는다(흐린 선 아래 검은 테가 또렷하면 흐린 선이 검은 선으로 읽힌다). */
.vlab-edge {
  --vl-edge-color: var(--vl-neutral);
  stroke: var(--vl-edge-color);
  stroke-width: 3;
  stroke-linecap: round;
}
.vlab-edge-border { stroke: rgba(0, 0, 0, 0.6); stroke-width: 5; stroke-linecap: round; }
/* ★ 성격 → 색(2026-09-15 Phase 2 · 계약 §3-1). 선·점·초안 선·범례 견본이 **같은 표**를 읽는다(`graphEdgeKinds.edgeRoleOf` 가
   종류쌍으로 data-role 을 정한다). 규칙 밖 선(옛 cut 출발 등)은 data-role 이 비어 중립 회색이다.
   ⚠️ 포트는 여기 없다 — 포트는 중립(계약 §2-2). */
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="prompt"] { --vl-edge-color: var(--vl-role-prompt); }
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="context"] { --vl-edge-color: var(--vl-role-context); }
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="character"] { --vl-edge-color: var(--vl-role-character); }
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="location"] { --vl-edge-color: var(--vl-role-location); }
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="board"] { --vl-edge-color: var(--vl-role-board); }
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="image"] { --vl-edge-color: var(--vl-role-image); }
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="chain"] { --vl-edge-color: var(--vl-role-chain); }
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="source"] { --vl-edge-color: var(--vl-role-source); }
:is(.vlab-edge, .vlab-reroute-dot, .vlab-reroute-slot, .vlab-legend-swatch)[data-role="start"] { --vl-edge-color: var(--vl-role-start); }
/* 출처(맥락 → *)는 프롬프트에도 참조에도 안 실린다 — **모양으로도** 다르게(점선). 색만으로 구분하지 않는다(§2-2). */
.vlab-edge[data-role="context"]:not(.vlab-edge--draft) { stroke-dasharray: 2 6; }
/* 선택한 카드에 닿는 선(`is-near`)은 그대로 또렷하다 · 흐름(`is-flow`) .6 · 공용·먼 선(`is-fan`) .35 · 관계 밖(`is-far`) .15.
   ★ 옛 근거는 그대로 산다: 선을 **지우지 않고 흐리게** 한다(지우면 "연결이 사라졌나" 가 된다) · 클릭 판정은 hit 가 그대로 맡는다. */
:is(.vlab-edge, .vlab-edge-border).is-flow { opacity: 0.6; }
:is(.vlab-edge, .vlab-edge-border).is-fan { opacity: 0.35; }
:is(.vlab-edge, .vlab-edge-border).is-far { opacity: 0.15; }
.vlab-edge.is-selected { stroke: var(--vl-selected); stroke-width: 4; }
.vlab-edge-border.is-selected { stroke-width: 6; }
/* 호버 — 집는 것은 투명 hit 라 형제 선택자로 그 선에 색을 옮긴다(흐린 선도 집으면 또렷해진다). */
.vlab-edge-hit:hover + .vlab-edge-border { opacity: 1; }
.vlab-edge-hit:hover + .vlab-edge-border + .vlab-edge { stroke: var(--vl-link-hover); opacity: 1; }
/* ★ **이어짐 선**(2026-09-15 오너: *"연결선을 잇는다거나 해서 블랙박스가 아니게"*) — 영상 → 영상 `kind:'seam'`.
   재료·연장 체인 선(실선)과 갈라 읽히게 **긴 점선**이다. 색은 새로 뜻을 만들지 않는다(성격 색이 이미 뜻을 가진다 —
   위 `data-role` 표) — 굵기·점선으로만 가른다. 흐림·고름 규칙(`is-far`·`is-selected`)은 그대로 얹힌다.
   ⚠️ 2026-09-16 병합: **선과 테에만** 건다(2026-10-19 중간점 dot 은 걷었다). 옛 점(`.vlab-edge-dot`)에 점선을 걸면
      반지름 4px 원의 테두리가 끊겨 부서진 점으로 보인다(`edgeClass` 가 종류 표식을 층 꼬리와 함께 셋에 나른다). */
.vlab-edge.vlab-edge--seam { stroke-width: 2.5; }
:is(.vlab-edge, .vlab-edge-border).vlab-edge--seam { stroke-dasharray: 10 6; }
/* 끌고 있는 초안 선 = 끌어 나온 포트의 타입색 점선(`showDraft(…, type)` · 타입을 안 주면 중립). */
.vlab-edge--draft {
  stroke-width: 3;
  stroke-dasharray: 8 6;
}
.vlab-edge-hit { pointer-events: stroke; cursor: pointer; }
/* ★ 실패 원인 길(2026-10-29 · 그루트님(슈드파) 피드백 — `videoLabFailCause` · `graphEdgeLayer.edgeAlertClass`) — 원인 카드에서 막힌 카드까지 가는 선을 붉게·굵게.
   ⚠️ 흐림(`is-far`·`is-fan`·`is-flow`)을 이긴다(같은 특이도 · 뒤에 온다) — 무엇을 골랐든 막힌 길은 보여야 한다. 고른 선은 고름 색이 이긴다(`:not(.is-selected)`).
   ⚠️ 색만 바꾼다 — 선 모양·리루트 점·히트 선은 그대로다. */
:is(.vlab-edge, .vlab-edge-border).is-fail-path { opacity: 1; }
.vlab-edge.is-fail-path:not(.is-selected) { stroke: var(--vl-error); stroke-width: 3.5; }
.vlab-edge-border.is-fail-path { stroke-width: 7; }
/* ★ 리루트 점(2026-10-19 · 계약 canvas-comfy-parity §7-8 B · `graphEdgeLayer` 의 `.vlab-reroutes`) — 선들 위 · 초안 선 아래.
   3차(오너 *"큰 메인 원은 선에 비해 너무 크고 · 옆 작은 점이 겹치고 · 안 올리면 안 보인다"*): 본체 지름 = 선의 3배(반지름 4.5 · 성격 색 + 검은 테) ·
   안쪽 밝은 점 · 고른 링(7 · 흰 테) · 슬롯 둘은 본체와 틈을 두고 **늘 은은하게**(테두리만) — 올리면 덧칠 층이 또렷하게(채움 + 밝은 테 · 잡는 커서).
   ⚠️ 포인터를 받지 않는다 — 누름 판정은 좌표다(`graphReroutes.reroutePartAt` · 칠 무대가 선 층을 숨겨도 그대로 집힌다).
   ⚠️ 테 굵기는 CSS 가 아니라 **속성**이다(`graphEdgeLayer` — 크기 계단마다 화면 1px 이 되게 · 축소 화면에서 그래프 1 = 화면 0.2px 라 안 보였다).
      CSS 에 stroke-width 를 두면 속성을 이긴다 — 여기 두지 말 것. */
.vlab-reroutes { pointer-events: none; }
.vlab-reroute-dot {
  --vl-edge-color: var(--vl-neutral);
  fill: var(--vl-edge-color);
  stroke: rgba(0, 0, 0, 0.6);
}
.vlab-reroute-inner { fill: rgba(255, 255, 255, 0.45); stroke: none; }
.vlab-reroute-ring { fill: none; stroke: #ffffff; }
.vlab-canvas.is-over-reroute { cursor: move; }
/* 슬롯(Comfy `RerouteSlot`) — idle = 테두리만 은은하게 · near = 둘레에 오면 진한 테 + 옅은 채움 · hover = 점 색 채움 + 흰 테. */
.vlab-reroute-slot { --vl-edge-color: var(--vl-neutral); fill: none; stroke: rgba(255, 255, 255, 0.4); }
.vlab-reroute-slot.is-near { fill: rgba(255, 255, 255, 0.18); stroke: rgba(255, 255, 255, 0.85); }
.vlab-reroute-slot.is-hover { fill: var(--vl-edge-color); stroke: #ffffff; }
.vlab-canvas.is-over-reroute-slot { cursor: crosshair; }
/* 드롭 링 — 끄는 선을 점에 놓을 수 있을 때(본체 지름의 두 배). */
.vlab-reroute-drop { fill: none; stroke: #ffffff; }

/* ── 그룹(2026-09-15 Phase 3 · 계약 §4-1 · `graphGroupLayer`) ─────────────────────────────────────────────
   ★ 월드의 **첫 자식** — 선·카드보다 아래. 레이어와 상자 몸통은 포인터를 **먹지 않는다**: 그룹 안 빈 곳에서도 마퀴·선택 해제·
     우클릭 "추가" 가 캔버스 그대로 돈다. 포인터를 받는 것은 제목바와 오른쪽 아래 손잡이 둘뿐이다.
   ⚠️ 크기 0 + overflow visible — 자식이 전부 absolute 라 레이어 상자는 의미가 없다(선 svg 와 달리 그리는 요소가 아니다). */
.vlab-groups { position: absolute; left: 0; top: 0; width: 0; height: 0; overflow: visible; pointer-events: none; }
.vlab-group {
  --vl-group-color: var(--vl-group-pale_blue);
  position: absolute;
  box-sizing: border-box;
  background: color-mix(in srgb, var(--vl-group-color) 12%, transparent);
  border: 2px solid color-mix(in srgb, var(--vl-group-color) 60%, transparent);
  border-radius: 6px;
  pointer-events: none;
}
.vlab-group[data-color="red"] { --vl-group-color: var(--vl-group-red); }
.vlab-group[data-color="brown"] { --vl-group-color: var(--vl-group-brown); }
.vlab-group[data-color="green"] { --vl-group-color: var(--vl-group-green); }
.vlab-group[data-color="blue"] { --vl-group-color: var(--vl-group-blue); }
.vlab-group[data-color="pale_blue"] { --vl-group-color: var(--vl-group-pale_blue); }
.vlab-group[data-color="cyan"] { --vl-group-color: var(--vl-group-cyan); }
.vlab-group[data-color="purple"] { --vl-group-color: var(--vl-group-purple); }
.vlab-group[data-color="yellow"] { --vl-group-color: var(--vl-group-yellow); }
.vlab-group[data-color="black"] { --vl-group-color: var(--vl-group-black); }
/* 선택 = 흰 테(노드 선택과 같은 토큰 — "고른 것은 흰 테" 한 어법). */
.vlab-group.is-selected { border-color: var(--vl-selected); }
/* 제목바 30px · 글자 15px(2026-09-17 오너 — 옛 18px 이름이 12px `그룹` 라벨보다 커서 카드 머리와 위계가 거꾸로였다). 채움은 상자와 같은 12% 위에 한 겹 더 — 제목바가 잡는 자리임을 보이게. */
.vlab-group-title {
  position: absolute; left: 0; top: 0; right: 0; height: 30px;
  display: flex; align-items: center; gap: 6px;
  padding: 0 10px; box-sizing: border-box;
  font-size: 15px; font-weight: 600; line-height: 30px; color: var(--vl-node-title);
  background: color-mix(in srgb, var(--vl-group-color) 22%, transparent);
  white-space: nowrap; overflow: hidden;
  cursor: grab; user-select: none;
  pointer-events: auto;
}
.vlab-group.is-pinned .vlab-group-title { cursor: default; }
.vlab-group-pin { font-size: 14px; flex: 0 0 auto; }
/* 종류 라벨 `그룹`(2026-09-17 오너) — 카드 머리 `.vlab-node-kind` 와 같은 위계: **라벨과 이름이 같은 크기**(카드 12/12 · 그룹은 상자가 커서 15/15) ·
   라벨은 굵게 · 색으로 구분 · 안 줄어듦 · 줄바꿈 없음. ⚠️ 이름을 라벨보다 크게 두지 않는다(오너 신고: "이름이 더 크고 라벨이 작다").
   색은 **그룹 색을 밝힌 것**: 카드는 종류색 글자이지만 그룹 색 아홉은 어두운 톤(#444·#3f789e…)이라 그대로 쓰면 제목바 위에서 안 읽힌다. */
.vlab-group-kind {
  flex: 0 0 auto; white-space: nowrap;
  font-size: 15px; font-weight: 600; line-height: 1;
  color: color-mix(in srgb, var(--vl-group-color) 45%, #ffffff);
}
.vlab-group-name { overflow: hidden; text-overflow: ellipsis; }
/* 빈 이름 = `이름 없음` 자리표시자 — 카드 제목(`.vlab-node-title.is-empty`)과 같은 흐림(2026-09-17 오너 "똑같이" · 옛 기울임 걷음). */
.vlab-group-name.is-empty { opacity: 0.45; font-weight: 500; }
.vlab-group-title-input {
  flex: 1 1 auto; min-width: 0; height: 24px;
  font: inherit; font-size: 15px; color: #ffffff;
  background: rgba(0, 0, 0, 0.55); border: 1px solid var(--vl-selected); border-radius: 4px; padding: 0 6px;
}
.vlab-group-resize {
  position: absolute; right: 0; bottom: 0; width: 16px; height: 16px;
  cursor: nwse-resize; pointer-events: auto;
  background: linear-gradient(135deg, transparent 50%, color-mix(in srgb, var(--vl-group-color) 60%, transparent) 50%);
}
/* ★ 제목바 버튼 둘(2026-09-17 · 오너 *"그룹도 펜이랑 접기 똑같은 매커니즘으로"*) — 맨 왼쪽 [▾/›] 접기 · **그 바로 오른쪽** [✎]→편집 중 [✓]
   (같은 날 오너 *"그룹은 특성상 제목바가 길 확률이 있으니까 펜을 접는 버튼 오른쪽에다가"* — 옛 판은 줄 끝 `margin-left:auto`).
   카드 머리 버튼(`.vlab-node-head-btn`)과 같은 모양·크기지만 **클래스는 따로**다(그룹 제목바는 18px 글자 줄이라 세로 자리가 다르다).
   ⚠️ 이름 버튼 자리는 왼쪽 접기 버튼만이 정한다 — 편집 중 입력칸(오른쪽)이 서도 자리가 같다(✎ 두 번 = 열고 저장). */
.vlab-group-btn {
  box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; width: 24px; height: 24px; padding: 0; margin: 0;
  font: inherit; font-size: 14px; line-height: 1; color: var(--vl-node-title);
  background: transparent; border: 0; border-radius: 4px;
  opacity: 0.75; cursor: pointer;
}
.vlab-group-btn:hover { opacity: 1; background: rgba(255, 255, 255, 0.14); }
.vlab-group-btn:focus-visible { opacity: 1; outline: 2px solid var(--vl-selected); outline-offset: -2px; }
.vlab-group-btn.is-active { opacity: 1; background: rgba(255, 255, 255, 0.18); }
.vlab-group-collapse { margin-left: -6px; }
/* 두 버튼은 한 묶음으로 붙인다(간격 6 → 2) — 라벨 `그룹` 과의 간격(6)이 버튼 사이보다 넓어야 "버튼 둘 · 이름 줄" 로 읽힌다. */
.vlab-group-rename { margin-left: -4px; }
.vlab-group-count { flex: 0 0 auto; font-size: 12px; opacity: 0.6; }
/* ⚠️ `display:inline-flex` 가 `hidden` 속성(UA display:none)을 이긴다 — 읽기 전용에서 버튼이 남지 않게 되돌린다. */
.vlab-group-btn[hidden], .vlab-group-count[hidden], .vlab-group[hidden] { display: none; }
/* 접힌 그룹 — 상자 = 제목바(높이는 JS 가 34px 로 적는다 · 저장된 h 는 펼친 크기 그대로). 몸통 채움이 없으니 제목바 아래 모서리도 둥글게. */
.vlab-group.is-collapsed .vlab-group-title { bottom: 0; height: auto; }
/* 접힌 그룹에 숨은 카드 — CSS 가 감춘다(카드는 만든다 · 카드 접기 §7-4 와 같은 근거). */
.vlab-node.is-group-hidden { display: none !important; }
/* 읽기 전용(일괄 실행 중 · 팀원 캔버스) — 그리기만 남는다. 포인터가 제목바를 지나 캔버스로 간다(JS 게이트와 이중 방어). */
.vlab-groups.is-readonly .vlab-group-title,
.vlab-groups.is-readonly .vlab-group-resize { pointer-events: none; cursor: default; }
/* ★★ 실행 중(내 캔버스 · `is-run-locked`)의 제목바는 포인터를 **받는다**(2026-09-17 오너 — 실행 중에도 [▾/›] 접기·[✎] 이름이 되고
   우클릭 [이름 바꾸기]·[접기]가 뜬다 · 그룹에는 생성 상태가 없다(결정 A안) · 판정 `videoLabHeadLock`). 끌기·고르기·두 번 줌은 JS 게이트(`graphGroupInteract` 의 `isReadOnly`)가 그대로 막는다.
   ⚠️ 손잡이는 여기 들지 않는다(실행 중에는 그려지지도 않는다 — `groupViewOf.resizable`) · 팀원 캔버스는 이 클래스가 안 붙어 오늘 그대로다. */
.vlab-groups.is-readonly.is-run-locked .vlab-group-title { pointer-events: auto; }

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

/* ── 노드 ──────────────────────────────────────────────────────────────── */
.vlab-node {
  /* 종류색 — 헤더 띠·헤더 배경 믹스·종류 이름이 읽는다(아래 `.vlab-node--<kind>`). 모르는 종류는 중립. */
  --vl-kind-color: var(--vl-neutral);
  position: absolute;
  width: 280px;
  background: var(--vl-node-body);
  border: 1px solid var(--vl-node-border);
  border-radius: var(--vl-node-radius);
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  cursor: grab;
  box-shadow: var(--vl-node-shadow);
}
/* ★ 커서가 곧 설명이다(2026-09-17 오너 신고 *"잡아 끌거나 확대되는 게 노드의 빈 회색 영역도"*) — 조작이 없는 자리는 전부 `grab`
   (카드 규칙을 자식이 물려받는다 · 버튼·입력·칸은 자기 커서가 이긴다 · 판정 정본은 `graphNodeGrab`).
   ⚠️ 잡을 수 없는 카드(고정 · 읽기 전용)는 잡히는 척하지 않는다 — `videoLabNodeView` 가 `is-grab-locked` 를 붙인다.
   ⚠️ 끄는 중(`graphNodeDrag` 가 문턱을 넘긴 뒤 붙이는 `is-dragging`)은 자식 커서까지 `grabbing` 으로 덮는다 — 글칸 위를 지나며
      글자 커서로 깜빡이면 "지금 무엇을 하고 있나" 가 흔들린다.
   ⚠️ 팬 모드(Space·H)의 커서가 이긴다 — 그때 카드는 끌리지 않고 화면이 끌린다(`graphNodeDrag.isPanActive`). */
.vlab-node.is-grab-locked { cursor: default; }
.vlab-node.is-dragging,
.vlab-node.is-dragging * { cursor: grabbing !important; }
.vlab-canvas.is-pan-mode .vlab-node,
.vlab-canvas.is-pan-mode .vlab-node * { cursor: grab !important; }
/* ★★ **조작 자리는 카드의 `grab` 을 물려받지 않는다**(2026-09-17 스테이징 실측 — 앵글 카드의 3D 뷰포트·아래 드롭다운 패널 위 커서가
   `grab` 인데 눌러 끌면 카드가 안 움직였다: `graphNodeGrab.grabZoneOf` 는 그 자리를 조작으로 판정한다). 판정과 **같은 목록**이다 —
   태그(`CONTROL_TAGS`) · 역할(`CONTROL_ROLES`) · `[data-nodrag]`·`[data-port]`·`[data-resize]`·`[data-edge-id]` · 편집 가능한 글.
   목록이 어긋나면 `videoLabNodeCursor.test.mjs` 가 JS 집합과 대조해 잡는다.
   ⚠️ `:where()` 로 **특이도 0** — 자기 커서를 가진 조작(`.vlab-angle-slider` ew-resize · 버튼 pointer · 포트 crosshair …)은 언제나 이긴다.
      여기서 정하는 것은 "자기 커서가 없는 조작이 카드의 grab 을 물려받지 않는다" 하나이고, 그 자손도 이 값을 물려받는다.
   ⚠️ 글 입력(`input`·`textarea`·편집 가능한 글)과 링크는 `auto`(브라우저가 글자 커서·손가락을 고른다) · 나머지는 `default`.
   ⚠️ 끄는 중(`is-dragging`)·팬 모드의 `!important` 는 그대로 이긴다. */
:where(.vlab-node) :where(
  select, option, button, video, audio, label,
  [data-nodrag], [data-port], [data-resize], [data-edge-id],
  [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="slider"], [role="switch"],
  [role="tab"], [role="menuitem"], [role="option"], [role="combobox"], [role="listbox"], [role="spinbutton"], [role="scrollbar"]
) { cursor: default; }
:where(.vlab-node) :where(input, textarea, a, [contenteditable]:not([contenteditable="false"])) { cursor: auto; }
/* ★ 종류색(계약 §2-2 · `--vl-kind-*`) — 클립(옛 cut 포함)은 한 색, 앵글(Phase 4)은 자기 색. 선 성격색(`--vl-role-*`)과 같은 계열. */
.vlab-node--text { --vl-kind-color: var(--vl-kind-text); }
.vlab-node--context { --vl-kind-color: var(--vl-kind-context); }
.vlab-node--image { --vl-kind-color: var(--vl-kind-image); }
.vlab-node--character { --vl-kind-color: var(--vl-kind-character); }
.vlab-node--location { --vl-kind-color: var(--vl-kind-location); }
.vlab-node--board { --vl-kind-color: var(--vl-kind-board); }
.vlab-node--clip, .vlab-node--cut { --vl-kind-color: var(--vl-kind-clip); }
.vlab-node--angle { --vl-kind-color: var(--vl-kind-angle); }
/* ★ 사람이 칠한 색(`LabNode.color` · 계약 §2-3) — 헤더·몸통 쌍만 바꾸고 종류색 띠는 남긴다(종류 구분 유지). */
.vlab-node[data-color="red"] { --vl-node-head: var(--vl-color-red-head); --vl-node-body: var(--vl-color-red-body); }
.vlab-node[data-color="brown"] { --vl-node-head: var(--vl-color-brown-head); --vl-node-body: var(--vl-color-brown-body); }
.vlab-node[data-color="green"] { --vl-node-head: var(--vl-color-green-head); --vl-node-body: var(--vl-color-green-body); }
.vlab-node[data-color="blue"] { --vl-node-head: var(--vl-color-blue-head); --vl-node-body: var(--vl-color-blue-body); }
.vlab-node[data-color="pale_blue"] { --vl-node-head: var(--vl-color-pale_blue-head); --vl-node-body: var(--vl-color-pale_blue-body); }
.vlab-node[data-color="cyan"] { --vl-node-head: var(--vl-color-cyan-head); --vl-node-body: var(--vl-color-cyan-body); }
.vlab-node[data-color="purple"] { --vl-node-head: var(--vl-color-purple-head); --vl-node-body: var(--vl-color-purple-body); }
.vlab-node[data-color="yellow"] { --vl-node-head: var(--vl-color-yellow-head); --vl-node-body: var(--vl-color-yellow-body); }
.vlab-node[data-color="black"] { --vl-node-head: var(--vl-color-black-head); --vl-node-body: var(--vl-color-black-body); }

/* ★ 관계의 층을 **카드에도** 입힌다(2026-08-24 — `graphFocusTiers`). 선만 갈라 놓으면 사람은 선을 눈으로
   따라가 카드를 찾아야 한다 — 카드가 자기 층을 말하면 그 걸음이 사라진다.
   ⚠️ 2026-09-15 부터 층은 **외곽선 굵기·투명도로만** 말한다(계약 §2-3) — 옛 `is-flow` 보라는 걷었다(색은 타입의 것이다).
     `is-lit`  직접 닿은 카드   — 흰 1px 외곽선
     `is-flow` 아래로 흐른 카드 — 외곽선 없이 조금 옅게(.85)
     `is-mute` 관계 밖         — 옅게. ⚠️ 지우지 않는다(구조는 계속 보여야 한다)
   ⚠️ 선택·실패 규칙이 **뒤에** 와야 이긴다(같은 특이도 — 고른 카드가 층 테로 가려지면 그건 선택이 아니다).
   🔴 **화면에 실제로 그려지는 값은 여기가 아니다.** `body.shell-v2` 는 이제 항상 붙으므로(`web/app.js:1386`)
      `v2/canvas-contrast.css` ②-A 가 이 세 줄을 전부 덮는다(흐름 .8 · 관계 밖 .4 · 손 얹으면 1 로 복구).
      값을 고칠 일이 있으면 **그 파일을 고친다** — 여기만 고치면 아무것도 안 바뀐다.
      (2026-09-22: 그 파일이 세 단을 opacity:1 로 되돌려 놓아 흐림이 전 사용자에게서 사라졌던 회귀가 있었다.
       가드는 `videoLabFocusTierCss.test.mjs`.) */
.vlab-node.is-lit { outline: 1px solid rgba(255, 255, 255, 0.45); outline-offset: 1px; }
.vlab-node.is-flow { opacity: 0.85; }
/* ⚠️ 0.5 보다 더 낮추면 카드 안 글씨가 안 읽힌다(선택은 잠깐이지만 그 사이 캔버스를 못 읽으면 안 된다). */
.vlab-node.is-mute { opacity: 0.5; }
/* 선택 = 흰 2px 외곽선 · 실패 = 빨강 3px(계약 §2-3). outline 이라 그림자·레이아웃을 건드리지 않고 `.is-sized` 의 overflow 에도 안 잘린다.
   고른 실패 카드는 흰 테가 안쪽, 빨간 고리가 바깥 — 둘 다 보여야 한다. */
.vlab-node.is-selected { outline: 2px solid var(--vl-selected); outline-offset: 1px; }
.vlab-node.is-failed { outline: 3px solid var(--vl-error); outline-offset: 1px; }
/* 사전 검증 오류(계약 §6) — 실패(실선)와 갈라 **점선**. 다음 실행·그래프 변경까지 남는다. */
.vlab-node.is-invalid-run { outline: 3px dashed var(--vl-error); outline-offset: 2px; }
.vlab-node.is-invalid-run.is-selected { outline: 2px solid var(--vl-selected); box-shadow: 0 0 0 6px var(--vl-error), var(--vl-node-shadow); }
/* ★★ 접힘(2026-09-16 · 계약 §7) — **제목 줄만 남긴다**.
   ★ 왜 CSS 가 감추나(카드를 덜 만들지 않고): `renderNodeCard` 는 종류마다 갈래가 여덟이고 갈래마다 헤더에 줄을
     더 붙이는 곳이 있다 — 빌드 중간에서 끊으면 빠진 갈래가 **접었다 편 뒤에야** 드러난다(그 파일의 같은 주석).
   ⚠️⚠️ **남기는 셋은 제목 줄·포트·손잡이다.** 포트를 감추면 선이 붙을 자리가 사라지고(접힌 카드도 이어져 있다),
      손잡이는 접힌 카드에 애초에 안 달리지만(미생성) 선택자에서 빼 두는 것이 두 파일의 규칙을 같게 한다.
   ⚠️ `display: none` 이라 감춘 입력칸은 **탭 순서에서도 빠진다**(`visibility: hidden` 이면 남는다 — 안 보이는
      칸으로 포커스가 들어가면 캐럿이 사라진 것처럼 보인다).
   ⚠️ 높이는 **선언하지 않는다** — 제목 줄이 정한다(`videoLabNodeView` 가 접힌 카드에 `style.height` 를 안 쓴다).
      모델 쪽 상한은 `videoLabBoardCells.COLLAPSED_BOX_H`(80)이고 이 규칙의 실측 44 가 그 산식의 근거다. */
.vlab-node.is-collapsed > *:not(.vlab-node-head):not(.vlab-port):not(.vlab-node-resize) { display: none; }
/* 접힘 표식 — 2026-09-17 부터 **머리 오른쪽 [▸] 버튼**이 말한다(옛 `::before` 의 ▸ 는 걷었다 — 같은 뜻의 표식이 두 자리면
   하나는 누를 수 있고 하나는 못 누르는데 모양이 같다). */
/* 고정 표식 — 그룹 핀(`.vlab-group-pin`)과 같은 뜻, 카드 헤더의 톤. 상태 배지와 갈라 **무채색**이다:
   배지는 *생성이 어디까지 갔나* 를 말하고 이것은 *자리가 잠겼나* 를 말한다(다른 축을 한 얼굴로 그리지 않는다). */
.vlab-node-pin {
  flex: 0 0 auto;
  padding: 0 5px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(0, 0, 0, 0.35);
  color: #cfcfcf;
  font-size: var(--fs-fine);
  line-height: 1.6;
  white-space: nowrap;
}
.vlab-node.is-failed.is-selected { outline: 2px solid var(--vl-selected); box-shadow: 0 0 0 6px var(--vl-error), var(--vl-node-shadow); }
.vlab-node--text { width: 260px; }
/* ★ 클립은 넓다(2026-08-21) — 대사 편집기가 `[화자][대사][×]` 를 **한 줄**에 놓는다. 280px 에서는 대사
   칸이 100px 짜리 홈이 된다(오너: *"가로 사이즈가 문제면 클립 노드의 크기를 키워도 돼"*).
   ⚠️ 이 값은 `videoLabBoardCells.NODE_BOX_W` 의 거울이다 — 한쪽만 고치면 배치가 겹침을 못 본다.
   ★ 2026-10-06 부터 보드 위 클립 카드는 폭을 **인라인으로** 적는다(보드 화면비 — 세로 280 · 가로 480 · `videoLabClipCardHeight.cardBoxOf`).
     이 420 은 화면비를 모를 때(보드 밖)의 폴백이고 `CLIP_CARD_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 자식이 줄어들 수 있다(안 그러면 내용이 카드를 밀어낸다). */
/* 🔴 **`hidden` 이 포트의 바깥 반원을 잘랐다**(2026-09-21 오너 *"[포트] 원이 이렇게 잘리는 문제도 있어"*).
   포트는 카드 밖으로 7px 나가 있고(`.vlab-port` left/right: -7px) 크기 손잡이도 3~4px 나가 있다 — `overflow: hidden`
   은 그 밖을 **그리지도, 누르게 하지도** 않는다(선택 outline 만 규칙상 안 잘렸다 · 위 `.is-selected` 주석).
   ⚠️ 고치는 길 셋 중 이것을 골랐다: ① 안쪽 래퍼로 감싸기 — `.vlab-node > .vlab-node-head` 를 읽는 규칙·테스트가
      셋 있어 카드 마크업을 못 바꾼다. ② 포트를 별도 층으로 — 카드마다 좌표를 다시 계산해야 한다(엣지 기하의 정본이
      흔들린다). ③ **`clip` + `overflow-clip-margin`** — 테두리 상자 밖 10px 까지만 살리고 그 너머는 그대로 자른다.
      DOM 도 선택자도 한 줄 안 바뀌고, 자르는 목적(고정 높이 카드에서 내용이 넘쳐 나오는 것)은 그대로 지킨다.
   ⚠️ `hidden` 을 **먼저** 적는다 — `clip` 을 모르는 브라우저는 그 줄에서 멈춰 오늘 동작 그대로가 된다(포트만 다시 잘린다). */
.vlab-node.is-sized { overflow: hidden; overflow: clip; overflow-clip-margin: 10px; }
.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-09-15 · 계약 §2-3): 높이 36(2026-09-17 · 옛 30 — 잡기 쉽게 · 버튼 26px 이 한 줄에 선다) · 왼쪽 4px 종류색 띠 ·
   배경 = 종류색 14% 믹스 · 카드 가장자리까지 꽉 채운다.
   ⚠️ 음수 margin 은 카드 padding(12px)의 거울이다 — `.vlab-node` 의 padding 을 바꾸면 여기도 바꾼다.
   ⚠️ 띠는 inset box-shadow — 요소를 더하지 않는다(카드 DOM 구조를 테스트들이 걷는다). */
.vlab-node > .vlab-node-head {
  position: relative;
  box-sizing: border-box;
  min-height: 36px;
  margin: -12px -12px 0;
  /* 왼쪽 6(옛 14) — 2026-09-17 부터 맨 왼쪽이 [▾/›] 버튼이다(버튼 26px 이 제 여백을 갖는다 · 아이콘은 띠에서 12px). */
  padding: 4px 5px 4px 6px;
  /* 간격 6(옛 8) — 버튼 둘이 한 줄에 서면서 280px 카드의 가장 붐비는 머리(고정·완료·낡음)가 넘쳤다(2026-09-17 실측). */
  gap: 6px;
  /* 순서 = DOM(`[▾] 종류 [📌] 제목 … [✎] [배지들]`) · 제목이 남는 폭을 먹어 ✎·배지를 오른쪽으로 민다. 제목 줄이 없는 카드(맥락)는
     왼쪽부터 붙는다(옛 `space-between` 은 종류 이름을 가운데로 띄웠다). */
  justify-content: flex-start;
  background: color-mix(in srgb, var(--vl-kind-color) 14%, var(--vl-node-head));
  border-bottom: 1px solid var(--vl-node-border);
  border-radius: calc(var(--vl-node-radius) - 1px) calc(var(--vl-node-radius) - 1px) 0 0;
  box-shadow: inset 4px 0 0 var(--vl-kind-color);
  color: var(--vl-node-title);
}
/* 머리 줄 안내(재료 카드 '평면도(GEO) · 칸 그림에 반영됩니다' · 맥락 카드 '… · 지난 초안') — 한 줄 말줄임(전문은 title) · 폭은 절반까지 · 제목보다 먼저 줄어든다(제목이 한 글자로 눌리지 않게).
   ⚠️ 줄바꿈되면 머리 줄이 두세 줄로 커져 텍스트 카드 높이 상한(`videoLabClipCardHeight.textCardHeightMax`)을 넘는다(2026-10-04 실측 271·307). */
.vlab-node > .vlab-node-head > .vlab-hint { flex: 0 4 auto; max-width: 50%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 카드 이름(`node.caption`) — 남는 폭을 먹고 한 줄 말줄임(전문은 title 속성). **줄어드는 것은 이 하나다.** */
.vlab-node-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 600;
  color: var(--vl-node-title);
}
/* ⚠️ 2026-09-17 — 제목 글자의 `cursor: text`·호버 배경을 걷었다(오너 신고). 글자는 **읽는 자리이자 카드를 잡는 자리**이고
   (커서는 카드의 `grab` 을 물려받는다), 이름은 제목 뒤 [✎] 버튼·F2·우클릭 메뉴로 고친다. */
/* ★★ 머리 버튼 둘(2026-09-17 오너) — 맨 왼쪽 [▾/› 접기·펴기](`videoLabNodeView.headFold`) · 제목 뒤·배지 앞 [✎ 이름 바꾸기](`headPen` ·
   편집 중엔 같은 자리 [✓ 이름 저장]). 자리는 **DOM 순서**가 정한다(`order` 없음 — Tab 순서와 화면 순서가 한 벌).
   ⚠️ **안 줄어든다**(`flex: 0 0 auto`) — 배지와 같은 규율(2026-09-16 `완료` 쪼개짐). 줄어드는 것은 제목 하나다.
   ⚠️ 누를 자리 26×26(최소 24 · 캔버스 축소에서도 잡히게) · 항상 보인다(호버에만 뜨면 처음 보는 사람에게는 없는 기능이다). */
.vlab-node-head-btn {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--vl-node-title);
  opacity: 0.7;
  cursor: pointer;
}
.vlab-node-head-btn:hover { opacity: 1; background: rgba(255, 255, 255, 0.12); }
/* ★ 생성 중 카드의 잠긴 [✎](2026-09-17 오너 결정 A안 · `videoLabHeadLock`) — 자리·크기는 그대로, **흐림 + 기본 커서**로 "지금은 안 눌린다" 를 말한다
   (이유는 툴팁 `생성 중에는 이름을 바꿀 수 없습니다`). ⚠️ 호버 배경을 켜지 않는다 — 눌리는 척하는 호버는 거짓말이다. */
.vlab-node-head-btn:disabled { opacity: 0.3; cursor: default; }
.vlab-node-head-btn:disabled:hover { opacity: 0.3; background: transparent; }
/* 이름 칸이 열린 동안의 [✓ 이름 저장] — ✎ 자리를 그대로 지키고(머리 줄이 안 흔들린다 · `headPen`) 켜진 모양으로 "누르면 저장" 을 말한다.
   ⚠️ 폭·여백을 바꾸지 않는다(켜진 모양은 색뿐) · 누를 수 있는 버튼이라 커서는 pointer 그대로다. */
.vlab-node-head-btn.is-active { opacity: 1; background: rgba(255, 255, 255, 0.16); }
.vlab-node-head-btn:focus-visible { opacity: 1; outline: 2px solid var(--vl-selected); outline-offset: -2px; }
/* 이름이 없는 카드 — **조용한** 자리표시자(이름은 선택이다. 카드가 이름을 요구하는 것처럼 보이면 안 된다). */
.vlab-node-title.is-empty { opacity: 0.45; font-weight: 500; }
/* 이름 칸 — 제목 줄 **그 자리에** 선다(카드가 길어지지 않는다). 글자 크기·굵기는 읽던 것과 같아야 자리가 안 튄다. */
input.vlab-node-title-input {
  box-sizing: border-box;
  height: 20px;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  color: #ffffff;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid var(--vl-selected);
  border-radius: 4px;
  padding: 0 4px;
  cursor: text;
}
input.vlab-node-title-input:focus { outline: none; }
/* ⚠️ **줄바꿈 금지**(오너 신고 2026-08-24: *"이 텍스트가 텍스하고 트로 두줄로 떨어지는 문제"*).
   머리는 flex 행이고 옆에 붙는 라벨(재료 카드의 `평면도(GEO) · …`)이 길어지면 플렉스가 이 종류
   이름부터 줄인다 — 두 글자에서 접히면 카드가 무엇인지 읽을 수 없다. 종류 이름은 **안 줄인다**. */
/* 종류 이름은 종류색으로 — 색만으로 구분하지 않는다는 계약(§2-2)의 글자 쪽. */
.vlab-node-kind { font-size: 12px; font-weight: 600; color: var(--vl-kind-color); white-space: nowrap; flex: 0 0 auto; }
/* 읽기 전용 카드는 [▾] 버튼이 없어 종류 이름이 머리 첫 자식이다 — 왼쪽 여백 6 만으로는 종류색 띠에 붙는다(headless 실측 x7). 옛 여백 14 에 맞춘다. */
.vlab-node > .vlab-node-head > .vlab-node-kind:first-child { margin-left: 8px; }
/* ★ 가장 붐비는 머리(고정 + 완료 + `설정·윗단 바뀜`)에서는 고정 표식이 **📌 하나로 줄어든다**(2026-09-17 headless 실측 —
   280px 이미지 카드에서 버튼 묶음이 카드 밖으로 27px 넘쳤다). 글자는 DOM·툴팁(`PINNED_TITLE`)에 그대로 있고 잘라 보일 뿐이다.
   ⚠️ 넓은 카드(클립·콘티 420)는 자리가 남아 줄이지 않는다 · 배지·버튼은 여전히 안 줄어든다(줄어드는 것은 제목 → 이 표식 순). */
.vlab-node:not(.vlab-node--clip):not(.vlab-node--cut):not(.vlab-node--board) > .vlab-node-head:has(> .vlab-badge--stale) > .vlab-node-pin {
  box-sizing: border-box;
  width: 24px;
  padding: 0;
  overflow: hidden;
  text-align: center;
  text-indent: 0;
  word-spacing: 40px;          /* 📌 뒤 글자를 상자 밖으로 민다 — 잘린 `고` 조각이 보이지 않게 */
}
/* 같은 머리에서 제목 **글자**(표시 상태)는 남는 폭이 몇 px 뿐이라 글자 조각만 비친다 — 이때는 감춘다(전문은 이름 칸·툴팁·메뉴가 든다).
   ⚠️ 이름 칸(`input`)은 감추지 않는다 — 고치는 중인 칸이 사라지면 안 된다. */
.vlab-node:not(.vlab-node--clip):not(.vlab-node--cut):not(.vlab-node--board) > .vlab-node-head:has(> .vlab-badge--stale):has(> .vlab-node-pin) > span.vlab-node-title {
  visibility: hidden;
}
/* 같은 머리에서 **이름 칸이 열리면** 칸이 10px 뿐이다(2026-09-17 headless 실측 — 순서 `[▾] 종류 📌 제목 [✓] 완료 설정·윗단 바뀜`).
   제목 **왼쪽**의 종류 이름·고정 표식을 잠시 걷어 칸을 넓힌다(실측 10 → 76px). ⚠️ 오른쪽(배지)은 안 걷는다 — [✓] 의 자리는 오른쪽 폭만이
   정하므로 왼쪽을 걷어도 [✎]→[✓] 가 **같은 자리**에 남는다(✎ 두 번 누르기 규율 · §2-3). 확정·취소로 칸이 닫히면 그대로 돌아온다.
   ★ 3차(2026-09-21)에서 **종류 아이콘(`.vlab-node-kind-ico`)도 같이 걷는다** — 이름 글자만 걷고 그림을 남기면 넓힌 칸을 16px+간격만큼 도로 잡아먹는다. */
.vlab-node:not(.vlab-node--clip):not(.vlab-node--cut):not(.vlab-node--board) > .vlab-node-head:has(> .vlab-badge--stale):has(> input.vlab-node-title-input) > :is(.vlab-node-kind-ico, .vlab-node-kind, .vlab-node-pin) {
  display: none;
}

/* ★ 이미지 카드 **종류** 줄(2026-09-16 · `videoLabNodeView.cardRoleRow`) — 몸통 첫 줄. 헤더에 세우지 않는 이유는 그 함수 주석.
   ⚠️ 셀렉트는 남는 폭을 다 먹지 않는다(`.vlab-model` 의 basis 0 을 덮는다) — 항목이 짧은 낱말 넷이라 넓으면 빈 상자로 읽힌다. */
.vlab-card-role { display: flex; align-items: center; gap: 6px; }
.vlab-card-role-label { font-size: 11px; opacity: 0.6; white-space: nowrap; flex: 0 0 auto; }
.vlab-card-role .vlab-card-role-select { flex: 0 0 auto; min-width: 104px; }

/* ★ 카드 머리의 **종류 아이콘**(3차 2026-09-21 · `videoLabNodeView` 의 `.vlab-node-kind` 바로 앞) — 종류 이름과 같은 색·같은 덩어리.
   ⚠️ 줄지 않는다(`.vlab-node-kind`·배지와 같은 규약 — 머리에서 줄어드는 것은 제목 하나다). v2 셸은 A 가 canvas-contrast.css 에서 덮는다. */
.vlab-node-kind-ico {
  display: inline-flex;
  align-items: center;
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  color: var(--vl-kind-color);
}
/* 맥락 카드만 종류 이름이 중립색이다(읽기 전용 사본) — 아이콘도 그 톤을 따른다. */
.vlab-node--context .vlab-node-kind-ico { color: var(--vl-node-title); opacity: 0.7; }

/* ★ 꼬리 컨트롤의 **낱말 라벨**(3차 · 클립/컷의 `화질`·`비트레이트`) — 값만 보이던 셀렉트가 무엇을 고르는지 말한다.
   ⚠️ 값·모양은 이미지 카드의 `종류` 라벨(`.vlab-card-role-label`)과 같다 — 같은 성격의 글이 두 크기로 서지 않게. */
.vlab-ctl-label { font-size: 11px; opacity: 0.6; 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 순서와 무관하게 맨 앞에 세운다 — 버튼 배선을 건드리지 않으려는 선택이다.
   ⚠️⚠️ **flex 두 줄은 foot(가로 flex) 안에서만**(2026-09-03). 같은 클래스가 카드 **직계 자식**으로도 선다
      (체인 헤드의 `이 씬을 다 만들면 …` 줄 — `appendChainExtras(card)`). 카드는 **세로** flex 라 거기서
      `flex-basis: 100%` 는 폭이 아니라 **높이의 100%** 이고, 카드에 고정 높이(`node.h`)가 걸리는 순간 그
      한 줄이 640px 가 되어 `order:-1` 로 맨 위에 서며 나머지 전부를 `overflow:hidden` 밖으로 밀어냈다 —
      2026-09-02 "AI 초안 첫 클립이 빈 상자" 의 진짜 원인이다(그날은 편집 카드의 높이를 버리는 우회로 덮었고,
      그 우회가 다음 날 "크기 조절이 되돌아온다 · 초안 카드가 작다" 두 신고를 낳았다). 고정 높이 없는 카드에서는
      basis 100% 가 내용 높이로 풀려 멀쩡했으므로 **단위·육안 검증 어디에도 안 잡혔다**. 타이포는 공용, 배치는 foot 만. */
.vlab-node-cost {
  font-size: 11px;
  opacity: 0.5;
  text-align: left;
  line-height: 1.3;
}
.vlab-node-foot > .vlab-node-cost {
  flex: 1 0 100%;
  order: -1;
}
/* ── 셀렉트 정본 ─────────────────────────────────────────────────────────
   ★ 이 탭의 **모든** 셀렉트가 `.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 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; }

.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 {
  --btn-gap: 3px; /* 크레딧 칩 구분선 간격이 이 gap 을 상쇄해 맞춘다(button.css 크레딧 토큰) */
  display: inline-flex;
  flex: 0 0 auto; /* ⚠️ 셀렉트가 길어질 때 **버튼이 줄어들지 않게**. 이게 없으면 '생성'이 찌그러진다. */
  align-items: center;
  gap: 3px;
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 8px;
  border: 1px solid 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%;
  /* ★ 포트는 중립색(계약 §2-2 · 2026-09-15 Phase 2 — 좌·우 하나 유지). 선 색이 종류쌍의 성격이라 포트 하나에 한 색을 주면 거짓이 된다. */
  --vl-port-color: var(--vl-port-neutral);
  background: var(--vl-port-color);
  border: 2px solid var(--vl-node-body);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6);
  cursor: crosshair;
  z-index: 1;
  /* 평소엔 조금 흐리게 두고 카드에 손이 오면 또렷해진다 — 점이 늘 도드라지면 카드가 시끄럽다. */
  opacity: 0.7;
  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(--vl-selected);
  opacity: 1;
  /* ⚠️ 면마다 margin 으로 중심을 잡으므로 scale 은 transform 을 덮지 않게 단독으로 쓴다. */
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--vl-port-color) 35%, transparent);
}
/* 스냅 반경 안이지만 연결이 안 되는 포트 — 붙는 시늉만 하고 거절하면 고장으로 읽힌다. */
.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); }

/* ── 노드 내부 ─────────────────────────────────────────────────────────── */
/* 🔴 **배지는 줄지도 접히지도 않는다**(2026-09-16 오너 신고 — 스크린샷: 제목이 긴 이미지 카드에서 `완료` 가
   `완`/`료` 두 줄로 쪼개졌다). 제목 줄은 flex 행이고 배지에 `flex` 선언이 없으면 기본값 `1 1 auto` 의 **축소**가
   걸린다 — 남는 폭이 모자랄 때 플렉스는 **줄일 수 있는 것부터** 줄이고, 두 글자짜리 배지는 그 순간 접힌다.
   ⚠️ 줄어야 하는 것은 **제목 하나**다(`.vlab-node-title` 이 `flex: 1 1 auto` + 말줄임을 진다) — 종류 이름
      (`.vlab-node-kind`)·고정 표식(`.vlab-node-pin`)도 같은 이유로 이미 `flex: 0 0 auto; white-space: nowrap` 이다.
   ⚠️ 접힌 카드(`.is-collapsed`)의 머리도 **같은 줄**이라 이 규칙 하나로 함께 고쳐진다(선택자를 나누지 않는다).
   ⚠️ 낡음 배지(`--stale`)·콘티 배지(`boardBadge`)도 같은 클래스를 쓴다 — 셋이 한 규칙을 지난다. */
.vlab-badge {
  flex: 0 0 auto;
  white-space: nowrap;
  font-size: 11px;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid transparent;
}
.vlab-badge--idle { opacity: 0.5; border-color: rgba(255, 255, 255, 0.12); }
.vlab-badge--running { color: var(--vl-status-running, #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-badge--stale { color: #ffcc66; border-color: rgba(255, 204, 102, 0.4); }
/* 사람이 취소한 카드(2026-10-30 · `LabNode.cancelledAt`) — 중립 회색(실패의 붉음이 아니다 · 대기보다 또렷하게). */
.vlab-badge--cancelled { color: rgba(255, 255, 255, 0.72); border-color: rgba(255, 255, 255, 0.28); }

/* 실행 큐 패널(계약 §6) — 왼쪽 아래(오른쪽 아래는 선 색 범례). */
.vlab-queue {
  position: absolute;
  left: 12px;
  bottom: 12px;
  z-index: 5;
  max-width: min(360px, calc(100% - 24px));
  max-height: 45%;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 8px;
  font-size: 12px;
  color: var(--vl-node-title, #dddddd);
  background: rgba(20, 20, 20, 0.92);
  border: 1px solid var(--vl-node-border, #3a3a3a);
  border-radius: 8px;
  box-shadow: var(--vl-node-shadow, 0 6px 18px rgba(0, 0, 0, 0.5));
}
.vlab-queue-toggle { all: unset; cursor: pointer; font-weight: 600; white-space: nowrap; }
.vlab-queue-list { list-style: none; margin: 0; padding: 0; overflow-y: auto; display: grid; gap: 2px; }
.vlab-queue-row { display: flex; align-items: center; gap: 8px; min-height: 22px; }
.vlab-queue-name { all: unset; cursor: pointer; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-queue-name:hover, .vlab-queue-name:focus-visible { text-decoration: underline; }
.vlab-queue-state { flex: none; opacity: 0.8; font-variant-numeric: tabular-nums; }
.vlab-queue-row[data-state="submitted"] .vlab-queue-state, .vlab-queue-row[data-state="submitting"] .vlab-queue-state { color: #8ab4ff; opacity: 1; }
.vlab-queue-row[data-state="done"] .vlab-queue-state { color: #6ee7a8; opacity: 1; }
.vlab-queue-row[data-state="failed"] .vlab-queue-state { color: #ff8a8a; opacity: 1; }
.vlab-queue-row[data-state="cancelled"], .vlab-queue-row[data-state="skipped"] { opacity: 0.55; }
.vlab-queue-note { flex: none; font-size: 11px; opacity: 0.6; }
.vlab-queue-cancel { all: unset; cursor: pointer; flex: none; font-size: 11px; padding: 1px 6px; border: 1px solid rgba(255, 138, 138, 0.5); border-radius: 4px; color: #ff8a8a; }

/* 확인 모달의 누를 수 있는 목록(사전 검증 오류 → 그 노드로 이동). */
.sm-items { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 4px; max-height: 200px; overflow-y: auto; }
.sm-item { all: unset; cursor: pointer; display: block; font-size: 12px; line-height: 1.4; padding: 4px 8px; border-left: 3px solid var(--vl-error, #ee3333); background: rgba(238, 51, 51, 0.08); }
.sm-item:hover, .sm-item:focus-visible { background: rgba(238, 51, 51, 0.16); }
.sm-item--plain { cursor: default; border-left-color: rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.04); }
.sm-item--plain:hover, .sm-item--plain:focus-visible { background: rgba(255, 255, 255, 0.04); }

.vlab-text {
  width: 100%;
  min-height: 120px;
  resize: vertical;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid 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: 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; }
/* 카드 얼굴의 클립 결과 — 프로젝트 화면비 틀 · 영상은 틀 안에 꽉(검은 띠 없음). */
/* (0,2,0) — 얼굴 결과 상한 220(`.vlab-face-media`)을 이긴다 · 높이를 정한 카드의 채우기(0,3,0)는 그대로 이긴다. */
/* ★ 2026-10-06 오너(*"세로 영상·가로 영상에 맞춰서 예쁘게"*) — 카드 폭이 화면비를 따른다(세로 280 · 가로 480 — 카드가 인라인으로 적는다 · `videoLabClipCardHeight.cardBoxOf`)
   그래서 틀 = 내용 폭 전체(세로 254×452 · 가로 454×255 — 좌우 빈 칸 0). 높이 상한 452 는 사람이 넓힌 세로 카드에서만 닿는다(틀 254 · 가운데).
   옛 판(10-04 선택 C)은 카드 420 고정 + 상한 420 이라 세로 틀이 236 폭으로 가운데 서고 양옆 79px 씩 비었다. */
/* 틀 안 영상·그림은 틀을 채운다 — 얼굴 결과 상한(`.vlab-face-media > .vlab-thumb` 220)·`.vlab-thumb`(300)을 특이도로 이긴다(0,3,0). */
.vlab-face-media.vlab-face-media--clip > :is(.vlab-thumb, video, img) { width: 100%; height: 100%; max-height: none; object-fit: contain; }
.vlab-face-media.vlab-face-media--clip { aspect-ratio: var(--vlab-face-ratio, 9 / 16); width: min(100%, calc(452px * (var(--vlab-face-ratio, 9 / 16)))); max-height: none; margin-inline: auto; }
/* 실측 길이 배지는 틀 **오른쪽 위** — 아래 모서리는 영상 재생 컨트롤(시크 바)이 덮는 자리라 둘이 겹쳤다(2026-10-06). */
.vlab-face-media--clip > .vlab-dur-tag { top: 6px; bottom: auto; }
.vlab-face-media--clip :is(video, img, .vlab-media-running) { width: 100%; height: 100%; max-height: none; object-fit: contain; }
/* ★ 배경 카드는 **가로**다(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-media-running { width: 100%; padding: 16px; box-sizing: border-box; }
/* 생성 중 뼈대(2026-09-17 — 스피너를 스켈레톤으로). 색은 skeleton.css 의 변수를 쓴다.
   ⚠️ 지속시간은 `RUN_ANIM_MS.shimmer` 와 같아야 한다(위상 고정 — videoLabRunStatus.js).
   🔴 **반짝임은 `transform` 으로 민다 — `background-position` 을 애니메이션하지 않는다**(오너 신고 2026-09-28 *"생성중에
      캔버스자체랑 노드들이 깜빡거리는 … 확대축소나 캔버스이동같은거하면 생기고"* · *"생성중일때 팝업을 열거나 뭘해도"* ·
      *"새로고침해도 그런 랜더링이 발생하면 문제는 똑같아"*). 배경 위치 애니메이션은 합성기가 못 돌리는 **매 프레임 다시
      칠하기**다 — 생성 중 카드마다 월드 층(`will-change: transform` 이라 통째로 한 층)의 타일을 초당 60번 무효화하고 다시
      래스터한다. 그 위에 줌·팬·팝업처럼 한 프레임에 일이 더 얹히면 래스터가 못 따라가 타일이 비어(검은 판) 깜빡였다.
      실측(이 앱을 목 모드로 헤드리스 크롬에 띄워 생성 중 카드 18장 · 조작 없이 3초): 래스터 작업 **초당 1,429 → 38**,
      메인 스레드 페인트 82 → 27ms/s · 팝업이 떠 있는 동안 2,142 → 44 · 카드 재생성·애니메이션 재시작은 전후 모두 0.
      옛 원형 스피너(`rotate`)는 합성기 애니메이션이었다 — 09-17 교체 때 그 성질을 잃은 것을 되찾는다.
      그래서 판(이 요소)은 **바탕색만** 들고, 반짝이는 띠는 `::after` 가 `translateX` 로 지나간다(모양은 옛 판 그대로:
      폭 200% 판의 한가운데 띠가 -50% → 150% 를 지난다 = 자기 폭의 -75% → 25%).
   ⚠️ 위상은 판에 인라인으로 심긴다(`anchorAnim`) — `::after` 가 `animation-delay: inherit` 로 받는다. */
.vlab-run-skel {
  display: block;
  position: relative;
  overflow: hidden;
  width: 100%;
  height: 96px;
  border-radius: var(--r-md, 11px);
  background-color: var(--skel-base, rgba(255, 255, 255, 0.06));
}
.vlab-run-skel::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 200%;
  pointer-events: none;
  background-image: linear-gradient(100deg, transparent 30%, var(--skel-hi, rgba(255, 255, 255, 0.12)) 50%, transparent 70%);
  animation: vlab-run-shimmer 1600ms ease-in-out infinite;
  animation-delay: inherit;
}
@keyframes vlab-run-shimmer {
  from { transform: translateX(-75%); }
  to { transform: translateX(25%); }
}
/* 팝업 상태 줄·행 안 — 글줄 하나 크기 */
.vlab-run-skel--inline { flex: 0 0 auto; width: 56px; height: 10px; border-radius: var(--r-xs, 5px); }
.vlab-running-label { font-size: 12px; font-variant-numeric: tabular-nums; color: #8ab4ff; }
.vlab-running-hint { font-size: 11px; opacity: 0.55; }
/* ★ 생성 중 안내(`node.error` — "생성을 준비하고 있습니다" 류)는 상자 흐름 안 · 기준선 아래(2026-10-16 · 옛 판은 틀 아래에 겹쳐 올려 "보통 4~7분" 을 덮었다).
   클립 틀은 화면비로 높이가 정해져 있어(카드 높이 = 배치 하한) 뼈대가 줄어 자리를 낸다 — 내용이 틀을 넘지 않는다. */
.vlab-media-running > .vlab-face-fail { align-self: stretch; text-align: left; }
.vlab-face-media--clip .vlab-media-running { overflow: hidden; }
.vlab-face-media--clip .vlab-media-running > .vlab-run-skel { flex: 0 1 96px; height: auto; min-height: 12px; }

/* ★ 생성 중 = **헤더 아래 2px 불확정 진행 바**(2026-09-15 · 계약 §2-3 — 옛 숨쉬는 테 `vlab-breathe` 대체).
   캔버스를 축소하면 11px 배지는 안 보인다 — 그래서 카드 폭 전체를 가로지르는 움직임이 필요하다.
   ⚠️ 외곽선은 이제 선택(흰 2px)·실패(빨강 3px)의 자리다 — 생성 중이 같은 자리를 숨쉬게 하면 두 신호가 섞인다.
   🔴 옛 판에서 배운 것(2026-09-10 · `videoLabRunFlicker.test.mjs`): 카드 규칙에 애니메이션을 두면 그 속성(box-shadow)을
      통째로 가져가 그림자·선택 테가 사라졌다. 그래서 **카드 규칙에는 애니메이션이 없고** 바는 의사 요소가 진다.
   ⚠️ 위상은 카드에 인라인으로 심긴다(`anchorAnim` · 재렌더에 되감기지 않게). 의사 요소는 JS 가 못 건드리므로
      `animation-delay: inherit` 사슬(카드 → 헤더 → ::after)로 내려보낸다 — 한 고리라도 빠지면 렌더마다 바가 되감긴다. */
.vlab-node.is-running {
  border-color: color-mix(in srgb, var(--vl-kind-color) 45%, var(--vl-node-border));
}
/* 🔴 바는 **`transform` 으로 지나간다**(2026-09-28 오너 신고 — 위 `.vlab-run-skel` 의 🔴 절과 같은 원인). 옛 판은
   `background-position` 을 움직여 생성 중 카드마다 매 프레임 헤더를 다시 칠했다. 이제 바는 카드 폭의 40% 짜리 띠이고
   `translateX(-100% → 250%)` 로 지난다 — 옛 판과 같은 궤적(왼쪽 밖 → 오른쪽 밖)이다.
   ⚠️ 띠가 헤더 밖으로 나가지 않게 헤더가 **가로만** 자른다(`overflow-x: clip` — 세로는 그대로라 헤더 아래 1px 에 걸친 바가
      잘리지 않는다 · `clip` 은 스크롤 상자를 만들지 않는다). 생성 중일 때만이다. */
.vlab-node.is-running > .vlab-node-head {
  animation-delay: inherit;
  overflow-x: clip;
}
.vlab-node.is-running > .vlab-node-head::after {
  content: '';
  position: absolute;
  left: 0;
  width: 40%;
  bottom: -1px;
  height: 2px;
  pointer-events: none;
  background-image: linear-gradient(90deg, transparent, var(--vl-kind-color), transparent);
  animation: vlab-headbar-slide 1.6s linear infinite;
  animation-delay: inherit;
}
@keyframes vlab-headbar-slide {
  from { transform: translateX(-100%); }
  to { transform: translateX(250%); }
}
/* ⚠️ 이 키프레임은 **초안 팝업의 대기 바**(`.vlab-imp-panel.is-waiting::before`)가 쓴다 — 캔버스 밖이라 위 무효화 문제와 무관하다.
   이동 거리는 background-position %(= (폭 − 40%) × p) — p −0.667 → 1.667 이면 바가 왼쪽 밖에서 오른쪽 밖으로 지나간다. */
@keyframes vlab-headbar {
  from { background-position: -66.667% 0; }
  to { background-position: 166.667% 0; }
}

/* 움직임을 줄여 달라는 설정은 지킨다 — 정보는 문구·경과 시간으로 이미 다 있다. */
@media (prefers-reduced-motion: reduce) {
  .vlab-run-skel::after,
  .vlab-node.is-running > .vlab-node-head::after,
  .vlab-progress--indet .vlab-progress-fill { animation: none; }
  /* 반짝임 띠는 걷는다 — 판의 바탕색만 남는다. */
  .vlab-run-skel::after { display: none; }
  /* 멈춘 바는 옅은 실선(헤더 폭 전체) — 생성 중이라는 사실은 남긴다. */
  .vlab-node.is-running > .vlab-node-head::after { width: auto; right: 0; transform: none; background-image: none; background-color: var(--vl-kind-color); opacity: 0.5; }
  .vlab-progress--indet .vlab-progress-fill { width: 100%; opacity: 0.35; }
}

/* ── 올리는 중(2026-10-19 오너 피드백 *"끌어다 넣었을때 … 로딩인디케이터를 넣고 ui도 신경쓰자"* · `videoLabNodeCommon.uploadingMedia`) ──
   고른 그림을 완료 카드와 **같은 틀**(`.vlab-face-media > img.vlab-thumb` · 그림 크기로 선다)에 흐리게 깔고 가운데 알약을 얹는다 —
   끝나면 같은 틀에 결과 그림이 앉아 카드 높이가 튀지 않는다. 진행 바는 생성 중 카드와 같은 `indetProgressBar` 다.
   ⚠️ 고리는 `transform: rotate`(합성기 애니메이션) — 배경 위치를 움직이지 않는다(`.vlab-run-skel` 의 🔴 절과 같은 이유). */
.vlab-media--uploading > .vlab-thumb { opacity: 0.4; filter: saturate(0.6); }
.vlab-media--uploading > .vlab-run-skel { height: 160px; }
.vlab-media--uploading > .vlab-progress { position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 1; background: rgba(255, 255, 255, 0.14); }
.vlab-upload-veil {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.32));
  pointer-events: none;
}
.vlab-upload-pill {
  display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px 6px 10px;
  border-radius: 999px; background: rgba(12, 14, 20, 0.78); border: 1px solid rgba(138, 180, 255, 0.35);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
}
.vlab-upload-pill > .vlab-running-label { color: var(--vl-status-running, #8ab4ff); font-weight: 500; white-space: nowrap; }
.vlab-upload-ring {
  flex: 0 0 auto; width: 14px; height: 14px; border-radius: 50%; box-sizing: border-box;
  border: 2px solid rgba(138, 180, 255, 0.25); border-top-color: var(--vl-status-running, #8ab4ff);
  animation: vlab-upload-spin 800ms linear infinite;
}
@keyframes vlab-upload-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .vlab-upload-ring { animation: none; border-color: var(--vl-status-running, #8ab4ff); opacity: 0.6; }
}

.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 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 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 rgba(255, 255, 255, 0.12);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
}
/* ★ [캔버스 설정] 팝오버(2026-10-19 · 계약 canvas-comfy-parity §7-8 A · `graphCanvasSettingsPanel`) — 메뉴와 같은 면·테두리(새 색 0).
   독 위 12px 에 뜬다(자리는 JS). 항목은 지금 선 모양 세그먼트 하나 + 저장 범위를 말하는 흐린 한 줄. */
.vlab-menu.vlab-canvas-settings { width: auto; min-width: 232px; max-width: 280px; padding: 12px; gap: 10px; }
.vlab-canvas-settings-title { font-size: 13px; font-weight: 600; color: #f2f2f5; }
.vlab-canvas-settings-note { font-size: 12px; line-height: 1.4; color: rgba(242, 242, 245, 0.55); word-break: keep-all; }
/* 카드 아이콘 단추의 호버 툴팁(2026-09-23 · `videoLabIconTip`) — body 에 하나. 메뉴와 같은 면·테두리를 쓴다(새 색 0).
   ⚠️ 클릭을 가로채지 않는다(pointer-events: none) — 툴팁이 단추 위에 겹쳐 떠도 누름은 단추로 간다. */
.vlab-icon-tip {
  position: fixed;
  z-index: 62;
  max-width: 240px;
  padding: 6px 9px;
  border-radius: 8px;
  background: rgba(24, 26, 34, 0.98);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  color: #f2f2f5;
  pointer-events: none;
}
.vlab-icon-tip-name { font-size: 12px; line-height: 16px; font-weight: 600; }
.vlab-icon-tip-desc { font-size: 11px; line-height: 15px; opacity: 0.7; margin-top: 2px; }
.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; }
/* ★ 유료 실행 항목의 크레딧 칩(2026-10-16 오너 "크레딧 써지는 거는 다 클릭할 때 알 수 있어야" · `graphContextMenu.withMenuCredits`) —
   라벨 한 줄 + 오른쪽 끝 칩(공용 `creditChip` · 라벨과 같은 색). 줄바꿈 금지 · 항목 높이는 그대로(`ITEM_H` 32) — 넘치면 라벨이 말줄임. */
.vlab-menu-item--credit:not(.vlab-menu-item--rich) { display: flex; align-items: center; gap: var(--btn-gap, 8px); white-space: nowrap; }
.vlab-menu-item--credit .vlab-menu-item-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 구분선·간격은 공용 `.credit-host > .credit-chip`(button.css) — 여기서는 오른쪽 끝으로만 민다(그 규칙의 margin-left 를 이겨야 해서 한 단 더 구체적). */
.vlab-menu-item .credit-chip.vlab-menu-credit { margin-left: auto; }
/* ★ 노드 색 견본 줄(2026-09-15 · `graphContextMenu.menuItemsFor` 의 `swatches`) — 라벨 한 줄 + 견본 다섯 개씩 두 줄.
   ⚠️ 높이는 `graphContextMenu.SWATCH_ROWS`(항목 세 줄)와 수동 동기화다(뷰포트 클램프 계산). 색은 토큰 쌍의 몸통색. */
.vlab-menu-swatches { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 6px 10px 8px; }
.vlab-menu-swatches-label { flex: 0 0 100%; font-size: 12px; opacity: 0.6; }
.vlab-menu-swatch {
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: transparent;
  cursor: pointer;
}
/* 색 없음 = 빈 칸에 빨간 사선(지우개의 관용 표기). */
.vlab-menu-swatch:not([data-color]) { background: linear-gradient(135deg, transparent 44%, #ee3333 44%, #ee3333 56%, transparent 56%); }
.vlab-menu-swatch:hover { border-color: #ffffff; }
.vlab-menu-swatch.is-on { outline: 2px solid #ffffff; outline-offset: 1px; }
.vlab-menu-swatch[data-color="red"] { background: var(--vl-color-red-body); }
.vlab-menu-swatch[data-color="brown"] { background: var(--vl-color-brown-body); }
.vlab-menu-swatch[data-color="green"] { background: var(--vl-color-green-body); }
.vlab-menu-swatch[data-color="blue"] { background: var(--vl-color-blue-body); }
.vlab-menu-swatch[data-color="pale_blue"] { background: var(--vl-color-pale_blue-body); }
.vlab-menu-swatch[data-color="cyan"] { background: var(--vl-color-cyan-body); }
.vlab-menu-swatch[data-color="purple"] { background: var(--vl-color-purple-body); }
.vlab-menu-swatch[data-color="yellow"] { background: var(--vl-color-yellow-body); }
.vlab-menu-swatch[data-color="black"] { background: var(--vl-color-black-body); }
/* 그룹 견본은 그룹색으로(노드 몸통색이 아니다 — 상자에 실제로 칠해질 색을 보여 준다 · `groupMenuItemsFor` 의 `swatchKind`). */
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="red"] { background: var(--vl-group-red); }
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="brown"] { background: var(--vl-group-brown); }
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="green"] { background: var(--vl-group-green); }
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="blue"] { background: var(--vl-group-blue); }
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="pale_blue"] { background: var(--vl-group-pale_blue); }
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="cyan"] { background: var(--vl-group-cyan); }
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="purple"] { background: var(--vl-group-purple); }
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="yellow"] { background: var(--vl-group-yellow); }
.vlab-menu-swatches--group .vlab-menu-swatch[data-color="black"] { background: var(--vl-group-black); }
/* ★ 빈 곳 드롭 메뉴(2026-09-15 Phase 2 · `graphContextMenu.dropMenuItemsFor`) — 성격 색 점 + 종류 이름 + 한 줄 설명 (+ 막힌 이유).
   ⚠️ 폭 256 은 `graphContextMenu.MENU_W_WIDE` 와, 설명·이유 줄 높이 16 은 `ITEM_DESC_H` 와 수동 동기화다(뷰포트 클램프). */
.vlab-menu--wide { width: 256px; }
.vlab-menu-item--rich { display: flex; flex-direction: column; gap: 1px; }
.vlab-menu-item-head { display: flex; align-items: center; gap: 8px; }
.vlab-menu-dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; background: var(--vl-neutral, #8a8a8a); }
/* ★ 점 = 만들 노드의 **종류 색**(2026-09-16 · 카드 머리 띠 `.vlab-node--<kind>` 와 같은 토큰 — 새 hex 없음). 클립(옛 cut 포함)은 한 색. */
.vlab-menu-dot[data-kind="text"] { background: var(--vl-kind-text); }
.vlab-menu-dot[data-kind="context"] { background: var(--vl-kind-context); }
.vlab-menu-dot[data-kind="image"] { background: var(--vl-kind-image); }
.vlab-menu-dot[data-kind="character"] { background: var(--vl-kind-character); }
.vlab-menu-dot[data-kind="location"] { background: var(--vl-kind-location); }
.vlab-menu-dot[data-kind="board"] { background: var(--vl-kind-board); }
.vlab-menu-dot[data-kind="clip"], .vlab-menu-dot[data-kind="cut"] { background: var(--vl-kind-clip); }
.vlab-menu-dot[data-kind="angle"] { background: var(--vl-kind-angle); }
/* 흐린 이유 줄(빈 곳 드롭의 빈 목록 · `graphContextMenu.dropMenuFor`) — 누를 수 없는 글. 줄바꿈(문의 줄)을 그대로 둔다.
   ⚠️ 줄 높이 16 · 위아래 6 은 `graphContextMenu.noteHeight` 와 수동 동기화(뷰포트 클램프). */
/* ★ 메뉴의 글 줄(흐린 이유 · 설명 · 막힌 이유)은 낱말 단위로 접는다(2026-09-16 · 거절 토스트와 같은 신고 부류) — `keep-all` 이 없으면
     256px 폭에서 한국어가 글자 중간(*"연 / 결"*)에서 끊긴다. 띄어쓰기 없는 긴 조각(메일 주소)은 `break-word` 가 넘칠 때만 자른다. */
.vlab-menu-note { font-size: 11px; line-height: 16px; padding: 6px 10px; opacity: 0.7; white-space: pre-line; word-break: keep-all; overflow-wrap: break-word; }
.vlab-menu-item-desc { font-size: 11px; line-height: 16px; opacity: 0.6; padding-left: 18px; word-break: keep-all; overflow-wrap: break-word; }
.vlab-menu-item-reason { font-size: 11px; line-height: 16px; color: #ffb4a8; padding-left: 18px; word-break: keep-all; overflow-wrap: break-word; }
.vlab-menu-item:disabled { cursor: default; }
.vlab-menu-item:disabled .vlab-menu-item-head { opacity: 0.45; }
.vlab-menu-item:disabled:hover { background: transparent; }
.vlab-menu-item:focus-visible { outline: 2px solid var(--vl-selected, #ffffff); outline-offset: -2px; }
/* ★ 연결 판정 이유 토스트(`videoLabView.showLinkReasonToast` 가 `cls` 로 붙인다) — 두 문장 사이 `\n` 을 살리고 낱말 단위로 접는다
     (2026-09-16 오너 *"문장 줄바꿈이 좀 필요할 듯"*). ⚠️ 공용 `.app-toast`(components.css)는 그대로 — 이 클래스를 받은 토스트만이다. */
.app-toast.vlab-toast-lines .app-toast-text { white-space: pre-line; word-break: keep-all; overflow-wrap: break-word; }
/* 구분선(툴바 [＋ 추가 ▾]의 `빈 그룹` 위) — 높이 9(4+1+4)는 `graphContextMenu.ITEM_SEP_H` 와 수동 동기화. */
.vlab-menu-sep { height: 1px; margin: 4px 6px; background: rgba(255, 255, 255, 0.1); flex: none; }
/* 성격 점 없는 설명·이유 줄(`opts.noDots`) — 점 자리(18px) 들여쓰기를 걷는다. */
.vlab-menu-item--nodot .vlab-menu-item-desc,
.vlab-menu-item--nodot .vlab-menu-item-reason { padding-left: 0; }

/* ── 선 성격 범례(캔버스 우하단 · 접이식 · `graphEdgeLegend`) ─────────────────────────────
   ⚠️ `.vlab-canvas` 에 붙는다(월드 아님) — 줌·팬을 따라 흐르지 않는다. 캔버스 조작(마퀴·팬)은 범례 위에서 시작하지 않는다(JS). */
.vlab-legend {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  font-size: 12px;
  color: var(--vl-node-title, #dddddd);
  pointer-events: auto;
}
.vlab-legend-toggle {
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--vl-node-border, #3a3a3a);
  background: rgba(20, 20, 20, 0.85);
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.vlab-legend-toggle:hover { border-color: rgba(255, 255, 255, 0.35); }
.vlab-legend.is-open .vlab-legend-toggle { border-color: rgba(255, 255, 255, 0.35); }
.vlab-legend-list {
  list-style: none;
  margin: 0;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--vl-node-border, #3a3a3a);
  background: rgba(20, 20, 20, 0.92);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  display: grid;
  gap: 5px;
}
.vlab-legend-row { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
/* 견본 = 짧은 선 한 토막(선과 같은 모양으로 읽히게). 색은 위 성격 표의 `--vl-edge-color`. */
.vlab-legend-line { width: 22px; height: 0; border-top: 3px solid var(--vl-edge-color, var(--vl-neutral, #8a8a8a)); border-radius: 2px; flex: 0 0 auto; }
.vlab-legend-line.is-dashed { border-top-style: dashed; }

/* ── 우하단 스택(미니맵 위 · 범례 아래) ─────────────────────────────────────
   ★ 왜 묶었나(2026-09-16 미니맵): 둘 다 우하단 12px 에 절대배치돼 있으면 **범례를 펼치는 순간** 목록이 미니맵 위로 자란다.
     한 세로 묶음으로 두면 높이가 변해도 간격(8px)이 지켜진다 — 자리 계산을 사람이 손으로 맞추지 않는다.
   ⚠️ 묶음 자신은 포인터를 받지 않는다 — 빈 틈을 누르면 그대로 캔버스(마퀴·빈곳 메뉴)여야 한다. 자식만 받는다. */
.vlab-corner-br {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;
}
/* 범례는 혼자서도 살 수 있어야 한다(자기 절대배치를 그대로 둔다) — 묶음 안에서만 흐름 배치로 내려앉는다. */
.vlab-corner-br > .vlab-legend { position: static; right: auto; bottom: auto; z-index: auto; }

/* ── 선택 도구상자(계약 §7-5 · 2026-09-16 · `graphSelectionToolbox.js`) ──────────────
   ★ 고른 것 **위**에 뜨는 한 줄짜리 막대. 자리는 JS 가 화면 좌표로 쓴다(`left`·`top`).
   ⚠️ `position: fixed` 다 — 캔버스(`.vlab-canvas`)는 `overflow:hidden` 이고 월드는 변환되지만, 이 막대는 둘 중
      어느 쪽에도 매이지 않아야 한다(마퀴 상자가 같은 이유로 화면 좌표를 쓴다). 캔버스에는 **변환이 없다**.
   ⚠️ 뿌리는 포인터를 받지 않는다 — 버튼 사이의 빈 틈은 그대로 캔버스(마퀴·팬·빈 곳 메뉴)여야 한다. 자식만 받는다.
   ⚠️ z-index 는 범례·미니맵(5) 위, 우클릭 메뉴(60) 아래다 — 메뉴가 열리면 막대를 덮는 것이 옳다(메뉴가 더 나중의
      의도이고, 둘이 같은 항목을 겹쳐 보여 주면 어느 쪽을 누를지 망설이게 된다). */
.vlab-seltool {
  position: fixed;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px;
  border-radius: 10px;
  border: 1px solid var(--vl-node-border, #3a3a3a);
  background: rgba(20, 20, 20, 0.95);
  box-shadow: var(--vl-node-shadow, 0 6px 18px rgba(0, 0, 0, 0.5));
  color: var(--vl-node-title, #dddddd);
  font-size: 12px;
  white-space: nowrap;
  pointer-events: none;
}
.vlab-seltool > * { pointer-events: auto; }
.vlab-seltool-btn {
  padding: 4px 9px;
  border-radius: 6px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.vlab-seltool-btn:hover { background: rgba(255, 255, 255, 0.08); }
.vlab-seltool-btn--danger { color: #ff8a8a; }
.vlab-seltool-btn:disabled { cursor: default; opacity: 0.45; }
.vlab-seltool-btn:disabled:hover { background: transparent; }
.vlab-seltool-btn:focus-visible { outline: 2px solid var(--vl-selected, #ffffff); outline-offset: -2px; }
/* 맨 왼쪽 `N개 선택`(2026-09-17 오너 — 항목마다 되풀이하던 개수를 한 번으로) · 누르는 것이 아니라 흐린 라벨 + 세로 구분선. */
.vlab-seltool-count { padding: 0 6px 0 8px; opacity: 0.6; }
.vlab-seltool-sep { align-self: stretch; width: 1px; margin: 3px 2px; background: rgba(255, 255, 255, 0.12); }
/* 색 견본 줄 — 메뉴와 **같은 견본 클래스**(`.vlab-menu-swatch`)를 쓴다. 여기서는 한 줄로 눕고 조금 작다. */
.vlab-seltool-swatches {
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 0 6px;
  border-left: 1px solid rgba(255, 255, 255, 0.12);
  border-right: 1px solid rgba(255, 255, 255, 0.12);
}
.vlab-seltool-swlabel { opacity: 0.6; margin-right: 2px; }
.vlab-seltool-swatches .vlab-menu-swatch { width: 14px; height: 14px; }

/* ── 미니맵(계약 §7 · 2026-09-16) ────────────────────────────────────────
   ⚠️ 판은 `<canvas>` 한 장이다 — 팬·줌은 **페인트**이지 DOM 변경이 아니다(`videoLabPanFlicker` 의 그 불변식).
   ⚠️ 접으면 캐럿 버튼만 남는다. 켜고 끄는 설정은 없다(계약 §1) — 기억하는 것은 이 접힘 하나뿐이다. */
.vlab-minimap {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  pointer-events: auto;
}
/* ★ 접기는 판 안의 아이콘 버튼(2026-09-19) — 펼치면 판 오른쪽 위 모서리에 겹쳐 서고, 접으면 그 아이콘 하나만 남는다. */
.vlab-minimap-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--vl-node-border, #3a3a3a);
  background: rgba(20, 20, 20, 0.85);
  color: var(--vl-node-title, #dddddd);
  cursor: pointer;
}
.vlab-minimap.is-open .vlab-minimap-toggle {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 1;
  width: 24px;
  height: 24px;
  border-color: transparent;
  background: rgba(0, 0, 0, 0.45);
  opacity: 0.7;
}
.vlab-minimap-toggle:hover { border-color: rgba(255, 255, 255, 0.35); opacity: 1; }
.vlab-minimap-toggle:focus-visible { outline: 2px solid var(--vl-selected, #ffffff); outline-offset: 1px; opacity: 1; }
.vlab-minimap-canvas {
  display: block;
  border-radius: 8px;
  border: 1px solid var(--vl-node-border, #3a3a3a);
  background: var(--vl-canvas-bg, #141414);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  cursor: pointer;
  /* ⚠️ 판 위의 두 손가락 스크롤·핀치를 브라우저에 넘기지 않는다 — 넘기면 wheel 이 안 오고 페이지가 움직인다. */
  touch-action: none;
}
.vlab-minimap-canvas[hidden] { display: none; }

/* ── 마퀴 ──────────────────────────────────────────────────────────────── */
/* 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 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 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; }

/* 레퍼런스로 고른 타일 — 테두리로만 말한다(체크 아이콘을 얹으면 썸네일을 가린다). */
.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)); }
/* 클립 히스토리(2026-10-23) — 영상 타일은 단추 둘이 서야 해서 넓게 · 타일은 버튼이 아니라 커서 기본 · 지금 판은 `.is-on` 테두리. */
.vlab-img-panel.vlab-cliphist-panel { max-width: 960px; width: min(960px, 94vw); }
.vlab-img-grid.vlab-cliphist-grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--sp-sm, 12px); max-height: min(70vh, 720px); overflow-y: auto; }
.vlab-cliphist-tile { cursor: default; }
.vlab-cliphist-tile video { width: 100%; aspect-ratio: var(--cliphist-ratio, 9 / 16); max-height: 300px; object-fit: contain; background: #000; border-radius: var(--r-sm); display: block; }
.vlab-cliphist-acts { display: flex; flex-wrap: wrap; gap: var(--sp-xxs, 4px); padding: 0 var(--sp-xxs, 4px) var(--sp-xxs, 4px); }
.vlab-cliphist-acts > .btn { flex: 1 1 auto; }
/* 단추 글은 한 줄 — 모달 공용 규칙(canvas-modals.css `.btn__label { white-space: normal }`)이 '이 영상으로 교체'를 두 줄로 꺾었다(스테이징 데이터 캡처 2026-10-23). */
body.shell-v2 .sm-panel.vlab-cliphist-panel .vlab-cliphist-acts .btn__label { white-space: nowrap; padding-block: 0; }
.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-09-04) ────────────────────────────────────
   오너 신고: *"캔버스에서 배경이랑 캐릭터 누르면 선택해서 넣는 버튼이 없어서 캔버스에 넣을 수가 없어"*.
   옛 판은 타일 자체가 `<button>` 이라 **누르면 앉았다** — 동작은 있었고 화면에 그 사실이 없었다.
   ⚠️ 그래서 타일이 `<div>` 가 됐다(버튼 안의 버튼 금지) — `.vlab-img-tile` 의 `cursor: pointer` 는
      그대로 두어 **카드 전체 클릭이 여전히 손잡이**임을 말한다.
   ⚠️ 행동은 카드 **맨 아래**다(클립 목록과 같은 어법): 그림 → 이름 → 상태 → 행동. 위로 올리면 썸네일을
      가리고, 옆에 두면 240px 칸에서 이름이 잘린다. */
.vlab-pick-act { margin-top: auto; padding: var(--sp-xxs); }
.vlab-pick-act .vlab-pick-add { width: 100%; }
/* 잠긴 카드는 **왜 잠겼는지**가 라벨에 있다('캔버스에 있음') — 흐리게만 두면 고장으로 읽힌다. */
.vlab-pick-add[disabled] { opacity: 0.55; cursor: default; }
/* 참조로 안 쓰인다는 사실 — `.vlab-hint`(정보)와 색으로 갈린다(`.vlab-node-err` 주석의 그 규율). */
.vlab-img-tile-cap--warn { color: var(--warn, #e0b34a); opacity: 0.9; }

/* ── 고른 것을 말하는 줄 — 목록과 [넣기] 사이(2026-09-08) ─────────────────────
   오너: *"배경 고르기도 선택하면 뷰 고르는부분과 목록부분이 ui가 너무 헷갈려"*.
   옛 판은 뷰 세그먼트를 **격자 흐름 한가운데**(`grid-column: 1 / -1`)에 끼워 넣었다 — 카드1 · 뷰줄 ·
   카드2 로 그려지니 어느 카드의 뷰인지 안 보이고, 세그먼트가 목록 필터처럼 읽혔다.
   ⚠️ 그래서 격자 **밖**이다. 목록(`.vlab-img-grid` 는 자기 max-height 안에서 스크롤한다)이 아무리
      길어도 이 줄은 늘 [넣기] 바로 위 같은 자리에 있다.
   ⚠️ 고른 것이 없으면 이 상자는 **비어 있고**(자식 0) padding·테두리가 보이면 빈 상자가 뜬다 —
      `:empty` 로 통째로 지운다(문구 0건 규율의 CSS 판). */
.vlab-pick-bar {
  margin-top: var(--sp-sm);
  padding: var(--sp-xs);
  border-radius: var(--r-md);
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(0, 0, 0, 0.25);
  display: grid;
  gap: var(--sp-xxs);
}
.vlab-pick-bar:empty { display: none; }
/* 어느 배경의 뷰인지 — 이 줄의 부재가 곧 옛 판의 혼란이었다. */
.vlab-pick-bar-title { font-size: 12px; font-weight: 600; }
/* 뷰가 다섯이라 좁은 팝업에서는 접힌다(가로 스크롤을 만들지 않는다). */
.vlab-pick-bar .vlab-tabs { flex-wrap: wrap; }
.vlab-pick-bar .vlab-img-empty { padding: 0; }
/* 실제로 캔버스를 바꾸는 유일한 버튼 — 푸터 맨 오른쪽. 잠겼으면 아직 고른 것이 없다는 뜻이다. */
.vlab-pick-confirm[disabled] { opacity: 0.5; cursor: default; }
/* 피커 푸터는 버튼이 넷이다(만들기·새로고침·닫기·넣기) — `.sm-actions` 는 감싸지 않는 한 줄 flex 이라
   좁은 창에서 [넣기]가 밖으로 밀린다. 이 팝업 안에서만 접는다. */
.vlab-img-panel .sm-actions { flex-wrap: wrap; }
/* 이미 앉아 있는 (배경, 뷰) — 칸을 지우지 않고 **잠근다**: 지우면 다섯 칸의 자리가 흔들려 어느 뷰가
   빠졌는지 읽히지 않는다. */
.vlab-tab[disabled] { opacity: 0.4; cursor: default; }
.vlab-tab[disabled]:hover { color: inherit; }

.vlab-img-tile { position: relative; }

/* 확대 버튼 — 그림 위 오른쪽 위. 타일 클릭('고르기')과 **다른 의도**라 별도 손잡이로 둔다. */
.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-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 이 그 어긋남을 없앤다. */

/* ── 모달 레이아웃 ────────────────────────────────────────────────────────
   보드 이미지 목록이 주인공이고 업로드는 한 줄이면 된다.
   ⚠️ 예전에는 파일 입력이 큰 블록을 차지해 목록이 눌렸다(오너 신고 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 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; }
}
/* 한 열짜리 팝업(이미지 카드의 [고르기·올리기] · [다른 회차] — 2026-09-23 오너 "남은것들 진행해")은 폭도 한 열이다.
   두 열 틀을 그대로 쓰면 오른쪽 절반이 빈 채 창만 넓었다. */
.vlab-fill-panel.vlab-fill-panel--single { max-width: 640px; width: min(640px, 96vw); }
.vlab-fill-panel--single .vlab-fill-cols { grid-template-columns: 1fr; }
/* ── 어디서 고를까(이미지 노드 팝업만 · 2026-09-23) ────────────────────────────
   오너: *"이미지노드에서 이전회차에서 가져올수있는 기능을제공해야해 회차를 고르고 그회차에서
   다른 이미지 노드를 선택할수있게"*. 목록 **바로 위**에 붙인다 — 이 줄이 아래 목록의 뜻을 정하므로
   둘 사이에 다른 것이 끼면 무엇이 바뀐 것인지 안 보인다.
   ⚠️ 셀렉트는 공용 `.vlab-model` 이다(자기 위젯을 만들지 않는다) — 여기서는 남는 폭을 먹게만 한다. */
.vlab-fill-from { display: flex; align-items: center; gap: var(--sp-xs); margin: var(--sp-xs) 0; }
.vlab-fill-from-label { font-size: 11px; opacity: 0.6; white-space: nowrap; }
.vlab-fill-from .vlab-model { flex: 1 1 0; min-width: 0; }
/* 고를 다른 회차가 없을 때 셀렉트 자리의 한 줄(2026-09-23) — 오류가 아니라 사실이라 흐린 본문 글자다. */
.vlab-fill-from-none { font-size: 12px; opacity: 0.75; }
.vlab-img-box {
  padding: var(--sp-sm);
  border: 1px solid 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 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; }
.vlab-img-grp-caret { display: inline-flex; vertical-align: middle; margin-right: 4px; }
/* 콘티 칸·내역은 **세로 프레임**(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 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; }
.btn.btn--icon-only.vlab-img-chip-x {
  width: 20px; min-width: 20px; height: 20px; display: grid; place-items: center; padding: 0;
  background: none; border: 0; color: inherit; opacity: 0.55; cursor: pointer;
}
.btn.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); }
/* 크럼 줄의 **오른쪽 끝**([프로젝트 설정] — 오너 지시 2026-09-07). 왼쪽은 나가는 문, 오른쪽은 프로젝트 공통 설정.
   ⚠️ `.vlab-imp-crumb` 에 `justify-content: space-between` 을 주지 않는다 — 이 클래스는 초안 팝업의
      상태 줄들(`videoLabBoardDraftModal` 4곳)도 쓰는데 그쪽 조각들은 왼쪽에 붙어 있어야 읽힌다.
      미는 것은 **밀 버튼 자신**이다(자리를 아는 쪽이 자기 마진을 진다). */
.vlab-imp-crumb-right { margin-left: auto; }
.vlab-imp-crumb-text { font-size: var(--fs-fine); opacity: 0.75; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ★ 팝업이 기다리는 동안(2026-09-26 UI 코워크 — 오너 *"팝업 자체가 뭔가 불러오는 느낌"*) — 두 겹이 함께 선다.
   ① **패널 맨 위 3px 진행 바**: 캔버스 노드 생성 중 바(`vlab-headbar`)와 같은 움직임 · 상태 글과 같은 파랑. 의사 요소라
      레이아웃 높이 0(본문을 안 민다). 패널이 `overflow: hidden` + 둥근 모서리라 양 끝이 모서리를 따라 잘린다.
   ② 상태 줄의 뼈대 막대는 패널 유리 위에서 거의 안 보였다(.06/.12) — 팝업 상태 줄에서만 한 단 밝힌다.
   ⚠️ `is-waiting` 은 `syncStatus` 가 도는 줄을 세울 때만 붙인다(고르는 중 · 목록 만드는 중 · 클립 n/m 가져오는 중). */
.vlab-imp-panel.is-waiting::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 3px;
  z-index: 1;
  pointer-events: none;
  background-image: linear-gradient(90deg, transparent, #8ab4ff 50%, transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: vlab-headbar 1.4s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.vlab-imp-crumb .vlab-run-skel--inline {
  --skel-base: rgba(138, 180, 255, 0.14);
  --skel-hi: rgba(138, 180, 255, 0.45);
}
/* 고르는 중 줄은 **제목 줄 오른쪽에 뜬다** — 흐름 밖이라 끝날 때 본문이 올라가지 않는다(위 ①과 같은 이유 · 실측 42px).
   ⚠️ top/right 는 패널 안쪽 여백(20px)과 제목 줄 높이에 맞춘다 — 제목과 같은 줄에 서야 "이 팝업이" 기다리는 것으로 읽힌다. */
.vlab-imp-crumb--float {
  position: absolute;
  top: 20px;
  right: 20px;
  height: 20px;
  max-width: calc(100% - 160px);
  margin: 0;
  z-index: 1;
}
.vlab-imp-crumb--float .vlab-running-label { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (prefers-reduced-motion: reduce) {
  /* 멈춘 바는 옅은 실선 — 기다린다는 사실은 남긴다. */
  .vlab-imp-panel.is-waiting::before { animation: none; background-image: none; background-color: #8ab4ff; opacity: 0.45; }
}
.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); }
/* 회차에서 인물·배경 찾기 — 초안이 도는 동안의 진행 상자(2026-09-26 · `videoLabDraftProgress.js`).
   막대는 비확정(`.vlab-progress--indet` 재사용 — 거짓 %를 그리지 않는다) · 단계 한 줄 · 찾은 이름 한 줄 · 경과·기준선 한 줄. */
.vlab-draft-progress {
  display: grid; gap: var(--sp-xs); margin-top: var(--sp-sm);
  padding: var(--sp-sm); border-radius: var(--r-sm);
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.03);
}
.vlab-draft-progress-live { display: grid; gap: 2px; min-width: 0; }
.vlab-draft-progress-step { font-size: 13px; line-height: 1.45; }
.vlab-draft-progress-found { font-size: var(--fs-fine); line-height: 1.5; opacity: 0.8; overflow-wrap: anywhere; }
.vlab-draft-progress-found[hidden] { display: none; }
.vlab-draft-progress-meta { font-size: var(--fs-fine); opacity: 0.55; font-variant-numeric: tabular-nums; }

.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 rgba(255, 255, 255, 0.08);
  background: 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 rgba(255, 255, 255, 0.08);
  background: 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: 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축 전문이 들어가므로 팝업 안에서 스크롤되는 큰 칸이다. */
/* 상태 줄 — 왼쪽 = 상태·글자 수 · 오른쪽 끝 = [기본값으로 되돌리기](직접 고친 문구일 때만 선다). 글상자 바로 위라 두 끝이 글상자와 맞는다.
   높이는 작은 버튼(28px)에 맞춰 둔다 — 버튼이 서고 사라질 때 글상자가 위아래로 튀지 않게. 버튼 모양은 `button.css` 의 것 그대로(새 버튼 CSS 0). */
.vlab-set-state-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-sm); min-height: 28px; min-width: 0; }
.vlab-set-state { font-size: var(--fs-fine); opacity: 0.6; min-width: 0; }
/* 직접 고친 상태는 **눈에 보여야** 한다 — 기본값과 구별되지 않으면 "왜 내 톤이 안 먹지"의 원인이 숨는다. */
.vlab-set-state.is-custom { color: var(--accent, #5b8cff); opacity: 1; }
/* ── 프로젝트 설정 **폼**(`buildProjectSettingsForm` · 2026-10-12) ──
   행은 구획(`.vlab-set-sec` — 기본 · 그림 · 영상 · 화면 톤 문구)으로 묶인다. 팝업은 구획을 **패널에 직접** 쌓고(패널 gap·고정 발 그대로),
   1단계 화면은 묶음(`.vlab-set-form`)에 담아 넓으면 2열로 놓는다.
   ★ 위계는 세 층이고 간격도 세 값이다(오너 2026-10-12 *"어수선해보이는거같아"*): 구획 사이 --sp-lg(24) > 항목 사이 --sp-md(17) > 라벨–컨트롤 --sp-xs(8).
   ⚠️ 간격은 한 곳이 정한다: 구획 gap 이 있으므로 행·힌트의 자기 margin 은 0(`.sm-panel > .vlab-new-row` 와 같은 규칙).
      매체·화풍 묶음(`.vlab-look`) 안의 두 행도 같은 항목 간격을 쓴다 — 묶음만 12px 이면 그 구획만 리듬이 다르다. */
.vlab-set-sec { display: flex; flex-direction: column; gap: var(--sp-md); min-width: 0; margin: 0; }
.vlab-set-sec > .vlab-new-row, .vlab-set-sec > .vlab-look > .vlab-new-row { margin-bottom: 0; gap: var(--sp-xs); }
.vlab-set-sec > .vlab-new-hint, .vlab-set-sec .vlab-new-row > .vlab-new-hint { margin: 0; }
.vlab-set-sec > .vlab-look { display: flex; flex-direction: column; gap: var(--sp-md); }
.vlab-set-sec > .vlab-look > .vlab-char-step-hint { margin: 0; }
.vlab-set-sec-title { margin: 0; padding-bottom: var(--sp-xs); border-bottom: 1px solid var(--hairline); font-size: var(--fs-body); font-weight: 600; line-height: 1.3; }
.sm-panel > .vlab-set-sec + .vlab-set-sec { margin-top: var(--sp-xs); }
/* 직접 고친 문구를 둔 채 매체를 바꿨다 — 흘려 읽히면 안 되는 한 줄(다음 생성이 앞 매체의 문구로 나간다). */
.vlab-set-sec > .vlab-new-hint.vlab-set-media-note { color: var(--warn, #ffb454); opacity: 1; }
/* 폼 전체의 각주 한 줄("이미 만든 그림·영상은 다시 생성해야 바뀝니다" — 행마다 되풀이하던 것을 한 번으로). */
.vlab-new-hint.vlab-set-note { margin: 0; }
.vlab-set-form { display: flex; flex-direction: column; gap: var(--sp-lg); max-width: 800px; }
/* 읽기 전용(남의 프로젝트) — 묶음이 `inert` 로 잠겨 있다. 잠긴 것이 눈에도 잠겨 보여야 한다. */
.vlab-set-form.is-readonly { opacity: 0.6; }
/* 1단계 화면은 본문이 구르므로 화풍 격자를 제 높이로 편다 — 피커의 안쪽 스크롤 상한이 남으면 둘째 줄 이름표가 잘린다(2026-10-12 로컬 육안). */
.vlab-set-step .vlab-char-style-grid { max-height: none; overflow: visible; }
/* ★ 1단계 화면의 구획은 **카드**다(오너 2026-10-12 — 밑줄 제목만으로는 2열에서 네 구획의 경계가 흐렸다).
   ②③ 스튜디오의 작업면(`.vstu-canvas` — 옅은 면 · 둥근 모서리 · 테두리 없음)과 같은 어법이고 토큰만 쓴다(새 색·그림자 0).
   면이 경계를 말하므로 제목의 밑줄은 걷는다(경계 표시가 둘이면 다시 어수선하다). 팝업은 패널이 이미 면이라 카드를 겹치지 않는다(밑줄 제목 그대로). */
.vlab-set-step .vlab-set-sec { padding: var(--sp-lg); border-radius: var(--r-lg); background: var(--surface-1); }
.vlab-set-step .vlab-set-sec-title { padding-bottom: 0; border-bottom: 0; }
/* 1단계 화면의 틀(오너 신고 2026-10-12 화면 점검 — 폼이 왼쪽 끝 560px 에 붙어 오른쪽이 비고 세로로 길었다).
   본문 시작선은 단계 줄 첫 글자와 같다(--sp-xl · 좁으면 --sp-md — 머리 줄과 같은 810 경계).
   폭은 이 칸에서 잰다(왼쪽 패널·레일을 뺀 실제 폭): 좁으면 1열 읽기 폭(800), 960 부터 2열.
   ★ 2열 배치 = 왼쪽 **기본 · 영상**(짧은 것 둘) | 오른쪽 **그림**(화풍 타일로 긴 것) · 아래 **화면 톤 문구**는 두 열에 걸친 전폭.
     옛 배치(기본 | 그림 / 영상 | 화면 톤 문구)는 `그림` 이 길어 왼쪽 `기본` 아래가 크게 비었다(오너 2026-10-12).
     DOM 순서(= 목차 · 1열 · 탭 순서)는 기본 → 그림 → 영상 → 화면 톤 문구 그대로다 — 자리만 영역 이름으로 옮긴다.
   ⚠️ 가운데 행이 `1fr` 인 이유: `그림` 이 두 행에 걸치므로 남는 높이가 두 행에 나뉘면 `기본` 과 `영상` 사이가 벌어진다 — 남는 것은 `영상` 아래로 몰아 둔다. */
.vlab-set-step { flex: none; container-type: inline-size; padding: var(--sp-lg) var(--sp-xl) 48px; min-width: 0; }
@container (min-width: 960px) {
  .vlab-set-step > .vlab-set-form {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: auto 1fr auto;
    grid-template-areas: "basic look" "video look" "tone tone";
    gap: var(--sp-lg); align-items: start; max-width: 1200px;
  }
  .vlab-set-step > .vlab-set-form > .vlab-set-sec[data-sec="basic"] { grid-area: basic; }
  .vlab-set-step > .vlab-set-form > .vlab-set-sec[data-sec="look"] { grid-area: look; }
  .vlab-set-step > .vlab-set-form > .vlab-set-sec[data-sec="video"] { grid-area: video; }
  .vlab-set-step > .vlab-set-form > .vlab-set-sec[data-sec="tone"] { grid-area: tone; }
  .vlab-set-step > .vlab-set-form > :not(.vlab-set-sec) { grid-column: 1 / -1; }
}
@media (max-width: 810px) { .vlab-set-step { padding: var(--sp-md) var(--sp-md) 40px; } .vlab-set-step .vlab-set-sec { padding: var(--sp-md); } }
/* ── 사람 표시(videoLabPeople) — 사진이 없어 머리글자 + uid 색이다. 크기는 `--ava-size` 하나가 정한다. ── */
.vlab-ava {
  --ava-size: 28px;
  width: var(--ava-size); height: var(--ava-size); flex: none;
  display: inline-grid; place-items: center; border-radius: 50%;
  background: var(--ava-bg, #5a6275); color: #fff;
  font-size: calc(var(--ava-size) * 0.44); font-weight: 600; line-height: 1;
  user-select: none;
}
/* 담당자 — 강조색 테두리. 안쪽에 바탕색 틈을 둬 원과 테두리가 붙어 보이지 않게. */
.vlab-ava.is-ring { box-shadow: 0 0 0 1.5px var(--canvas, #0f0f17), 0 0 0 3px var(--accent, #5b8cff); }
/* 겹친 묶음 — 원마다 바탕색 테두리로 떼어 놓는다(겹친 가장자리가 한 덩어리로 뭉개지지 않게). */
.vlab-ava-stack { display: inline-flex; align-items: center; padding-left: 3px; }
.vlab-ava-stack > .vlab-ava { --ava-size: 22px; box-shadow: 0 0 0 2px var(--canvas, #0f0f17); }
.vlab-ava-stack > .vlab-ava + .vlab-ava { margin-left: -6px; }
.vlab-ava-stack > .vlab-ava.is-ring { box-shadow: 0 0 0 1.5px var(--canvas, #0f0f17), 0 0 0 3px var(--accent, #5b8cff); margin-right: 3px; }
.vlab-ava-more { background: var(--surface-3, rgba(255, 255, 255, 0.09)); color: var(--ink-dim, #9aa3b2); font-size: 10px; }

/* 프로젝트 설정의 협업자 섹션 — 머리(라벨·인원·저장 상태) · 담당자 줄 · 셀렉트(담당자) 또는 협업자 줄(협업자). */
.vlab-collab-head { display: flex; align-items: baseline; gap: var(--sp-xs, 8px); min-width: 0; }
.vlab-collab-count { font-size: var(--fs-fine); color: var(--ink-dim, #9aa3b2); }
.vlab-collab-status { margin-left: auto; font-size: var(--fs-fine); color: var(--ink-dim, #9aa3b2); transition: opacity 0.3s ease; }
.vlab-collab-status:empty { opacity: 0; }
.vlab-collab-status.is-done { color: var(--ink-dim, #9aa3b2); }
.vlab-collab-status.is-error { color: var(--danger, #e06a6a); }
.vlab-collab-people { display: grid; gap: 2px; }
.vlab-collab-person {
  display: flex; align-items: center; gap: 10px; min-width: 0; padding: 6px 2px;
}
.vlab-collab-person-text { display: grid; min-width: 0; flex: 1 1 auto; line-height: 1.3; }
.vlab-collab-person-name { font-size: 13px; font-weight: 600; color: var(--ink, #e8ecf4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-collab-person-email { font-size: 11px; color: var(--ink-dim, #9aa3b2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-collab-pill {
  flex: none; font-size: 11px; line-height: 1; padding: 4px 8px; border-radius: 999px;
  color: var(--accent, #5b8cff); background: rgba(91, 140, 255, 0.12); border: 1px solid rgba(91, 140, 255, 0.28);
}

/* 사람 멀티 셀렉트(videoLabPeopleSelect) — 칸은 `.vlab-cb-trigger` 치수, 팝업·검색·줄은 `.vlab-cb-*` 를 그대로 쓴다. */
.vlab-ps { display: grid; gap: var(--sp-xxs); }
.vlab-ps-field {
  display: flex; align-items: center; gap: 8px; width: 100%; min-height: 40px;
  padding: 5px 11px 5px 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-ps-field:hover { border-color: var(--accent, #5b8cff); }
.vlab-ps-field:focus-visible,
.vlab-ps-field.is-open { outline: none; border-color: var(--accent, #5b8cff); box-shadow: 0 0 0 2px rgba(91, 140, 255, 0.28); }
.vlab-ps-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; flex: 1 1 auto; min-width: 0; }
.vlab-ps-placeholder { font-size: 13px; opacity: 0.5; padding-left: 5px; }
.vlab-ps-chip {
  display: inline-flex; align-items: center; gap: 5px; max-width: 100%; height: 26px;
  padding: 0 2px 0 4px; border-radius: 999px; font-size: 12px;
  background: var(--surface-2, rgba(255, 255, 255, 0.06));
  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
}
.vlab-ps-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 140px; }
.vlab-ps-chip-x {
  width: 20px; height: 20px; display: grid; place-items: center; padding: 0; border-radius: 50%;
  background: none; border: 0; color: inherit; opacity: 0.5; cursor: pointer; font-size: 14px; line-height: 1;
}
.vlab-ps-chip-x:hover,
.vlab-ps-chip-x:focus-visible { opacity: 1; background: var(--surface-3, rgba(255, 255, 255, 0.09)); outline: none; }
.vlab-cb-count.is-warn { color: var(--warn, #ffb454); opacity: 1; }
.vlab-ps-list { gap: 2px; }
.vlab-ps-opt { justify-content: flex-start; padding: 6px 8px; }
.vlab-ps-opt:hover { background: var(--surface-1, rgba(255, 255, 255, 0.04)); }
/* 고른 줄은 테두리가 아니라 체크 표시로 말한다 — 여러 줄이 한꺼번에 테두리를 두르면 목록이 격자처럼 시끄럽다. */
.vlab-ps-opt.is-on { border-color: transparent; }
.vlab-ps-opt-text { display: grid; min-width: 0; flex: 1 1 auto; line-height: 1.3; }
.vlab-ps-opt-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-ps-opt-email { font-size: 11px; color: var(--ink-dim, #9aa3b2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vlab-ps-check { flex: none; width: 18px; text-align: center; color: var(--accent, #5b8cff); font-weight: 700; opacity: 0; }
.vlab-ps-opt.is-on .vlab-ps-check { opacity: 1; }
.vlab-ps-empty { padding: 10px 4px; margin: 0; }
.vlab-ps-err { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 4px; }
.vlab-ps-err .vlab-imp-error { padding: 0; }
.vlab-ps-retry {
  font: inherit; font-size: var(--fs-fine); padding: 4px 10px; border-radius: 7px; cursor: pointer; color: inherit;
  background: var(--surface-1, rgba(255, 255, 255, 0.04)); border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
}
.vlab-ps-retry:hover { border-color: var(--accent, #5b8cff); }
.vlab-set-style {
  width: 100%;
  box-sizing: border-box;
  min-height: 160px;
  max-height: 40vh;
  resize: vertical;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid 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 rgba(255, 255, 255, 0.08);
  background: 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 rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.03);
}
.vlab-cb-search {
  width: 100%; padding: 8px 11px; border-radius: 8px; font-size: 13px;
  border: 1px solid 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 rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.03);
}
/* 호버는 중립 테두리 — 선택(파란 테두리 + 안쪽 링)과 같은 색이면 두 줄이 다 고른 것처럼 보였다(2026-10-06 팀리드 시안 검토). */
.vlab-new-plan:hover { border-color: rgba(255, 255, 255, 0.24); }
.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; }
/* 새 캔버스 — 고른 회차의 시트 없는 에셋(2026-10-06 오너 넛지). 사실 두 줄만 · 경고색을 쓰지 않는다(막는 게 아니라 권하는 자리). */
.vlab-new-prep { margin-top: var(--sp-sm); padding: var(--sp-sm) var(--sp-md); border-radius: var(--r-md); background: var(--surface-1); display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
.vlab-new-prep[hidden] { display: none; }
.vlab-new-prep-hd { font-size: var(--fs-fine); color: var(--ink-48); }
.vlab-new-prep-row { display: flex; gap: var(--sp-sm); min-width: 0; }
.vlab-new-prep-k { flex: none; width: 4.5em; color: var(--ink-48); }
.vlab-new-prep-v { flex: 1 1 auto; min-width: 0; color: var(--ink-80); overflow-wrap: anywhere; }
/* 인물·배경 찾기의 "캔버스에 있음" 줄 — 고를 것이 없어 잠근다(2026-09-18 · D8). hover 강조도 끈다. */
.vlab-new-plan[aria-disabled="true"] { opacity: 0.55; cursor: default; }
.vlab-new-plan[aria-disabled="true"]:hover { border-color: rgba(255, 255, 255, 0.08); }
/* 초안이 도는 동안(인물·배경 찾기 — `is-locked`) — 읽고 있는 회차는 **선명하게** 남기고 나머지만 흐린다.
   커서는 `progress`(기다리는 중) — 옛 `default` 는 "못 누른다" 만 말하고 "기다린다" 는 말하지 않는다. */
.vlab-new-plan.is-locked { cursor: progress; }
.vlab-new-plan.is-locked.is-on { opacity: 1; }
.vlab-new-plan.is-locked.is-on:hover { border-color: var(--accent, #5b8cff); }
.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-10-05 오너 · 시안 승인): [캐릭터 | 배경] · 오른쪽 [프로젝트 설정](이름 ▾ 전환은 2026-10-08 걷었다).
   한 줄 flex 다 — 옛 격자(크럼 줄 + 단계 칩 줄 · 2026-08-27 ~ 10-04)는 단계 칩과 [← 프로젝트 목록]이 걷히며 줄이 하나가 됐다.
   설정은 이 줄 오른쪽 끝(`margin-left: auto` — 그 버튼의 클래스가 민다 · `videoLabCrumb.settingsButton` 머리주석). */
.vlab-tabs-wrap { max-width: 1080px; margin: 0 auto; padding: var(--sp-md) 24px 0; }
.vlab-tabs-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-sm); min-width: 0; } /* 좁은 폭(전환 + 세그먼트 + 설정이 안 들어가면)은 다음 줄로 — 가로 넘침 0 */
.vlab-tabs-bar > .vlab-imp-crumb-right { margin-left: auto; }
.vlab-tabs {
  display: inline-flex; gap: 2px; padding: 3px; border-radius: 10px;
  border: 1px solid 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: rgba(255, 255, 255, 0.06); border-color: var(--accent, #5b8cff); font-weight: 600; }
/* (단계 스텝퍼 `.vlab-steps`·`.vlab-step*` 와 단계 발 `.vlab-steps-foot*` 은 2026-10-05 오너 결정으로 걷었다 — 에셋 탭이 [캐릭터 | 배경], 영상 탭은 캔버스 목록만.) */

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

/* 썸네일 — 없으면 이니셜 판(빈 상자를 그리지 않는다). */
.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);
}

/* 그림이 커진 만큼 이니셜도 커져야 빈 카드가 초라해 보이지 않는다. */
.vlab-char-thumb .vlab-char-initial { font-size: 34px; }
/* ★ **자르기는 상자 모양이 정한다**(피커 타일 `.vlab-charpick-grid` · `videoLabCharacterPicker`·`videoLabCharacterModel` 이 이 주석을 근거로 가리킨다).
     · 시트 모양(16:9) 상자: 시트 = `cover` — 산출이 **1920×1088**(≈16:9)이라 잘리는 곳이 사실상 없다(1.765 vs 1.778).
       그 밖(레거시 세로 그림·직접 올린 그림) = `contain` — 세로 3:4 를 16:9 상자에 `cover` 로 넣으면 **위아래가 잘려 머리가 날아간다**.
     · 세로(3:4) 상자(이 공용 상자): 기본 `cover`, 가로 시트(`.is-sheet`)는 아래에서 `contain`(가운데만 남지 않게).
     · 판정은 `characterThumbKind` 한 곳이고, 클래스(`.is-sheet`)와 순서가 한 몸이다.
   ⚠️ 2026-08-20 캐릭터 목록용 16:9 상자(`--wide`) 규칙이 따로 있었으나, 그 격자가 2026-10-03 캐스트 스튜디오로 걷히면서
      고아가 돼 2026-10-04 지웠다(M24). 시트 모양 상자의 규칙은 지금 피커 타일에만 산다. */
.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; }

.vlab-char-thumb.is-empty { border: 1px dashed rgba(255, 255, 255, 0.18); }
.vlab-char-initial { font-size: 20px; font-weight: 700; opacity: 0.55; }

/* 이름이 카드의 **첫 번째 사실**이다 — 굵게, 상태는 흐리게(타이포 위계). */
.vlab-char-name {
  font-size: 14px; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ★ 대기 중은 **생성 중보다 옅다**(2026-08-28) — 같은 목록에 둘이 섞이므로 세기로 갈라야 "지금 도는 것"이
   먼저 읽힌다. 색은 같은 계열을 쓴다(다른 색을 주면 무관한 상태로 읽힌다).
   ⚠️ 이 배지는 문장이 길다(`대기 중 · 앞선 생성이 끝나면 시작`) — 알약이 카드 폭을 넘지 않게
      `white-space` 를 여기서만 푼다(공용 규칙은 nowrap 그대로). */

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

   ★ **간격 계약**(오너 지적 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; }

/* 힌트는 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; }

/*
 * ★★ 외형 설정 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-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 > summary { list-style: none; }
.vlab-char-adv > summary::-webkit-details-marker { display: none; }
.vlab-char-adv > summary::after {
  content: ''; display: inline-block; vertical-align: middle; width: 14px; height: 14px; margin-left: 4px;
  background: currentColor; transition: transform 0.15s ease;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
}
.vlab-char-adv[open] > summary::after { transform: rotate(180deg); }
.vlab-char-adv .vlab-title,
.vlab-char-adv .vlab-img-prompt { margin-bottom: var(--sp-xs); }

/* 인라인 안내(충돌·유효성·저장 결과) — 토스트로 내보내면 팝업 안의 맥락을 잃는다. 비어 있으면 자리도 없다.
   ⚠️ **버튼 줄과 같은 왼쪽 끝에서 시작하는 한 줄 배너**다: 예전에는 색만 있는 글줄이라 버튼과 어긋나
      떠 보였다(오너 지적 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-draft-main { min-width: 0; display: grid; gap: 2px; }

/* ── 매체 · 화풍 피커(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; }

/* ── 깨진 이미지 자리표시자 ────────────────────────────────────────────────
   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 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; }
/* ⚠️ 내려받기(2026-09-18)도 같은 줄의 버튼이다 — 줄바꿈 때 혼자 늘어나지 않게 같은 규칙을 받는다. */
.vlab-asset-edit, .vlab-asset-swap, .vlab-asset-dl { 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 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: var(--sp-xs); margin: var(--sp-xs) 0 var(--sp-xxs); }
/* 선택 요약 — 접힘과 무관하게 "몇 명 골랐나" 를 든다(2026-08-27 codex 지적 #10). */
.vlab-draft-castcount { font-size: var(--fs-fine); opacity: 0.72; }
/* 명단 제안이 앉기 전 — "0명 선택" 이 아니라 **고르는 중**이다(2026-09-23 오너 신고).
   ★ 2026-09-26 오너 재신고(*"로딩처럼 안 느껴진다 · 칩이 눌린다 · 고르기 전에 만들기가 눌린다"*): 1단 전체가 기다린다.
   ★ 같은 날 UI 코워크 2차 — 겉모양을 **한 목소리**로 다시 짰다(스테이징 하네스 실측):
     · 팝업 전체의 신호 = 패널 맨 위 3px 진행 바(`.vlab-imp-panel.is-waiting` · 아래) + 제목 줄 오른쪽 상태 글(`--float`).
       둘 다 흐름 밖이라 끝나도 본문이 안 움직인다(옛 판: 제목 아래 줄이 걷히며 본문이 42px 올라가 커서 밑 칩이 바뀌었다).
     · 칩은 **이름을 든 채** 흐리게 서고 그 위로 반짝임이 지나간다 — 명단은 이미 있고 기다리는 것은 "누가 켜지나" 뿐이다.
       옛 판은 이름을 지운 빈 알약이라 "명단을 불러오는 중" 으로 읽혔고, 끝날 때 이름이 한꺼번에 떠서 깜빡였다.
     · 흐림은 **한 겹**(.55)이다 — 옛 판은 컨테이너 .45 × 제 `:disabled`(.4 · glass.css `button:disabled` · `.vlab-tab[disabled]` ·
       `.vlab-clip-pick:disabled`)가 겹쳐 갈래 탭이 실효 .18 로 안 읽혔다(shell-v2 겹침 함정과 같은 부류). 잠긴 것의 제 흐림은 1 로 되돌린다.
     · 움직임은 **쓸고 지나가는 것 두 가지**(바 · 칩 반짝임)뿐 — 안내 줄의 원형 스피너는 걷었다(세 종류가 서로 다른 말을 했다).
   ⚠️ 잠금은 `disabled` + `aria-disabled` + 손잡이 가드가 진다 — 여기 `pointer-events` 는 겉모양의 보탬이다. */
.vlab-imp-body.is-cast-picking .vlab-draft-chips .vlab-draft-chip {
  pointer-events: none;
  cursor: progress;
  opacity: 1;
  background-image: linear-gradient(100deg, transparent 30%, var(--skel-hi, rgba(255, 255, 255, 0.12)) 50%, transparent 70%);
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: skel-shimmer 1600ms ease-in-out infinite;
}
.vlab-imp-body.is-cast-picking .vlab-draft-chips .vlab-draft-chip > *,
.vlab-imp-body.is-cast-picking .vlab-draft-chips .vlab-draft-chip::before { opacity: 0.55; }
.vlab-imp-body.is-cast-picking { cursor: progress; }
.vlab-imp-body.is-cast-picking .vlab-draft-mode,
.vlab-imp-body.is-cast-picking .vlab-draft-source,
.vlab-imp-body.is-cast-picking .vlab-clip-bulk--noprops,
.vlab-imp-body.is-cast-picking .vlab-draft-castfoot button {
  opacity: 0.55;
  pointer-events: none;
  cursor: progress;
}
/* 한 겹만 흐린다 — 잠긴 것들의 제 `:disabled` 흐림을 되돌린다(위 머리주석). */
.vlab-imp-body.is-cast-picking .vlab-draft-mode .vlab-tab[disabled],
.vlab-imp-body.is-cast-picking .vlab-clip-bulk--noprops .vlab-clip-pick:disabled { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .vlab-imp-body.is-cast-picking .vlab-draft-chips .vlab-draft-chip { animation: none; background-image: none; }
}

/* 긴 명단은 접는다(2026-08-27) — 32명 프로젝트에서 칩이 팝업 절반을 덮어 그 아래의 재료 경고와 유료
   버튼이 화면 밖으로 밀렸다. ⚠️ 줄이는 것은 **보이는 높이**뿐이다: 칩은 전부 DOM 에 남아야 제안이
   제자리에서 켤 수 있다(`applyCastSuggestion` 이 노드를 직접 만진다).
   아래를 흐리게 해 "더 있다" 를 모양으로도 말한다 — 갑자기 잘리면 그건 결함처럼 보인다. */
/* ⚠️ **정확히 두 줄에서 끊는다**(2026-08-27 codex 3차 #2). 옛 84px 은 세 번째 줄을 반쯤 남겨
   "잘린 것" 처럼 보였다 — 페이드도 마지막 12px 안에서만 진다(윗줄까지 흐리면 읽히던 이름이 사라진다).
   🔴 **칩이 커지면 이 수도 함께 커진다**(2026-09-04): 칩이 두 층(이름·상태)이 되면서 최소 높이가
   27px → 32px 가 됐고, 줄 간격도 6 → 8(`--sp-xs`)이다. 60px 그대로 두면 두 줄이 아니라 **한 줄 반**에서
   끊겨 두 번째 줄의 이름이 반쯤 잘린다 — 그것이 이 주석이 막으려던 바로 그 모양이다.
   칩 한 줄 32px · 줄 간격 8px → 두 줄 72px. */
.vlab-draft-chips.is-folded {
  max-height: 72px; 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);
}
/* 빼 둔 인물 줄은 요약 옆에 선다(칩이 안 움직이게 — `castMentionEl` 주석). 요약은 줄지 않고, 이 줄이 남은 폭을 쓴다. */
.vlab-draft-castfoot .vlab-draft-castcount { flex: 0 0 auto; line-height: 1.5; }
.vlab-draft-castfoot > .vlab-new-hint { flex: 1 1 auto; min-width: 0; margin: 0; }
/* ── 인물 칩(2026-09-04 재설계 · 코덱스 §3) ────────────────────────────────────
   ★ 칩은 **고르는 물건**이므로 켜짐/꺼짐을 색이 아니라 **모양**으로도 말한다: 왼쪽 16px 원형 표식이
     꺼졌을 때도 보이고(빈 원), 켜지면 accent 로 차며 ✓ 가 든다.
   🔴 옛 판은 켜짐을 테두리·틴트·글자색으로만 말했다 — 서른 개가 깔리면 무엇이 켜졌는지 훑어 읽기
     어렵고(색약이면 더), 꺼진 칩에는 "고를 수 있다" 는 표시가 아예 없었다.
   ⚠️ `align-items: baseline` 을 `center` 로 바꿨다 — 표식이 생기면 baseline 정렬은 원을 글자 밑동에
      맞춰 위로 튀어 오르게 한다.
   ⚠️ 최소 높이 32px — 손가락·마우스가 겨냥할 수 있는 바닥값(옛 칩은 27px 이었다). */
.vlab-draft-chip {
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: 6px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.035);
  color: inherit;
  /* ⚠️ 크기를 **여기서** 정한다: 이 클래스를 쓰는 세 자리 중 둘(2단 씬 탭 · 칸 인물 칩)은 이름 span 에
     클래스가 없어, 크기를 `-name` 에만 두면 그 둘이 본문 크기(17px)를 물려받는다. */
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
}
/* ★★ **선택 표식은 고르는 칩에만 단다**(`--pick`). 이 클래스를 쓰는 자리가 셋인데 하나(2단 씬 탭)는
   고르는 것이 아니라 **넘기는 것**이다 — 거기에 빈 원이 서면 "고를 수 있다" 는 거짓 신호가 된다.
   ⚠️ 새 이름은 **더하기만** 한다(기존 `.vlab-draft-chip` 은 소스 스캔·테스트가 보는 이름이다).
   ★ 꺼졌을 때도 **빈 원으로 보인다** — 고를 수 있다는 사실 자체가 표시다. */
.vlab-draft-chip--pick { padding-left: 8px; }
.vlab-draft-chip--pick::before {
  content: "";
  width: 16px; height: 16px; flex: 0 0 16px;
  display: grid; place-items: center; box-sizing: border-box;
  border: 1px solid rgba(255, 255, 255, 0.32); border-radius: 50%;
  color: #fff; font-size: 10px; font-weight: 800; line-height: 1;
}
.vlab-draft-chip:hover { border-color: rgba(255, 255, 255, 0.24); background: rgba(255, 255, 255, 0.055); }
/* 키보드로 오는 사람에게도 지금 어디인지 보여 준다 — 이 칩은 `button` 이라 탭으로 온다. */
.vlab-draft-chip:focus-visible {
  outline: none;
  border-color: var(--accent, #5b8cff);
  box-shadow: 0 0 0 2px rgba(91, 140, 255, 0.28);
}
/* 고른 상태를 **테두리 하나로만** 말하지 않는다(배경 틴트 + 표식 — 컷 타일에서 겪은 그 결함).
   ⚠️ 글자색은 accent 로 물들이지 않는다: 이름이 파래지면 이름 자체가 링크처럼 읽히고, 칩 안에서
      이름·상태의 층이 무너진다(색 역할 표 — accent 는 선택 표식과 테두리가 든다). */
.vlab-draft-chip.is-on {
  border-color: var(--accent, #5b8cff);
  background: rgba(91, 140, 255, 0.16);
  color: inherit;
}
.vlab-draft-chip--pick.is-on::before {
  content: "✓";
  border-color: var(--accent, #5b8cff);
  background: var(--accent, #5b8cff);
}
/* ⚠️ **이름이 주인공이다**(2026-08-27 codex 3차 #6): 서른 개가 깔리면 이름과 상태가 같은 무게로 섞여
   누가 고를 수 있는 사람인지 안 읽힌다. 상태는 뒤따르는 작은 글씨로 물린다. */
.vlab-draft-chip-name { font-size: 12px; font-weight: 600; line-height: 1.25; }
.vlab-draft-chip-meta { font-size: 10px; font-weight: 400; line-height: 1.2; opacity: 0.48; }
/* 켜진 칩에서는 상태도 한 단 또렷하게 — 고른 인물의 참조 사정이 가장 중요한 순간이다. */
.vlab-draft-chip.is-on .vlab-draft-chip-meta { opacity: 0.7; }
/* 시트 없는 인물의 칩 메타(2026-08-26) — 이 한 낱말이 "그 얼굴은 참조 없이 그려진다" 는 뜻이라
   흐린 회색으로 두면 옆 낱말과 구별되지 않는다(그게 옛 판이었다). 경고색이지만 칩 자체는 고를 수
   있다 — 막지 않는 것이 이 자리의 결정이다. */
.vlab-draft-chip-meta.is-missing { color: var(--warn, #ffb454); opacity: 0.9; }
/* 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 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 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;
}
/* ── 클립 목록(2026-09-04 재설계 · 코덱스 §2) ──────────────────────────────────
   오너: *"클립 목록 가져오는 화면 체크박스랑 UI 가 너무 못생겼어."*
   🔴 원인은 장식이 아니라 **층이 없었던 것**이다: 번호·제목·메타·결과·경고가 전부 11px 한 덩어리로
     쌓여 카드 하나가 고르는 화면이 아니라 **처리 로그**처럼 읽혔고, 강조색(accent)이 선택·성공·일반
     설명을 동시에 뜻해 무엇이 중요한지 색으로도 안 갈렸다.
   ★ 색 역할을 못박는다: **accent = 선택·주요 액션만** · `#6ee7a8` = 완료만 · `--warn` = 사람이 조치할
     예외만 · 그 밖의 안내는 상속색을 흐리게(58~78%). 값을 새로 짓지 말고 이 표를 따를 것.
   ⚠️ `--warn` 은 tokens.css 에 **없다** — 이 파일의 규약대로 언제나 폴백을 함께 적는다(2026-09-15 어디에도 없던 `--glass-*` 는 폴백 값으로 걷었다). */
.vlab-clip-rows { display: grid; gap: var(--sp-xs); margin-top: var(--sp-xs); }
/* 🔴 **열이 셋이다**(2026-09-04). 옛 값은 넷(체크 · 번호 · 본문 · 행동)이었고, 번호가 제목 위 눈썹으로
   올라가면서 하나가 줄었다. 행동은 이제 `.vlab-clip-actions` 한 열이 통째로 든다 — 상태마다 다른 자식을
   행에 바로 붙이던 옛 구조가 2026-08-31 의 그 결함(자식 수 ≠ 열 수 → [가져오기]가 다음 줄로 밀림)을
   만든 자리다.
   ⚠️ 발치(`.vlab-clip-issues`)는 **네 번째 자식이지만 `grid-column` 으로 제자리를 스스로 정한다** —
      그래서 열 수를 늘리지 않는다. 자식을 더 붙일 거면 그쪽도 같은 방식으로 놓을 것. */
.vlab-clip-row {
  display: grid; grid-template-columns: 20px minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--sp-sm); row-gap: var(--sp-xs);
  padding: var(--sp-sm);
  border-radius: var(--r-md, 11px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.035);
  transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
}
.vlab-clip-row:hover { border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.045); }
/* ★ **고른 상태를 카드가 든다**(옛 판은 16px 체크박스 하나가 유일한 표시였다) — 테두리 하나로만 말하지
   않고 채움 + 안쪽 링을 함께 준다(칩·컷 타일에서 이미 배운 것). */
.vlab-clip-row:has(.vlab-clip-pick:checked) {
  border-color: var(--accent, #5b8cff);
  background: rgba(91, 140, 255, 0.08);
  box-shadow: 0 0 0 1px rgba(91, 140, 255, 0.16) inset;
}
/* 이미 담은 행 — **전체를 흐리게 하지 않는다**: 제목은 여전히 읽혀야 다음에 무엇을 고를지 정한다. */
.vlab-clip-row.is-existing { border-color: rgba(110, 231, 168, 0.18); background: rgba(110, 231, 168, 0.035); }
.vlab-clip-row.is-blocked { border-color: rgba(255, 180, 84, 0.2); }
.vlab-clip-mid { min-width: 0; display: grid; gap: var(--sp-xxs); }
/* 눈썹 — 제목 **위**에 앉는 10px 표식이다(옛 판은 제목 앞자리를 차지해 자릿수마다 제목이 어긋났다). */
.vlab-clip-no {
  display: block; color: inherit;
  font-size: 10px; font-weight: 700; line-height: 1.2; letter-spacing: 0.04em;
  opacity: 0.5; white-space: nowrap;
}
/* 제목이 이 행의 주인공이다 — 14/650(옛 12px 은 메타와 같은 무게라 훑어 읽히지 않았다). */
.vlab-clip-title {
  overflow: hidden; color: inherit;
  font-size: 14px; font-weight: 650; line-height: 1.35;
  text-overflow: ellipsis; white-space: nowrap;
}
.vlab-clip-meta { color: inherit; font-size: var(--fs-fine); font-weight: 400; line-height: 1.4; opacity: 0.58; }
/* 결과 한 줄(`담음:`·예고) — **강조색을 뗐다**(2026-09-04 오너 결정). 사고도 할 일도 아니라 메타와 같은
   3차 층이다. 사람이 조치할 것만 `.is-warn` 으로 올라간다. */
.vlab-clip-note { color: inherit; font-size: var(--fs-fine); font-weight: 400; line-height: 1.45; opacity: 0.62; }
.vlab-clip-note.is-warn { color: var(--warn, #ffb454); font-weight: 500; opacity: 0.92; }
/* 발치 — 헤어라인 뒤에 **사람이 조치할 것만** 선다(`못 담음:` · `확인:` · 명단 밖 인물).
   ⚠️ 두 열을 가로질러 앉는다: 체크박스 열 아래로 들어가면 글이 20px 만큼 좁아진다. */
.vlab-clip-issues {
  grid-column: 2 / -1;
  display: grid; gap: var(--sp-xxs);
  margin: 0; padding: var(--sp-xs) 0 0;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  list-style: none;
}
.vlab-clip-issues > .vlab-clip-note { position: relative; padding-left: 16px; opacity: 0.78; }
/* 표식은 **자기 색을 갖지 않는다**(`currentColor`) — `확인:` 은 할 일이고 `못 담음:` 은 경고라, 여기서
   색을 박으면 둘의 무게가 같아진다(그 뭉개짐이 이 라운드가 걷어낸 바로 그 증상이다). */
.vlab-clip-issues > .vlab-clip-note::before {
  content: "!"; position: absolute; top: 1px; left: 0;
  color: currentColor; font-size: 11px; font-weight: 800;
}
/* 액션 열 — 버튼이든 배지든 **언제나 여기 하나**가 든다(위 격자 주석). */
.vlab-clip-actions { display: flex; align-items: center; justify-content: flex-end; min-width: max-content; }
.vlab-clip-busy { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
/* 완료 배지 — `#6ee7a8` 은 **완료에만** 쓴다(색 역할 표). */
.vlab-clip-done {
  display: inline-flex; align-items: center; min-height: 26px;
  padding: 0 var(--sp-xs);
  border: 1px solid rgba(110, 231, 168, 0.24); border-radius: 999px;
  background: rgba(110, 231, 168, 0.08);
  color: #6ee7a8; font-size: 11px; font-weight: 600; white-space: nowrap;
}
/* 자리가 없어 살 수 없는 행 — 잠긴 버튼 대신 **사실**을 적는다(잠긴 버튼은 "곧 될 것" 으로 읽힌다). */
.vlab-clip-unavailable { color: var(--warn, #ffb454); font-size: 11px; font-weight: 600; white-space: nowrap; }
/* 좁으면 행동을 아래로 내린다 — 셋을 한 줄에 욱여넣으면 제목이 먼저 잘린다(그 제목이 고르는 근거다). */
@media (max-width: 620px) {
  .vlab-clip-row { grid-template-columns: 20px minmax(0, 1fr); }
  .vlab-clip-actions,
  .vlab-clip-issues { grid-column: 2; justify-self: stretch; }
  .vlab-clip-actions > .btn { width: 100%; }
}

/* ⚠️ **읽기 전용 칩은 손가락 커서를 쓰지 않는다** — `.vlab-draft-chip` 은 고르는 칩에서 태어나
   `cursor: pointer` 를 들고 있고, 그대로 쓰면 못 누르는 것이 눌리는 것처럼 보인다(어포던스가 거짓). */
.vlab-draft-chip--static { cursor: default; }
.vlab-draft-chip--static:hover { border-color: rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.035); }

/* 클립 목록 일괄 실행(2026-08-31 · 2026-09-04 재설계) — 행의 체크박스와 머리줄이 **같은 폭**을
   차지해야 글자가 안 어긋난다. 2026-09-04 부터 제 카드로 서서 목록과 층이 갈린다(옛 판은 테두리 없는
   플렉스 줄이라 첫 행처럼 읽혔다). */
.vlab-clip-bulk {
  display: flex; align-items: center; gap: var(--sp-sm); flex-wrap: wrap;
  margin-bottom: var(--sp-xs);
  padding: var(--sp-sm);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--r-md, 11px);
  background: rgba(255, 255, 255, 0.025);
}
.vlab-clip-bulk-label { font-size: var(--fs-fine); font-weight: 600; opacity: 0.82; margin-right: auto; }
/* 상자와 글을 한 눌림 자리로(2026-10-03 · `videoLabBoardDraftModal` 전체 선택) — 줄 안의 자리는 옛 글(span)의 `margin-right:auto` 를 이 묶음이 받는다. */
.vlab-clip-bulk-pick { display: inline-flex; align-items: center; gap: var(--sp-sm); margin-right: auto; cursor: pointer; }
.vlab-clip-bulk-pick .vlab-clip-bulk-label { margin-right: 0; }
/* 소품·착장 줄(2026-09-23 오너 *"선택 4/7명이랑 너무 붙어있는거같음 다른 섹션인데 그리고 체크박스도 너무
   긴거같고"*) — 위 요약 줄과 **한 칸 띄우고**, 폭은 **글자만큼**이다. 전폭 카드는 목록 머리줄의 모양이다. */
.vlab-clip-bulk--noprops {
  display: inline-flex; flex-wrap: nowrap; max-width: 100%;
  margin-top: var(--sp-md, 16px); margin-bottom: var(--sp-xxs, 4px);
  padding: var(--sp-xs) var(--sp-sm);
  cursor: pointer;
}
.vlab-clip-bulk--noprops .vlab-clip-bulk-label { margin-right: 0; }
/* ★★ **체크박스를 직접 그린다**(2026-09-04 · 코덱스 §2-4). 옛 판은 네이티브 16px 에 `accent-color` 만
   얹은 것이라 이 팝업의 다른 무엇과도 안 닮았고(오너: *"체크박스가 못생겼다"*), 크기·포커스링·비활성
   모양을 브라우저가 각자 정했다. 18px · radius 5 · 체크 시 accent 채움 — 색 역할 표 그대로(선택=accent).
   ⚠️ `appearance:none` 은 **네이티브 체크 표시까지 지운다** — 그래서 `::before` 로 갈고리를 직접 그린다.
      그 줄을 지우면 켜진 칸이 파란 사각형만 되고 켜짐/꺼짐이 색 하나로만 구별된다. */
.vlab-clip-pick {
  width: 18px; height: 18px; flex: 0 0 18px; margin: 0;
  appearance: none; -webkit-appearance: none;
  display: grid; place-content: center; box-sizing: border-box;
  border: 1px solid rgba(255, 255, 255, 0.34); border-radius: 5px;
  background: rgba(0, 0, 0, 0.18);
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}
.vlab-clip-pick::before {
  content: ""; width: 9px; height: 5px;
  border: solid #fff; border-width: 0 0 2px 2px;
  transform: rotate(-45deg) scale(0); transform-origin: center;
  transition: transform 120ms ease;
}
.vlab-clip-pick:checked { border-color: var(--accent, #5b8cff); background: var(--accent, #5b8cff); }
.vlab-clip-pick:checked::before { transform: rotate(-45deg) scale(1); }
/* 반쯤 고른 상태(전체 고르기) — 가로줄 하나. 안 그리면 화면이 "아무것도 안 골랐다"고 적는 셈이다. */
.vlab-clip-pick:indeterminate { border-color: var(--accent, #5b8cff); background: rgba(91, 140, 255, 0.28); }
.vlab-clip-pick:indeterminate::before {
  width: 9px; height: 0; border-width: 0 0 2px 0; transform: none;
}
.vlab-clip-pick:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(91, 140, 255, 0.32); }
.vlab-clip-pick:disabled { cursor: default; opacity: 0.45; }
/* 고를 수 없는 행의 자리지킴 — 보이지 않지만 폭은 그대로다(격자가 흔들리지 않게). */
.vlab-clip-pick--off { visibility: hidden; }
@media (max-width: 620px) {
  .vlab-clip-bulk { align-items: flex-start; }
  .vlab-clip-bulk > .btn { width: 100%; }
}

/* ── 클립 목록 단의 머리·발(2026-09-04 · 코덱스 §2) ────────────────────────────
   ★ 되돌아가는 문은 **길잡이 컨트롤**이지 흐린 산문이 아니다 — 최소 높이를 주고 hover·focus 에서
     또렷해진다(옛 판은 본문 문단과 같은 무게라 눌러도 되는 것으로 안 읽혔다). */
.vlab-draft-back { min-height: 32px; padding-inline: var(--sp-xs); font-size: var(--fs-fine); opacity: 0.78; }
.vlab-draft-back:hover, .vlab-draft-back:focus-visible { opacity: 1; }
/* ★★ **갈래의 성질을 목록 위에서 한 번만 말하는 자리.** 옛 판은 이 문장을 행마다 붙여(`note.cards`)
   아홉 행이 같은 말을 아홉 번 했고, 정작 그 행에만 있는 사실이 그 반복 사이에 묻혔다.
   ⚠️ 경고가 아니다 — 상속색을 흐리게 둔다(색 역할 표: 일반 안내는 accent 도 warn 도 아니다). */
.vlab-draft-list-note {
  margin: 0 0 var(--sp-sm);
  padding: var(--sp-xs) var(--sp-sm);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--r-sm, 8px);
  background: rgba(255, 255, 255, 0.025);
  color: inherit; font-size: var(--fs-fine); line-height: 1.5; opacity: 0.72;
}
/* 감독 메모(2026-09-07 W2) — 목록 **위**에 서므로 아래 일괄 바와 붙지 않게 띄우기만 한다.
   ⚠️ 새 색·새 토큰을 만들지 않는다: 라벨·힌트·칸은 이미 `.vlab-new-label`·`.vlab-new-hint`·
   `.vlab-draft-source` 가 제 모양을 갖고 있고, 여기서 필요한 것은 **여백과 높이**뿐이다.
   min-height 는 3줄(12px × 1.6 + 상하 패딩 16px) — 1단의 보충 지시 칸(`--extra` 56px = 2줄)은
   건드리지 않게 이 래퍼 안으로 범위를 좁힌다. */
.vlab-draft-memo { margin: 0 0 var(--sp-sm); }
.vlab-draft-memo .vlab-draft-source { min-height: 74px; }
/* 이 단의 푸터에는 살 것을 적는 유료 버튼이 없다([닫기] 하나) — 헤어라인 뒤 오른쪽 끝에 물린다.
   ⚠️ 클래스는 `render()` 가 매 단 시작에 걷는다(안 걷으면 1단·2단 푸터까지 이 모양을 물려받는다). */
.vlab-draft-list-foot { justify-content: flex-end; padding-top: var(--sp-sm); border-top: 1px solid var(--hairline); }

/* ── 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); }

/* ── 회차 편집기 화면(2026-09-30 · spec_video_editor §6 — 옛 「클립 이어서 보기」 팝업·「타임라인」 화면 자리) ──────────
   속(`.ed-*`)은 편집기 셸이 자기 스타일(web/editor/ui/css/*)로 그린다 — 여기는 화면 상자와 여는 중 한 줄만 둔다.
   ⚠️ 무과금 화면이다 — 비용 배지·유료 강조 클래스를 두지 않는다(렌더는 브라우저가 한다). */
.vlab-screen--editor { display: flex; flex-direction: column; height: 100vh; min-height: 0; }
/* ★ 2026-10-01 오너 "이거 짤리는데 스크롤이 안돼" — 편집기 화면은 프로젝트 상단바(.pshell-top) **아래**에 선다. 100vh 로 두면 상단바만큼
   (56px) 창 밖으로 밀려 타임라인 맨 아래 행·패널 바닥이 잘렸다 — 타임라인 스크롤 상자는 자기가 다 보인다고 여기고(scrollHeight = 내용),
   휠은 타임라인·미리보기가 가로채 페이지도 안 내려간다. 보드(.vlab-screen--board, v2/canvas.css)와 같이 상단바 실높이를 뺀다
   (--pshell-top-h = projectShell 이 잰 실높이, 없으면 --top-h). */
body.shell-v2 .vlab-screen--editor { height: calc(100dvh - var(--pshell-top-h, var(--top-h, 0px))); }
.vlab-editor-loading {
  display: flex; align-items: center; justify-content: center; flex: 1 1 auto; min-height: 40vh;
  font-size: var(--fs-body, 13px); color: var(--fg-3, rgba(255, 255, 255, 0.55));
}

/* ── 카메라 앵글 노드 · 위젯 (계약 §5 Phase 4 · 2026-09-16) ──────────────────────
   ⚠️⚠️ **여기 적힌 캡 둘은 `videoLabBoardCells.NODE_BOX_H.angle`(860 · 2026-09-16 staging 실측 최대 799) 과 한 몸이다** —
      3D 뷰포트 높이 · 결과 미디어 180. 캡을 풀면 카드 높이를 그림 비율이 정하게 되고, 그 순간 표가 거짓이 되어 아래
      카드를 파고든다(인물·배경 카드가 정확히 그 형태로 두 번 겹쳤다 — `.vlab-node--character` 절).
   ⚠️ 폭도 같은 규약이다: `.vlab-node--angle` 의 width 와 `NODE_BOX_W.angle`(340)이 같은 값이어야 한다. */
.vlab-node--angle { width: 340px; }
.vlab-node--angle .vlab-media,
.vlab-node--angle .vlab-thumb { max-height: 180px; }

.vlab-angle { display: flex; flex-direction: column; gap: 8px; }
/* 원본이 못 쓰는 상태라는 것을 색으로도 말한다(글이 이미 말하지만, 카드 여럿을 훑을 때는 색이 먼저 읽힌다). */
.vlab-angle-bad { color: #ff8a8a; opacity: 0.95; }
/* 각도 초안 한 줄(2026-09-17 오너 신고 4) — 조용한 안내 + 작은 [되돌리기]. ⚠️ `display:flex` 가 `hidden` 을 이기지 않게 따로 끈다. */
.vlab-angle-draft { display: flex; align-items: center; gap: 8px; opacity: 0.7; }
.vlab-angle-draft[hidden] { display: none; }
.vlab-angle-draft-revert {
  background: none; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; color: inherit;
  font-size: 11px; padding: 0 6px; cursor: pointer;
}
.vlab-angle-draft-revert:disabled { opacity: 0.4; cursor: default; }

/* ── qwen multiangle 과 같은 구성(2026-09-16 오너 지시) — 슬라이더 셋 · [카메라 뷰] · 3D 뷰포트(오버레이 둘) ──
   축 색 셋은 원본 값 그대로(`ControlPanel.vue` · `CameraWidget.ts`): 방위 분홍 · 고도 청록 · 거리 금색.
   ⚠️ 슬라이더 채움 · 드롭다운 라벨 · 값 · 3D 손잡이·선이 **같은 변수**를 쓴다 — 축을 색으로 짝짓는 것이 원본의 읽는 법이다. */
.vlab-angle {
  --vl-angle-az: #E93D82;
  --vl-angle-el: #00FFD0;
  --vl-angle-dist: #FFB800;
  /* 카메라 표식(잡을 수 없는 물건) — 원본 뿔 재질 색 그대로(`CameraWidget.ts` 0xE93D82) · 모서리는 원본 빛 구 색(0xff6ba8) */
  --vl-angle-cam: #E93D82;
  --vl-angle-cam-edge: #ff6ba8;
}
.vlab-angle [data-axis="azimuth"] { --vl-angle-axis: var(--vl-angle-az); }
.vlab-angle [data-axis="elevation"] { --vl-angle-axis: var(--vl-angle-el); }
.vlab-angle [data-axis="distance"] { --vl-angle-axis: var(--vl-angle-dist); }

/* ① 슬라이더(ComfyUI 숫자 슬라이더 모양 — 이름 가운데 · 값 오른쪽 · 누른 자리가 곧 값) */
.vlab-angle-sliders { display: flex; flex-direction: column; gap: 3px; }
.vlab-angle-slider {
  position: relative;
  height: 20px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.45);
  border: 1px solid var(--vl-node-border);
  overflow: hidden;
  cursor: ew-resize;
  touch-action: none;     /* 포인터 드래그가 스크롤 제스처로 새지 않게 */
  font-size: 11px;
  line-height: 18px;
}
.vlab-angle-slider:focus-visible { outline: 2px solid var(--vl-angle-axis); outline-offset: 1px; }
.vlab-angle-slider-fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  background: color-mix(in srgb, var(--vl-angle-axis) 32%, transparent);
  pointer-events: none;
}
.vlab-angle-slider-ko { position: absolute; left: 0; right: 0; text-align: center; opacity: 0.85; pointer-events: none; }
.vlab-angle-slider-val { position: absolute; right: 8px; font-weight: 600; color: var(--vl-angle-axis); pointer-events: none; }

/* ② [카메라 뷰] 토글 */
.vlab-angle-toggle {
  display: flex; align-items: center; justify-content: space-between;
  height: 20px; padding: 0 8px;
  font: inherit; font-size: 11px;
  border-radius: 4px;
  border: 1px solid var(--vl-node-border);
  background: rgba(0, 0, 0, 0.45);
  color: var(--vl-node-title, #ddd);
  cursor: pointer;
}
.vlab-angle-toggle-knob {
  position: relative; width: 22px; height: 12px; border-radius: 6px;
  background: rgba(255, 255, 255, 0.18);
}
.vlab-angle-toggle-knob::after {
  content: ''; position: absolute; left: 2px; top: 2px; width: 8px; height: 8px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.7);
}
.vlab-angle-toggle.is-on .vlab-angle-toggle-knob { background: color-mix(in srgb, var(--vl-angle-az) 70%, transparent); }
.vlab-angle-toggle.is-on .vlab-angle-toggle-knob::after { left: 12px; background: #fff; }

/* ③ 뷰포트 — ★ 높이 290 은 캡이다(위 ⚠️⚠️ · `NODE_BOX_H.angle`). 장면 상자 300×290 은 `videoLabAngleWidget.SCENE_W/H` 와 한 몸 —
   SVG viewBox·투영·포인터 역산이 같은 수를 써야 손잡이가 포인터 밑에 선다. */
.vlab-angle-view {
  position: relative;
  height: 290px;
  border-radius: 8px;
  background: #0a0a0f;
  overflow: hidden;
}
.vlab-angle-scene {
  position: absolute;
  left: 50%; top: 4px;       /* 위 요약 줄(2줄)과 아래 패널 사이 가운데로 */
  width: 300px; height: 290px;
  margin-left: -150px;
}
.vlab-angle-svg { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.vlab-angle-grid { fill: none; stroke: #1a1a2e; stroke-width: 1; }
.vlab-angle-ring { fill: none; stroke: var(--vl-angle-az); stroke-width: 3; stroke-opacity: 0.7; }
.vlab-angle-glow { fill: none; stroke: var(--vl-angle-az); stroke-width: 1.5; stroke-opacity: 0.4; }
.vlab-angle-arc { fill: none; stroke: var(--vl-angle-el); stroke-width: 3; stroke-opacity: 0.8; stroke-linecap: round; }
.vlab-angle-line3d { fill: none; stroke: var(--vl-angle-dist); stroke-width: 2; stroke-opacity: 0.8; }
/* ★ 카메라 표식은 손잡이가 아니다 — 원본과 같은 분홍 네모 뿔(꼭짓점이 피사체 쪽) · 반투명(면 fill-opacity 는 위젯이 면 방향으로) ·
   빛무리 없음(손잡이의 둥근 빛무리와 모양으로 갈린다) · 포인터를 받지 않는다(2026-09-17 오너 *"comfy 랑 비슷하게"*). */
.vlab-angle-cam { pointer-events: none; }
.vlab-angle-cam-face { fill: var(--vl-angle-cam); stroke: var(--vl-angle-cam-edge); stroke-width: 1; stroke-opacity: 0.75; stroke-linejoin: round; pointer-events: none; }
.vlab-angle-handle {
  fill: var(--vl-angle-axis);
  stroke: color-mix(in srgb, var(--vl-angle-axis) 35%, transparent);
  stroke-width: 8;          /* 잡는 자리 — 원본의 빛무리(glow) 구 */
  pointer-events: all;
  cursor: move;             /* ⚠️ `grab` 이 아니다 — `grab` 은 이 캔버스에서 "카드를 잡는 자리" 의 커서다(판정 `graphNodeGrab`) */
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 80ms ease-out;
}
.vlab-angle-handle:hover { transform: scale(1.15); }
.vlab-angle-handle.is-grabbed { transform: scale(1.3); cursor: move; }
.vlab-angle-handle.is-behind { opacity: 0.55; }
.vlab-angle-view.is-camera-view .vlab-angle-svg { display: none; }
/* 카메라 뷰 — 어디를 끌어도 궤도 회전이다(`move` · 카드 잡기의 `grab` 과 다른 말). 궤도 시점의 빈 곳은 아무 일도 없어 `default`(위 `:where` 규칙). */
.vlab-angle-view.is-camera-view .vlab-angle-scene { cursor: move; touch-action: none; }

/* 그림 카드 — `matrix3d` 가 100×100 을 투영된 네 점에 앉힌다(원점 왼쪽 위). */
.vlab-angle-subject {
  position: absolute; left: 0; top: 0;
  width: 100px; height: 100px;
  transform-origin: 0 0;
  outline: 1px solid color-mix(in srgb, var(--vl-angle-az) 80%, transparent);
  background: #3a3a4a;
}
.vlab-angle-view.is-camera-view .vlab-angle-subject { outline: none; }
.vlab-angle-src { width: 100%; height: 100%; overflow: hidden; display: flex; align-items: center; justify-content: center; }
.vlab-angle-src img, .vlab-node--angle .vlab-angle-src .vlab-thumb { width: 100%; height: 100%; max-height: none; object-fit: fill; display: block; }
.vlab-angle-src-empty { font-size: 11px; opacity: 0.5; }
/* 뒷면 — 원본처럼 격자(뒤에서 보면 그림이 거울로 비치지 않는다). */
.vlab-angle-backface {
  position: absolute; inset: 0; display: none;
  background:
    repeating-linear-gradient(0deg, #2a2a3a 0 1px, transparent 1px 6px),
    repeating-linear-gradient(90deg, #2a2a3a 0 1px, #1a1a2a 1px 6px);
}
.vlab-angle-subject.is-back .vlab-angle-src { visibility: hidden; }
.vlab-angle-subject.is-back .vlab-angle-backface { display: block; }

/* 거리 = 결과에서 남길 자리(점선 · 금색). 궤도 시점은 카드 위, 카메라 뷰는 화면 위(카드와 함께 돌지 않는다). */
.vlab-angle-crop {
  position: absolute;
  border: 1px dashed var(--vl-angle-dist);
  pointer-events: none;
}
.vlab-angle-crop.is-screen { border-width: 2px; }
/* ⚠️ 궤도 시점의 틀은 100px 카드 안에서 `matrix3d` 로 약 0.5 배 줄어든다 — 1px 점선은 반 픽셀이 되어, 특히 와이드(= 카드 가장자리와
   겹치는 틀)에서 분홍 테두리에 묻혀 "노란 영역이 사라졌다" 로 보였다(2026-09-17 브라우저 재현). 카드 좌표 3px(화면 ≈1.5px)로 둔다. */
.vlab-angle-crop:not(.is-screen) { border-width: 3px; }
.vlab-angle-crop.is-off { display: none; }
/* 틀은 거리의 연속 함수다(2026-09-17) — 드롭다운·↺ 처럼 한 번에 뛰는 변경만 짧게 옮긴다.
   ⚠️ 입력 중(`is-live` · 끌기·휠·방향키)에는 전환이 없다 — 매 입력마다 150ms 가 다시 시작돼 틀이 손을 늦게 따라왔다. */
.vlab-angle-crop { transition: left 150ms ease-out, top 150ms ease-out, width 150ms ease-out, height 150ms ease-out; }
.vlab-angle-view.is-live .vlab-angle-crop,
.vlab-angle-view.is-live .vlab-angle-crop-ko { transition: none; }
/* 와이드 = 자르지 않음 — 그림 전체 테두리(숨기면 "줄이다 보니 사라졌다" 로 읽힌다 · 2026-09-16 오너 신고).
   ⚠️ 옅게 두지 않는다(옛 opacity 0.7) — 카드 가장자리와 겹치는 틀이라 옅으면 테두리에 묻혀 사라진 것처럼 보인다(2026-09-17). */
.vlab-angle-crop.is-wide { opacity: 1; }
/* 틀 라벨 — 틀 왼쪽 아래 모서리 바로 밑(잘려 나갈 쪽). 밑에 자리가 없으면 틀 안쪽으로 올린다. 높이 = `CROP_LABEL_H`(16). */
.vlab-angle-crop-ko {
  position: absolute;
  height: 16px; line-height: 14px;
  padding: 0 4px;
  border-radius: 3px;
  border: 1px solid rgba(255, 184, 0, 0.45);
  background: rgba(10, 10, 15, 0.85);
  color: var(--vl-angle-dist);
  font-size: 9px; font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  box-sizing: border-box;
  transition: left 150ms ease-out, top 150ms ease-out;
}
/* 자리(틀 모서리 밑·안쪽 · 손잡이 비키기)는 JS 한 곳(`cropLabelBox`)이 left/top 으로 정한다 — 여기서 transform·margin 으로 옮기지 않는다. */
.vlab-angle-crop-ko[hidden] { display: none; }

/* 위 오버레이 — 요약 줄(원본의 프롬프트 줄 자리) */
.vlab-angle-line {
  position: absolute; left: 6px; right: 6px; top: 6px;
  padding: 3px 8px;
  border-radius: 5px;
  border: 1px solid rgba(233, 61, 130, 0.3);
  background: rgba(10, 10, 15, 0.9);
  color: var(--vl-angle-az);
  font-size: 11px; font-weight: 600; line-height: 1.35;
  pointer-events: none;
}

/* 아래 오버레이 — 드롭다운 셋 · 값 셋 + ↺ (원본 `ControlPanel.vue`) */
.vlab-angle-panel {
  position: absolute; left: 6px; right: 6px; bottom: 6px;
  display: flex; flex-direction: column; gap: 3px;
  padding: 4px 8px;
  border-radius: 5px;
  border: 1px solid rgba(233, 61, 130, 0.3);
  background: rgba(10, 10, 15, 0.9);
  font-size: 11px;
  color: #e0e0e0;
}
.vlab-angle-row { display: flex; justify-content: space-around; align-items: center; gap: 4px; }
.vlab-angle-pick { display: flex; align-items: center; gap: 3px; min-width: 0; }
.vlab-angle-pick-ko { font-size: 10px; color: var(--vl-angle-axis); white-space: nowrap; }
.vlab-angle-select {
  font: inherit; font-size: 10px;
  max-width: 64px; min-width: 0;
  padding: 1px 2px;
  border-radius: 4px;
  border: 1px solid rgba(100, 100, 120, 0.4);
  background: rgba(10, 10, 15, 0.9);
  color: #e0e0e0;
  cursor: pointer;
}
.vlab-angle-select:focus { outline: none; border-color: var(--vl-angle-axis); }
.vlab-angle-select option { background: #1a1a2e; color: #e0e0e0; }
.vlab-angle-val { flex: 1; text-align: center; font-size: 13px; font-weight: 600; color: var(--vl-angle-axis); }
.vlab-angle-reset {
  width: 22px; height: 22px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font: inherit; font-size: 14px;
  border-radius: 4px;
  border: 1px solid rgba(233, 61, 130, 0.4);
  background: rgba(10, 10, 15, 0.8);
  color: var(--vl-angle-az);
  cursor: pointer;
}
.vlab-angle-reset:hover { background: rgba(233, 61, 130, 0.2); border-color: var(--vl-angle-az); }
.vlab-angle-reset:disabled, .vlab-angle-select:disabled { opacity: 0.45; cursor: default; }

@media (prefers-reduced-motion: reduce) {
  .vlab-angle-handle, .vlab-angle-crop, .vlab-angle-crop-ko { transition: none; }
}

/* ── 다른 회차에서 가져오기(2026-09-18 다회차 라운드 D2 · D7 · 2026-09-23 완성도 라운드) ──────────────
   ★ 색은 전부 위 토큰(`--vl-kind-*`·`--vl-group-*`·`--vl-selected`) — 새 색 0. 팝업은 body 에 붙어 캔버스 밖이라
     토큰 스코프에 `.vlab-import` 를 함께 걸었다(`.vlab-menu` 와 같은 이유). */

/* 카드 머리 출처 배지 — 고정 표식(`.vlab-node-pin`)과 같은 무채색: *어디서 왔나* 는 생성의 진행(상태 배지 색)과 다른 축이다. */
.vlab-node-from {
  flex: 0 0 auto;
  padding: 0 6px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(0, 0, 0, 0.35);
  color: #cfcfcf;
  font-size: var(--fs-fine);
  line-height: 1.6;
  white-space: nowrap;
}

.vlab-import.sm-panel {
  /* ★ 지도 높이 = 팝업 높이(85vh)에서 **위아래의 고정 줄 전부**를 뺀 만큼(2026-09-23 완성도 라운드).
     옛 값은 `85vh - 150px`(제목 + 바닥)이었는데, 그 위에 머리줄·조작 안내·상세 영역을 더하면 팝업이
     85vh 를 넘어 **하단 [가져오기]가 화면 밖으로 밀린다**. 뺄 값을 함께 키우는 것이 이 줄의 계약이다.
       제목 두 줄 ~58 + 머리줄 ~26 + 안내 ~20 + 상세 124 + gap 4칸 ~48 + 바닥 ~36 + 패널 패딩 ~48 = 360.
     ⚠️ 그래도 안전망을 둔다 — 아래 `flex: 0 1 auto` + `min-height` 로 **지도가 먼저 줄어든다**.
        짧은 화면에서 어느 줄도 잘리지 않아야 하고, 잘려도 되는 것은 지도뿐이다. */
  --vlab-import-map-h: clamp(220px, calc(85vh - 360px), 720px);
  /* 상세 영역 높이 — 아스트라 권고 112~144px. 카드에서 마우스가 빠져도 **자리를 비우지 않는다**(깜빡임 방지). */
  --vlab-import-detail-h: 124px;
  width: min(1080px, 94vw); max-width: none; max-height: 85vh; display: flex; flex-direction: column; gap: 12px;
  min-height: 0;
}
/* 제목 위계(아스트라 B-6) — 제목을 키우고 한 줄 부제를 둔다. 새 색 없이 크기·굵기·투명도만. */
.vlab-import-head { display: flex; flex-direction: column; gap: 2px; }
.vlab-import .sm-title { font-size: 19px; line-height: 1.3; }
.vlab-import-sub { margin: 0; font-size: 13px; opacity: 0.62; }
.vlab-import-body { display: grid; grid-template-columns: 200px 1fr; gap: 12px; min-height: 0; flex: 1 1 auto; }
.vlab-import-eps { display: flex; flex-direction: column; gap: 4px; overflow-y: auto; min-height: 0; }
/* ★ 목록 행은 **늘 두 줄**이다 — 굵은 회차명 / 보조 글자로 카드 수.
   ⚠️ 그림 수를 넣지 않는다: 불러온 회차만 조각이 붙어 클릭한 줄만 길어졌다(형식이 행마다 달라지는 비대칭). */
.vlab-import-ep {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
  min-height: 60px;
  justify-content: center;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid transparent;
  background: transparent;
  color: inherit;
  font-size: 13px;
  cursor: pointer;
}
.vlab-import-ep-name { font-size: 14px; font-weight: 600; color: var(--vl-node-title); }
.vlab-import-ep-sub { font-size: 12px; opacity: 0.6; }
.vlab-import-ep:hover { background: rgba(255, 255, 255, 0.05); }
.vlab-import-ep.is-on { border-color: var(--vl-selected); background: var(--vl-node-body); }
.vlab-import-ep:focus-visible { outline: 2px solid var(--vl-selected); outline-offset: -2px; }
.vlab-import-ep:disabled { cursor: default; opacity: 0.5; }
.vlab-import-right { display: flex; flex-direction: column; gap: 8px; min-height: 0; }
/* 머리줄 — **고른 회차 하나**의 카드·그림·영상 수 + 미리보기 진행. 목록은 이 수를 말하지 않는다. */
.vlab-import-maphead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 22px; }
.vlab-import-maphead-line { font-size: 13px; font-weight: 600; color: var(--vl-node-title); }
.vlab-import-progress { font-size: 12px; opacity: 0.62; }
.vlab-import-progress:empty { display: none; }
/* ⚠️ 실패만 오류색을 쓴다 — 진행 중(`미리보기 14/22개 준비됨`)은 오류가 아니다. */
.vlab-import-progress.is-fail { color: var(--vl-error); opacity: 1; }
.vlab-import-retry[hidden] { display: none; }
/* ★ 조작 안내는 **지도 위에 항상** 선다(선택 요약은 하단). 한 칸을 번갈아 쓰면 고르는 순간 조작법이 사라진다. */
.vlab-import-tip { font-size: 12px; opacity: 0.55; }
.vlab-import-map {
  position: relative;
  /* ⚠️ `flex: 0 1 auto` — 높이는 위 변수가 정하되 **줄어들 수는 있다**. 짧은 화면에서 하단 버튼이 밀리는 대신
     지도가 먼저 작아진다(세로로 긴 Ep.1 에서 특히 중요 — 아스트라 B-3 의 "두 화면을 함께 검증"). */
  flex: 0 1 auto;
  height: var(--vlab-import-map-h);
  /* ⚠️ 하한이 너무 높으면 짧은 화면에서 팝업이 85vh 를 넘어 **하단 버튼이 밀린다** — 140 + 위아래 고정줄 360 ≈ 500px,
     즉 590px 높이의 창까지 버튼이 보인다. 그보다 짧으면 아래 미디어쿼리가 상세 영역을 함께 줄인다. */
  min-height: 140px;
  overflow: hidden;
  border-radius: 8px;
  border: 1px solid var(--vl-node-border);
  background: var(--vl-canvas-bg);
  cursor: crosshair;
  user-select: none;
  touch-action: none;
}
/* ★ 지도 아래 상세 영역 — 보고 있는 카드의 큰 미리보기 · 전체 제목 · 종류 · 상자 · 결과 · 선택 여부.
   ⚠️ 높이를 **미리 확보한다**: 내용에 따라 높이가 변하면 지도가 위아래로 흔들린다. */
.vlab-import-detail {
  flex: 0 0 auto;
  height: var(--vlab-import-detail-h);
  display: flex;
  gap: 10px;
  padding: 10px;
  overflow: hidden;
  border-radius: 8px;
  border: 1px solid var(--vl-node-border);
  background: var(--vl-node-body);
}
.vlab-import-detail-shot {
  --vl-kind-color: var(--vl-neutral);
  flex: 0 0 auto;
  width: 104px;
  height: 100%;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  border-radius: 6px;
  border: 1px solid var(--vl-node-border);
  background: var(--vl-canvas-bg);
  color: var(--vl-kind-color);
}
.vlab-import-detail-shot[data-kind="text"] { --vl-kind-color: var(--vl-kind-text); }
.vlab-import-detail-shot[data-kind="context"] { --vl-kind-color: var(--vl-kind-context); }
.vlab-import-detail-shot[data-kind="image"] { --vl-kind-color: var(--vl-kind-image); }
.vlab-import-detail-shot[data-kind="character"] { --vl-kind-color: var(--vl-kind-character); }
.vlab-import-detail-shot[data-kind="location"] { --vl-kind-color: var(--vl-kind-location); }
.vlab-import-detail-shot[data-kind="board"] { --vl-kind-color: var(--vl-kind-board); }
.vlab-import-detail-shot[data-kind="clip"],
.vlab-import-detail-shot[data-kind="cut"] { --vl-kind-color: var(--vl-kind-clip); }
.vlab-import-detail-shot[data-kind="angle"] { --vl-kind-color: var(--vl-kind-angle); }
.vlab-import-detail-media { width: 100%; height: 100%; object-fit: cover; }
.vlab-import-detail-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.vlab-import-detail-title {
  font-size: 14px; font-weight: 600; color: var(--vl-node-title);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vlab-import-detail-meta { font-size: 12px; opacity: 0.7; }
.vlab-import-detail-body {
  flex: 1 1 auto; min-height: 0;
  font-size: 12px; line-height: 1.45; overflow: hidden;
}
.vlab-import-detail-body.is-note { opacity: 0.6; }
.vlab-import-detail-pick { font-size: 12px; opacity: 0.55; }
.vlab-import-detail-pick.is-on { opacity: 1; font-weight: 600; color: var(--vl-selected); }
.vlab-import-note { padding: 16px; opacity: 0.65; font-size: 13px; }
.vlab-import-map > .vlab-import-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.vlab-import-detail > .vlab-import-note { margin: auto; padding: 0; }
/* 짧은 화면 — 상세 영역을 한 단 줄여 지도와 하단 버튼을 함께 살린다(둘 중 무엇도 잘리면 안 된다). */
@media (max-height: 700px) {
  .vlab-import.sm-panel { --vlab-import-detail-h: 96px; }
  .vlab-import-detail-shot { width: 80px; }
}
.vlab-import-group {
  --vl-group-color: var(--vl-group-pale_blue);
  position: absolute;
  border: 1px solid color-mix(in srgb, var(--vl-group-color) 60%, transparent);
  background: color-mix(in srgb, var(--vl-group-color) 10%, transparent);
  border-radius: 4px;
  pointer-events: none;
}
.vlab-import-group[data-color="red"] { --vl-group-color: var(--vl-group-red); }
.vlab-import-group[data-color="brown"] { --vl-group-color: var(--vl-group-brown); }
.vlab-import-group[data-color="green"] { --vl-group-color: var(--vl-group-green); }
.vlab-import-group[data-color="blue"] { --vl-group-color: var(--vl-group-blue); }
.vlab-import-group[data-color="cyan"] { --vl-group-color: var(--vl-group-cyan); }
.vlab-import-group[data-color="purple"] { --vl-group-color: var(--vl-group-purple); }
.vlab-import-group[data-color="yellow"] { --vl-group-color: var(--vl-group-yellow); }
.vlab-import-group[data-color="black"] { --vl-group-color: var(--vl-group-black); }
.vlab-import-node {
  --vl-kind-color: var(--vl-neutral);
  position: absolute;
  box-sizing: border-box;
  overflow: hidden;
  border-radius: 3px;
  border: 1px solid var(--vl-node-border);
  border-top: 3px solid var(--vl-kind-color);
  background: var(--vl-node-body);
  cursor: pointer;
}
.vlab-import-node[data-kind="text"] { --vl-kind-color: var(--vl-kind-text); }
.vlab-import-node[data-kind="context"] { --vl-kind-color: var(--vl-kind-context); }
.vlab-import-node[data-kind="image"] { --vl-kind-color: var(--vl-kind-image); }
.vlab-import-node[data-kind="character"] { --vl-kind-color: var(--vl-kind-character); }
.vlab-import-node[data-kind="location"] { --vl-kind-color: var(--vl-kind-location); }
.vlab-import-node[data-kind="board"] { --vl-kind-color: var(--vl-kind-board); }
.vlab-import-node[data-kind="clip"],
.vlab-import-node[data-kind="cut"] { --vl-kind-color: var(--vl-kind-clip); }
.vlab-import-node[data-kind="angle"] { --vl-kind-color: var(--vl-kind-angle); }
/* ★ 선택 — 흰 외곽선 + 체크 표식(글자 아님 · 회전한 테두리 두 변). 40px 타일에서도 읽힌다. */
.vlab-import-node.is-on { outline: 2px solid var(--vl-selected); outline-offset: 1px; }
.vlab-import-node.is-on::after {
  content: '';
  position: absolute; right: 3px; top: 4px;
  width: 4px; height: 8px;
  border: solid var(--vl-selected);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  pointer-events: none;
}
.vlab-import-node[data-size="xs"].is-on::after { display: none; }
/* ★ **보고 있는 카드**(상세 영역이 가리키는 것)는 선택과 **다른 표시**여야 한다(아스트라 B-2).
   선택 = 흰 외곽선(`--vl-selected`) · 보는 중 = 그 카드의 **종류색** 테두리. 축이 다르니 색도 다른 축을 쓴다. */
.vlab-import-node.is-peek { box-shadow: 0 0 0 2px color-mix(in srgb, var(--vl-kind-color) 75%, transparent); }
.vlab-import-node:focus-visible { outline: 2px solid var(--vl-selected); outline-offset: 1px; }
.vlab-import-thumb { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0.85; pointer-events: none; }

/* ★★ 카드 얼굴(2026-09-23 · 아스트라 B-1) — 오너 *"깨진거같이 보이는느낌"* 의 정체는 **정상·로딩·실패가 모두
   같은 검은 빈 상자**였다는 것이다(그림 0 장인 정상 회차 Ep.1 의 카드 64장). 그래서 모든 카드는 먼저
   **종류 표식**(아이콘 + 종류색)으로 완성되고, 썸네일은 있을 때 그 위에 얹는다.
   ⚠️ 글 카드에 **흐린 가로줄(스켈레톤 같은 것)을 넣지 않는다** — 그게 바로 "로딩 중"으로 읽힌다.
      문서 아이콘 한 개의 **정지된** 표식이어야 한다.
   ⚠️ 새 색 0 — 밝기·굵기·선 종류(점선)·표식만으로 다섯 상태를 가른다(이 절 머리주석의 계약). */
.vlab-import-ico {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--vl-kind-color);
  opacity: 0.8;
  pointer-events: none;
}
.vlab-import-tag {
  position: absolute; left: 0; right: 0; bottom: 1px;
  text-align: center;
  font-size: 10px; line-height: 1.2;
  color: var(--vl-node-title);
  opacity: 0.72;
  white-space: nowrap; overflow: hidden;
  pointer-events: none;
}
/* 종류명이 서는 `md` 타일은 아이콘을 위로 올려 둘이 겹치지 않게 한다. */
.vlab-import-node[data-size="md"] .vlab-import-ico { bottom: 12px; }
/* 결과가 아직 없는 카드 — **점선**(정지된 신호 · 애니메이션 아님). 글 카드와 뜻이 다르므로 얼굴도 다르다. */
.vlab-import-node[data-face="none"] { border-style: dashed; border-top-style: solid; }
.vlab-import-node[data-face="none"] .vlab-import-ico { opacity: 0.5; }
/* 미리보기를 불러오는 중 — 종류 표식은 그대로 두고 **숨만 쉰다**(자리·크기는 바뀌지 않는다). */
.vlab-import-node[data-face="pending"] .vlab-import-ico { animation: vlab-import-breathe 1.4s ease-in-out infinite; }
@keyframes vlab-import-breathe { 0%, 100% { opacity: 0.28; } 50% { opacity: 0.8; } }
/* 썸네일이 붙으면 종류 표식은 **구석의 작은 배지**로 물러난다(그림이 먼저 읽히게). */
.vlab-import-node[data-face="ready"] .vlab-import-ico {
  inset: auto 1px 1px auto;
  padding: 1px;
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.55);
  opacity: 1;
}
.vlab-import-node[data-face="ready"] .vlab-import-tag { display: none; }
/* 미리보기 실패 — 종류 표식 + 왼쪽 위 오류 표식. **영구 로딩으로 남기지 않는다**(머리줄에 [다시 불러오기]). */
.vlab-import-mark {
  position: absolute; left: 1px; top: 2px;
  display: flex;
  color: var(--vl-error);
  pointer-events: none;
}
.vlab-import-node[data-size="xs"] .vlab-import-mark { display: none; }
@media (prefers-reduced-motion: reduce) {
  .vlab-import-node[data-face="pending"] .vlab-import-ico { animation: none; opacity: 0.5; }
}
/* 복사 중에는 회차 전환·지도 조작을 전부 잠근다 — 진행 중인 대상과 화면이 달라지지 않게(아스트라 B-6). */
.vlab-import.is-busy .vlab-import-map { pointer-events: none; opacity: 0.6; cursor: default; }
.vlab-import.is-busy .vlab-import-eps { pointer-events: none; }
.vlab-import-band {
  position: absolute;
  border: 1px dashed var(--vl-selected);
  background: color-mix(in srgb, var(--vl-selected) 8%, transparent);
  pointer-events: none;
}
/* ★ 하단은 **선택 요약 + [닫기]/[가져오기]** 로 자리가 고정이다(2026-09-23). 조작 안내는 지도 위(`.vlab-import-tip`)가
   진다 — 옛 판은 한 칸을 안내문과 선택 요약이 번갈아 써서 **고르는 순간 조작법이 사라졌다**(아스트라 B-6).
   좁은 폭에서 줄 글이 버튼과 겹치지 않게 접어 내린다. */
.vlab-import-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; flex: 0 0 auto; }
.vlab-import-count { flex: 1 1 240px; min-width: 0; font-size: 13px; }
.vlab-import-count.is-hint { opacity: 0.6; }
.vlab-import-count.is-over { color: var(--vl-error); }
.vlab-import-err { flex: 1 1 100%; order: 3; color: var(--vl-error); font-size: 12px; }
.vlab-import-err:empty { display: none; }

/* ═════════ compact face(2026-10-04 오너 결정 · `videoLabNodeView.nodeFace`) ═════════
 *   머리 아래 몸통이 **읽기 전용 한 판**이다: ① 결과 ② 글 미리보기 ③ 칩 ④ 꼬리([수정] + 흰 주 버튼 하나).
 *   ⚠️ 이 파일은 index.html 에서 `v2/canvas-contrast.css` **앞**에 실린다 — 그 파일이 이미 칠하는 속성(`.vlab-media` 바탕 ·
 *      `.vlab-node-run` 높이·모서리 · `.vlab-asset-acts` 여백)을 덮는 줄만 `body.shell-v2 .vlab-node …`(0,4,1)로 적는다.
 *   ⚠️ 높이를 정한 카드(`is-sized` · `node.h`)는 고정 상한 대신 **남는 자리를 채운다** — 그 규칙은
 *      `.vlab-node.is-sized .vlab-media`·`.vlab-thumb`(0,3,0)가 이미 들고 있어 여기 상한은 (0,1,0)·(0,2,0)에 둔다
 *      (상한을 더 세게 적으면 그 규칙을 이겨 채우기가 죽는다). */
.vlab-face { display: flex; flex-direction: column; gap: var(--sp-xs, 8px); min-width: 0; }
.vlab-face:empty { display: none; }
/* 콘티 칸 격자는 얼굴 안에서 제 위아래 여백을 버린다(줄 간격은 얼굴의 gap 이 준다). */
.vlab-face > .vlab-board { margin: 0; }
/* ① 결과(이미지·클립·앵글) — 상한 220 · contain · 어두운 우물 위. 인물·배경은 옛 미디어 그대로(캡 180 · 카드색 바탕). */
.vlab-face-media { max-height: 220px; }
.vlab-face-media > .vlab-thumb { max-height: 220px; }
/* ★ 이미지·앵글 카드의 결과 그림은 **그림 크기로 선다**(2026-10-06 — 세로 그림이 254×220 우물 가운데 좁게 서고 양옆이 검게 비었다).
   우물 폭 = 그림 폭(높이 상한 220 안에서 비율대로) · 가운데. 높이는 옛 판과 같거나 낮다(표 `NODE_BOX_H.image` 아래).
   ⚠️ 그림이 붙은 우물만(`:has(> img)`) — 깨진 그림 자리표시자는 고유 폭이 없어 접힌다. 높이를 정한 카드(`is-sized`)는 그대로(채우기 규칙). */
.vlab-node:is(.vlab-node--image, .vlab-node--angle):not(.is-sized) .vlab-face-media:has(> img.vlab-thumb) { width: fit-content; max-width: 100%; margin-inline: auto; }
.vlab-node:is(.vlab-node--image, .vlab-node--angle):not(.is-sized) .vlab-face-media > img.vlab-thumb { width: auto; max-width: 100%; }
body.shell-v2 .vlab-node .vlab-face .vlab-face-media { background: var(--vl-well-bg, #121212); }
/* ② 글 미리보기 — 메타 단(ink-muted) · 줄바꿈 유지 · 종류별 접는 줄 수. */
.vlab-face-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  white-space: pre-line;
  overflow-wrap: anywhere;
  font-size: 12px;
  line-height: 18px;
  color: var(--vl-node-meta, #a8a8a8);
}
.vlab-face-text--l2 { -webkit-line-clamp: 2; }
/* 🔴 뺀 것·착장 자동 연결 줄(2026-11-06 · `videoLabLeftOut`) — 카드에 늘 보이는 한 줄씩(넘치면 말줄임 · 전문은 툴팁).
   줄 높이는 `videoLabClipCardHeight.NOTE_LINE_H`(18)와 한 몸 — 바꾸면 그 상수도 바꾼다(겹침 점검). */
.vlab-face-notes { display: flex; flex-direction: column; flex: 0 0 auto; min-width: 0; }
.vlab-face-note {
  height: 18px;
  line-height: 18px;
  font-size: 11px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--vl-node-meta, #a8a8a8);
}
/* 클립 얼굴 — 높이를 노드 값만으로 정한다(`videoLabClipCardHeight` 와 한 몸): 글 칸은 줄 수만큼 고정 높이 · 칩 줄은 한 줄 고정(넘치면 잘림).
   ⚠️ 크기 손잡이로 높이를 정한 카드(`is-sized`)는 남는 자리를 채우는 옛 규칙 그대로다. */
.vlab-node:not(.is-sized) > .vlab-face--clip > .vlab-face-text { height: 54px; }
.vlab-node:not(.is-sized) > .vlab-face--clip > .vlab-face-text--l2 { height: 36px; }
/* ⚠️ `flex: 0 0 auto` — 칩 줄은 `overflow: hidden` 이라 자동 최소 높이가 0 이다. 높이를 정한 카드(`is-sized`)에서 글이 자리보다 길면
   얼굴(세로 flex)이 글과 칩 줄을 **같은 비율로** 줄여 칩이 반쯤 잘렸다(2026-10-13 오너 신고 — 카드를 끝까지 늘려야 `15초`·`대사 3` 이 보였다). 줄어드는 것은 글뿐이다. */
.vlab-face--clip > .vlab-face-chips { flex: 0 0 auto; flex-wrap: nowrap; overflow: hidden; height: 20px; }
.vlab-face-text--l8 { -webkit-line-clamp: 8; }
/* ③ 칩 — 20px 알약 · 11px · 메타 글자. 누르는 것이 아니다(버튼 30px·굵은 글자와 갈린다). */
.vlab-face-chips { display: flex; flex-wrap: wrap; gap: var(--sp-xxs, 4px); min-width: 0; }
.vlab-chip {
  display: inline-block;
  box-sizing: border-box;
  max-width: 100%;
  height: 20px;
  padding: 0 var(--sp-xs, 8px);
  border-radius: var(--r-pill, 999px);
  background: var(--vl-ctl-bg, rgba(255, 255, 255, 0.08));
  color: var(--vl-node-meta, #a8a8a8);
  font-size: 11px;
  line-height: 20px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 확인할 것 — 본문 단(ink)으로 한 단 세우고 ⚠ 를 앞에. 붉은 톤(`--vl-error` 파생)은 2026-10-13 부터 칩이 아니라 실패 블록(`.vlab-face-fail.is-danger` · 아래)이 쓴다 —
   실패·참조 사라짐은 칩에 세지 않는다. `.is-danger` 칩 규칙은 같은 색의 정본으로 남긴다. */
.vlab-face-warn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xxs, 4px);
  color: var(--vl-node-text, #e6e6e6);
  background: var(--vl-ctl-hover, rgba(255, 255, 255, 0.10));
  font-weight: 600;
}
.vlab-face-warn > svg { flex: 0 0 auto; }
.vlab-face-warn.is-danger {
  color: color-mix(in srgb, var(--vl-error, #ee3333) 50%, var(--vl-node-title, #ffffff));
  background: color-mix(in srgb, var(--vl-error, #ee3333) 18%, transparent);
}
/* ★ 실패 원인 칩(2026-10-29 · `videoLabNodeFace.faceCauseChip`) — 막힌 카드는 누르는 버튼(원인 카드로 간다) · 원인 카드는 읽기 전용 알약. 둘 다 붉은 톤. */
.vlab-face-cause {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xxs, 4px);
  font-weight: 600;
  color: color-mix(in srgb, var(--vl-error, #ee3333) 45%, var(--vl-node-title, #ffffff));
  background: color-mix(in srgb, var(--vl-error, #ee3333) 22%, transparent);
  border: 0;
  font-family: inherit;
}
.vlab-face-cause > svg { flex: 0 0 auto; }
.vlab-face-cause.is-blocked { cursor: pointer; }
.vlab-face-cause.is-blocked:hover { background: color-mix(in srgb, var(--vl-error, #ee3333) 34%, transparent); }
/* ★ 실패 블록(2026-10-13 오너 지시 — 실패 원인을 카드에 글로 · `videoLabNodeFace.faceFailBlock`).
   2026-10-15 오너 *"가독성이 너무 안좋은거같아 정리좀 해야할듯"* — 붉은 바탕에 붉은 글 한 덩어리였다 → 첫 문장(`-lead` · 제목색 굵은 13px · 2줄)
   + 나머지(`-rest` · 메타 회색 12px · 4줄)로 가른다. 붉은 신호는 ⚠ + 왼쪽 3px 띠 + 아주 옅은 바탕만 — 글은 붉히지 않는다.
   ⚠️ 줄 높이 18 · 2줄 + 4줄 · 두 칸 사이 2 · 위아래 여백 8 은 `videoLabClipCardHeight.FAIL_BLOCK_MAX_H` 와 한 몸이다(테스트가 대조) — 결과 틀이 없는 클립은 이 블록만큼 자란다.
   ⚠️ `flex: 0 0 auto` — 높이를 정한 카드(`is-sized`)에서 글 미리보기와 같은 비율로 줄어 사유가 잘리지 않게(칩 줄과 같은 처방).
   ⚠️ `pointer-events` 를 끄지 않는다 — 접힌 글의 전문을 `title` 툴팁이 든다. */
.vlab-face-fail {
  flex: 0 0 auto;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-xs, 8px);
  box-sizing: border-box;
  min-width: 0;
  padding: 8px 10px;
  border-left: 3px solid var(--vl-node-meta, #a8a8a8);
  border-radius: 6px;
  font-size: 12px;
  line-height: 18px;
  color: var(--vl-node-meta, #a8a8a8);
  background: var(--vl-ctl-bg, rgba(255, 255, 255, 0.06));
}
.vlab-face-fail > svg { flex: 0 0 auto; margin-top: 2px; color: var(--vl-error, #ee3333); }
.vlab-face-fail-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.vlab-face-fail-lead,
.vlab-face-fail-rest {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: pre-line;
  word-break: keep-all;     /* 한글 어절 안에서 끊지 않는다(위 `.vlab-proj-meta` 와 같은 처방) */
  overflow-wrap: anywhere;
}
.vlab-face-fail-lead { -webkit-line-clamp: 2; font-size: 13px; font-weight: 600; color: var(--vl-node-title, #ffffff); }
.vlab-face-fail-rest { -webkit-line-clamp: 6; color: var(--vl-node-meta, #a8a8a8); }
.vlab-face-fail.is-danger {
  border-left-color: var(--vl-error, #ee3333);
  background: color-mix(in srgb, var(--vl-error, #ee3333) 9%, transparent);
}
/* ★ 생성 막힘 사유 블록(2026-10-28 · `runBlock` · 슈드파 안나님 신고) — 실패 블록과 같은 꼴이되 **언제나 흐름 안 · 카드 폭 전체 · 말줄임 없음**.
   스테이징 실측: 그림 틀 안 오버레이 + 머리 2줄 말줄임에서 긴 카드 이름이 머리를 다 먹어 "…만들 수 없습니다" 가 안 보였다.
   ⚠️ 높이는 문장 길이를 따른다(300자 상한) — `videoLabClipCardHeight.runBlockHeightMax` 가 같은 치수로 상한을 잰다(줄 높이 18 · 여백 8 · 띠 3 · 아이콘 14). */
.vlab-face-fail--block .vlab-face-fail-lead,
.vlab-face-fail--block .vlab-face-fail-rest {
  display: block;
  -webkit-line-clamp: unset;
  overflow: visible;
}
/* 결과 틀이 있으면 그 틀 안 **아래쪽에 겹쳐** 올린다(`.vlab-media` 는 `position: relative`) — 카드 높이가 안 변한다(결과 있는 클립 높이는 배치 하한이다).
   그림 위에서 읽히게 어두운 바탕(0.82) · 머리 흰 글 · 설명 밝은 회색 · 붉은 띠(같은 틀 위 `.vlab-stale-tag` 와 같은 붉은 값).
   ⚠️ 낮은 틀(가로 그림)에서 틀 밖으로 넘치지 않게 높이를 틀 안으로 묶는다. */
.vlab-media > .vlab-face-fail--over {
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: 6px;
  max-height: calc(100% - 12px);
  overflow: hidden;
  background: rgba(0, 0, 0, 0.82);
  color: #d0d0d0;
}
.vlab-media > .vlab-face-fail--over .vlab-face-fail-lead { color: #ffffff; }
.vlab-media > .vlab-face-fail--over .vlab-face-fail-rest { color: #c8c8c8; }
.vlab-media > .vlab-face-fail--over.is-danger { background: rgba(0, 0, 0, 0.82); border-left-color: #ff6b6b; }
.vlab-media > .vlab-face-fail--over > svg { color: #ff6b6b; }
/* 영상이 선 틀은 아래 모서리가 재생 컨트롤(시크 바) 자리다 — 그 위로 올려 둘이 겹치지 않게(길이 배지를 위로 옮긴 것과 같은 이유 · 2026-10-06). */
.vlab-media:has(> video) > .vlab-face-fail--over { bottom: 48px; max-height: calc(100% - 54px); }
/* ④ 꼬리 — 왼쪽 자산의 아이콘 행동 · 오른쪽 [수정] → 흰 주 버튼(줄 끝 · 크레딧 칩이 버튼 안에 선다 — 2026-10-16).
 *   컨테이너 쿼리 — 카드가 좁아 [✦ 생성 │ ⓒ 259] 가 안 들어가면 라벨만 숨겨 [✦ ⓒ 259](줄바꿈·잘림 금지). */
.vlab-face-foot { display: flex; align-items: center; gap: var(--sp-xs, 8px); min-width: 0; container-type: inline-size; }
@container (max-width: 250px) {
  .vlab-face-btns .vlab-node-run:has(> .credit-chip:not([hidden])) > .vlab-node-run__label { display: none; }
  .vlab-face-btns .vlab-node-run > .credit-chip { margin-left: 0; }
  .vlab-face-btns .vlab-node-run > .credit-chip > .credit-chip__sep { display: none; }
}
.vlab-face-btns { display: flex; align-items: center; gap: var(--sp-xs, 8px); margin-left: auto; flex: 0 0 auto; }
body.shell-v2 .vlab-node .vlab-face-foot .vlab-asset-acts { margin: 0; }
body.shell-v2 .vlab-node .vlab-face-foot :is(.vlab-face-edit, .vlab-face-dl, .vlab-node-run--primary) {
  height: 30px;
  min-height: 30px;
  border-radius: var(--r-pill, 999px);
}
body.shell-v2 .vlab-node .vlab-face-foot :is(.vlab-face-edit, .vlab-face-dl) { padding: 0 var(--sp-sm, 12px); }
/* [히스토리] — 클립 꼬리의 아이콘 알약(2026-10-23 · 라벨 없이 30×30 · 꼬리 높이 불변). 넷이 서는 좁은 꼬리는 흰 버튼 라벨을 먼저 숨긴다(칩은 남는다). */
.vlab-face-foot .vlab-face-hist { width: 30px; height: 30px; min-height: 30px; padding: 0; justify-content: center; border-radius: var(--r-pill, 999px); }
@container (max-width: 300px) {
  .vlab-face-btns:has(> .vlab-face-hist) .vlab-node-run:has(> .credit-chip:not([hidden])) > .vlab-node-run__label { display: none; }
  .vlab-face-btns:has(> .vlab-face-hist) .vlab-node-run > .credit-chip { margin-left: 0; }
  .vlab-face-btns:has(> .vlab-face-hist) .vlab-node-run > .credit-chip > .credit-chip__sep { display: none; }
}
/* [부분 고치기] — 꼬리 줄 [수정] 왼쪽의 같은 회색 알약(2026-11-09 오너 *"수정이랑 비슷한 뎁스"* · 모양은 `.vlab-face-edit` 그대로).
 *   좁은 꼬리(알약 넷 이상)는 라벨을 숨기고 붓 아이콘만 남긴다(전체 이름은 툴팁). */
.vlab-face-foot .vlab-face-inpaint { gap: 6px; white-space: nowrap; }
.vlab-face-foot .vlab-face-inpaint > .vlab-face-inpaint__label { white-space: nowrap; }
/* 흰 주 버튼([다시 생성 ⓒ])까지 서는 꼬리는 붓 아이콘만(라벨은 툴팁) — 넷이 한 줄에 서야 한다. */
.vlab-face-btns:has(> .vlab-node-run--primary) > .vlab-face-inpaint { width: 30px; padding: 0; justify-content: center; }
.vlab-face-btns:has(> .vlab-node-run--primary) > .vlab-face-inpaint > .vlab-face-inpaint__label { display: none; }
/* 부분 고치기 창 — 왼쪽 지금 영상(소리 끔 · 반복) · 오른쪽 글칸 + 카드 칩 + 한 줄. 좁으면 위아래로. */
.vlab-img-panel.vlab-clipedit-panel { max-width: 760px; width: min(760px, 94vw); }
.vlab-clipedit-body { display: flex; gap: var(--sp-md, 16px); align-items: flex-start; }
.vlab-clipedit-video { flex: 0 0 auto; width: min(260px, 38%); aspect-ratio: var(--clipedit-ratio, 9 / 16); max-height: 52vh; object-fit: contain; background: #000; border-radius: var(--r-sm); display: block; }
.vlab-clipedit-side { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-sm, 12px); }
.vlab-clipedit-text { width: 100%; min-height: 110px; resize: vertical; box-sizing: border-box; }
.vlab-clipedit-chips { display: flex; flex-wrap: wrap; gap: var(--sp-xxs, 4px); }
.vlab-clipedit-chip { display: inline-flex; align-items: center; gap: 6px; }
.vlab-clipedit-chip__thumb { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 5px; object-fit: cover; background: var(--surface-2, rgba(127, 127, 127, 0.15)); }
.vlab-clipedit-note { margin: 0; color: var(--text-muted, #8a8a8a); font-size: 12px; }
@media (max-width: 560px) {
  .vlab-clipedit-body { flex-direction: column; }
  .vlab-clipedit-video { width: 100%; max-height: 40vh; }
}
/* 높이를 정한 카드 — 얼굴이 남는 자리를 먹는다. 결과가 있으면 결과가 채우고 글은 두 줄, 없으면 글이 채운다. 꼬리는 바닥. */
.vlab-node.is-sized > .vlab-face { flex: 1 1 auto; min-height: 0; }
/* ⚠️ 채우는 글은 줄 수로 못 자른다(높이가 사람 손에 달렸다) — 바닥 한 줄 높이만큼 흐리게 지워 "더 있다" 를 말한다(높이 지정 글 카드에만 선다). */
.vlab-node.is-sized > .vlab-face > .vlab-face-text {
  flex: 1 1 auto;
  min-height: 0;
  display: block;
  -webkit-line-clamp: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
}
.vlab-node.is-sized > .vlab-face > .vlab-media ~ .vlab-face-text {
  flex: 0 0 auto;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-mask-image: none;
  mask-image: none;
}
.vlab-node.is-sized > .vlab-face > .vlab-face-foot { margin-top: auto; }

/* ── 노드 편집 창(큰 시트 · 2026-10-04 오너 결정 시안 A · `videoLabNodeEditor`) ──
   머리 줄(종류·제목·상태 · ‹ › · 비용 + 주 버튼 · ×) / 큰 단(주인공) / 오른쪽 단(결과·지난 생성·재료).
   ⚠️ 행동은 머리 줄에만 — 스크롤되는 내용 위에 띄운 줄이 없다(10-03 오너 거부 사유).
   ⚠️ 카드의 v2 규칙(canvas-contrast)은 `.vlab-node` 범위라 이 창에 안 닿는다 — 쓰는 것만 여기서 토큰으로 다시 적는다. */
.vne-sheet {
  /* ★ 높이 = 화면(위 56 · 아래 32) — 2026-10-05 오너 스테이징 피드백 *"창 전체 높이가 너무 낮다 · 긴 프롬프트인데 칸이 작다"*:
     내용 높이로 줄이던 10-04 판(감사 C1)을 되돌려 주인공 칸(프롬프트·글·격자)이 남는 높이를 채운다. 넘기기에서 머리 줄도 안 움직인다.
     ★ 폭 상한 1600(가운데) — 넓은 화면에서 창이 끝까지 늘어 글 줄이 ~96자가 되던 것. 열 비율은 `.vne-body`. */
  position: fixed;
  top: 56px;
  bottom: 32px;
  left: max(clamp(16px, 6vw, 120px), calc(50% - 800px));
  right: max(clamp(16px, 6vw, 120px), calc(50% - 800px));
  /* 결과 미리보기 높이 — 오른쪽 단(본문 = 화면 − 창 여백 88 · 머리 64 · 단 여백 48)에 미리보기 + 지난 생성(엄지 = 미리보기의 ¼ 높이) +
     행동 줄 + 재료(≈200)가 **스크롤 없이** 들어가는 값: (본문 − 200) × 0.9 · 상한 720. 오른쪽 단 기둥 폭은 이 높이 × 비율(아래 `--vne-col`). */
  --vne-mh: min(720px, calc((100vh - 400px) * 0.9));
  z-index: 211;
  display: flex;
  flex-direction: column;
  background: var(--surface-1, #141414);
  border: 1px solid var(--hairline, #262626);
  border-radius: var(--r-xl, 20px);
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
  color: var(--ink, #fff);
  overflow: hidden;
}
.vne-head {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 64px;
  padding: 12px 14px 12px 24px;
  border-bottom: 1px solid var(--hairline-soft, #1a1a1a);
  min-width: 0;
}
.vne-kind { display: inline-flex; align-items: center; gap: 6px; flex: none; color: var(--ink-48, #999); font-size: 13px; }
.vne-title { margin: 0; min-width: 0; font-size: 19px; font-weight: 600; letter-spacing: -0.3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vne-head .vlab-badge { flex: none; }
.vne-sp { flex: 1; }
.vne-pager { display: inline-flex; align-items: center; gap: 2px; flex: none; color: var(--ink-48, #999); font-size: 13px; font-variant-numeric: tabular-nums; }
.vne-pager-n { min-width: 48px; text-align: center; }
.vne-cost { flex: none; color: var(--ink-48, #999); font-size: 13px; white-space: nowrap; }
.vne-primary { display: inline-flex; flex: none; }
/* 머리 줄 [다운로드] — [다시 생성] 옆 · 내용 폭 · 같은 높이(2026-10-05 오너). 결과 줄의 전폭 규칙을 이긴다. */
.vne-overlay .vne-sheet .vne-head > :is(.vne-dl, .vne-act) { flex: none; width: auto; margin: 0; height: 32px; min-height: 32px; padding: 0 12px; font-size: 13px; }
/* 머리 줄 묶음 사이 간격 — [가져오기][올리기] 와 [내려받기] 사이를 조금 더(결과를 바꾸는 행동 ↔ 꺼내는 행동). */
.vne-overlay .vne-sheet .vne-head > .vne-act + .vne-dl { margin-left: 6px; }
/* 한 줄을 넘치면(셸 `fitHead`) 행동 단추는 아이콘만 — 이름은 보조기술·툴팁에 남는다. */
.vne-overlay .vne-sheet .vne-head.vne-head--tight > :is(.vne-dl, .vne-act) { width: 32px; padding: 0; justify-content: center; }
.vne-overlay .vne-sheet .vne-head.vne-head--tight > :is(.vne-dl, .vne-act) > .vlab-btn-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* 머리 줄이 넘치면(`fitHead` 실측 — 좁은 세로 연장 창 · 좁은 화면 · 영어) 종류 글·비용 줄도 덜어낸다(≤860 규칙과 같은 덜어내기). 넓은 창에선 그대로(독립 검증 9c — 고정 규칙이 넓은 연장 창에서도 아이콘만 만들던 것). */
.vne-sheet .vne-head.vne-head--tight :is(.vne-kind > span, .vne-cost) { display: none; }
/* ⚠️ `.vne-overlay` 를 앞에 붙여 특이도를 올린다 — v2 전역 `body.shell-v2 .vlab-node-run`(0,2,1)이 흰 주 버튼을 회색으로 덮었다(목 모드 실측). */
.vne-overlay .vne-sheet .vlab-node-run {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: var(--r-pill, 100px);
  background: var(--surface-3, #242424);
  color: var(--ink, #fff);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.vne-overlay .vne-sheet .vlab-node-run:hover:not(:disabled) { background: #2e2e2e; }
.vne-overlay .vne-sheet .vlab-node-run--primary { background: #fff; color: #000; }
.vne-overlay .vne-sheet .vlab-node-run--primary:hover:not(:disabled) { background: #fff; color: #000; box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.12); }
.vne-overlay .vne-sheet .vlab-node-run:disabled { opacity: 0.38; cursor: default; }
.vne-body { flex: 1 1 auto; display: grid; grid-template-columns: minmax(0, min(728px, 56%)) minmax(360px, 1fr); min-height: 0; }
/* ★ 왼쪽(글) 단은 읽기 좋은 폭까지만(본문 680 ≈ 한 줄 45자) · 오른쪽(결과·지난 생성·재료) 단이 나머지를 가져 결과 미리보기가 커진다
     (2026-10-05 오너 *"왼쪽은 가로로 너무 길고 오른쪽은 너무 좁다"* — 옛 판은 오른쪽 360 고정). 콘티는 아래 `--board` 규칙(격자가 주인공). */
.vne-body--single { grid-template-columns: minmax(0, 1fr); }
/* 두 단 모두 머리 줄과 같은 왼쪽 여백(24)에서 시작하고 같은 위끝·아래끝(24)을 쓴다 — 단은 따로 스크롤한다. */
.vne-main { display: flex; flex-direction: column; min-height: 0; overflow: auto; padding: 24px 24px 24px; }
.vne-side { display: flex; flex-direction: column; align-items: stretch; gap: 16px; min-height: 0; overflow: auto; padding: 24px 24px 24px; border-left: 1px solid var(--hairline-soft, #1a1a1a); }
/* ★ 두 단의 첫 요소가 같은 위끝선(본문 위 24)에서 시작하고, 오른쪽 단의 모든 요소는 같은 왼쪽선 — 결과 상자를 가운데 두지 않는다. */
.vne-main > .vne-sec:first-child, .vne-side > .vne-sec:first-child { padding-top: 0; }
.vne-side .vne-media { margin: 0; }
/* ★ 오른쪽 단 = 한 기둥(결과 · 지난 생성 · 결과 행동 · 재료가 미리보기와 같은 폭) — 기둥 폭 = 미리보기 폭(높이 × 비율 · 단보다 넓지 않게). */
.vne-sheet .vne-side.vne-side--col, .vne-sheet .vne-main.vne-side--col { gap: 12px; } /* 기둥 단은 조각 사이를 조금 좁혀 미리보기에 높이를 준다(재료 6개 · 1440 세로 ≥ 운영판 223×396 · 팀리드 합격선) */
.vne-side--col { --vne-col: min(100%, calc(var(--vne-mh, 460px) * (var(--vne-ratio, 9 / 16)))); }
.vne-side--col > * { width: var(--vne-col); max-width: 100%; margin-inline: auto; } /* 재료가 많아 미리보기가 줄면 기둥이 단보다 좁아진다 — 가운데에 */
/* 세로 클립 — 단을 기둥만큼으로 좁히고 창도 그만큼 좁힌다(남는 빈 폭 0). 글 단은 그대로 읽기 좋은 폭. */
@media (min-width: 861px) { /* 좁은 화면(≤860)은 한 단으로 쌓는다 — 세로 창 규칙이 그 쌓기를 이기지 않게 */
  /* ★ 단 폭 = 결과물의 실제 비율 × 쓸 수 있는 높이(+ 단 여백) — 보드 화면비가 아니다(2026-10-05 오너: 가로 보드의 2:3 그림 오른쪽 ~400px 빈 공간).
     행동 단추 줄이 더 넓으면 그 폭(`--vne-tools-w` · SideFit 이 잰다) · 상한 = 화면 − 여백 − 글 단 하한(min(728, 50vw)) · 창은 그만큼만 넓다. */
  .vne-sheet--tall {
    --vne-gut: clamp(16px, 6vw, 120px);
    --vne-side-w: min(max(200px, calc(var(--vne-mh, 460px) * (var(--vne-ratio, 9 / 16)) + 49px), calc(var(--vne-tools-w, 0px) + 49px)), calc(100vw - 2 * var(--vne-gut) - var(--vne-text-min, min(728px, 50vw))));
    left: max(var(--vne-gut), calc(50% - (728px + var(--vne-side-w)) / 2)); right: max(var(--vne-gut), calc(50% - (728px + var(--vne-side-w)) / 2));
  }
  .vne-sheet--tall .vne-body { grid-template-columns: minmax(0, 1fr) var(--vne-side-w); }
  /* 가로로 긴 결과물 — 글 단 하한 640(한 줄 ≥ 42자 · 620 은 실측 41자) · 바깥 여백 32 로 단을 넓혀 미리보기가 높이를 쓰게(팀리드: 16:9 단 아래 빈 ≤ 120). 설정 격자는 단 재료 아래(본문). */
  .vne-sheet--tall.vne-sheet--wide { --vne-gut: 32px; --vne-text-min: 640px; }
  /* 두 단의 아래 끝선을 맞춘다 — 남는 높이는 맨 아래 묶음(설정 격자) **위**에 둔다(팀리드 ② · 오른쪽 마지막 요소 아래 = 프롬프트 칸 아래). */
  .vne-sheet--tall .vne-side > :last-child { margin-top: auto; }
  /* 오른쪽 단에 비어 있는 미리보기뿐이면(이미지 노드의 첫 화면) 맨 아래 비율 상자로 두지 않고 단을 꽉 채운다(2026-10-12 오너). 결과가 서면 비율 상자로 돌아간다. */
  .vne-sheet--tall .vne-side > .vne-media.is-empty:only-child { margin-top: 0; flex: 1 1 auto; width: 100%; aspect-ratio: auto; }
  /* 단 안의 설정 격자는 기둥 폭에 맞춰 칸을 늘인다 — 넓은 단에서 4열(컨테이너 규칙)로 바뀌어 오른쪽 절반이 비던 것(팀리드 ① · 1920). */
  .vne-sheet .vne-sec--side-settings .vne-fields:not(.vne-fields--clip) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* 세로 연장 클립(한 단) — 창을 기둥 + 단 여백까지 좁히고(머리 줄 자리 400 하한) 기둥을 가운데에(독립 검증 2차 — 760 창에 기둥 203 · 빈 폭 532). */
  .vne-sheet.vne-sheet--single.vne-sheet--tall { --vne-single-w: min(max(400px, calc(var(--vne-mh, 460px) * (var(--vne-ratio, 9 / 16)) + 48px)), calc(100vw - 32px)); left: max(16px, calc(50% - var(--vne-single-w) / 2)); right: max(16px, calc(50% - var(--vne-single-w) / 2)); }
  .vne-sheet--single.vne-sheet--tall .vne-body { grid-template-columns: minmax(0, 1fr); }
  .vne-sheet--single .vne-side--col > * { margin-inline: auto; }
}
/* 한 단 종류(텍스트·연장 클립)는 창 자체가 좁다 — 머리와 본문이 같은 폭·같은 왼쪽선(레이아웃 감사 C2). */
.vne-sheet.vne-sheet--single { left: max(16px, calc(50% - 380px)); right: max(16px, calc(50% - 380px)); } /* 글 한 줄 ≈ 45자(옛 920 판은 64자) */
.vne-sec { display: flex; flex-direction: column; gap: 10px; padding: 12px 0; }
.vne-main > .vne-sec + .vne-sec { border-top: 1px solid var(--hairline-soft, #1a1a1a); }
.vne-sec--grow { flex: 1 0 auto; display: flex; flex-direction: column; }
/* 주인공 칸(프롬프트·글)이 남는 높이를 채운다 — 옆 단이 더 길 때 큰 단 아래가 비지 않게. autoGrow 가 박은 인라인 높이는 여기서 무시한다. */
.vne-sec--grow > .vlab-prompt { flex: 1 1 auto; }
.vne-sec--grow > .vlab-prompt > .vlab-text--own, .vne-sec--grow > .vne-text-big { flex: 1 1 auto; height: auto !important; }
.vne-sec-h { margin: 0; font-size: 13px; font-weight: 500; color: var(--ink-80, #ccc); }
/* 프롬프트 — 이 창의 주인공(큰 글씨·넓은 줄간 · 쓴 만큼 늘어난다). */
.vne-sheet .vlab-prompt { display: flex; flex-direction: column; gap: 8px; }
.vne-sheet .vlab-prompt-label { display: flex; align-items: center; justify-content: space-between; min-height: 28px; }
/* [AI로 고치기] — 글 도구라 프롬프트 이름표 줄 오른쪽 작은 버튼(레이아웃 감사 K2). */
.vne-overlay .vne-sheet .vlab-prompt-label .vlab-refine-btn { flex: none; width: auto; margin-left: auto; min-height: 28px; padding: 0 12px; font-size: 12px; font-weight: 500; background: transparent; border-color: var(--hairline, #262626); }
.vne-sheet .vlab-ctl-label { font-size: 13px; color: var(--ink-80, #ccc); }
.vne-sheet .vlab-text {
  min-height: 120px;
  padding: 14px 16px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--hairline, #262626);
  border-radius: var(--r-md, 10px);
  font-size: 15px;
  line-height: 1.65;
}
.vne-sheet .vlab-text--own { min-height: 220px; }
.vne-sheet .vne-text-big { min-height: min(56vh, 520px); }
.vne-sheet .vlab-text:focus { border-color: var(--accent, #0099ff); }
.vne-sheet .vlab-text[readonly] { opacity: 0.7; }
.vne-sheet :is(.vlab-model, .vlab-card-role-select, .vlab-cam) {
  height: 34px;
  padding: 0 28px 0 12px;
  background-color: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--hairline, #262626);
  border-radius: var(--r-md, 10px);
  color: var(--ink, #fff);
  font-size: 13px;
}
/* 설정 격자 — 이름표 위·컨트롤 아래 · 같은 폭 칸(레이아웃 감사 K1·I2). 빈 칸(길이를 체인이 소유 등)은 칸째 숨는다. */
/* 칸 수는 넷이 한 줄에 모델 이름까지 다 들어갈 폭(820)부터 4열, 그보다 좁으면 2×2 — 3+1 로 하나만 떨어지지 않게(감사 D4). */
.vne-sec:has(> .vne-fields) { container-type: inline-size; }
.vne-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 12px; }
@container (min-width: 820px) { .vne-fields { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.vne-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.vne-field-label { font-size: 12px; color: var(--ink-48, #999); }
.vne-field .vlab-node-dur { display: block; min-width: 0; }
.vne-field .vlab-node-dur .vlab-ctl-label { display: none; }
/* 화질 칸 = 해상도 · 비트레이트 한 줄(2026-10-05 오너 — 고급 접기 안에 숨어 있던 비트레이트를 화질 옆에). 비트레이트가 없는 모델은 해상도만 꽉 찬다. */
.vne-fields.vne-fields--clip { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.vne-fields--clip > .vne-field--wide { grid-column: span 2; }
.vne-field:has(> .vlab-node-dur:empty) { visibility: hidden; } /* 비트레이트를 못 고르는 모델 — 칸만 비우고 자리는 둔다(길이가 3열에 그대로 · 열선 유지) */
.vne-sheet .vne-field :is(select, .vlab-model) { width: 100%; min-width: 0; flex: none; }
.vne-sheet .vlab-foot-adv { margin-top: 12px; }
.vne-sheet .vlab-foot-adv > .vlab-node-dur { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
/* 한 단 종류는 창이 좁아(.vne-sheet--single) 본문이 머리와 같은 왼쪽선에서 그대로 선다 — 가운데 섬을 만들지 않는다. */
.vne-body--single .vne-main { gap: 16px; padding-top: 20px; }
.vne-body--single .vne-media { margin: 0; }
.vne-sheet .vlab-clip-settings { display: flex; flex-direction: column; gap: 8px; }
.vne-sheet .vlab-foot-adv summary { cursor: pointer; font-size: 13px; color: var(--ink-48, #999); }
/* 결과 자리 — 결과가 설 비율로 미리 자리를 잡는다(결과가 서도 단이 출렁이지 않게). */
.vne-media {
  /* 결과 비율 그대로(검은 띠 없이) — 높이 상한에 닿으면 폭을 줄여 가운데. */
  /* 폭 = min(단 폭, 높이 상한 × 비율) → 높이는 상한을 안 넘고 비율은 그대로(옆으로도 위아래로도 검은 띠 없음). */
  position: relative;
  /* 높이 상한 = 화면의 62%(최대 720) — 오른쪽 단이 넓어진 만큼 결과 미리보기가 주인공이 된다(2026-10-05 오너). */
  width: min(100%, calc(var(--vne-mh, 460px) * (var(--vne-ratio, 9 / 16))));
  aspect-ratio: var(--vne-ratio, 9 / 16);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: var(--r-lg, 15px);
  background: #0b0b0b;
}
.vne-media.is-empty { color: var(--ink-48, #999); background: transparent; border: 1px dashed var(--hairline, #262626); }
/* 생성 중 — 뼈대가 틀 전체를 채우고 시간·기준선은 가운데(작은 막대가 큰 검은 판 가운데 떠 있던 것 · 레이아웃 감사 K4). */
.vne-media .vlab-media-running { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; }
.vne-media .vlab-media-running .vlab-run-skel { position: absolute; inset: 0; height: auto; border-radius: inherit; margin: 0; }
.vne-media .vlab-media-running > :not(.vlab-run-skel) { position: relative; }
.vne-media > .vlab-media { width: 100%; height: 100%; margin: 0; max-height: none; border-radius: 0; background: transparent; }
.vne-media :is(video, img) { width: 100%; height: 100%; max-height: none; object-fit: contain; display: block; }
.vne-media .vlab-thumb { max-height: none; }
/* 결과 행동 — 같은 폭의 세로 목록(오른쪽 단 폭에 셋이 한 줄로 안 들어가 두 줄로 접히며 폭이 제각각이던 것 · 감사 I3). */
.vne-sheet .vlab-result-acts { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.vne-overlay .vne-sheet .vlab-result-acts > button { width: 100%; margin: 0; justify-content: flex-start; }
/* 결과 행동 = 미리보기 바로 아래 한 줄의 작은 단추들(내용 폭 · 높이 30) — 왼쪽 가져오기·고르기, 오른쪽 끝 내려받기
   (2026-10-05 오너 — 옛 기둥 전체 폭 막대). 좁으면 줄을 바꿔 내려간다(내려받기는 그 줄의 오른쪽 끝). */
.vne-media-tools { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* 결과 + 행동 줄 한 덩어리 — 폭 = max(미리보기 폭, 단추 한 줄 폭) · 단을 넘지 않는다 · 왼쪽선은 미리보기와 같다(팀리드 결정 (다)).
   미리보기는 덩어리 안에서 % 가 아니라 '높이 상한 × 비율' 로 폭을 내야 fit-content 가 그 값을 잰다(넘치면 max-width 가 단에 맞춘다). */
.vne-media-stack { display: flex; flex-direction: column; gap: 8px; width: fit-content; max-width: 100%; min-width: 0; }
.vne-media-stack .vne-media { width: calc(var(--vne-mh, 460px) * (var(--vne-ratio, 9 / 16))); max-width: 100%; margin: 0; }
.vne-overlay .vne-sheet .vne-media-tools > button { flex: 0 1 auto; min-width: 0; width: auto; min-height: 30px; height: 30px; margin: 0; padding: 0 10px; font-size: 12px; }
/* 한 줄을 지킨다(독립 검증 C — 영어 1280 에서 [Download]가 둘째 줄로 떨어졌다): 왼쪽 단추는 줄어들며 글을 말줄임 · 내려받기는 줄지 않는다. */
.vne-overlay .vne-sheet .vne-media-tools > button > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vne-overlay .vne-sheet .vne-media-tools > .vlab-dlbtn { flex-shrink: 0; margin-left: auto; }
/* 재료 — 이 노드에 이어진 상류(읽기 전용 줄). 고치는 것은 그 노드의 창이다. */
.vne-sheet .vlab-materials { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 6px; align-content: start; }
/* 재료 구획 = 단에서 남는 높이만 쓴다 — 미리보기가 하한에 닿아도 넘치면 **재료 격자 자체**가 스크롤된다(단 전체 스크롤 금지 · 팀리드 2026-10-05). */
.vne-sheet :is(.vne-side, .vne-main.vne-side--col) > .vne-sec:has(> .vlab-materials) { flex: 0 1 auto; min-height: 0; padding: 4px 0 0; gap: 6px; }
.vne-sheet :is(.vne-side, .vne-main.vne-side--col) > .vne-sec > .vlab-materials { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
/* 칩 = 썸네일 + 이름 한 줄(말줄임) · 종류·사정 글은 숨기고(보조기술엔 남김) 툴팁이 전문을 든다 — 한 줄에 하나씩 쌓던 목록이 미리보기를 깎던 것(팀리드). */
.vne-sheet .vlab-material { position: relative; display: flex; align-items: center; gap: 6px; min-width: 0; height: 26px; padding: 0 8px 0 3px; border-radius: 7px; background: var(--surface-2, #1c1c1c); font-size: 12px; }
.vne-sheet .vlab-material-thumb { flex: none; width: 20px; height: 20px; border-radius: 5px; object-fit: cover; background: #242424; display: inline-flex; align-items: center; justify-content: center; color: var(--ink-48, #999); }
.vne-sheet .vlab-material-kind { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.vne-sheet .vlab-material-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vne-sheet .vlab-material-state { position: absolute; top: -4px; right: -4px; padding: 0; width: 16px; height: 16px; min-height: 0; justify-content: center; border-radius: 50%; }
.vne-sheet .vlab-material-state > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.vne-sheet .vlab-materials > .vlab-hint { grid-column: 1 / -1; }
/* 콘티 격자 — 칸이 한 화면에 다 보이는 크기까지만(세로 칸이 단 폭을 다 쓰면 아래 줄이 화면 밖으로 나간다 · 목 모드 실측). */
/* 콘티 격자 — 머리 왼쪽선에서 시작하는 고정 폭 칸(칸 수만큼 줄바꿈) · 아래에 [칸 추가](레이아웃 감사 B1·B2). */
/* ★ 콘티 — 열 수는 합성 그림과 같고(카드와 같은 열 수), 칸 크기는 **높이 예산**(min(64vh, 620px))을 줄 수로 나눠 정한다.
   큰 단은 격자 폭만큼만(auto) 쓰고 나머지 폭은 오른쪽 단(연출 지시)이 가진다 — 칸이 적을 때 왼쪽 본문 오른쪽이 비어 '한쪽 몰림'이 되던 것.
   칸 비율 10:16 은 `.vlab-board-cell` 그대로. 폭이 모자라면(칸이 많을 때) 큰 단 최대 폭에서 1fr 로 줄어든다. */
.vne-body--board { grid-template-columns: minmax(0, 1fr) minmax(360px, 600px); } /* 연출 지시 글도 읽기 좋은 폭까지(옛 판은 1fr 이라 1920 에서 1,158px) — 남는 폭은 격자 단이 갖는다 */
/* 연출 지시 글칸이 오른쪽 단 높이를 채운다(아래 빈 판 · 감사 D5) — 재료는 그 아래. */
.vne-body--board .vne-side > .vne-sec:first-child { flex: 1 1 auto; }
.vne-body--board .vne-side > .vne-sec:first-child > .vlab-text { flex: 1 1 auto; height: auto !important; min-height: 160px; }
.vne-body--board .vne-main { max-width: calc(100vw - 2 * clamp(16px, 6vw, 120px) - 360px); }
.vne-sheet .vne-grid .vlab-board {
  gap: 12px; margin: 0; max-width: 100%;
  /* 칸 높이 예산 = 큰 단에서 실제로 남는 높이(`--vne-board-h` — `videoLabBoardFit` 이 잰다) · 못 재면 화면 기준 폴백(넉넉히 작게 280).
     상수 하나로 잡던 판은 [칸 추가] 줄을 12px 덜 잡아 1440 가로에서 왼쪽 단이 스크롤됐다(독립 검증 3차 · 팀리드 — 회귀). */
  --vne-cell-w: calc((min(var(--vne-board-h, calc(100vh - 280px)), 900px) - (var(--vne-board-rows, 1) - 1) * 12px) / var(--vne-board-rows, 1) * 10 / 16);
  width: calc(var(--vne-board-cols, 2) * var(--vne-cell-w) + (var(--vne-board-cols, 2) - 1) * 12px);
}
.vne-sheet .vne-grid + .vlab-board-foot { margin-top: 16px; justify-content: flex-start; }
/* 칸 번호·지우기 — 창의 칸은 커서 모서리에서 띄우고 키운다(번호가 칸 모서리에 붙어 있던 것). */
.vne-sheet .vlab-board-cell-no { top: 8px; left: 8px; font-size: 11px; padding: 1px 6px; border-radius: 6px; }
.vne-sheet .btn.btn--icon-only.vlab-board-cell-del { top: 6px; right: 6px; width: 24px; min-width: 24px; height: 24px; }
.vne-sheet .vlab-board-foot > button { flex: 0 0 auto; width: auto; }
.vne-sheet .vne-grid .vlab-board-cell { border-radius: 10px; }
/* 앵글 위젯은 자기 폭을 부모에서 받는다 — 가운데 정렬(shrink)로 두면 폭이 접혀 글자가 세로로 쌓인다(목 모드 실측). */
/* 앵글 — 큰 단 폭 전체 · 머리 왼쪽선(레이아웃 감사 A1). 조작은 3D 손잡이 + 막대 셋(A2): 드롭다운·두 번째 값 패널은 숨기고 ↺ 는 막대 줄 끝. */
.vne-sheet .vlab-angle-section { display: flex; flex-direction: column; align-items: stretch; gap: 12px; width: 100%; }
.vne-sheet .vlab-angle-section > .vlab-hint:first-child { font-size: 12px; color: var(--ink-48, #999); }
.vne-sheet .vlab-angle-section > .vlab-hint.vlab-angle-bad:first-child { align-self: flex-start; padding: 4px 10px; border-radius: var(--r-pill, 100px); background: rgba(255, 138, 138, 0.1); color: #ff8a8a; }
.vne-sheet .vlab-angle { gap: 12px; }
.vne-sheet .vlab-angle-view { order: -1; height: min(440px, 46vh); border-radius: 12px; }
/* 장면(300×290)을 키운다 — 포인터 역산은 장면 사각형으로 나눠 축척이 바뀌어도 손잡이가 포인터 밑에 선다(`videoLabAngleWidget` 손잡이 끌기). */
.vne-sheet .vlab-angle-scene { transform: scale(var(--vne-angle-k, 1.4)); transform-origin: 50% 0; top: 10px; }
.vne-sheet .vlab-angle-panel { display: none; }
.vne-sheet .vlab-angle-sliders { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) auto; gap: 8px; align-items: center; }
.vne-sheet .vlab-angle-slider { height: 34px; line-height: 32px; font-size: 12px; border-radius: 8px; }
.vne-sheet .vlab-angle-sliders .vlab-angle-reset { width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--hairline, #262626); background: rgba(255, 255, 255, 0.04); color: var(--ink, #fff); cursor: pointer; }
.vne-sheet .vlab-angle-toggle { align-self: flex-start; gap: 10px; height: 30px; padding: 0 12px; font-size: 12px; border-radius: 8px; }
@media (max-width: 1100px) {
  .vne-body { grid-template-columns: minmax(0, 52%) minmax(320px, 1fr); }
  .vne-body.vne-body--board { grid-template-columns: auto minmax(320px, 1fr); }
  .vne-body--board .vne-main { max-width: calc(100vw - 2 * clamp(16px, 6vw, 120px) - 320px); }
}
@media (max-width: 860px) {
  .vne-sheet, .vne-sheet.vne-sheet--single { top: 48px; bottom: 16px; left: 12px; right: 12px; }
  .vne-body { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto; overflow: auto; }
  .vne-main, .vne-side { overflow: visible; }
  .vne-side { border-left: 0; border-top: 1px solid var(--hairline-soft, #1a1a1a); }
  .vne-kind > span, .vne-cost { display: none; }
}

/* ── 이미지 [가져오기] 갤러리(2026-10-05 오너 — `videoLabImportGallery.js`) ──────────────────────────────
   껍데기는 `.vlab-import` 토큰 스코프를 그대로 쓴다(새 색 0). 격자는 화면 폭을 따라 열 수가 바뀐다(최소 칸 140px).
   ⚠️ 스크롤은 `.vlab-gal-scroll` 한 곳 — 무한 스크롤 관찰(IntersectionObserver)의 root 가 이 상자다. */
.vlab-gal.sm-panel { width: min(1080px, calc(100vw - 32px)); height: min(85vh, 860px); white-space: normal; }
.vlab-gal-search {
  width: 100%; box-sizing: border-box; padding: 8px 12px; border-radius: 8px;
  border: 1px solid var(--vl-node-border); background: var(--vl-node-head); color: var(--vl-node-title);
  font: inherit; font-size: 14px;
}
.vlab-gal-search:focus-visible { outline: 2px solid var(--vl-selected); outline-offset: 1px; }
.vlab-gal-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.vlab-gal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.vlab-gal-card {
  display: flex; flex-direction: column; gap: 2px; min-width: 0; padding: 6px; text-align: left; cursor: pointer;
  border: 1px solid var(--vl-node-border); border-radius: var(--vl-node-radius); background: var(--vl-node-body);
  color: var(--vl-node-title); font: inherit;
}
.vlab-gal-card:hover { border-color: var(--vl-neutral); }
.vlab-gal-card:focus-visible { outline: 2px solid var(--vl-selected); outline-offset: 1px; }
.vlab-gal-card[aria-busy="true"] { border-color: var(--vl-selected); }
.vlab-gal.is-busy .vlab-gal-card:not([aria-busy="true"]) { opacity: 0.45; pointer-events: none; }
.vlab-gal-thumb {
  aspect-ratio: 1 / 1; border-radius: 6px; overflow: hidden; background: var(--vl-canvas-bg);
  display: flex; align-items: center; justify-content: center; color: var(--vl-neutral); margin-bottom: 4px;
}
.vlab-gal-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.vlab-gal-cap { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vlab-gal-meta, .vlab-gal-owner { font-size: 12px; opacity: 0.62; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vlab-gal-status { padding: 12px 0 4px; font-size: 13px; opacity: 0.7; text-align: center; }
.vlab-gal-status:empty { display: none; }
.vlab-gal-sentinel { height: 1px; }
.vlab-gal-foot { display: flex; justify-content: flex-end; }
@media (max-width: 640px) {
  .vlab-gal.sm-panel { height: calc(100vh - 32px); }
  .vlab-gal-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; }
}

/* ★ 메뉴 검색 줄(2026-10-19 4차 · Comfy #15 · `graphContextMenu` opts.search) — 메뉴 맨 위 한 줄. */
.vlab-menu-search {
  box-sizing: border-box; width: 100%; margin: 2px 0 4px; padding: 6px 8px; border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.16); background: rgba(255, 255, 255, 0.06); color: #f2f2f5; font: inherit; font-size: 13px; outline: none;
}
.vlab-menu-search:focus { border-color: rgba(255, 255, 255, 0.4); }
/* ★ 선 끝 옮기기(4차 #5) — 끄는 동안 원래 선은 흐리게 · 입력 점 바로 앞의 선 끝 위는 잡는 커서. */
:is(.vlab-edge, .vlab-edge-border).is-moving { opacity: 0.2; }
.vlab-canvas.is-over-link-end { cursor: grab; }

/* ★ 글칸 @멘션 목록(2026-11-02 · 보드 alx9QbKqHD42ufJdMxII · `videoLabMentionPicker`) — 편집 창 덮개(`.vne-overlay`)에 붙는 fixed 목록.
   ⚠️ 창(`.vne-sheet` z 211 · overflow hidden) 안에 두면 잘린다 — 덮개의 자식으로 창 위(z 212)에 선다. 면·테두리는 우클릭 메뉴와 같은 값(새 색 0).
   ⚠️ v2 글라스 층(`body.shell-v2 .vlab-menu`)이 닿지 않게 `.vlab-menu` 를 쓰지 않는다 — 자리·크기를 스스로 든다. */
.vlab-mention {
  position: fixed;
  z-index: 212;
  min-width: 180px;
  max-width: 320px;
  max-height: 240px;
  overflow-y: auto;
  padding: 4px;
  border-radius: 10px;
  background: rgba(24, 26, 34, 0.98);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
  color: #f2f2f5;
  display: flex;
  flex-direction: column;
}
.vlab-mention-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 7px; font-size: 13px; line-height: 18px; cursor: pointer; }
.vlab-mention-item.is-active { background: rgba(255, 255, 255, 0.08); }
/* 종류 배지는 캔버스 노드 종류색(헤더 띠와 같은 값 · 2026-11-03 오너 "노드 종류별 색을 목록에") — 소품 카드는 이미지색.
   착장·상태 참조·소품은 이미지 카드라 이미지색(2026-11-03 오너 "이미지 노드 파란색인데 보라색인 것도 이상" — 태그 = 노드 제목 · 색 = 그 노드 색).
   ⚠️ 목록은 편집 창 덮개·body 에 붙어 `.vlab-canvas` 밖이다 — 같은 hex 를 대체값으로 적는다. */
.vlab-mention-kind { --vlab-mark: var(--vl-kind-image, #64B5F6); flex: none; padding: 1px 6px; border-radius: 6px; font-size: 11px; font-weight: 600; line-height: 16px; background: color-mix(in srgb, var(--vlab-mark) 18%, transparent); color: var(--vlab-mark); }
.vlab-mention-kind--character { --vlab-mark: var(--vl-kind-character, #B39DDB); }
.vlab-mention-kind--location { --vlab-mark: var(--vl-kind-location, #81C784); }
.vlab-mention-kind--board { --vlab-mark: var(--vl-kind-board, #FFA931); }
.vlab-mention-text { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.vlab-mention-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 둘째 줄 — 그 이름이 가리키는 카드 설명(착장 · 회상 · 상태: …) · 2026-11-03 */
.vlab-mention-desc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; line-height: 15px; color: rgba(255, 255, 255, 0.5); }
/* 태그를 눌러 연 목록 — 지금 가리키는 줄(2026-11-03 오너 "지금 선택되어 있는 게 뭔지") */
.vlab-mention-item.is-current { box-shadow: inset 2px 0 0 rgba(255, 255, 255, 0.55); }
.vlab-mention-current { flex: none; margin-left: auto; font-size: 11px; line-height: 16px; color: rgba(255, 255, 255, 0.6); }

/* ★ 글칸 @멘션 색(2026-11-02 오너 · `videoLabMentionMarks`) — 글칸 위에 겹친 투명 거울. 사람이 치는 글자는 textarea 의 것이고 거울은 태그 글자만 종류색으로 덧그린다.
   ⚠️ pointer-events none — 클릭·선택·IME 는 글칸이 받는다. 색은 종류색(헤더 띠·선과 같은 값 · 새 색 0) 글자 · 마우스를 올리면 바탕.
   ⚠️ 종류색 변수는 `.vlab-canvas` 범위에만 있고 편집 창 덮개는 그 밖이다 — 같은 hex 를 대체값으로 적는다(없으면 색이 통째로 빠진다). */
.vlab-mention-marks { position: absolute; pointer-events: none; overflow: hidden; border-color: transparent !important; border-style: solid; background: transparent; color: transparent; white-space: pre-wrap; overflow-wrap: break-word; z-index: 1; }
.vlab-mention-marks[hidden] { display: none; }
.vlab-mention-marks-inner { will-change: transform; }
/* 평소에는 바탕 없이 **글자만 종류색**(2026-11-03 오너 "호버하기전에는 배경색빼고 · 눈이 너무 아픔" → "밑줄만은 이상 · 글자에 색깔").
   거울의 같은 자리·같은 글꼴 글자를 종류색으로 그려 글칸의 흰 글자를 덮는다. 바탕은 투명이라 글칸의 선택 영역은 그대로 비친다. */
.vlab-mention-mark { --vlab-mark: var(--vl-kind-image, #64B5F6); color: var(--vlab-mark); background: transparent; border-radius: 3px; }
.vlab-mention-mark--character { --vlab-mark: var(--vl-kind-character, #B39DDB); }
.vlab-mention-mark--location { --vlab-mark: var(--vl-kind-location, #81C784); }
.vlab-mention-mark--board { --vlab-mark: var(--vl-kind-board, #FFA931); }
/* 마우스를 올린 태그 — 바탕을 진하게(눌러서 다른 이름으로 바꿀 수 있다 · 2026-11-03 오너). 커서는 글칸에 스크립트가 준다(거울은 마우스를 안 받는다). */
.vlab-mention-mark.is-hover { background: color-mix(in srgb, var(--vlab-mark) 22%, transparent); }
/* 풀리지 않는 옛 태그(선을 끊었거나 이름이 바뀜 · 2026-11-03) — 흐린 글자 + 점선 밑줄. 눌러서 다시 고른다. */
.vlab-mention-mark--stray { color: #8E8E93; text-decoration: underline dashed #8E8E93; text-underline-offset: 3px; }
/* 태그 위 말풍선 — 전체 노드 제목(태그는 길면 `…` 로 줄여 쓴다 · 2026-11-03) */
.vlab-mention-tip { position: absolute; z-index: 3; max-width: 360px; padding: 4px 8px; border-radius: 6px; background: #2b2b30; color: #ececec; font-size: 12px; line-height: 17px; white-space: pre-line; pointer-events: none; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35); border: 1px solid rgba(255, 255, 255, 0.08); }
.vlab-mention-tip[hidden] { display: none; }
