/* 새 셸 완성도 층 — canvas-contrast(영상탭 캔버스 색·카드 기하·카드 안 컨트롤 눈금·빈 캔버스 안내).
 *   body.shell-v2 스코프 · 영상탭(.vlab-*)만. **다른 탭은 한 픽셀도 안 바뀐다** — 모든 선택자가 `.vlab-*` 안에서 끝난다.
 *
 *   ★★ 2026-09-21 오너 2차 지적 ④ *"캔버스 가독성은 좋아졌지만 전체가 흐리멍텅하다 → **반대로**: 캔버스 배경은
 *      옅게(밝게), 노드는 진하게(어둡게)"* · ① *"빈 캔버스 안내의 설명 문장이 설명답게 안 보인다"* ·
 *      ⑦ *"버튼·셀렉트가 인풋보다 너무 크다 — 중요한 건 인풋(프롬프트)인데"* → 코덱스 명세 2차 §1·§3.
 *
 *   ★★★ **3차(2026-09-21 저녁 · 오너)**: *"캔버스와 노드 디자인이 마음에 안 들어 너무 심하게 흐리멍텅해졌는데
 *      노드도 너무 까매지고 노드들의 디자인도 너무 구리고 라벨같은것도 눈에 안띄고 너무 전체적으로 프로토타입같아."*
 *      진단은 밝기 **순서**가 아니라 **위계와 색의 부재**였다(세 판 모두 "흐리멍텅" 이 나온 이유):
 *        ① 2차가 종류 이름을 회색(`--ink-48`)으로, 머리 종류색 믹스를 6% 로 내려 여덟 종류가 전부 검정 판이 됐다.
 *        ② 메타 글자가 `--ink-48`(#6a6a7a 상당) 이라 카드 위 2.6:1 — "라벨이 안 보인다" 의 직접 원인.
 *        ③ 캔버스가 중간 회색(#383a40)이라 검정 카드와 **둘 다 탁했다**(1.53:1) · 선 격자가 화면 전체의 잡음이었다.
 *        ④ 인풋·셀렉트·버튼이 카드와 같은 검정 계열이라 컨트롤이 "구멍" 으로 읽혔다.
 *        ⑤ 배지가 외곽선이라 칩이 아니라 미완성 UI 로 읽혔다.
 *
 *   ★★★★ **4차(2026-09-21 밤 · 오너)**: *"캔버스 격자도 너무 안 보이고, 노드랑 캔버스 색도 너무 비슷하고,
 *      노드 구성들 디자인이 아직 너무 구린데 … 메뉴 독도 좀 예뻤으면 좋겠고, [＋ 노드] 메뉴 누르면 배경도
 *      너무 어두워서 안 어울리고 애니메이션도 예쁘게 떴으면 좋겠고."* → 아스트라(코덱스 gpt-6-astra) 명세 8절 ·
 *      계약서 4(`CONTRACT4.md`)가 그 명세에 리드 결정을 얹은 정본이다. 4차가 바꾼 것은 넷이다:
 *        ① **면끼리의 대비를 값으로 세웠다.** 3차는 카드/캔버스 1.16:1 을 테두리·그림자로 갚는 판이었는데,
 *           오너 눈에는 그 갚음이 안 보였다("색도 너무 비슷하고"). 4차는 카드를 **확실히 밝게** 올려
 *           1.48:1 로 세운다(아스트라 §3 의 목표 구간 1.45~1.60). 테두리·그림자는 그대로 얹는다.
 *        ② **격자는 두 단**이고 알파를 흰색에서 **찬 회청색**(rgba(160,177,205,…))으로 바꿨다 — 어두운 판 위의
 *           흰 알파는 색이 없어 탁했다. ⚠️ 그림은 처음엔 명세대로 **점**이었는데 오너가 화면을 보고
 *           *"ComfyUI 식 선 격자로"* 를 지시해 **선으로 되돌렸다**(4차-B · 아래 ①-B).
 *        ③ **머리에서 신호를 걷었다**(아스트라 §3 진단 ③: 띠·틴트·아이콘·종류색 이름·제목·연필·배지가 한 줄에서
 *           서로 싸웠다). 4px 띠 **제거** · 종류 이름은 중립색(#d7dde8) · 색은 **아이콘 하나가** 진다 ·
 *           연필은 카드 hover/focus-within 에서만 보인다(자리는 유지).
 *        ④ **hover·선택을 pseudo-element 의 opacity 로** 옮겼다(아스트라 §7) — 테두리 폭·그림자를 보간하지 않는다.
 *
 *   ★★★★★ **5차(2026-09-21 밤 늦게 · 오너 4차 판정)**: *"너무 퍼런 스타일 · 격자 배경이 노드와 너무 비슷 ·
 *      노드 흐리멍텅 · 도크도 퍼렇고 우리 서비스 톤앤매너와 너무 다르다."*
 *      원인은 구조가 아니라 **색 계열**이었다 — 아스트라 팔레트가 전부 청회색 슬레이트였다
 *      (#353a46 · #596273 · #aeb7c8 · #9bc2ff · #d7dfec · rgba(160,177,205,…)). 우리 앱은 **중립 검정 + 흰 알파 층**이다.
 *      5차는 **청색 계열 값을 전부 걷고 앱 토큰 계열로 교체**한다 — 구조·간격·애니메이션·줄 구성은 4차 그대로다:
 *        캔버스 `#0e0e0e`(= 앱 `--canvas`) · 카드 `#1e1e26` · 머리 `#25252e` · 테두리·채움은 **흰 알파** ·
 *        글자 `#ffffff`/`#e4e4e4`/`#a0a0a0` · 주 버튼은 **흰 알약**(앱 주 CTA) · 선택은 `--accent`.
 *      🔴 **리드에게 보고한 두 수**: ① 카드/캔버스 면 대비는 **1.15:1** 이다(계약서 5차의 *"≈1.7"* 은 계산이 안 맞는다) —
 *         3차와 같은 구조로 돌아간 것이고, 3차가 *"흐리멍텅"* 판정을 받은 그 구조다. ② 격자 major 합성색(#2c2c33)이
 *         **카드보다 밝다**(1.19:1) — 오너의 *"격자가 노드와 너무 비슷"* 이 이 조합에서는 남는다. 값은 계약서대로 넣었다.
 *
 *   🔴 **팔레트 이력(다섯 판).** 1차는 캔버스 #090909 < 카드 #262626(카드가 빛나는 판) · 2차는 **뒤집어서**
 *      캔버스 #383a40 > 카드 #181a1f(둘 다 탁함) · 3차는 v1 의 뼈대로 돌아가 캔버스 #1b1c20 < 카드 #26282e
 *      (면끼리 1.16:1 — 테두리·그림자가 갚는 판). **4차는 그 뼈대를 지키고 카드를 한 단 더 올린다**:
 *      찬 숯 캔버스(#1b1d23) · 밝은 카드(#353a46)였는데 **전부 청회색**이라 오너가 되돌렸다.
 *      **5차는 그 구조에 앱의 중립 검정 램프를 끼운다**: 캔버스 #0e0e0e · 카드 #1e1e26 · 머리 #25252e ·
 *      테두리는 흰 알파 .14(합성 #3e3e44 · 카드 위 1.56:1 · 캔버스 위 1.79:1 — **양쪽 바탕 모두에서** 네 변이 보인다.
 *      1차 판의 실패가 정확히 그 한쪽이 사라진 것이었다). 값을 바꾸면 `graphEdgeKinds.test.mjs` 의 대비 눈금이
 *      다시 잰다(V2_CANVAS·V2_BODY·테두리 원문 + 카드 그림자 선언 + 배지 칩).
 *
 *   ⚠️ **링크 순서가 이 파일의 전부다.** index.html 이 canvas.css · canvas-modals.css · apple-rules.css **뒤**,
 *      리퀴드 글라스 셋 **앞**에 링크한다(`liquidGlassLayer.test.mjs` 가 "글라스가 v2 전 파일 뒤" 를 잠근다).
 *      그래서 앞 층은 **같은 특이도 + 순서**로 이기고(특이도를 함부로 올리면 다음 층이 못 덮는다),
 *      글라스가 칠하는 자리(`.vlab-guide-card` · 모달 글칸 · `.btn--secondary`)만 특이도를 한 단 올려 이긴다 —
 *      그 자리마다 이유를 적었다.
 *   ⚠️ 새 색을 만들지 않는다. 캔버스 쪽 값은 `videolab.css` 의 `--vl-*` 토큰에 매고, 나머지는 framer 토큰을 쓴다.
 *      3차에서 **새로 필요한 값은 전부 ① 블록의 토큰으로 세웠다**(본문·메타 글자 · 우물 · 보조 컨트롤 · 점검 줄). */

/* ════════ ① 토큰 — 어두운 캔버스 · 한 단 밝은 카드 · 글자 세 단 ════════
 *   ★★ 5차-D/E(오너: "퍼런 끼 · 물 빠진 느낌" → E: 카드를 다시 어둡고 진하게 #1e1e1e · 테두리 흰 .16 · 종류색 14%+띠 · 캔버스 #0a0a0a — 앱 홈 카드처럼 검정 위 어두운 카드 + 또렷한 테) — **청색 기 0 의 순중립 회색**으로 전부 갈았다: 캔버스 #0e0e0e · 카드 #1e1e1e(면 1.51) ·
 *      머리 #262626 · 제목 순백 · 본문 #e4e4e4 · 메타 #a0a0a0 · 우물 #161616 · 격자 흰 .04/.06(major 합성 #1d1d1d < 카드).
 *      B 채널이 R·G 보다 큰 값은 이 파일에 하나도 없어야 한다(앱 토큰 #0f0f17·#c8c8d0 도 그 이유로 안 읽는다).
 *   ⚠️ 스코프에 `.vlab-menu`·`.vlab-import` 가 함께 있는 이유는 `videolab.css` 의 토큰 블록과 같다 —
 *      우클릭 메뉴·가져오기 판은 body 직계라 캔버스 안에서 상속을 못 받는데, 색 견본이 **같은 쌍**을 읽어야 한다.
 *   ★ 3차에서 `.vlab-dock`·`.vlab-seltool` 을 이 스코프에 **더했다** — 계약상 둘 다 **카드와 같은 표면**이다
 *     (아래 ⑤ · canvas-dock.css). 도크는 캔버스의 **형제**라(`videoLabView` 가 `.vlab-toolbar` 안에 짓는다)
 *     상속으로 토큰을 못 받는다. 선택 막대는 캔버스의 자식이라 원래 받았지만(`mountSelectionToolbox(canvasEl)` —
 *     `position: fixed` 는 **상속을 끊지 않는다**: 2차 주석의 "캔버스 밖" 은 배치 기준 이야기였지 상속 이야기가 아니다),
 *     이름을 한 줄에 모아 두면 다음 라운드에 "이 판도 카드 표면인가" 를 여기서 한 번에 읽는다.
 *     2차에는 그 값들이 두 파일에 hex 로 흩어져 있었다 — 고칠 곳이 한 자리여야 다음 라운드에 안 어긋난다. */
body.shell-v2 :is(.vlab-canvas, .vlab-menu, .vlab-import, .vlab-dock, .vlab-seltool) {
  /* 캔버스 — **앱과 같은 바탕**(`tokens.css --canvas #0e0e0e`)이다. 4차의 찬 숯(#1b1d23)은 청회색이었다. */
  --vl-canvas-bg: #141414;   /* 5차-F 오너: "캔버스 배경을 조금만 밝게 해서 노드와 대비를" — 검정 속 구멍이 아니라 판 위의 카드로 */
  /* 격자 — 흰 알파 두 단(5차). 4차의 `rgba(160,177,205,…)` 청회색을 걷었다.
     ⚠️ 간격은 JS 가 정한다(`graphViewport.GRID_MINOR 20 / GRID_MAJOR 100`) — 여기서는 **색**만 정한다.
     🔴🔴 **격자는 카드보다 어두워야 한다**(5차-C · 리드 채택). 5차 첫 판(.06/.12)은 major 합성이 #2c2c33 이고
        카드가 #1e1e26 이라 **격자가 카드보다 밝았다**(1.19:1 · 뒤집힌 쪽) — 카드가 어두운 구멍처럼 읽히고
        오너 4차 지적 *"격자 배경이 노드와 너무 비슷"* 이 그대로 남았다(4차의 실제 결함도 같은 것이었다:
        격자 #333842 vs 카드 #353a46 = 1.03:1). 격자를 한 단 내리고 카드를 두 단 올려 **틈을 만든다**.
        실측: minor 합성 #1b1b23(1.12:1) · major 합성 #25252c(1.25:1) · **major vs 카드 1.31:1**(카드가 더 밝다).
        ⚠️ 이 관계를 `graphEdgeKinds.test.mjs` 가 한 줄로 잠근다(4차·5차에 두 번 물린 결함이다). */
  --vl-grid-minor: rgba(255, 255, 255, 0.03);
  --vl-grid-major: rgba(255, 255, 255, 0.04);  /* 합성 #1d1d1d — 카드 #1e1e1e 와 거의 같아 5차-F 에 .045 로 */
  /* 카드 — 중립 검정 램프 **위쪽**(앱 `--divider-soft #3a3a47` 언저리 · R=G, B +10 으로 같은 색조).
     ★ 5차-C: #1e1e26 → **#1e1e1e**. 캔버스 위 **1.65:1** 로 면끼리가 제 몫을 하고(위 격자 🔴🔴 참고),
       그 위에 테두리(1.54 / 2.54)와 그림자가 얹힌다 — 면·테두리·그림자 셋이 함께 카드를 세운다. */
  --vl-node-body: #242424;   /* 5차-G 오너 "캔버스에 노드가 묻힌다": 카드를 한 단 올리고(#14 위 1.24) 테를 또렷하게 · 그림자를 깊게 — 판 위에 떠 있는 카드 */
  /* 머리 바탕은 몸통보다 한 단 밝다. 그 위에 종류색 **8%** 를 섞는다(4차에 4px 띠를 걷은 결정은 유지 —
     한 줄에 색 신호가 넷이면 서로 싸운다. 색은 아이콘 하나가 진다).
     결과 머리색(실측 5차-C · 글/맥락/이미지/인물/배경/콘티/클립/앵글):
     #514e45 · #494952 · #454b59 · #4b4957 · #474d50 · #514a49 · #514959 · #464f54 —
     머리 위 최저 대비: 제목 6.37 · 종류 이름 5.07 · 종류 아이콘 3.32:1(그래픽 요소 3:1). */
  --vl-node-head: #2c2c2c;
  /* 🔴 테두리·hover 테는 **흰 알파**다(5차) — 홈 카드 칩(`rgba(255,255,255,.14)` + inset 흰 .10)과 같은 테다.
     ⚠️ `background-clip` 기본값이 `border-box` 라 이 알파는 **카드 자기 바탕 위**에 합성된다(캔버스가 아니다):
        합성색 #54545c · 카드 위 1.54:1 · 캔버스 위 2.54:1 — 양쪽 바탕 모두에서 네 변이 보인다(hover 는 #68686f · 2.10:1).
     ⚠️ `graphEdgeKinds.test.mjs` 가 이 두 줄을 **원문으로 읽어** 합성까지 다시 계산한다(hex 가 아니어서 그렇다). */
  --vl-node-border: rgba(255, 255, 255, 0.22);
  --vl-node-border-hover: rgba(255, 255, 255, 0.28);
  /* 카드 모서리 14 · 머리 위쪽 13(= 14 − 테두리 1px · videolab.css 의 `calc(--vl-node-radius - 1px)`).
     ⚠️ v1(옛 셸)의 10 은 건드리지 않는다 — 이 토큰을 shell-v2 스코프에서만 덮는다. */
  --vl-node-radius: 14px;
  /* 글자 세 단 — 제목 · 본문 · 메타. 카드 #1e1e1e 위 8.87 / 8.49 / 5.16:1(실측 5차-C).
     🔴 제목은 **순백이 아니다.** shell-v2 의 `--ink` 는 `framer.css` 의 #ffffff 가 아니라 `apple-rules.css` ③ 이
        덮은 **`rgba(255,255,255,.85)`** 다(그 파일이 뒤에 실린다) — *"중성 다크 표면 위 순백은 번진다"* 가 그
        규칙의 근거이고 카드가 정확히 그 표면이다. 그래서 여기 값은 **그 알파를 카드 위에 합성한 hex**(#ddddde)다.
     ⚠️ hex 로 적는 이유: 가드가 이 세 줄에서 **색을 읽어 대비를 잰다**(`var()` 는 어느 파일의 정의가 이기는지를
        가드가 알 수 없다 — 이 저장소에서 `--ink*` 는 tokens.css · framer.css · apple-rules.css 세 곳에 있다).
     ⚠️ 카드 바탕을 바꾸면 이 합성값도 다시 계산해야 한다(`색 = 흰 85% over --vl-node-body`). */
  --vl-node-title: #ffffff;
  --vl-node-text: #e6e6e6;
  /* 🔴 **카드 안에서 `--ink-48`(shell-v2 #999999)를 쓰지 않는다** — 2차의 "라벨이 안 보인다" 가 그것이었다.
     메타는 종류 셀렉트 라벨·비용·힌트·설명·맥락 카드의 종류 이름이 함께 읽는다. */
  --vl-node-meta: #a8a8a8;
  /* 카드 그림자 — 가까운 한 겹이 변을 세우고 먼 한 겹이 카드를 띄운다. 면 대비가 1.15 뿐이라 이 값이 카드의 뼈대다.
     ⚠️ hover 에서 **보간하지 않는다** — 더 깊은 그림자는 ②-B 의 pseudo-element 가 opacity 로 드러낸다. */
  --vl-node-shadow: 0 2px 6px rgba(0, 0, 0, 0.55), 0 12px 32px rgba(0, 0, 0, 0.6);
  /* 카드 윗변 반사광 — 홈 카드 칩의 `inset 흰 .10` 과 같은 성격(여기는 카드가 커서 .06). */
  --vl-node-sheen: inset 0 1px 0 rgba(255, 255, 255, 0.10);
  /* 머리/몸통 구분선 — 흰 알파 7%. 불투명 헤어라인은 카드 테두리와 같은 세기로 서서 머리가 별도 상자로 떠 보인다. */
  --vl-node-divider: rgba(255, 255, 255, 0.07);
  /* 인풋 우물 — 카드보다 **어두운** 구덩이다(5차-C 실측 1.61:1 · 카드가 밝아진 만큼 이 관계가 더 또렷해졌다).
     캔버스색과는 다른 값이다(3차는 우물 = 캔버스라 카드에 뚫린 구멍으로 읽혔다).
     테두리는 흰 알파이고 우물 위 1.38:1 · hover 1.84:1 · 본문 글자 우물 위 13.66:1 · 메타 8.30:1. */
  --vl-well-bg: #121212;
  --vl-well-border: rgba(255, 255, 255, 0.12);
  --vl-well-border-hover: rgba(255, 255, 255, 0.20);
  --vl-well-radius: 9px;
  /* 보조 컨트롤(보조 버튼·아이콘 버튼·선택 막대 버튼) — **흰 알파 채움**이다(앱의 `--surface-*` 성격).
     🔴 `var(--surface-2)` 를 그대로 쓰지 않는다: `framer.css` 가 shell-v2 에서 그 토큰을 **불투명 hex**
        (#141414/#1c1c1c/#242424)로 덮어 두었고(tokens.css 의 흰 알파 .04/.06/.09 가 아니다), 그러면 컨트롤이
        카드(#1e1e26)보다 **어두워져** 2차의 "컨트롤이 구멍으로 읽힌다" 가 그대로 돌아온다.
        그래서 계약서가 묘사한 **흰 알파**를 직접 적는다 — 합성 #44444d(카드보다 **밝다** · 1.20:1) · hover ≈.10 · 누름 ≈.14.
     ⚠️ 셀렉트는 이 채움이 아니라 **우물**을 쓴다 — 고르는 칸은 누르는 단추가 아니다. */
  --vl-ctl-bg: rgba(255, 255, 255, 0.08);
  /* 🔴 **호버는 또렷해지는 쪽이다**(5차-I · 오너 2026-09-21: *"흰 생성버튼에 마우스를 올렸을 때 사라져버리는
     느낌이야 취소버튼도 색이 옅어져서 사라지는 느낌"*). 그 전 값은 hover 흰 .04(합성 8%→11.7% · 사람 눈에
     거의 없는 차이)라 손을 올려도 **아무 일도 안 난 것처럼** 보였고, 그 무반응이 "옅어졌다" 로 읽혔다.
     여기서는 한 단이 아니라 **두 단**(.10 → 합성 ≈17%)을 올리고 테두리 링을 함께 세운다(아래 ③-hover). */
  --vl-ctl-hover: rgba(255, 255, 255, 0.10);
  --vl-ctl-press: rgba(255, 255, 255, 0.16);
  /* 색 채움이 필요한 자리(우클릭 메뉴 줄·가져오기 줄·미니맵 버튼 호버) */
  --vl-control-hover: rgba(255, 255, 255, 0.12);
  /* 주 행동(돈을 쓰는 버튼 하나) — **흰 알약**이다(5차 · 앱 주 CTA 와 같은 얼굴 · 21:1).
     4차의 연한 파랑(#9bc2ff)은 폐기했다 — 그 색이 오너의 *"너무 퍼런 스타일"* 의 한가운데였다. */
  --vl-primary-bg: #ffffff;
  --vl-primary-ink: #000000;
  /* 🔴 **흰 알약은 호버에서 어두워지지 않는다**(5차-I). 흰 채움 위에 검정 알파를 얹는 것이 그때까지의 호버였는데
     (#ffffff → #f0f0f0), 어두운 캔버스 위에서 그것은 **버튼이 뒤로 물러나는** 신호다 — 오너가 *"사라져버리는
     느낌"* 이라고 부른 그 변화다. 흰색은 이미 밝기의 천장이라 **더 밝게** 할 수 없으므로, 달라지는 것을
     **버튼 바깥**(흰 후광 링 + 바깥으로 퍼지는 빛 = 떠오름)으로 옮긴다.
     ⚠️ 링은 2px 이다 — 컨트롤 사이 간격이 8(`--vl-ctl-gap`)·자산 줄이 6 이라 3px 을 넘으면 이웃과 닿는다.
     ⚠️ `transform: scale()` 로 키우지 않는다: 이 버튼들은 캔버스의 zoom 변환 **안**에 있어 배율이 곱해지고,
        글자가 다시 표본화되어 흐려진다(오너가 싫어하는 그 "흐리멍텅" 과 같은 얼굴이 된다). */
  /* ⚠️ 바깥 겹은 **흰 글로우**다(검정 드롭이 아니다) — 카드도 캔버스도 이미 검정이라 검정 그림자는
        그 위에서 아무것도 안 보인다(5차-I 에 브라우저로 두 값을 나란히 놓고 골랐다). */
  --vl-primary-lift: 0 0 0 2px rgba(255, 255, 255, 0.34), 0 0 18px rgba(255, 255, 255, 0.20);
  /* 못 누르는 컨트롤 — **카드를 흐리지 않는다**. 채움·글자를 제 값으로 적는다. */
  --vl-ctl-off-bg: rgba(255, 255, 255, 0.03);
  --vl-ctl-off-ink: #9a9a9a;   /* 흰 3% 바탕(#3e3e48) 위 4.38:1 — 5차-C 에 카드가 밝아져 한 단 올렸다 */
  /* 점검 — 중립 바탕 + **왼쪽 2px 호박 선**(5차: 4차의 호박 물듦 블록은 카드 안에 노란 판을 만들었다).
     블록 위 글 6.15:1 · 선 4.42:1(5차-C 실측). */
  --vl-warn-bg: rgba(255, 255, 255, 0.05);
  --vl-warn-ico: #d9a441;
  --vl-warn-ink: #e3c98a;
  --vl-lint-bar: #d9a441;
  /* 연결점 — 밝은 중립 채움(카드 위 6.97:1 · 캔버스 위 11.47:1) + 카드색 테 2px + 바깥 고리 흰 18%. */
  --vl-port-neutral: #cccccc;
  --vl-port-ring: rgba(255, 255, 255, 0.18);
  /* 선택은 파랑 하나다 — **앱 포커스색**(`--accent`)이다(4차의 #7aaeff 슬레이트 블루를 걷었다). */
  --vl-selected: var(--accent);
  --vl-selected-halo: color-mix(in srgb, var(--accent) 18%, transparent);

  /* ★★ 카드 안 컨트롤 **한 눈금**(2026-09-21 오너 ⑦ · 3차) — 버튼·셀렉트·아이콘 버튼·선택 막대가
     전부 이 넷을 읽는다. 크기를 고칠 곳이 **한 자리**여야 "버튼이 인풋보다 크다" 가 다시 안 생긴다.
     ★ 4차: 28 한 단에서 **세 단**으로 갈랐다 — 고르는 칸·보조 32 · 주 행동 34 · 아이콘만 28.
        주인공은 여전히 프롬프트 칸이다(글칸 최소 높이 144~208). 셋 다 툴바·도크의 36 과는 다른 자다. */
  --vl-ctl-h: 32px;                  /* 셀렉트 · 보조 버튼 */
  --vl-ctl-h-primary: 34px;          /* [생성] — 한 눈금 위 */
  --vl-ctl-h-icon: 28px;             /* 아이콘만 있는 버튼(정사각) */
  --vl-ctl-fs: var(--fs-footnote);   /* 12px */
  --vl-ctl-px: 10px;                 /* 좌우 안쪽 여백 */
  --vl-ctl-gap: var(--sp-xs);        /* 8px — 한 줄 안 컨트롤 사이 */
  --vl-ctl-radius: 8px;              /* 카드 안은 알약이 설 폭이 없다 */
}

/* ════════ ①-B 격자 — 선(2차) → 점(3·4차) → **다시 선**(4차-B · 오너) ════════
 *   🔴 레이어 순서가 **JS 와의 계약**이다: `graphViewport.gridBackgroundOf` 가 `background-size` 를
 *      `큰 칸, 큰 칸, 작은 칸, 작은 칸` 네 쌍으로 쓴다(graphViewport.js 77~100행 · 114~128행).
 *      그래서 **레이어 1·2 = 큰 칸 가로·세로 선 · 레이어 3·4 = 작은 칸 가로·세로 선**이다.
 *      간격·미는 양은 JS 가 정하므로 여기서 건드리지 않는다(한쪽만 고치면 격자가 월드와 어긋난다).
 *   ⚠️ **네 레이어를 다 쓴다** — 3·4차의 점 판은 두 자리를 `none` 으로 비워 뒀는데, 선 격자는 축이 둘이라
 *      단마다 두 레이어가 필요하다. 개수는 그때도 지금도 **넷**이고, 그것이 JS 와의 계약이다.
 *   ⚠️ 이 선언은 `videolab.css` 의 `.vlab-grid` 와 **같은 그림**이다(옛 셸도 선 격자다) — 그래도 여기 다시 적는
 *      이유는 ① 3·4차에 점으로 갈아 끼웠던 이력이 있고 ② 다음 라운드가 "새 셸의 격자는 어디서 정하나" 를
 *      이 파일에서 찾기 때문이다. 색은 위 ① 블록의 토큰 두 개가 정한다.
 *   ⚠️ 굵기는 1px **화면 px 이 아니라 세계 px** 이다(배율을 따라 굵어진다) — 지름/굵기를 화면에 고정하는 일은
 *      `gridBackgroundOf` 가 레이어마다 값을 다시 쓰는 일이고, 이 라운드는 JS 를 안 건드린다(계약서 4 리드 결정 §2).
 *   ⚠️ 50% 아래에서 작은 선을 숨기는 일도 JS 몫이다 — `gridBackgroundOf` 가 화면 8px 아래로 내려가면 작은 칸을
 *      큰 칸 간격으로 접으므로, 지금도 **무아레 대신 한 단**이 된다. */
body.shell-v2 .vlab-grid {
  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);
}

/* ════════ ② 노드 카드 ════════
 *   canvas.css 가 카드를 표면(surface-1)+투명 테두리로 바꿔 놨던 것을 **토큰으로 되돌린다** —
 *   hex 를 여기 베끼지 않는 이유: `data-color` 로 사용자가 고른 카드색이 `--vl-node-body` 를 갈아 끼운다.
 *   ⚠️ 카드에는 블러를 걸지 않는다(글라스 층이 `.vlab-node` 를 칠하지 않지만, 선언으로 못 박아 둔다) —
 *      캔버스 위 수십 장이 각자 배경을 샘플링하면 팬 한 번이 그 수만큼의 합성 작업이 된다. */
body.shell-v2 .vlab-node {
  /* 5차-H 오너 "2번 해보자" — 평판이 아니라 **재질**로 읽히게 아주 옅은 세로 그라데이션(위 +2% 흰 · 아래 +6% 검정 ≈ #262626 → #222222).
     `--vl-node-body` 에서 파생하므로 사람이 고른 카드색(`[data-color]`)도 같은 결을 받는다. 색 자체는 그대로다. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--vl-node-body), #ffffff 2%),
    color-mix(in srgb, var(--vl-node-body), #000000 6%));
  border: 1px solid var(--vl-node-border);
  /* 반경 14(4차 · ① 토큰) — 토큰 하나가 카드·머리 띠·크기 손잡이 모서리를 함께 정한다. */
  border-radius: var(--vl-node-radius);
  box-shadow: var(--vl-node-sheen), var(--vl-node-shadow);
  /* 줄 간격 12 — 아스트라 §4 의 리듬(4/8/12/16)에서 "덩이 사이" 값이다(3차의 8 은 "한 줄 안" 값이었다). */
  gap: var(--sp-sm);
  padding: var(--sp-sm);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* 글자 — 흐리게(opacity) 말고 색 단계로. 카드 안의 기본은 **본문 단**이다(3차 · 옛 `--ink` 는 제목과 같은 흰색이었다). */
body.shell-v2 .vlab-node { color: var(--vl-node-text); }

/* ── 머리 줄 ──────────────────────────────────────────────────────────────
 *   ★★ 4차: **띠를 걷었다.** 3차의 머리에는 색 신호가 넷(4px 띠 · 종류색 10% 틴트 · 종류색 아이콘 · 종류색 이름)이라
 *      36px 한 줄에서 서로 싸웠다(아스트라 §3 진단 ③ *"too many competing signals"*). 남긴 것은 **아이콘 하나**다.
 *   ★ 높이 40(3차 36) · 좌우 여백 12 · 사이 간격 8 — 40 은 280px 카드에서 [▾]26·✎24·배지 20 이 함께 서는 최소치다
 *     (명세의 44 는 280 카드에 과하다 — 계약서 4 리드 결정 §3).
 *   ⚠️ 왼쪽 여백 12 는 이제 띠를 피할 필요가 없다 — 아스트라 §4 의 리듬 값 그대로다.
 *   ⚠️ 종류색 틴트는 **8%** 다(명세 6% 는 60% 배율에서 안 보인다 · 계약서 4). 결과 머리색(실측 · 글/맥락/이미지/
 *      인물/배경/콘티/클립/앵글): #4b4d48 · #434854 · #3e4a5b · #454859 · #414c52 · #4b494c · #4b485c · #3f4e56 —
 *      종류마다 갈리면서도 여덟이 모두 같은 밝기 단에 앉는다(제목 대비 최저 7.77:1). */
body.shell-v2 .vlab-node > .vlab-node-head {
  min-height: 40px;
  padding: 4px var(--sp-sm);
  gap: var(--sp-xs);
  background: color-mix(in srgb, var(--vl-kind-color) 14%, var(--vl-node-head));
  border-bottom: 1px solid var(--vl-node-divider);
  /* 🔴 4px 종류색 띠 **제거** — `videolab.css` 의 `inset 4px 0 0` 을 여기서 끊는다(값을 지우는 것이 아니라
     이 셸에서만 none 으로 덮는다 · 옛 셸은 띠를 그대로 쓴다). */
  /* 5차-E — 종류색 왼쪽 띠 3px 를 되살린다(4차가 걷었다): 오너 "물 빠진 느낌" 은 색이 빠진 것이다 — 머리 14% 믹스 + 띠가 종류를 말한다. */
  box-shadow: inset 3px 0 0 var(--vl-kind-color);
  color: var(--vl-node-title);
}
/* 읽기 전용 카드의 첫 자식(종류 이름) 왼쪽 여백 — v1 은 띠를 피해 8 을 더했다. 띠가 없어졌으니 0 이다. */
body.shell-v2 .vlab-node > .vlab-node-head > .vlab-node-kind:first-child { margin-left: 0; }
/* ★★ 종류 아이콘(3차 · DOM 은 `videoLabNodeView` 가 넣는다 — `.vlab-node-kind` **앞**의 `<span class="vlab-node-kind-ico">`).
 *   색은 종류색이다 — 이름과 **같은 색**이어야 아이콘이 종류의 두 번째 신호가 된다(색만으로 구분하지 않는다는 §2-2 의 짝).
 *   ⚠️ 음수 margin 으로 아이콘→이름 사이만 4 로 좁힌다(머리 줄의 간격은 8 이다) — 아이콘과 이름은 한 덩이로 읽혀야 한다. */
body.shell-v2 .vlab-node .vlab-node-kind-ico {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  margin-right: -4px;
  /* ⚠️ `videolab.css`(v1 판)는 맥락 카드의 아이콘을 `opacity: .7` 로 흐린다 — 새 셸은 흐림이 아니라 **색**으로
     단을 말하므로(아래 맥락 예외) 여기서 1 로 되돌린다. 안 되돌리면 메타색이 한 번 더 깎인다. */
  opacity: 1;
  color: var(--vl-kind-color);
}
body.shell-v2 .vlab-node .vlab-node-kind-ico svg { width: 16px; height: 16px; stroke-width: 1.75; }
/* 종류 이름 — 12/16 **650** · **중립색 #d7dde8**(4차). 3차는 이 글자도 종류색이라 아이콘과 같은 말을 두 번 했고,
   여덟 종류의 이름이 전부 다른 색으로 서서 머리 줄이 색 조각의 나열이 됐다(아스트라 §3 *"remove its coloured text treatment"*).
   ⚠️ 자간은 적지 않는다 — 본문 단(≤15px)의 원시 자간은 `v2/typeScale.test.mjs` 가 잡는다(토큰만 허용).
   ⚠️ 색 하나로 종류를 말하지 않는다는 계약(§2-2)은 **이름 글자 자신**이 이미 지킨다(`글`·`이미지`·`클립` …).
   ★ 5차: 4차의 #d7dde8(청회색)을 앱의 `--ink-80` 으로 바꿨다 — 카드 위 10.31:1 · 머리 위 최저 7.83:1. */
body.shell-v2 .vlab-node .vlab-node-kind { font-weight: 650; color: var(--ink-80); }
/* 맥락 카드만 예외 — 읽기 전용 사본이라 머리 전체가 한 단 내려간다. */
body.shell-v2 .vlab-node--context .vlab-node-kind,
body.shell-v2 .vlab-node--context .vlab-node-kind-ico { color: var(--vl-node-meta); }
/* 제목 14/20 600 — 카드에서 가장 굵은 글자. 2차의 13 은 종류 이름(12)과 한 단 차이뿐이었다.
   ⚠️ 36px 띠 안에 [▾] 26 · ✎ 24 · 배지 20 이 함께 서므로 **줄어드는 것은 제목 하나**라는 규율이 이 크기를 버틴다. */
body.shell-v2 .vlab-node-title { font-size: 14px; line-height: 20px; font-weight: 600; color: var(--vl-node-title); }
/* 이름 없는 카드 — 흐림(opacity)이 아니라 메타 단 + 400(2차는 `opacity: .45` 라 대비가 목표 아래였다). */
body.shell-v2 .vlab-node .vlab-node-title.is-empty { opacity: 1; color: var(--vl-node-meta); font-weight: 400; }
/* 머리 버튼 24×24 · 배지 20 높이 · 둘 다 **안 줄어든다**(2차 규칙 유지 — 줄어드는 것은 제목 하나다). */
body.shell-v2 .vlab-node .vlab-node-head-btn { width: 24px; height: 24px; }
body.shell-v2 .vlab-node .vlab-badge { flex: 0 0 auto; min-height: 20px; padding: 0 6px; display: inline-flex; align-items: center; }

/* ★★ 연필(✎ 이름 바꾸기) — **카드에 손이 왔을 때만 보인다**(4차 · 아스트라 §3 *"Edit action appears on header hover
 *   or focus-within; its slot stays reserved"* · §4 Remove *"persistent edit pencils"*).
 *   🔴 v1 주석의 반대 결정이다(*"항상 보인다 — 호버에만 뜨면 처음 보는 사람에게는 없는 기능이다"*). 근거가 바뀌었다:
 *      그 이름은 **F2 · 우클릭 메뉴 · 카드 더블클릭** 셋으로도 고칠 수 있고(조작법 표에 다 있다), 캔버스에 카드가
 *      수십 장 서는 화면에서 연필 수십 개는 "기능" 이 아니라 잡음이었다(오너 4차 *"노드 구성들 디자인이 너무 구린데"*).
 *   ⚠️ **자리는 유지한다** — `opacity` 만 0 으로 둔다. `display:none` 으로 걷으면 배지가 그 자리로 밀려오고
 *      ✎ 두 번 누르기가 다른 자리를 누르게 된다(2026-09-17 그 신고의 재발 경로 · videolab.css `headPen` 주석).
 *   ⚠️ 켜져 있어야 하는 세 상태는 hover 밖에서도 보인다: 이름 편집 중(`.is-active` = [✓ 이름 저장]) ·
 *      키보드 포커스(`:focus-visible`) · 카드 안 어딘가에 포커스가 있을 때(`:focus-within`).
 *   ⚠️ 잠긴 ✎(생성 중)은 hover 에서도 흐리다 — v1 의 `:disabled { opacity: .3 }` 이 (0,1,1)이라 이 규칙들에 지므로 다시 적는다. */
body.shell-v2 .vlab-node .vlab-node-head-btn.vlab-node-rename {
  opacity: 0;
  transition: opacity 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
body.shell-v2 .vlab-node:hover .vlab-node-rename,
body.shell-v2 .vlab-node:focus-within .vlab-node-rename,
body.shell-v2 .vlab-node .vlab-node-rename:focus-visible,
body.shell-v2 .vlab-node .vlab-node-rename.is-active { opacity: 1; }
body.shell-v2 .vlab-node:hover .vlab-node-rename:disabled,
body.shell-v2 .vlab-node:focus-within .vlab-node-rename:disabled { opacity: 0.3; }

/* 본문 13/21 · 메타 12/18 — 세 단뿐이다(제목·본문·메타 · 아스트라 §4). canvas.css 의 `12px/1.4` 를 순서로 이긴다. */
body.shell-v2 .vlab-node :is(.vlab-text, .vlab-caption, .vlab-char-look-input) { font-size: 13px; line-height: 21px; color: var(--vl-node-text); }
body.shell-v2 .vlab-node .vlab-node-kind { font-size: 12px; line-height: 16px; }
body.shell-v2 .vlab-node .vlab-node-cost,
body.shell-v2 .vlab-node .vlab-lint,
body.shell-v2 .vlab-node .vlab-lint-line,
body.shell-v2 .vlab-node .vlab-char-node-stage,
body.shell-v2 .vlab-node .vlab-hint { font-size: 12px; line-height: 18px; }
/* 🔴 메타는 **전부 한 색**이다(`--vl-node-meta`) — 2차에는 이 줄들이 `--ink-48` 이라 카드 위 2.6:1 이었다. */
body.shell-v2 .vlab-node .vlab-hint,
body.shell-v2 .vlab-node .vlab-node-cost { opacity: 1; color: var(--vl-node-meta); }

/* 사람이 칠한 색(`LabNode.color`) — **몸통은 그대로 두고** 고른 색은 머리 띠에만 남긴다(코덱스 §1 모순 해소).
 *   3차: 옛 판은 몸통 토큰만 되돌려 머리가 v1 의 어두운 쌍(#322 · #332922 …)을 그대로 썼다 — 카드가 얼룩으로 읽혔다.
 *   지금은 머리 바탕을 **고른 색 30% 믹스**로 다시 만든다(그 위에 종류색 10% 가 또 얹힌다 — 둘 다 보인다).
 *   ⚠️ 여기 hex 는 위 토큰과 **같은 값**이다 — `var()` 로 잇지 않는 이유는 `[data-color]` 가 그 토큰 자신을 덮기 때문이다. */
body.shell-v2 .vlab-node[data-color] { --vl-node-body: #242424; }
body.shell-v2 .vlab-node[data-color="red"] { --vl-node-head: color-mix(in srgb, var(--vl-color-red-body) 30%, #2c2c2c); }
body.shell-v2 .vlab-node[data-color="brown"] { --vl-node-head: color-mix(in srgb, var(--vl-color-brown-body) 30%, #2c2c2c); }
body.shell-v2 .vlab-node[data-color="green"] { --vl-node-head: color-mix(in srgb, var(--vl-color-green-body) 30%, #2c2c2c); }
body.shell-v2 .vlab-node[data-color="blue"] { --vl-node-head: color-mix(in srgb, var(--vl-color-blue-body) 30%, #2c2c2c); }
body.shell-v2 .vlab-node[data-color="pale_blue"] { --vl-node-head: color-mix(in srgb, var(--vl-color-pale_blue-body) 30%, #2c2c2c); }
body.shell-v2 .vlab-node[data-color="cyan"] { --vl-node-head: color-mix(in srgb, var(--vl-color-cyan-body) 30%, #2c2c2c); }
body.shell-v2 .vlab-node[data-color="purple"] { --vl-node-head: color-mix(in srgb, var(--vl-color-purple-body) 30%, #2c2c2c); }
body.shell-v2 .vlab-node[data-color="yellow"] { --vl-node-head: color-mix(in srgb, var(--vl-color-yellow-body) 30%, #2c2c2c); }
body.shell-v2 .vlab-node[data-color="black"] { --vl-node-head: color-mix(in srgb, var(--vl-color-black-body) 30%, #2c2c2c); }

/* ════════ ②-A 관계의 층 — **세 단이 눈으로 갈린다**(2026-09-22 회귀 복구) ════════
 *   🔴 **여기서 기능이 하나 사라져 있었다.** 이 자리에는 세 단을 전부 `opacity: 1` 로 되돌리는 예외가 있었고,
 *      근거는 코덱스 §1(*"카드를 반투명하게 만들면 안 글씨가 목표 대비 아래로 떨어진다"*)이었다. 그 근거 자체는
 *      맞다 — 틀린 것은 **적용 범위**다: `body.shell-v2` 는 2026-09-21 `b1e35422` 부터 **모든 사용자에게 항상**
 *      붙는다(`web/app.js:1386` — 플래그가 걷혔다, 새 셸이 유일한 화면이다). 그래서 "대비를 지키는 예외" 가 곧
 *      **전 사용자 기능 삭제**가 됐다: 카드는 한 장도 안 흐려지고, 선만 흐려지는 반쪽 화면이 남았다
 *      (선의 층 `is-near/is-flow/is-fan/is-far` 은 v2 오버라이드가 없어 그대로 산다 · `videolab.css:855-859`).
 *      오너: *"노드를 고르면 연결된 것만 또렷하고 나머지는 흐려져야 한다"*.
 *   ★ 지금은 **둘 다** 만족시킨다 — 흐림을 되살리고, 읽고 싶은 카드는 **손을 얹으면** 즉시 또렷해진다(아래 hover 복구).
 *      흐려 놓고 `:hover` 로 되살리는 것은 이 캔버스가 선에서 이미 쓰는 수법이다(`videolab.css:863-866` 의
 *      `.vlab-edge-hit:hover`). 🔴 **대비 반론의 답은 흐림을 끄는 것이 아니라 읽는 길을 주는 것**이다 —
 *      흐림을 끄면 글씨는 읽히지만 "무엇이 관계인가" 라는 질문 자체가 화면에서 사라진다.
 *   ⚠️ 세 단의 값(직접 1 · 흐름 .8 · 관계 밖 .4)은 **서로 눈에 보이게 벌어져야** 한다:
 *      · 흐름을 .9 대로 올리면 직접과 붙어 "직접 관계인가 사슬인가" 를 못 읽는다 — 그 구분이 이 기능의 전부다
 *        (오너 2026-08-24 지시 · `graphFocusTiers` 머리주석).
 *      · 관계 밖을 .5 위로 올리면 "흐리다" 가 아니라 "조금 탁하다" 로 읽힌다(오너가 사라졌다고 신고한 그 상태).
 *   ⚠️ **카드 전체가 함께 가라앉는다** — 이미지 썸네일까지. 글씨만 내리면 사진이 또렷해서 관계 밖 카드가 오히려
 *      더 눈에 띈다(카드 절반이 그림인 종류가 넷이다).
 *   ⚠️ 움직이는 것은 **opacity 하나**다. border·box-shadow 를 함께 보간하면 카드 수십 장 위에서 팬이 흔들린다
 *      (`videolab-canvas-frame-cost` · 아래 ②-B 의 그 규율). 카드를 띄우거나 scale 하지도 않는다 — 포트와 선이
 *      어긋난다(아스트라 §3).
 *   ⚠️ 고른 카드는 이 셋 중 어느 클래스도 받지 않는다(`nodeFocusClass` 가 빈 문자열을 준다 · 선택은 `is-selected`
 *      가 이미 말한다) — 즉 **선택한 카드는 언제나 opacity 1** 이고, 여기서 따로 예외를 적을 것이 없다. */
/* 전이는 **카드 자신**에 적는다 — `.is-flow`/`.is-mute` 쪽에 적으면 클래스가 **떨어질 때**(선택 해제·다른 카드 선택)
   규칙이 같이 사라져 되돌아오는 길만 툭 끊긴다. 들어갈 때와 나올 때가 같아야 한다.
   ⚠️ opacity 만 적는다(`all` 금지 — 위 그라데이션·테두리까지 보간된다). 값이 안 바뀌는 카드에는 비용이 0 이다. */
body.shell-v2 .vlab-node { transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1); }
body.shell-v2 .vlab-node.is-flow { opacity: 0.8; }
body.shell-v2 .vlab-node.is-mute {
  opacity: 0.4;
  /* ⚠️ 테두리 알파를 **따로 더 낮추지 않는다.** 옛 판은 흐림이 없어 테두리가 층의 유일한 신호였고 그래서
     .07 까지 내렸다 — 이제 opacity .4 가 그 위에 곱해져 실효 알파가 .028 이 된다. 그러면 카드 윤곽이
     사라져 "구조는 계속 보여야 한다"(`videolab.css:1050`)를 깬다. 토큰(.22)에 .4 가 곱해진 ≈.088 이
     v1 의 관계 밖 카드(.22 × .5 ≈ .11)와 같은 자리다.
     ⚠️ 그림자·sheen 은 끈다 — 가라앉은 카드가 그림자로 앞으로 떠 보이면 층이 거꾸로 읽힌다. */
  box-shadow: none;
}
/* 🔴 **읽는 길** — 흐린 카드에 손을 얹으면 **그 카드만** 즉시 또렷해진다. 코덱스 §1 의 대비 반론은 여기서 답한다:
   글씨가 영원히 안 읽히는 카드는 없다(얹기만 하면 된다).
   ⚠️ 끌기·우클릭·포트 판정은 건드리지 않는다 — 판정 정본은 `graphNodeGrab` 이고 이것은 칠하기뿐이다.
   ⚠️ 특이도 (0,4,1)로 위 두 규칙(0,3,1)을 이긴다 — `!important` 를 쓰지 않는다(`:hover` 가 클래스 한 단을 더한다). */
body.shell-v2 .vlab-node.is-flow:hover,
body.shell-v2 .vlab-node.is-mute:hover { opacity: 1; }
/* 직접 닿은 카드(`is-lit`)의 흰 테 — v1(`videolab.css:911`)의 `outline` 을 **그대로 쓰고 색만 올린다**.
   ★ 왜 `::before`/`::after` 로 옮기지 않나: 그 두 겹은 이미 임자가 있다(hover 테두리 · 선택 고리 · 아래 ②-B).
     층이 그중 하나를 빌리면 고른 카드나 손 얹은 카드에서 테가 둘로 읽힌다. `outline` 은 **전이하지 않는
     정적 선언**이라 ②-B 의 "보간하는 것은 opacity 뿐" 규율을 깨지 않는다.
   ⚠️ .45 → .72: v2 카드는 제 테두리가 흰 .22 라 .45 면 "테두리가 조금 밝다" 로만 읽혔다(v1 카드 테두리는 더 어두웠다).
   ⚠️ **흰 계열이어야 한다** — `--vl-selected`(파랑) 고리와 색이 닿으면 "직접 관계" 와 "내가 고른 것" 이 섞인다. */
body.shell-v2 .vlab-node.is-lit { outline-color: rgba(255, 255, 255, 0.72); }

/* ════════ ②-B hover·선택 — **pseudo-element 의 opacity** 로만(4차 · 아스트라 §7) ════════
 *   🔴 이 라운드가 고친 것은 *모양*이 아니라 **무엇이 움직이나**다. 3차는 hover·선택이 `border-color`·`outline`·
 *      `box-shadow` 를 갈아 끼웠다 — 그 셋은 보간하면 레이아웃·재도색을 부르고(카드 수십 장 위에서 팬이 흔들린다
 *      · `videolab-canvas-frame-cost`), 테두리 폭이 바뀌면 포트·선의 좌표가 반 픽셀 흔들린다.
 *      4차는 카드의 **치수를 고정**하고 장식 두 겹의 `opacity` 만 켰다 끈다(transform·opacity 만 · 계약서 4 §7).
 *   ⚠️ 두 겹의 자리:
 *      · `::before` = hover 테두리. `inset: 0` + **inset** box-shadow 라 카드 밖으로 **한 픽셀도 안 나간다**.
 *      · `::after`  = 선택 고리. `inset: -2px` + 3px 번짐이라 **바깥으로 5px** 나간다.
 *   🔴 5px 인 것이 중요하다: 크기를 정한 카드는 `.vlab-node.is-sized { overflow: clip; overflow-clip-margin: 10px }`
 *      라(videolab.css) 그 10px 안에 들어와야 고리가 살아남는다. 옛 `outline` 은 규칙상 안 잘렸지만
 *      pseudo-element 는 잘린다 — 번짐을 키우려면 저 10px 도 함께 봐야 한다.
 *   ⚠️ `z-index` 를 주지 않는다 — 위치를 가진 자식은 카드 바탕 위에 그려지고, 포트(`z-index: 1`)·크기 손잡이(2)는
 *      그 위에 남는다. 고리가 손잡이를 덮으면 크기를 고치는 중에 손잡이가 사라진 것처럼 보인다.
 *   ⚠️ `pointer-events: none` — 장식이 카드의 끌기·우클릭을 먹지 않게(판정 정본은 `graphNodeGrab`).
 *   ⚠️ 카드를 **띄우거나 키우지 않는다**(아스트라 §3 *"Do not lift or scale cards on hover"*) — 포트와 선이 어긋난다. */
body.shell-v2 .vlab-node::before,
body.shell-v2 .vlab-node::after {
  content: '';
  position: absolute;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
}
body.shell-v2 .vlab-node::before {
  inset: 0;
  box-shadow: inset 0 0 0 1px var(--vl-node-border-hover);
  transition: opacity 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* ⚠️ `inset` 의 기준은 카드의 **padding box** 다(테두리 1px 안쪽) — 그래서 −3 이 "카드 테두리 바로 밖에 붙는
   2px 고리" 다(테두리 상자 기준 −2 … 0). 바깥으로 나가는 양은 3 + 번짐 3 = 6px 이내이고, 위 clip 10px 안이다.
   ⚠️ 이 블록의 첫 선언 앞에 주석을 두지 않는다 — `graphEdgeKinds.test.mjs` 의 `declIn` 이 `(^|;)` 뒤의 선언만 읽는다. */
body.shell-v2 .vlab-node::after {
  inset: -3px;
  border: 2px solid var(--vl-selected);
  border-radius: calc(var(--vl-node-radius) + 3px);
  box-shadow: 0 0 0 3px var(--vl-selected-halo);
  /* ★★ 5차-B: 고리가 **살짝 크게 나타나 제자리로 조여든다**(160ms 스프링 · 팝오버와 같은 이징).
     ⚠️ 겹(`::after`)에서만 한다 — 카드 자신에 scale 을 걸면 포트와 선이 어긋난다(아스트라 §3 의 그 금지).
     ⚠️ 1.02 는 이 겹의 **바깥 도달치**를 키운다: 고리는 쉬는 자리에서 카드 밖 6px(inset 3 + 번짐 3)이고,
        긴 카드(높이 ~600)에서 1.02 면 세로로 +6px 더 나가 `.is-sized` 의 `overflow-clip-margin: 10px` 을 넘는다
        → 그 카드에서만 scale 을 끈다(바로 아래 규칙). 잘린 고리가 한 프레임 비치는 것이 더 나쁘다. */
  transform: scale(1.02);
  transition: opacity 160ms cubic-bezier(0.34, 1.56, 0.64, 1), transform 160ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
body.shell-v2 .vlab-node.is-sized::after { transform: none; }
/* 고른 카드에서는 hover 테두리를 끈다 — 파란 고리와 회색 테두리가 겹치면 테가 둘로 읽힌다(3차의 그 함정). */
body.shell-v2 .vlab-node:hover:not(.is-selected)::before { opacity: 1; }
body.shell-v2 .vlab-node.is-selected::after { opacity: 1; transform: none; }

/* 🔴🔴 **카드 자신의 outline·box-shadow 상태 규칙을 여기서 끊는다.** 안 끊으면 고리가 **둘·셋**이 된다:
 *    · `videolab.css`  의 고른 카드 규칙(0,2,0) — `outline: 2px solid` + `outline-offset: 1px`
 *    · `v2/canvas.css` 의 고른 카드 규칙(0,3,1) — `border-color: var(--accent)` + `box-shadow: 0 0 0 1px var(--accent)`
 *    · `videolab.css`  의 실패·사전검증 규칙(고른 상태 포함)의 outline·box-shadow
 *    3차는 이 파일이 같은 자리를 다시 적어 이겼는데, 4차는 그 표시를 pseudo-element 로 옮겼으므로 **비워서** 이긴다.
 *    ⚠️ 두 클래스짜리(`.is-failed.is-selected`)는 (0,4,1)이라 v1 의 (0,3,0)을 이기고, 한 클래스짜리는 (0,3,1)로
 *       canvas.css 와 동률이지만 이 파일이 **뒤에 로드**돼 이긴다(파일 머리주석의 그 규율). */
body.shell-v2 .vlab-node.is-selected,
body.shell-v2 .vlab-node.is-failed,
body.shell-v2 .vlab-node.is-invalid-run,
body.shell-v2 .vlab-node.is-failed.is-selected,
body.shell-v2 .vlab-node.is-invalid-run.is-selected {
  outline: none;
  border-color: var(--vl-node-border);
  box-shadow: var(--vl-node-sheen), var(--vl-node-shadow);
}
/* 실패·사전검증 오류 — 고리는 ::after 한 겹이 다 진다(고르지 않았어도 보인다 · 계약 §2-3).
   ⚠️ 사전검증(`is-invalid-run`)은 **점선**이다 — "아직 못 산다" 와 "사다가 실패했다" 는 다른 말이다(v1 의 그 구분 유지). */
body.shell-v2 .vlab-node.is-failed::after,
body.shell-v2 .vlab-node.is-invalid-run::after {
  border-color: var(--vl-error);
  box-shadow: none;
  opacity: 1;
  transform: none;
}
body.shell-v2 .vlab-node.is-invalid-run::after { border-style: dashed; }
/* 실패·오류 + 선택 — 안쪽 파란 테, 바깥 빨간 고리. 둘 다 보여야 한다.
   ⚠️ 빨간 고리는 바깥으로 **4px**(테 2 + 번짐 4 = 6px)까지다 — 위 clip 10px 안이다. */
body.shell-v2 .vlab-node.is-failed.is-selected::after,
body.shell-v2 .vlab-node.is-invalid-run.is-selected::after {
  border-color: var(--vl-selected);
  border-style: solid;
  box-shadow: 0 0 0 4px var(--vl-error);
  opacity: 1;
  transform: none;
}
/* ★ 실패 원인 카드(2026-10-29 · 그루트님(슈드파) 피드백 — `videoLabFailCause`) — 실패 고리를 **굵게(3px) + 붉은 후광**으로 두른다.
 *   "이 카드 때문에 막혔다" 를 다른 실패 카드(2px 고리)와 가른다. 막힌 카드는 고리를 두르지 않는다(사전 검증 점선과 섞이지 않게) —
 *   칩 줄 맨 앞 [← 앞 카드 실패] 와 붉은 선이 말한다.
 *   ⚠️ 바깥 도달치: inset 3 + 번짐 5 = 8px · 고른 상태 inset 3 + 번짐 7 = 10px — `.is-sized` 의 clip 10px 안이다(위 ②-B 머리주석). */
body.shell-v2 .vlab-node.is-failed.is-fail-cause::after {
  border-width: 3px;
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--vl-error) 45%, transparent);
}
body.shell-v2 .vlab-node.is-failed.is-fail-cause.is-selected::after {
  box-shadow: 0 0 0 4px var(--vl-error), 0 0 0 7px color-mix(in srgb, var(--vl-error) 45%, transparent);
}

/* 🔴 **상태 테두리를 되살린다.** 위 `.vlab-node` 는 `body.shell-v2` 가 붙어 (0,2,1)이라, 클래스 둘짜리
 *    `videolab.css` 규칙들(`.vlab-node.is-running` 등 (0,2,0))을 **이겨 버린다** — 테두리 한 줄을 다시 적으면
 *    생성 중 종류색 · 드롭 대상 · 거절 표시가 조용히 사라진다(canvas.css 판에서 이미 그랬다).
 *    선택 규칙 **뒤에** 둔다: 고른 채로 돌고 있는 카드는 종류색 테를 그대로 들고 있어야 한다. */
/* ⚠️ 5차: 알파 테두리와 섞으면 결과도 알파가 되어 종류색이 옅어진다 — 종류색을 **투명에** 섞어 55% 로 세운다. */
body.shell-v2 .vlab-node.is-running { border-color: color-mix(in srgb, var(--vl-kind-color) 55%, transparent); }
body.shell-v2 .vlab-node.is-target { border-color: var(--accent); }
body.shell-v2 .vlab-node.is-invalid { border-color: #ff8a8a; }
/* 미리보기 상자는 카드와 **같은 표면**이다 — contain 으로 넣은 세로 그림의 좌우 여백이 밝은 띠로 읽히지 않게. */
body.shell-v2 .vlab-node .vlab-media { background: var(--vl-node-body); }

/* 연결점 — 지름 12 · 채움 #9a9ca4(캔버스 위 6.21:1 · 카드 위 5.38:1 · 그래픽 요소 3:1 · WCAG 1.4.11) · 흰 테 2px 가 카드색이
 *   무엇이든 원을 세운다.
 *   ⚠️ 평소 0.7 로 흐려 두면 채움의 대비가 그만큼 깎인다 — 늘 또렷하게 두고, 강조는 호버 테두리가 진다.
 *   ★★ **누를 자리는 24×24 다**(3차 · apple-rules ② 의 최소 과녁). 보이는 원을 키우면 카드가 시끄러워지므로
 *      투명한 `::before` 로만 넓힌다 — `pointer-events` 는 상속이라 이 판이 포트의 클릭을 그대로 받는다.
 *   ⚠️ 포트는 카드 밖 6px 로 나간다 — `.vlab-node.is-sized` 의 `overflow-clip-margin: 10px` 이 그 반원을 살린다. */
body.shell-v2 .vlab-port {
  width: 10px;
  height: 10px;
  background: var(--vl-port-neutral);
  /* 테는 **카드색**이다(4차 · 아스트라 §4) — 흰 테는 밝아진 카드 위에서 원을 두 겹 링으로 보이게 했다.
     바깥 고리 1px(#8795ac)이 캔버스 위에서 원의 윤곽을 진다. */
  border: 2px solid var(--vl-node-body);
  box-shadow: 0 0 0 1px var(--vl-port-ring);
  opacity: 1;
  /* 호버에서 **원만** 커진다(1 → 1.2 · 아스트라 §7). 카드·선의 좌표는 안 움직인다. */
  transition: transform 120ms cubic-bezier(0.16, 1, 0.3, 1);
}
body.shell-v2 .vlab-port--left { left: -5px; margin-top: -5px; }
body.shell-v2 .vlab-port--right { right: -5px; margin-top: -5px; }
/* 누를 자리는 24×24 다(3차 · apple-rules ② 의 최소 과녁) — 투명한 `::before` 로만 넓힌다.
   ⚠️ `transform` 이 이 판까지 함께 커지지만(1.2배 = 28.8px) 과녁이 줄지는 않으므로 문제 없다. */
body.shell-v2 .vlab-port::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 24px;
  height: 24px;
  margin: -12px 0 0 -12px;
  border-radius: 50%;
}
/* 종류색 **후광**(4차 · 아스트라 §4 *"reveal 4px halo in kind colour at 18%"*) — 두께가 아니라 opacity 로 켜진다.
   ⚠️ 후광을 `box-shadow` 두 번째 값으로 얹는다(첫 값은 늘 있는 바깥 고리다) — 색만 바뀌므로 카드 기하가 안 흔들린다. */
body.shell-v2 .vlab-port:hover,
body.shell-v2 .vlab-port.is-target {
  border-color: var(--vl-node-body);
  transform: scale(1.2);
  box-shadow: 0 0 0 1px var(--vl-port-ring), 0 0 0 5px color-mix(in srgb, var(--vl-kind-color) 18%, transparent);
}

/* 못 잇는 포트(스냅 반경 안이지만 거절) — 🔴 바로 위 `.vlab-port`(0,2,1)가 `videolab.css` 의 `.vlab-port.is-invalid`(0,2,0)를
   **이겨 버린다**: 다시 적지 않으면 거절 표시가 조용히 사라진다(3차 판이 그랬다 · 카드 상태 테두리와 같은 함정). */
body.shell-v2 .vlab-port.is-invalid {
  background: #ff8a8a;
  border-color: var(--vl-node-body);
  box-shadow: 0 0 0 1px #ff8a8a, 0 0 0 5px rgba(255, 138, 138, 0.25);
}

/* 상태 배지 — **채움 칩**이다(3차 진단 ⑤: 외곽선 배지는 칩이 아니라 미완성 UI 로 읽혔다).
 *   ★★ 4차: 바탕을 `currentColor 16%` 한 줄에서 **상태마다 제 값**으로 갈랐다(아스트라 §4).
 *      🔴 이유는 취향이 아니라 **수**다: 카드가 #26282e → #353a46 으로 밝아지자 "글자색 16% 물듦" 칩이
 *      글자 쪽으로 끌려 올라가 넷 다 AA 아래로 떨어졌다(실측 idle 4.06 · running 4.00 · done 3.87 · failed 3.85).
 *   ★★ 5차: 카드가 어두워져 글자를 토큰 원값으로 되돌렸는데, **5차-C 에 카드가 다시 밝아지면서(#1e1e1e)
 *      그 원값이 칩 위 3.9~4.1 로 떨어졌다** — 4차에 한 번 겪은 그 산법 그대로 글자를 **흰색으로 한 단 밝힌다**
 *      (`color-mix(<base> 55%, #ffffff)`). 실측: idle 5.74 · running 6.23 · done 6.08 · failed 6.12 · stale 6.56:1.
 *      🔴 이 줄의 교훈: **카드 밝기를 바꾸면 칩 넷의 대비가 같이 움직인다** — 팔레트를 손대는 라운드는 이 다섯 수를 꼭 다시 잰다.
 *   ⚠️ 실행 중·완료는 **토큰에서 파생**한다(`color-mix`) — 미니맵(`graphMinimap.readMinimapColors`)과 같은 값을 읽는
 *      `--vl-status-running` 을 글자에 직접 쓰면 4.00:1 이라, 그 토큰을 **흰색으로 한 단 밝힌 것**을 글자로 쓴다.
 *      토큰이 바뀌면 칩과 미니맵이 함께 따라온다 — hex 를 새로 박으면 같은 뜻의 색이 둘이 된다.
 *   ⚠️ `graphEdgeKinds.test.mjs` 가 이 네 쌍(바탕·글자)을 선언에서 읽어 **칩 위 4.5:1** 을 다시 잰다.
 *   ⚠️ 모서리는 **알약**이다 — 명세의 6px 을 안 쓴다(`v2/typeScale.test.mjs` ② 가 배지·칩의 알약을 래칫으로 잠근다). */
body.shell-v2 .vlab-badge {
  background: var(--vl-badge-bg, #485160);
  color: var(--vl-badge-ink, #dee5ef);
  border: 0;
  border-radius: 999px;
  font-size: 10px;
  line-height: 14px;
  font-weight: 600;
  opacity: 1;
}
/* 🔴 대기 배지는 **새 셸에서 안 보인다**(아스트라 §4 *"Remove decorative Waiting badges from idle cards"*) —
   상태는 진행·완료·실패를 말할 때만 선다. DOM·테스트는 건드리지 않고 화면에서만 걷는다(가드는 색을 계속 잰다). */
body.shell-v2 .vlab-badge--idle { --vl-badge-bg: color-mix(in srgb, #ffffff 14%, transparent); --vl-badge-ink: var(--vl-node-title); }
/* idle 배지는 숨기되 **단계 배지**(자산 카드 머리의 `시트 미완성` 류 · 5차에 바닥 글줄에서 머리로 옮김)는 idle 톤으로 보인다 —
 *   오너 5차: "미완성은 글자 유지". `.vlab-char-node-stage` 바닥 규칙은 DOM 에서 사라져 죽은 선택자다. */
body.shell-v2 .vlab-node .vlab-badge--idle:not(.vlab-badge--stage) { display: none; }
body.shell-v2 .vlab-badge--running {
  --vl-badge-bg: color-mix(in srgb, var(--vl-status-running) 16%, transparent);
  --vl-badge-ink: color-mix(in srgb, var(--vl-status-running) 55%, #ffffff);
}
body.shell-v2 .vlab-badge--done {
  --vl-badge-bg: color-mix(in srgb, var(--positive) 16%, transparent);
  --vl-badge-ink: color-mix(in srgb, var(--positive) 55%, #ffffff);
}
/* 실패·낡음은 hex — `--danger`(#ff5577)는 칩 위에서 AA 아래였다(3차 실측 3.89:1). v1 배지가 쓰던 #ff8a8a·#f4c771 이다. */
body.shell-v2 .vlab-badge--failed { --vl-badge-bg: color-mix(in srgb, #ff8a8a 16%, transparent); --vl-badge-ink: color-mix(in srgb, #ff8a8a 55%, #ffffff); }
body.shell-v2 .vlab-badge--stale { --vl-badge-bg: color-mix(in srgb, #f4c771 16%, transparent); --vl-badge-ink: color-mix(in srgb, #f4c771 55%, #ffffff); }
/* 취소됨(2026-10-30) — 대기와 같은 중립 바탕이지만 **숨지 않는다**(사람이 멈춘 사실은 말한다). */
body.shell-v2 .vlab-badge--cancelled { --vl-badge-bg: color-mix(in srgb, #ffffff 14%, transparent); --vl-badge-ink: var(--vl-node-title); }

/* ★★ 점검 — **한 덩이 블록**이다(4차 · 아스트라 §4). 3차의 "왼쪽 2px 호박색 선 + 회색 글" 은 카드 안에서
 *   *여백이 이상한 문단*으로 읽혔다(오너 4차 *"노드 구성들 디자인이 아직 너무 구린데"*).
 *   ★ 5차: 바탕을 **중립 흰 알파 5%** 로 내리고 호박은 **왼쪽 2px 선**으로만 남겼다 — 4차의 호박 물듦 바탕은
     카드 안에 노란 판을 만들어 오너가 말한 얼룩 쪽에 섰다. 지금은 중립 판 + 호박 한 줄 + 호박 글자다(8.92:1 / 6.41:1).
 *   ⚠️ 글자는 11/16 이고 블록 위 5.92:1 — 3차의 메타 회색보다 진하다(점검은 보조 산문이 아니라 **결과**다).
 *   ⚠️ `Details 접기` 는 이 라운드에서 안 만든다(계약서 4 §4 — 보류). 접힌 `+N건`(`.vlab-lint-more`)은 그대로 쓴다. */
body.shell-v2 .vlab-node .vlab-lint {
  padding: var(--sp-xs);
  border-left: 2px solid var(--vl-warn-ico);
  border-radius: 0 7px 7px 0;
  background: var(--vl-warn-bg);
  opacity: 1;
  color: var(--vl-warn-ink);
  font-size: 11px;
  line-height: 16px;
}
body.shell-v2 .vlab-node .vlab-lint-line { color: var(--vl-warn-ink); }
body.shell-v2 .vlab-node .vlab-lint-line:first-child::before { opacity: 1; color: var(--vl-warn-ico); }
/* `+N건` 은 글줄의 일부다 — 칸을 꽉 채워 **가운데 정렬로 떠 보이지 않게** 왼쪽에 붙인다(`.vlab-lint` 는 grid 다). */
body.shell-v2 .vlab-node .vlab-lint-more { justify-self: start; text-align: left; opacity: 1; color: var(--vl-warn-ink); }
/* 🔴 꼬리 안의 경고 블록은 **전폭**이다. `.vlab-node-foot` 은 `justify-content: flex-end` 인 가로 flex 라
   (videolab.css) 이 줄을 그냥 두면 블록이 **오른쪽에 반 폭으로** 선다(4차 하네스 실측 — 그 모양이 곧 결함이다). */
body.shell-v2 .vlab-node .vlab-node-foot > .vlab-lint { flex: 1 0 100%; }

/* ════════ ③ 카드 안 컨트롤 — 오너 지적 ⑦ *"버튼·셀렉트가 인풋보다 너무 크다"* · 코덱스 3차 §1·§2·§5 ════════
 *   ★ 규율 하나: **주인공은 프롬프트 칸이다.** 컨트롤은 전부 한 눈금(`--vl-ctl-*`)에 서고, 인풋만 넓고 크다.
 *   🔴 옛 판이 컸던 이유 둘: ① 이 파일이 카드 안 버튼에 `min-height: 36px` 를 걸었다(툴바 눈금을 카드로 가져왔다) ·
 *      ② `canvas.css` 가 `.vlab-node-run` 을 알약 + `--surface-2` 로 칠해 11px 짜리 셀렉트 옆에서 버튼만 부풀었다.
 *      둘 다 여기서 28px 한 눈금으로 눌러 앉힌다(코덱스 3차 §5 *"both v2 36px card-button overrides"*).
 *   ★ 3차: 채움을 **카드보다 밝게**(`--vl-ctl-bg` #33363d) 돌리고 테두리를 세웠다(`--vl-ctl-border` #737884) —
 *      2차의 어두운 채움 + 회색 테두리는 컨트롤을 "구멍" 으로 보이게 했다(진단 ④).
 *   ⚠️ `height` 를 고정하지 않고 `min-height` 로 준다 — 라벨이 길어 줄이 바뀌는 버튼이 글자를 잘라 먹지 않게
 *      (카드 폭은 260~420 로 제각각이다). 세로 padding 0 + `align-items: center` 가 28 을 지킨다. */
body.shell-v2 .vlab-node :is(.vlab-node-run, .vlab-model, .vlab-angle-select, .vlab-angle-reset, .vlab-card-role-select) {
  box-sizing: border-box;
  min-height: var(--vl-ctl-h);
  font-size: var(--vl-ctl-fs);
  line-height: 18px;
  border-radius: var(--vl-ctl-radius);
}
/* 버튼 — 알약이 아니라 네모(반경 6). 카드 안에는 알약이 설 만큼의 폭이 없고, 셀렉트와 모양이 갈리면 한 줄이 들쭉날쭉해진다.
 *   ⚠️⚠️ **`.btn` 을 잡지 않는다.** 이 파일(`videoLabNodeView`)이 스스로 적어 둔 관례대로 카드 안 버튼은 `.vlab-node-run`
 *      한 종류이고, 카드 안에 서는 유일한 공용 `btn()` 은 **콘티 칸의 [×] 지우기**(`.btn.btn--icon-only.vlab-board-cell-del`)다 —
 *      그것은 20px 검은 원이고 평소 `display: none` 이다. `.btn` 을 함께 잡으면 같은 특이도(0,3,0)에 순서로 이겨
 *      그 ×가 **항상 보이는 28px 네모**가 된다(발견: 2차 라운드의 CSS 검토). */
body.shell-v2 .vlab-node .vlab-node-run {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 0 var(--vl-ctl-px);
}
/* 셀렉트 — **우물과 같은 재질**이다(4차 · 아스트라 §4). 고르는 칸은 누르는 단추가 아니라 값이 들어 있는 칸이고,
   그래서 프롬프트 글칸과 같은 바탕(#272c36)·같은 테두리(#69758b)를 쓴다. 3차는 셀렉트를 보조 버튼과 같은
   밝은 채움으로 뒀는데, 그러면 한 줄에서 [생성] 옆의 셀렉트가 또 하나의 단추로 읽혔다.
   ⚠️ 오른쪽 24 는 화살표 자리다(라벨이 화살표를 지나가지 않게). 옵션 목록은 네이티브 그대로. */
body.shell-v2 .vlab-node :is(.vlab-model, .vlab-card-role-select) {
  padding: 0 24px 0 var(--vl-ctl-px);
  background-position: right 8px center;
  background-color: var(--vl-well-bg);
  border: 1px solid var(--vl-well-border);
  color: var(--vl-node-text);
}
body.shell-v2 .vlab-node :is(.vlab-model, .vlab-card-role-select):hover:not(:disabled) { border-color: var(--vl-well-border-hover); }
/* 🔴 포커스 테는 **하나**다(아스트라 §4 *"No nested blue outlines"*) — 3차는 테두리색 + 안쪽 그림자 + 아래 outline
   셋이 겹쳐 파란 테가 두세 겹이었다. 여기서는 아무것도 적지 않고 아래 `:focus-visible` 한 자리가 진다. */

/* ★★ 아이콘만 있는 버튼 — 자산 카드의 [교체][수정][내려받기]·이미지 카드의 [고르기·올리기].
 *   라벨 글자는 DOM 에 그대로 있고(접근 가능한 이름 · `videoLabDownload.test.mjs` 가 읽는다) **눈에서만** 감춘다.
 *   ⚠️ `display: none` 이 아니다 — 그러면 이름이 사라져 스크린리더에 빈 버튼이 된다. 설명은 `title` 이 진다. */
body.shell-v2 .vlab-node .vlab-node-run--icon {
  position: relative;   /* 감춘 라벨(position:absolute)이 이 버튼을 기준 삼게 — 없으면 카드가 기준이 된다 */
  width: var(--vl-ctl-h-icon);
  min-width: var(--vl-ctl-h-icon);
  min-height: var(--vl-ctl-h-icon);
  padding: 0;
  flex: 0 0 auto;
  border-radius: 7px;
}
body.shell-v2 .vlab-node .vlab-node-run--icon .vlab-btn-label {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* 보조 버튼 채움 — 카드보다 밝은 한 단(#414957) · **테두리는 투명**이다(4차 · 아스트라 §4).
 *   3차는 보조 버튼에 실선 테두리가 있어 카드 안에 외곽선 상자가 셋넷 서 있었다(진단 ④ *"a collection of defaults"*).
 *   호버·누름은 색을 새로 만들지 않고 **흰 알파 겹**으로 한 단씩 올린다.
 *   ⚠️ 보조 규칙 **뒤에** 아이콘 단추·주 버튼을 적는다(같은 특이도라 순서가 곧 우선이다). */
body.shell-v2 .vlab-node .vlab-node-run {
  position: relative;
  isolation: isolate;
  background: var(--vl-ctl-bg);
  border: 1px solid transparent;
  color: var(--vl-node-text);
  box-shadow: none;
  font-weight: 600;
  /* 겹(opacity)만으로는 호버가 안 읽힌다 — 테두리·그림자도 함께 전환한다(5차-I).
     ⚠️ `background` 는 **넣지 않는다**: 채움 보간이 프레임마다 버튼을 다시 칠하는 그 비용을 피하려고
        겹을 만든 것이다(아스트라 §7 · `videolab-canvas-frame-cost`). 테두리·그림자는 호버 중인 버튼
        하나에만 걸린다. */
  transition: border-color 120ms cubic-bezier(0.2, 0.8, 0.2, 1),
    box-shadow 120ms cubic-bezier(0.2, 0.8, 0.2, 1),
    color 120ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* 🔴 호버·누름은 **겹 한 장의 opacity** 다(아스트라 §7 *"Fill-overlay opacity"*) — 채움색을 보간하면 그 버튼이
   프레임마다 다시 칠해지고, 카드 수십 장 위에서 그 값이 쌓인다(`videolab-canvas-frame-cost` 의 그 교훈).
   ⚠️ `z-index: -1` + `isolation: isolate` — 겹은 버튼 바탕 **위**, 글자·아이콘 **아래**에 앉는다(부모로 새지 않는다). */
body.shell-v2 .vlab-node .vlab-node-run::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 0;
  border-radius: inherit;
  background: var(--vl-ctl-hover);
  opacity: 0;
  transition: opacity 120ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
body.shell-v2 .vlab-node .vlab-node-run:hover:not(:disabled)::before { opacity: 1; }
body.shell-v2 .vlab-node .vlab-node-run:active:not(:disabled)::before {
  background: var(--vl-ctl-press);
  opacity: 1;
  transition-duration: 70ms;
}
/* 🔴 호버에는 **테두리 링이 선다**(5차-I · 오너 *"색이 옅어져서 사라지는 느낌"*). 4차가 걷은 것은 **평소 상태의**
   실선 테두리였다(카드 안에 외곽선 상자가 셋넷 서는 문제) — 호버에서만 나타나는 테를 세우는 것은 그 결정과
   싸우지 않는다: 쉴 때 카드는 여전히 조용하고, 손이 올라간 **한 버튼만** 윤곽을 얻는다.
   ⚠️ 폭은 늘지 않는다 — 평소에도 `1px solid transparent` 가 이미 자리를 잡고 있어 색만 바뀐다(밀림 0). */
body.shell-v2 .vlab-node .vlab-node-run:hover:not(:disabled) {
  color: var(--vl-node-title);
  border-color: var(--vl-node-border-hover);
}
/* 아이콘만 있는 단추 — 채움이 **없다**(아스트라 §4 *"transparent background"*). 자산 카드의 [교체][수정][내려받기]가
   셋 나란히 서는 자리라, 채움이 있으면 그 줄이 단추 띠로 읽힌다. 호버 겹만 흰 알파 8% 로 켜진다.
   ⚠️ 위 보조 규칙 **뒤에** 적는다(같은 (0,3,1) 특이도 — 앞에 두면 채움에 진다 · 3차에 실제로 그랬다). */
body.shell-v2 .vlab-node .vlab-node-run--icon {
  background: transparent;
  border-color: transparent;
  color: var(--ink-80);
}
body.shell-v2 .vlab-node .vlab-node-run--icon::before { background: rgba(255, 255, 255, 0.12); }
/* 주 행동 — 이 카드에서 **돈을 쓰는 버튼 하나**. 5차에 **흰 알약으로 되돌렸다**(21:1):
   4차의 연한 파랑(#9bc2ff)은 아스트라 §4 의 *"the isolated stark-white Generate treatment"* 를 피한 값이었지만,
   오너 4차 판정 *"너무 퍼런 스타일 · 우리 서비스 톤앤매너와 너무 다르다"* 의 한가운데가 그 색이었다.
   이 앱에서 흰 알약은 "튀는 것" 이 아니라 **주 CTA 의 정본 얼굴**이다(framer.css `.btn--primary`).
   ⚠️ 높이는 한 눈금 위(34) · 최소 폭 84 로 `생성` 두 글자와 아이콘이 안 찌그러진다. */
body.shell-v2 .vlab-node .vlab-node-run--primary {
  min-height: var(--vl-ctl-h-primary);
  min-width: 84px;
  padding: 0 14px;
  background: var(--vl-primary-bg);
  border: 1px solid transparent;
  /* ★ 5차: **알약**이다(4차의 네모 반경 9 를 되돌렸다) — 앱 주 CTA 와 같은 얼굴이어야 한다
     (DESIGN.md *"Compose every CTA as a pill"* · 다른 탭의 [저장]·[생성]과 한 벌). */
  border-radius: var(--r-pill);
  color: var(--vl-primary-ink);
  font-weight: 650;
  box-shadow: none;
}
/* 주 버튼의 겹은 **흰 채움 위**에 앉으므로 검정이다(흰 알파는 보이지 않는다) — **누름에만** 쓴다.
   🔴 5차-I 에 **호버에서 이 겹을 껐다**(오너: *"흰 생성버튼에 마우스를 올렸을 때 사라져버리는 느낌"*).
      흰 알약이 #f0f0f0 으로 내려앉으면 어두운 캔버스에서 **물러나 보인다** — 손을 올렸는데 뒤로 가는 버튼이다.
      누를 때는 반대로 그 어두워짐이 맞다(손가락 아래로 눌려 들어간다). 그래서 겹은 남기고 **호버만** 뺀다.
   ⚠️ 이 두 줄은 보조의 `…run:hover…::before { opacity: 1 }`(같은 특이도)을 **순서로** 이긴다 — 위로 옮기면 안 된다. */
body.shell-v2 .vlab-node .vlab-node-run--primary::before { background: rgba(0, 0, 0, 0.12); }
body.shell-v2 .vlab-node .vlab-node-run--primary:hover:not(:disabled)::before { opacity: 0; }
/* ⚠️ 누름 규칙은 **채움까지 다시 적는다** — 보조의 `…run:active…::before { background: var(--vl-ctl-press) }` 가
   같은 (0,5,1)이라, 여기서 검정을 안 적으면 흰 채움 위에 흰 16% 가 얹혀 누름이 사라진다. */
body.shell-v2 .vlab-node .vlab-node-run--primary:active:not(:disabled)::before {
  background: rgba(0, 0, 0, 0.12);
  opacity: 1;
}
/* 대신 호버에서 달라지는 것은 **버튼 바깥**이다 — 흰 후광 링 + 퍼지는 빛으로 카드에서 한 겹 떠오른다.
   테두리는 투명으로 눌러 둔다: 위 보조 규칙(0,5,0)이 흰 28% 테를 주지만 흰 채움 위에서는 보이지 않는 선이고,
   같은 자리에 링과 테가 겹쳐 서면 굵기가 어긋나 보인다. */
body.shell-v2 .vlab-node .vlab-node-run--primary:hover:not(:disabled) {
  color: var(--vl-primary-ink);
  background: var(--vl-primary-bg);
  border-color: transparent;
  box-shadow: var(--vl-primary-lift);
}
/* 못 누르는 컨트롤 — **흐리지 않는다**(아스트라 §4 *"Do not fade an entire card"*). 3차의 `opacity: .45` 는
   버튼 안 글자의 대비를 그만큼 깎았고, 여러 개가 겹치면 카드가 반쯤 지워진 것처럼 보였다. 제 색을 적는다. */
body.shell-v2 .vlab-node .vlab-node-run:disabled,
body.shell-v2 .vlab-node .vlab-node-run--primary:disabled {
  opacity: 1;
  background: var(--vl-ctl-off-bg);
  border-color: transparent;
  color: var(--vl-ctl-off-ink);
}
body.shell-v2 .vlab-node :is(.vlab-model, .vlab-card-role-select):disabled {
  opacity: 1;
  background-color: var(--vl-ctl-off-bg);
  border-color: transparent;
  color: var(--vl-ctl-off-ink);
}
/* 포커스 테는 **한 겹**이다 — 2px `--accent-focus`(앱 포커스색) · 간격 2(아스트라 §4 · 5차에 청회색 #8ebbff 를 걷었다). */
body.shell-v2 .vlab-node :is(.vlab-node-run, .vlab-model, .vlab-card-role-select):focus-visible {
  outline: 2px solid var(--accent-focus);
  outline-offset: 2px;
  border-color: transparent;
  box-shadow: none;
}

/* 액션 줄 — `.vlab-node-foot` 하나가 여덟 종류의 행동 줄을 다 맡는다. 같은 폭의 카드끼리 줄이 같은 자리에 선다.
 *   ⚠️ `flex-wrap: wrap` 은 **남긴다**: 클립 카드는 셀렉트가 넷(모델·길이·화질·비트레이트)이라 한 줄에 넣으면
 *      라벨이 전부 `…` 가 된다 — 읽히는 것이 한 줄에 넣는 것보다 중요하다(videolab.css 의 그 판정 그대로).
 *   ⚠️ 비용 줄의 `order: -1`(foot 안에서만)도 남긴다 — 무엇을 사는지는 [생성] **위**에 있어야 한다. */
body.shell-v2 .vlab-node .vlab-node-foot {
  gap: var(--vl-ctl-gap);
  /* 꼬리는 몸통에서 **선 한 줄**로 갈린다(4차 · 아스트라 §4 clip footer) — 3차에는 줄 간격만으로 갈려 있었고,
     셀렉트가 넷 서는 카드에서는 그 간격이 줄 사이 간격과 같아 "어디까지가 설정인가" 가 안 보였다. */
  padding-top: var(--sp-sm);
  border-top: 1px solid var(--vl-node-divider);
}
/* 빈 꼬리는 선만 남기지 않는다 — 읽기 전용 카드에서 행동이 하나도 없으면 줄 자체가 사라진다. */
body.shell-v2 .vlab-node .vlab-node-foot:empty { display: none; }
body.shell-v2 .vlab-node .vlab-node-foot .vlab-model { min-width: 112px; }
/* 비용 줄 — 11/16 메타 · 숫자는 **자리폭 고정**(아스트라 §4 *"tabular numerals"*)이라 값이 바뀌어도 줄이 안 흔들린다.
   ⚠️ `order: -1`(videolab.css · foot 안에서만)은 남긴다 — 무엇을 사는지는 [생성] **위**에 있어야 한다. */
body.shell-v2 .vlab-node .vlab-node-cost {
  /* 5차-B: 11 → **12/16**(메타 단과 같은 크기 — 돈이 적힌 줄이 카드에서 가장 작은 글자일 이유가 없다).
     숫자는 자리폭 고정이라 값이 바뀌어도 줄이 안 흔들리고, 왼쪽 정렬이라 [생성]과 눈이 안 겹친다. */
  font-size: var(--vl-ctl-fs);
  line-height: 16px;
  text-align: left;
  font-variant-numeric: tabular-nums;
}

/* ★★ 클립·컷 카드의 꼬리 — **고정 세 줄**(4차 · 아스트라 §4 clip footer layout · DOM 은 `videoLabNodeView` 가 세운다).
 *   ① 카메라 무빙 + [AI로 고치기]  ② 모델 + 화질(+ 길이 안내)  ③ 비용 줄 ← | → [생성]
 *   그 사이에 점검 블록(`.vlab-lint`)이 서고, 드물게 고치는 값(비트레이트)은 `<details class="vlab-foot-adv">` 안이다.
 *   ⚠️ 줄 클래스가 없는 옛 DOM 에서도 무너지지 않게 `flex-wrap: wrap` 과 `margin-left: auto` 는 남긴다
 *      (줄이 접히면 마지막 줄만 오른쪽 정렬돼 버튼이 앞 셀렉트에 붙는다 — auto 여백이 그 줄에서도 버튼을 끝으로 민다). */
body.shell-v2 :is(.vlab-node--clip, .vlab-node--cut) .vlab-node-foot { row-gap: var(--vl-ctl-gap); }
body.shell-v2 :is(.vlab-node--clip, .vlab-node--cut) .vlab-node-foot > .vlab-model { flex: 1 1 160px; }
body.shell-v2 :is(.vlab-node--clip, .vlab-node--cut) .vlab-node-foot > .vlab-node-run--primary { margin-left: auto; }
/* 줄 하나 — 전폭 flex. 빈 줄은 **자리를 안 먹는다**(아스트라 §4 *"Hide empty rows completely"*). */
body.shell-v2 .vlab-node .vlab-foot-row {
  flex: 1 0 100%;
  display: flex;
  align-items: center;
  gap: var(--vl-ctl-gap);
  min-width: 0;
}
/* 빈 줄은 **자리를 안 먹는다** — B 가 "모델을 바꿔 목록이 생겼을 때 담을 자리가 미리 DOM 에 있어야 한다" 를
   지키려고 줄을 조건 없이 세우므로, 비었을 때 걷는 일은 CSS 몫이다(연장 카드의 설정 줄이 그 경우다). */
body.shell-v2 .vlab-node .vlab-foot-row:empty,
body.shell-v2 .vlab-node .vlab-foot-row--settings:empty { display: none; }
body.shell-v2 .vlab-node .vlab-foot-row--camera > .vlab-model { flex: 1 1 0; min-width: 0; }
body.shell-v2 .vlab-node .vlab-foot-row--settings > .vlab-model { flex: 1 1 0; min-width: 112px; }
/* ③ 줄 — 비용이 왼쪽에서 남는 폭을 먹고 [취소]·[생성]이 오른쪽 아래에 **붙어서** 선다.
   🔴 `margin-left: auto` 는 **첫 버튼**에 건다(`:first-of-type` = 이 줄의 첫 `<button>`). [생성]에 걸면
      [취소]가 있는 카드에서 그 여백이 두 버튼 **사이**에 생겨 [취소]만 왼쪽에 떨어진다(B 의 DOM 은
      `비용 span → [취소] button → [생성] button` 순서다 · `videoLabNodeView` runHost).
   ⚠️ 비용 줄의 `flex: 1 1 auto` 도 남긴다 — 비용 문장이 빈 카드(`costEl` 초기값 '')에서도 grow 로 버튼을 민다.
   ⚠️ 옛 `.vlab-node-foot > .vlab-node-cost { order: -1 }`(videolab.css)은 이 줄에 **안 걸린다** — 비용이
      꼬리의 직계가 아니라 이 줄의 자식이 됐다. 그래서 순서를 `order` 가 아니라 DOM 이 정한다. */
body.shell-v2 .vlab-node .vlab-foot-row--run { align-items: flex-end; gap: var(--sp-sm); }
body.shell-v2 .vlab-node .vlab-foot-row--run > .vlab-node-cost { flex: 1 1 auto; }
body.shell-v2 .vlab-node .vlab-foot-row--run > .vlab-node-run:first-of-type { margin-left: auto; }
/* ⚠️ 좁은 카드(360 아래)에서 설정 줄이 접히는 일은 **`flex-wrap`** 이 한다 — 컨테이너 쿼리를 쓰지 않는다:
   `container-type: inline-size` 는 카드마다 격리 문맥을 만들고, 카드는 이 캔버스에서 수십 장 서는 요소다
   (아스트라 §4 의 *"Below 360px card width, stack Row 2"* 를 같은 결과로 더 싸게 얻는다). */
body.shell-v2 .vlab-node .vlab-foot-row--settings { flex-wrap: wrap; }
/* ★ 고급 — 드물게 고치는 값(비트레이트) 한 자리. 접힌 상태가 기본이라 카드가 길어지지 않는다.
   ⚠️ DOM 은 `<details class="vlab-foot-adv"><summary>고급</summary>…` 다(B) — summary 안에 아이콘이 없으므로
      꺾쇠는 `::before` 가 그리고, 펴질 때 **글리프를 바꾸지 않고 90° 돈다**(transform · 계약 §7).
   🔴 **빈 서랍은 숨긴다** — `settingsHost`/`advBox` 는 조건 없이 세워지고 비트레이트 목록이 없는 모델·연장
      카드에서는 안이 빈 채로 붙는다. 열어도 아무것도 없는 서랍은 고장으로 읽힌다. */
body.shell-v2 .vlab-node .vlab-foot-adv { flex: 1 0 100%; min-width: 0; font-size: var(--vl-ctl-fs); }
body.shell-v2 .vlab-foot-adv:not(:has(select)) { display: none; }
body.shell-v2 .vlab-node .vlab-foot-adv > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xxs);
  min-height: 20px;
  padding: 0;
  color: var(--vl-node-meta);
  font-size: var(--vl-ctl-fs);
  line-height: 18px;
  cursor: pointer;
  list-style: none;
}
body.shell-v2 .vlab-node .vlab-foot-adv > summary::-webkit-details-marker { display: none; }
body.shell-v2 .vlab-node .vlab-foot-adv > summary::before {
  content: '\203A';
  display: inline-block;
  width: 10px;
  text-align: center;
  transition: transform 120ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
body.shell-v2 .vlab-node .vlab-foot-adv[open] > summary::before { transform: rotate(90deg); }
body.shell-v2 .vlab-node .vlab-foot-adv > summary:hover { color: var(--vl-node-text); }
body.shell-v2 .vlab-node .vlab-foot-adv > summary:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: 2px; border-radius: var(--r-xs); }
body.shell-v2 .vlab-node .vlab-foot-adv[open] > summary { margin-bottom: var(--sp-xxs); }

/* 설정 한 덩이(`라벨 + 셀렉트`) — `videoLabNodeView` 가 길이·화질·비트레이트를 각각 `span.vlab-node-dur` 로 싼다.
   그 span 에는 CSS 가 없어 라벨과 셀렉트가 인라인으로 떨어져 있었다(3차에 라벨이 붙으면서 드러난다). */
body.shell-v2 .vlab-node .vlab-node-dur { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
/* 🔴 5차-F — **빈 덩이는 자리를 안 먹는다.** `videoLabNodeView` 는 길이·화질·비트레이트 span 셋을 *조건 없이*
   꼬리에 붙인다(모델을 바꿔 목록이 생겼을 때 담을 자리가 미리 DOM 에 있어야 하므로 — 그 파일의 그 규율).
   그런데 이미지 카드에서는 셋 다 영영 비고, 폭이 0 이어도 **flex 항목이라 `gap` 은 그대로 셋**이다
   = 죽은 24px. 그 24 가 `[모델 ▾][취소][올리기][생성]` 을 280px 카드에서 접히게 만들어, 오너가 본
   *"혼자 떨어진 이미지 아이콘 버튼"* 이 됐다. 빈 줄을 걷는 `.vlab-foot-row:empty` 와 같은 처방이다. */
body.shell-v2 .vlab-node .vlab-node-dur:empty { display: none; }
/* 칸 라벨 — 11/16 **600** 메타(4차 · 아스트라 §4 field label).
   ★★ 5차-B(오너 *"인풋이랑 글자랑 어떤 게 어떤 것의 제목인지 모르겠다"*): 라벨과 값을 **한 덩이**로 묶는다 —
      라벨은 컨트롤 **왼쪽**에 붙고 그 사이만 **6**(줄 안 컨트롤 사이 8 보다 좁다 · 좁은 쪽이 한 덩이로 읽힌다).
      그 위계를 흐리지 않으려고 대문자·자간·구두점을 넣지 않는다(사람 말 그대로).
   ⚠️ **글칸 위에는 라벨을 두지 않는다** — 프롬프트 칸이 무엇인지는 플레이스홀더가 말한다(라벨을 얹으면
      카드에서 가장 큰 요소 위에 또 하나의 제목이 서서 "무엇의 제목인가" 가 오히려 흐려진다).
   ⚠️ 설정 라벨(`비율`·`화질`·`비트레이트`)과 이미지 카드의 `종류` 라벨이 **같은 꼴**이다(같은 줄에 나란히 서므로). */
body.shell-v2 .vlab-node :is(.vlab-ctl-label, .vlab-card-role-label) {
  flex: 0 0 auto;
  white-space: nowrap;
  font-size: 11px;
  line-height: 16px;
  font-weight: 600;
  opacity: 1;
  color: var(--vl-node-meta);
}

/* 이미지 카드 ① 종류 줄 — 라벨 11 · 셀렉트 96(옛 104 는 카드 폭의 3분의 1 이었다). */
body.shell-v2 .vlab-node .vlab-card-role { gap: 6px; }
body.shell-v2 .vlab-node .vlab-card-role .vlab-card-role-select { flex: 0 0 96px; min-width: 96px; }

/* ★★ 5차-F — **몸통의 `라벨 + 컨트롤` 한 줄**(`.vlab-ctl-row` · 배경 카드의 `카메라 뷰` 가 첫 손님이다).
 *   오너 2026-09-21: *"`카메라 뷰` 라벨이 셀렉트와 너무 멀다"* — 옛 판은 라벨이 `.vlab-hint` **제 줄**이고
 *   셀렉트가 그 아래 또 한 줄이라, 카드의 덩이 간격(12)이 둘 사이에 들어가 라벨이 어느 칸의 제목인지 흐려졌다.
 *   ⚠️ 값은 이미지 카드의 `종류` 줄(`.vlab-card-role`)과 **같다** — 같은 꼴의 줄이 카드마다 다른 간격으로
 *      서면 그게 곧 "정리가 안 된 화면" 이다. 라벨↔컨트롤 6 은 §③ 의 그 규율(줄 안 컨트롤 사이 8 보다 좁다).
 *   ⚠️ 이 줄은 **꼬리가 아니라 몸통**이다 — 카드 세로 flex 의 자식이라 위아래 간격은 카드 `gap`(12)이 준다. */
body.shell-v2 .vlab-node .vlab-ctl-row {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
/* 줄 안의 셀렉트는 남는 폭을 먹는다(뷰 이름이 `…` 로 잘리지 않게) — `.vlab-card-role` 의 고정 96 과 갈리는
   점이 이것 하나다: 종류는 낱말 둘짜리 닫힌 목록이고, 뷰 이름은 배경마다 길이가 제각각이다. */
body.shell-v2 .vlab-node .vlab-ctl-row > .vlab-model { flex: 1 1 0; min-width: 0; }

/* ★★ 5차-F — 이미지 카드 꼬리는 **한 줄**이다: `[모델 ▾] … [취소][올리기][▷ 생성]`(DOM 순서 = 읽는 순서).
 *   🔴 옛 판은 셀렉트 최소 폭이 112 라 280px 카드(안쪽 254)에서 [취소]가 서는 순간 줄이 접혔고, 접힌 줄에
 *      혼자 남는 것이 **[올리기] 아이콘**이었다(오너: *"혼자 떨어진 이미지 아이콘 버튼"*). 셀렉트를 96 까지
 *      양보시키면 흔한 경우(취소 없음: 96+28+84+16=224)가 한 줄에 든다.
 *   ⚠️ `flex-wrap` 은 그대로 남긴다 — 더 좁은 카드에서는 접히는 편이 잘리는 것보다 낫고, 접혀도 줄 끝에
 *      남는 것은 **[생성]**(주 버튼)이다(돈 쓰는 버튼이 줄 끝이라는 규율 · DOM 순서가 그것을 보장한다). */
body.shell-v2 .vlab-node--image .vlab-node-foot > .vlab-model { flex: 1 1 96px; min-width: 96px; }

/* 자산 카드(인물·배경) — 이름 14/20 600 · 행동은 **접히지 않는 아이콘 한 줄**(왼쪽 정렬). */
body.shell-v2 .vlab-node .vlab-asset-head { gap: var(--vl-ctl-gap); }
body.shell-v2 .vlab-node .vlab-char-node-name { font-size: 14px; line-height: 20px; font-weight: 600; color: var(--vl-node-title); }
/* ★★ 5차-B(오너 캐릭터 카드 지적 *"이런 거 버튼인지 뭔지 구분도 잘 안 가고 간격도 애매하다"*):
 *   [교체][수정][내려받기] 는 **채움과 테두리를 갖는다** — 4차의 "아이콘 버튼은 투명" 규칙을 **이 줄에서만** 걷는다.
 *   근거: 그 규칙은 *"셋이 나란히 서면 단추 띠로 읽힌다"* 를 피한 것인데, 자산 카드에서는 그 셋이 **카드의 유일한
 *   행동**이고 라벨 글자가 없다(아이콘만) — 채움이 없으면 누를 것이 있다는 신호가 화면에 한 개도 없다.
 *   ⚠️ 특이도 (0,4,1)로 적어 위의 일반 아이콘 버튼 규칙(0,3,1)을 이긴다(다른 카드의 아이콘 버튼은 그대로 투명).
 *   ⚠️ 채움은 `var(--surface-2)` 가 아니라 **흰 알파 토큰**(`--vl-ctl-bg`)이다 — shell-v2 에서 그 토큰은
 *      불투명 #1c1c1c 라 카드(#1e1e26)보다 어둡다(① 블록의 그 🔴 와 같은 함정).
 *   ⚠️ 줄 위아래 8 · 버튼 사이 6 — 카드의 12 리듬 안에서 "한 덩이" 로 읽히는 간격이다. */
body.shell-v2 .vlab-node .vlab-asset-acts {
  flex-wrap: nowrap;
  justify-content: flex-start;
  gap: 6px;
  margin: var(--sp-xs) 0;
}
body.shell-v2 .vlab-node .vlab-asset-acts .vlab-node-run--icon {
  background: var(--vl-ctl-bg);
  border: 1px solid var(--vl-node-border);
  border-radius: var(--vl-ctl-radius);
  color: var(--ink-80);
}
body.shell-v2 .vlab-node .vlab-asset-acts .vlab-node-run--icon:hover:not(:disabled) { color: var(--ink); }
/* ⚠️ 겹은 `--vl-ctl-hover` 다(5차-I 에 `--vl-ctl-press` 에서 옮겼다). 그 전에는 "한 단 세게" 를 누름 토큰을
   빌려 썼는데, 5차-I 이 호버 토큰 자체를 .04 → .10 으로 올리면서 빌릴 이유가 사라졌다 — 채움 있는 버튼의
   호버는 카드 어디서나 같은 값이어야 한다(누름은 위 보조 규칙이 .16 으로 한 단 더 간다). */
body.shell-v2 .vlab-node .vlab-asset-acts .vlab-node-run--icon::before { background: var(--vl-ctl-hover); }
body.shell-v2 .vlab-node .vlab-char-node-stage { opacity: 1; color: var(--vl-node-meta); }
/* 이미지 카드 꼬리의 같은 줄(2026-09-23 오너 *"캐릭터시트랑 같은스타일로"*) — 꼬리는 오른쪽 정렬 flex 라 줄이 폭을 채워야
   인물 카드처럼 왼쪽부터 선다 · 꼬리가 이미 위아래 여백을 가져 줄 여백은 뺀다. */
body.shell-v2 .vlab-node .vlab-node-foot > .vlab-node-acts { flex: 1 1 auto; margin: 0; }

/* 콘티 카드 — 칸 격자·발치 줄도 같은 눈금. */
body.shell-v2 .vlab-node .vlab-board { gap: var(--vl-ctl-gap); }
body.shell-v2 .vlab-node .vlab-board-cell { border-radius: var(--vl-ctl-radius); }
body.shell-v2 .vlab-node .vlab-board-foot { gap: var(--vl-ctl-gap); }
body.shell-v2 .vlab-node .vlab-board-count,
body.shell-v2 .vlab-node .vlab-board-cap { font-size: var(--vl-ctl-fs); line-height: 16px; opacity: 1; color: var(--vl-node-meta); }

/* 앵글 카드 — 프리셋 줄·되돌리기를 같은 눈금으로(3D 무대의 치수·투영은 건드리지 않는다). */
body.shell-v2 .vlab-node--angle .vlab-angle-sliders,
body.shell-v2 .vlab-node--angle .vlab-angle-row { gap: var(--vl-ctl-gap); }
body.shell-v2 .vlab-node--angle .vlab-angle-select { flex: 1 1 0; min-width: 0; padding: 0 20px 0 var(--vl-ctl-px); }
body.shell-v2 .vlab-node--angle .vlab-angle-reset { padding: 0 var(--vl-ctl-px); }

/* 클립 카드 — 카메라 줄은 프롬프트에 붙어 있는 **도구**다(제 줄을 새로 만들지 않는다). */
body.shell-v2 .vlab-node .vlab-cam-row { gap: var(--vl-ctl-gap); margin-top: var(--vl-ctl-gap); }

/* ★★ 프롬프트 — 카드에서 가장 큰 요소. 13/20 · 안쪽 10 · 종류마다 바닥 높이가 다르다(코덱스 3차 §2).
 *   ⚠️ 크기를 지정한 카드(`.is-sized`)에서는 바닥을 0 으로 푼다 — 안 그러면 손잡이로 줄여도 글칸이 카드를 밀어낸다
 *      (videolab.css 의 `.is-sized .vlab-text { min-height: 0 }` 와 같은 뜻을 여기서도 지킨다). */
body.shell-v2 .vlab-node :is(.vlab-text, .vlab-caption) {
  padding: var(--sp-sm);
  border-radius: var(--vl-well-radius);
}
/* 바닥 높이 — 글·이미지 프롬프트 144 · 클립 208(4차 · 아스트라 §4 textarea sizing).
   ⚠️ 천장은 320 이고 그 뒤로는 **칸 안에서 스크롤**한다 — 프롬프트가 길어도 카드가 캔버스를 먹지 않는다.
   ⚠️ 사람이 크기를 정한 카드(`.is-sized`)는 바닥·천장을 **둘 다 푼다** — 안 그러면 손잡이로 줄여도 글칸이 카드를 밀어낸다
      (videolab.css 의 `.is-sized .vlab-text { min-height: 0 }` 와 같은 뜻을 여기서도 지킨다). */
body.shell-v2 .vlab-node :is(.vlab-text, .vlab-caption--multi) { max-height: 320px; overflow-y: auto; }
body.shell-v2 .vlab-node--text .vlab-text { min-height: 144px; }
body.shell-v2 .vlab-node--image .vlab-text--own { min-height: 144px; }
body.shell-v2 :is(.vlab-node--clip, .vlab-node--cut) .vlab-text--own { min-height: 208px; }
body.shell-v2 .vlab-node--board .vlab-text--own { min-height: 120px; }
body.shell-v2 .vlab-node--location .vlab-caption--multi { min-height: 100px; }
body.shell-v2 .vlab-node.is-sized :is(.vlab-text, .vlab-text--own, .vlab-caption--multi) { min-height: 0; max-height: none; }

/* ════════ ④ 글칸 — 카드의 주인공 ════════
 *   바탕을 카드보다 **한 단 내리고**(우물 #272c36 · 카드 위 1.23:1) 테두리(#69758b · 우물 위 3.01:1)를 세워 칸의 경계를
 *   눈에 보이게 한다. ★ 4차: 우물을 **캔버스색에서 뗐다** — 3차는 우물 = 캔버스(#1b1c20)라 큰 글칸이 카드에 뚫린
 *   구멍으로 읽혔고(아스트라 §3 진단 ② *"another hole in the canvas"*), 카드가 밝아지면서 그 구멍이 더 깊어졌다.
 *   🔴 포커스는 **테 한 겹**이다(4차 · 아스트라 §4 *"No nested blue outlines"*). 3차는 테두리색 + 안쪽 그림자 +
 *      바깥 번짐 셋이 겹쳐 파란 테가 세 겹이었다. 지금은 2px `outline` 하나 · 간격 2 다.
 *      ⚠️ `outline` 은 **overflow 에 잘리지 않는다**(규칙상) — 3차가 안쪽 그림자를 본체로 삼은 이유(모달·크기 지정
 *         카드의 스크롤 상자에 잘린다)가 `outline` 에는 해당되지 않는다. 그래서 한 겹으로 줄일 수 있었다.
 *   ⚠️ 카드 안 글칸은 토큰으로 적는다. **모달 글칸은 hex 다** — 모달은 `.vlab-canvas` 밖(body 직계 오버레이)이라
 *      `--vl-*` 토큰이 닿지 않는다(토큰으로 적으면 색이 통째로 비어 테두리가 글자색으로 떨어진다).
 *   ⚠️ 특이도를 한 단 올린 곳: 모달 글칸은 **뒤에 로드되는** liquid-glass-overlays 의 `--lg-input-fill`(0,2,2)도
 *      덮어야 한다 — 그래서 `:not()` 둘을 단 (0,5,2) 로 적는다. */
body.shell-v2 .vlab-node :is(.vlab-text, .vlab-caption) {
  background: var(--vl-well-bg);
  border: 1px solid var(--vl-well-border);
  border-radius: var(--vl-well-radius);
  color: var(--vl-node-text);
}
body.shell-v2 .sm-panel:is(.vlab-char-panel-modal, .vlab-dsheet-panel, .vlab-fill-panel, .vlab-gal, .vlab-img-panel, .vlab-imp-panel, .vlab-import, .vlab-lib-lightbox-panel, .vlab-new-panel) :is(input, textarea, select):not([type="checkbox"]):not([type="radio"]),
body.shell-v2 .sm-panel:is(.vlab-char-panel-modal, .vlab-dsheet-panel, .vlab-fill-panel, .vlab-gal, .vlab-img-panel, .vlab-imp-panel, .vlab-import, .vlab-lib-lightbox-panel, .vlab-new-panel) .vlab-char-look-input {
  background: #161616;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--r-md);
  color: #e4e4e4;
}
body.shell-v2 .vlab-node :is(.vlab-text, .vlab-caption):hover:not(:focus) { border-color: var(--vl-well-border-hover); }
body.shell-v2 .sm-panel:is(.vlab-char-panel-modal, .vlab-dsheet-panel, .vlab-fill-panel, .vlab-gal, .vlab-img-panel, .vlab-imp-panel, .vlab-import, .vlab-lib-lightbox-panel, .vlab-new-panel) :is(input, textarea, select):not([type="checkbox"]):not([type="radio"]):hover:not(:focus),
body.shell-v2 .sm-panel:is(.vlab-char-panel-modal, .vlab-dsheet-panel, .vlab-fill-panel, .vlab-gal, .vlab-img-panel, .vlab-imp-panel, .vlab-import, .vlab-lib-lightbox-panel, .vlab-new-panel) .vlab-char-look-input:hover:not(:focus) { border-color: rgba(255, 255, 255, 0.20); }
body.shell-v2 .vlab-node :is(.vlab-text, .vlab-caption):focus,
body.shell-v2 .sm-panel:is(.vlab-char-panel-modal, .vlab-dsheet-panel, .vlab-fill-panel, .vlab-gal, .vlab-img-panel, .vlab-imp-panel, .vlab-import, .vlab-lib-lightbox-panel, .vlab-new-panel) :is(input, textarea, select):not([type="checkbox"]):not([type="radio"]):focus,
body.shell-v2 .sm-panel:is(.vlab-char-panel-modal, .vlab-dsheet-panel, .vlab-fill-panel, .vlab-gal, .vlab-img-panel, .vlab-imp-panel, .vlab-import, .vlab-lib-lightbox-panel, .vlab-new-panel) .vlab-char-look-input:focus {
  outline: 2px solid var(--accent-focus);
  outline-offset: 2px;
  border-color: var(--vl-well-border-hover);
  box-shadow: none;
}
/* placeholder — 보조 글자와 **같은 단**이다(우물 위 6.94:1). 흐림(opacity)으로 만들지 않는다(아스트라 §4). */
body.shell-v2 .vlab-node :is(.vlab-text, .vlab-caption)::placeholder,
body.shell-v2 .sm-panel:is(.vlab-char-panel-modal, .vlab-dsheet-panel, .vlab-fill-panel, .vlab-gal, .vlab-img-panel, .vlab-imp-panel, .vlab-import, .vlab-lib-lightbox-panel, .vlab-new-panel) :is(input, textarea)::placeholder { opacity: 1; color: var(--vl-node-meta, #a0a0a0); }

/* ════════ ⑤ 선택 도구상자 — 카드와 **같은 표면·같은 눈금** ════════
 *   ⚠️ 막대는 `position: fixed` 지만 DOM 으로는 캔버스의 자식이다(`mountSelectionToolbox(canvasEl)`) —
 *      2차 주석의 "캔버스 밖이라 상속이 안 온다" 는 틀린 말이었고, 그래서 여기서 눈금·색을 다시 hex 로 적고 있었다.
 *      3차는 ① 스코프에 `.vlab-seltool` 을 이름으로 넣고 **토큰만 읽는다**(고칠 곳이 한 자리다). */
body.shell-v2 .vlab-seltool {
  /* ⚠️ 44 = 버튼 32(`--vl-ctl-h`) + 위아래 6 + 테두리 1×2. 4차에 버튼이 28 → 32 로 올라가면서 옛 40 짜리 막대는
     안쪽이 30 뿐이라 버튼의 위아래가 2px 잘렸다 — 막대 높이는 버튼 눈금에서 따라온다. */
  min-height: 44px;
  gap: 4px;
  padding: 6px;
  border: 1px solid var(--vl-node-border);
  border-radius: var(--r-md);
  background: var(--vl-node-body);
  box-shadow: var(--vl-node-shadow);
  color: var(--vl-node-text);
  font-size: var(--vl-ctl-fs);
}
body.shell-v2 .vlab-seltool-btn {
  box-sizing: border-box;
  min-height: var(--vl-ctl-h);
  padding: 0 var(--vl-ctl-px);
  border-radius: var(--vl-ctl-radius);
  color: var(--vl-node-title);
}
/* 🔴 위 규칙이 (0,3,1) 이라 `videolab.css` 의 `.vlab-seltool-btn--danger`(0,1,0)를 **이겨 버린다** —
   [삭제]가 조용히 보통 글자색이 된다(2차 판이 그랬다). 위험 색을 여기서 다시 적어 되돌린다. */
body.shell-v2 .vlab-seltool-btn--danger { color: #ff8a8a; }
body.shell-v2 .vlab-seltool-btn:hover:not(:disabled) { background: var(--vl-control-hover); }
body.shell-v2 .vlab-seltool-btn:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: -2px; }
body.shell-v2 .vlab-seltool-count { opacity: 1; color: var(--vl-node-meta); }
body.shell-v2 .vlab-seltool-swlabel { opacity: 1; color: var(--vl-node-meta); }
body.shell-v2 .vlab-seltool-sep { background: var(--vl-node-border); }
body.shell-v2 .vlab-seltool-swatches { background: none; border-left: 1px solid var(--vl-node-border); border-right: 1px solid var(--vl-node-border); }

/* ════════ ⑤-B 캔버스 위 판들 — 하나의 표면으로 통일(3차) ════════
 *   우클릭 메뉴 · 가져오기 판이 각자의 색(rgba(24,26,34,.98) · 흰 알파 테두리)을 들고 있었다.
 *   빈 캔버스 안내(⑥) · 선택 막대(⑤) · 도크 · [＋ 노드] 팝오버(canvas-dock.css)와 **같은 카드 표면**으로 모은다:
 *   바탕 `--vl-node-body` · 테두리 `--vl-node-border` · 그림자 `--vl-node-shadow` · 글자 세 단. */
/* 🔴🔴 **4차: 이 규칙은 3차까지 한 번도 화면에 안 나왔다.** `liquid-glass-overlays.css` 가
 *    `body.shell-v2 .vlab-menu`(0,2,1)에 검정 70% 틴트 + 블러를 칠하고 그 파일이 **뒤에 실린다**
 *    (`liquidGlassLayer.test.mjs` ⑤ 가 그 순서를 잠근다) — 같은 (0,2,1)로 적은 이 줄은 순서로 졌다.
 *    그래서 우클릭 메뉴·가져오기 판은 "카드 표면" 이라는 주석과 달리 **검정 유리**로 떠 있었고,
 *    [＋ 노드] 팝오버도 같은 이유로 어두웠다(오너 4차 *"배경도 너무 어두워서 안 어울리고"* · canvas-dock.css 머리주석).
 *    → `[class]` 한 단(0,3,1)으로 올려 **특이도로** 이긴다. 클래스를 새로 만들지 않으려고 고른 방법이다
 *      (모든 `.vlab-menu` 는 클래스가 있으므로 이 속성 선택자는 늘 참이고, 무엇을 고르는지는 안 바꾼다).
 *    ⚠️ 블러도 함께 끈다 — 불투명 판에 블러가 남으면 판 뒤가 합성만 되고 안 보이는 헛일이 된다. */
body.shell-v2 .vlab-menu[class] {
  background: var(--vl-node-body);
  border: 1px solid var(--vl-node-border);
  box-shadow: var(--vl-node-shadow);
  color: var(--vl-node-text);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
body.shell-v2 .vlab-menu-item { color: var(--vl-node-text); }
/* ⚠️ 바로 위 줄이 (0,3,1) 이라 `videolab.css` 의 `.vlab-menu-item--danger`(0,1,0)를 이긴다 — 여기서 되돌린다
   (선택 막대의 [삭제]와 같은 함정 · 위 ⑤). */
body.shell-v2 .vlab-menu-item--danger { color: #ff8a8a; }
body.shell-v2 .vlab-menu-item:hover:not(:disabled) { background: var(--vl-control-hover); }
body.shell-v2 .vlab-menu-item:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: -2px; }
/* 🔴 막힌 항목의 **이유 줄**이 안 읽혔다(2026-09-30 실측 · 실행 중 선 메뉴 [연결 끊기]) — `glass.css` 의 전역 `button:disabled { opacity: .4 }`
 *    가 버튼째 흐려 이유(#ffb4a8)까지 40% 가 됐다. 라벨 흐림은 `videolab.css` 의 머리 .45 가 따로 지므로 버튼은 되돌린다
 *    (파인더 메뉴 `finder-overlays.css` 와 같은 처방). 이유가 있는 항목은 전부 rich 다. */
body.shell-v2 .vlab-menu-item--rich:disabled { opacity: 1; }
body.shell-v2 .vlab-menu-item-desc,
body.shell-v2 .vlab-menu-note,
body.shell-v2 .vlab-menu-swatches-label { opacity: 1; color: var(--vl-node-meta); }
body.shell-v2 .vlab-menu-sep { background: var(--vl-node-border); }
/* 가져오기 판 — 껍데기(`.sm-panel`)는 모달 층이 소유한다. 여기서는 **안쪽 판**만 같은 표면으로 맞춘다. */
body.shell-v2 .vlab-import-ep { color: var(--vl-node-text); }
body.shell-v2 .vlab-import-ep:hover { background: var(--vl-control-hover); }
body.shell-v2 .vlab-import-note { opacity: 1; color: var(--vl-node-meta); }
body.shell-v2 .vlab-import-count.is-hint { opacity: 1; color: var(--vl-node-meta); }

/* ════════ ⑥ 빈 캔버스 안내 — 오너 2차 지적 ① · 코덱스 명세 2차 §3 ════════
 *   카드 셋(번호 붙은)을 **카드 하나**로 접었다. 껍데기(`.vlab-guide`)가 곧 그 카드다.
 *   ⚠️ 한 단 높은 특이도(`.vlab-canvas > .vlab-guide` 경유)를 쓴다: **뒤에 로드되는** liquid-glass-overlays 가
 *      `.vlab-guide-card`(0,2,1)에 글라스(틴트·블러·그림자)를 입힌다 — 안내는 캔버스 위에서 **불투명**해야 읽힌다.
 *   ★ 3차: 글자를 카드와 **같은 세 단**으로 맞췄다(제목 `--vl-node-title` · 본문 `--vl-node-text` · 메타 `--vl-node-meta`). */
body.shell-v2 .vlab-canvas > .vlab-guide {
  inset: auto; left: 50%; top: 50%; translate: -50% -50%;
  display: block; width: 480px; max-width: calc(100% - 40px); padding: 0; gap: 0;
  /* 갈래 둘 + 준비물이면 키가 커진다(2026-10-19) — 낮은 화면에서 위아래가 잘리지 않게 카드 안에서 스크롤한다. */
  max-height: calc(100% - 32px); overflow-y: auto; overscroll-behavior: contain;
  pointer-events: auto;
  background: var(--vl-node-body);
  border: 1px solid var(--vl-node-border);
  border-radius: var(--r-lg);
  box-shadow: var(--vl-node-shadow);
}
/* 🔴 간격은 **줄마다 다르다**(옛 판은 flex gap 15 하나로 넷을 같은 거리에 뒀다 — 그래서 설명이 제목과 같은 무게로 읽혔다).
 *    제목→설명 8 · 설명→비용 8 · 비용→주 버튼 12(코덱스 §3). gap 을 0 으로 닫고 각 줄이 자기 아래 여백을 갖는다. */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0;
  width: auto; padding: var(--card-pad); border: 0; border-radius: inherit;
  background: none; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-title {
  margin: 0 0 var(--sp-xs); opacity: 1; color: var(--vl-node-title);
  font-size: var(--fs-title-2); line-height: var(--lh-title-2); font-weight: 600;
}
/* ★ 설명은 **보조 텍스트**다(오너 ① *"설명답게 보이게"*) — 13/20 · 400 · 메타 단(카드 위 7.16:1).
 *    옛 판은 15px `--ink-80` 이라 제목 바로 아래에서 본문처럼 섰다.
 *    ⚠️ `>` 로 카드 직계만 잡는다. 충돌 상대였던 갈래 줄의 `.vlab-guide-reason`(같은 클래스)은 2026-09-22 에
 *       걷혔지만 **`>` 는 그대로 둔다** — 카드 직계 한 줄만 이 모양을 갖는다는 뜻이 선택자에 남아 있어야
 *       하고, `videoLabEmptyGuide.test.mjs` 가 이 선택자를 글자 그대로 읽는다.
 *    ⚠️ 이 네 줄(크기·색·흐림·줄높이)과 아래 세 margin 은 `videoLabEmptyGuide.test.mjs` 가 **선언으로 읽는다**. */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-card > .vlab-guide-card-desc {
  margin: 0 0 var(--sp-xs); opacity: 1; color: var(--vl-node-meta);
  font-size: var(--fs-fine); line-height: 20px; font-weight: 400;
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-note {
  margin: 0 0 var(--sp-sm); opacity: 1; color: var(--vl-node-meta);
  font-size: var(--fs-fine); line-height: 20px;
}
/* 주 버튼은 이 화면에 하나다 — 흰 알약(framer ②)이 그대로 서고 자리만 정한다. */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-primary { align-self: flex-start; min-height: 40px; margin: 0; }
/* 글자 버튼 — 누를 자리 36px · 파란 글자 · 호버 밑줄(링크의 관용).
 * ⚠️ 2026-09-22 에 **소비자가 하나로 줄었다**: 준비물 줄의 행동 버튼([시트 만들기] 등)뿐이다.
 *    오너가 [캐릭터 앉히기]·[직접 만들기] 두 갈래를 물렀고(*"캐릭터앉히기랑 직접만들기 버튼 필요없어"*),
 *    그 둘을 담던 `.vlab-guide-links` · 종류 펼침 `.vlab-guide-kinds` · 갈래를 설명하던
 *    `.vlab-guide-reason` 세 규칙이 여기 있었다 — 소비자가 없어져 함께 걷었다.
 *    되살릴 일이 있으면 `videoLabEmptyGuide.js` 머리주석의 🔴 절을 먼저 읽어라(오너 자리다). */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-link {
  min-height: 36px; padding: 0 var(--sp-xxs); border-radius: var(--r-sm);
  background: none; border: 0; box-shadow: none; color: var(--accent-focus); font-weight: 500;
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-link:hover { background: none; color: var(--accent-focus); text-decoration: underline; }
/* ★ 갈래 칸 둘(2026-10-19 · `videoLabEmptyGuide.js` 머리주석 🔴🔴🔴) — [AI 초안으로 시작](추천) · [자유 캔버스로 시작].
 *   추천 칸은 파란 테두리 + 옅은 파란 바탕 + 배지로 **무게**를 갖고, 자유 칸은 같은 모양의 우물 칸에 보조 버튼만 둔다.
 *   ⚠️ 두 칸의 폭·안쪽 여백은 같다 — 크기를 다르게 하면 "하나는 덤"이 아니라 "다른 종류의 것"으로 읽힌다. */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-opt {
  width: 100%; box-sizing: border-box; display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  margin-top: var(--sp-sm); padding: var(--sp-sm) 14px 14px;
  background: var(--vl-well-bg); border: 1px solid var(--vl-node-border); border-radius: var(--r-md);
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-opt.is-recommended {
  background: color-mix(in srgb, var(--accent) 9%, var(--vl-well-bg));
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-opt-head { display: flex; align-items: center; gap: var(--sp-xs); margin: 0; }
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-opt-name { color: var(--vl-node-title); font-size: 15px; line-height: 22px; font-weight: 600; }
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-badge {
  padding: 1px 8px; border-radius: var(--r-pill); font-size: 11px; line-height: 18px; font-weight: 600;
  color: var(--accent-focus); background: var(--accent-soft);
}
/* ⚠️ `opacity: 1` — 옛 셸 규칙(`videolab.css` · 0.65)이 같이 걸린다. 보조는 흐림이 아니라 색으로 말한다(위 설명 줄과 같은 규율). */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-opt-desc {
  margin: 0; opacity: 1; color: var(--vl-node-meta); font-size: var(--fs-fine); line-height: 20px;
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-opt > .btn { margin-top: 6px; }
/* 얻는 것 세 줄 — 파란 체크 + 본문 단 글자(설명보다 한 단 밝게: 읽어야 할 내용이라서). 체크는 글리프다(svg 헤어라인은 이 카드에서 깨진다). */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-points { list-style: none; margin: 2px 0 0; padding: 0; display: grid; gap: 4px; }
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-points > li {
  position: relative; padding-left: 20px; color: var(--vl-node-text); font-size: var(--fs-fine); line-height: 20px;
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-points > li::before {
  content: '✓'; position: absolute; left: 2px; top: 0; color: var(--accent-focus); font-weight: 700;
}
/* 자유 캔버스 — 칸이 아니라 카드 맨 아래 한 줄(권하는 길과 같은 무게로 세우지 않는다 · `videoLabEmptyGuide.js` 문구 규율). */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-alt {
  width: 100%; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 2px var(--sp-xxs);
  margin-top: var(--sp-sm);
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-alt-text { color: var(--vl-node-meta); font-size: var(--fs-fine); line-height: 20px; }
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-free {
  min-height: 32px; padding: 0 var(--sp-xxs); background: none; border: 0; box-shadow: none;
  color: var(--vl-node-text); font-size: var(--fs-fine); font-weight: 500; text-decoration: underline; text-underline-offset: 3px;
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-free:hover { background: none; color: var(--vl-node-title); }
/* 추천 칸 안의 준비물은 칸 안쪽 구분선으로 — 칸 밖 카드 구분선과 같은 무게면 다른 덩이로 읽힌다. */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-opt .vlab-guide-prep { margin-top: 6px; }
/* 준비물 — 조건부 네 번째 덩이. 카드를 하나 더 세우지 않고 **얇은 사실 목록**으로 둔다. */
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-prep {
  width: 100%; display: grid; gap: var(--sp-xs); margin-top: var(--sp-sm);
  padding-top: var(--sp-sm); border-top: 1px solid var(--vl-node-border);
}
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-guide-card-title { margin: 0; color: var(--vl-node-meta); font-size: var(--fs-fine); font-weight: 500; }
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-prep-row { background: var(--vl-well-bg); border-radius: var(--r-sm); }
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-prep-step { opacity: 1; color: var(--vl-node-meta); border-color: var(--vl-node-border); }
body.shell-v2 .vlab-canvas > .vlab-guide .vlab-prep-note { opacity: 1; color: var(--vl-node-meta); }

/* ════════ ⑦ 손가락 — 카드 안 눈금만 44 로 ════════
 *   ⚠️ 제품 규칙(apple-rules ②)의 44×44 는 `pointer: coarse` 에서만 요구된다. 카드 안 컨트롤은 눈금이
 *      한 자리(`--vl-ctl-*`)라 **그 넷만 갈아 끼우면** 카드 여덟 종류가 함께 따라온다.
 *   ⚠️ `coarseTouchTarget.test.mjs` 는 v2 의 `canvas*.css` 를 읽지 않는다(캔버스는 그 가드의 시야 밖이다) —
 *      그래서 이 블록은 가드가 아니라 **규칙 자신**이 지킨다. */
@media (pointer: coarse) {
  body.shell-v2 :is(.vlab-canvas, .vlab-menu, .vlab-import, .vlab-dock, .vlab-seltool) {
    --vl-ctl-h: 44px;
    --vl-ctl-h-primary: 44px;
    --vl-ctl-h-icon: 44px;
    --vl-ctl-fs: var(--fs-fine);   /* 13px */
  }
}

/* ════════ ⑧ 미니맵 — 접기 버튼은 **불투명**(오너 신고 2026-09-21 저녁) ════════
 *   `videolab.css` 의 펼친 상태 버튼이 `opacity .7` + 검정 45% 라, 판에 그려지는 흰 뷰포트 사각형(graphMinimap :361)의
 *   윗변이 1:1 배율에서 버튼 자리를 지나며 **버튼을 뚫고 보였다**. 버튼을 카드 표면으로 세운다 — 판·버튼·테두리가 한 표면.
 *   🔴 원인은 **겹침이 아니라 재질**이다: 판은 `<canvas>` **한 장**이라 z-index 로는 아무것도 못 가린다
 *      (그 불변식은 `videoLabPanFlicker` 의 것이다) — 버튼이 불투명해야 그 선이 끊긴다.
 *   ⚠️ 접힌 상태(아이콘 하나만 남는다)도 **같은 표면**이다: 두 상태의 재질이 갈리면 접었다 펼 때 버튼이 바뀐 것처럼 보인다.
 *   ⚠️ 크기(펼침 24 · 접힘 28)와 자리는 건드리지 않는다 — `graphMinimap` 이 판 모서리에 겹쳐 놓은 값이다. */
body.shell-v2 .vlab-minimap-toggle,
body.shell-v2 .vlab-minimap.is-open .vlab-minimap-toggle {
  opacity: 1;
  background: var(--vl-node-body);
  border: 1px solid var(--vl-node-border);
  color: var(--vl-node-title);
  box-shadow: var(--vl-node-shadow);
}
/* ⚠️ 호버는 `.vlab-minimap` 를 거쳐 (0,4,1)로 적는다 — `.vlab-minimap-toggle:hover`(0,3,1)로 두면
   바로 위 `.vlab-minimap.is-open .vlab-minimap-toggle`(0,4,1)의 배경에 져서 **펼친 상태에서만 호버가 안 먹는다**. */
body.shell-v2 .vlab-minimap .vlab-minimap-toggle:hover:not(:disabled) {
  background: var(--vl-control-hover);
  /* ⚠️ 4차에 `--vl-ctl-border` 가 **transparent** 가 됐다(보조 컨트롤은 테두리를 안 쓴다) — 그 토큰을 그대로 두면
     호버에서 접기 버튼의 네 변이 사라진다. 미니맵 버튼은 카드 표면이므로 카드 테두리를 밝힌 값을 쓴다. */
  border-color: var(--vl-node-border-hover);
  opacity: 1;
}
body.shell-v2 .vlab-minimap-canvas { border-color: var(--vl-node-border); background: var(--vl-canvas-bg); box-shadow: var(--vl-node-shadow); }

/* ════════ ⑧-B 막대·상단바의 **흰 알약** — 호버에서 어두워지지 않는다(5차-I) ════════
 *   오너 2026-09-21: *"흰 생성버튼에 마우스를 올렸을 때 사라져버리는 느낌이야 … 다른 이런 버튼도 맥락이 비슷해."*
 *   카드 안 [생성]과 **같은 원인**이다: 흰 채움 위의 호버가 `framer.css` 의 `.btn--primary:hover{background:#ededed}`
 *   라, 어두운 캔버스에서 알약이 한 단 내려앉아 **뒤로 물러난다**. 캔버스의 흰 알약은 [＋ 노드]·[전체 실행] 둘이고
 *   둘 다 이 화면의 주 행동이므로, 호버에서 달라지는 것을 **바깥**(후광 링 + 퍼지는 빛)으로 옮긴다.
 *   ⚠️ 스코프는 **캔버스뿐**이다(`.vlab-dock`·`.vlab-topbar`) — 다른 탭의 주 CTA 는 한 픽셀도 안 바뀐다.
 *      앱 전역의 `.btn--primary` 호버는 `framer.css`(공용 셸)의 것이고 여기서 건드릴 자리가 아니다.
 *   ⚠️ 특이도 (0,6,0)으로 `framer.css`(0,2,0)를 **순서와 무관하게** 이긴다 — 그 파일은 index.html 에서 뒤에 실린다.
 *   ⚠️ [＋ 노드]는 열림·누름에 제 변환이 있어 `canvas-dock.css` 가 자기 자리에서 따로 적는다(여기 규칙과 같은 결). */
body.shell-v2 :is(.vlab-dock, .vlab-topbar) .btn--primary {
  transition: box-shadow 120ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 100ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
body.shell-v2 :is(.vlab-dock, .vlab-topbar) .btn--primary:hover:not(:disabled) {
  background: var(--vl-primary-bg);
  border-color: var(--vl-primary-bg);
  color: var(--vl-primary-ink);
  box-shadow: var(--vl-primary-lift);
}
/* 누를 때는 반대로 **들어간다** — 후광을 걷고 button.css 의 `scale(0.96)` 이 그대로 보이게 둔다. */
body.shell-v2 :is(.vlab-dock, .vlab-topbar) .btn--primary:active:not(:disabled) { box-shadow: none; }

/* ════════ ⑧-C [전체 실행] 중 편집 잠금 — 잠긴 것이 **보이게**(2026-09-30 오너 신고) ════════
 *   *"생성중이라 선연결이나 프롬프트 수정안될때 ui는 그대로라 버그처럼 느껴져"* — 규칙은 그대로 두고(1안) 모양만 말하게 한다.
 *   `is-run-locked` 는 `videoLabRunLock` 이 **내 캔버스 + 실행 중**일 때만 캔버스에 단다 — 팀원 캔버스는 여기 안 걸린다
 *   (같은 `readonly` 글칸이어도 모양이 오늘 그대로다 · 그쪽 표시는 상단바 `읽기 전용` 하나).
 *   표면은 ⑤-B 의 카드 표면 한 벌(바탕·테두리·그림자·글자 단) — 새 색 0. */
body.shell-v2 .vlab-runlock {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: calc(100% - 24px);
  height: 28px;
  padding: 0 12px;
  border-radius: 999px;
  background: var(--vl-node-body);
  border: 1px solid var(--vl-node-border);
  box-shadow: var(--vl-node-shadow);
  color: var(--vl-node-text);
  font-size: 12px;
  line-height: 16px;
  white-space: nowrap;
  /* 글자뿐인 줄이다 — 그 아래 카드·빈 곳의 누름을 가로채지 않는다(버튼은 상단바 [남은 생성 취소]가 이미 있다). */
  pointer-events: none;
}
body.shell-v2 .vlab-runlock[hidden] { display: none; }
body.shell-v2 .vlab-runlock-icon { flex: none; color: var(--vl-node-meta); }
body.shell-v2 .vlab-runlock-text { overflow: hidden; text-overflow: ellipsis; }
/* 포트 — 흐리게 + `not-allowed`. 호버 확대·종류색 후광도 끈다(붙을 것처럼 부풀면 그게 약속이 된다).
 *   🔴 `.vlab-port:hover`(0,3,1)·`.vlab-node:hover .vlab-port`(v1)를 **특이도로** 이긴다 — 순서에 기대지 않는다.
 *   커서는 상속이라 누를 자리를 넓힌 `::before` 도 같이 `not-allowed` 다. */
body.shell-v2 .vlab-canvas.is-run-locked .vlab-port,
body.shell-v2 .vlab-canvas.is-run-locked .vlab-node:hover .vlab-port,
body.shell-v2 .vlab-canvas.is-run-locked .vlab-port:hover {
  opacity: 0.35;
  cursor: not-allowed;
  transform: none;
  box-shadow: 0 0 0 1px var(--vl-port-ring);
}
/* 잠긴 글칸(`videoLabNodeView.lockField` — `readonly` + `vlab-field-locked`) — 살짝 흐리게 · `not-allowed`.
 *   포커스는 된다(선택·복사는 된다) — 다만 파란 포커스 링은 "칠 수 있다" 는 약속이라 걷고, 테두리도 호버에서 안 밝힌다. */
body.shell-v2 .vlab-canvas.is-run-locked .vlab-node .vlab-field-locked {
  opacity: 0.6;
  cursor: not-allowed;
}
body.shell-v2 .vlab-canvas.is-run-locked .vlab-node .vlab-field-locked:hover,
body.shell-v2 .vlab-canvas.is-run-locked .vlab-node .vlab-field-locked:focus {
  outline: none;
  border-color: var(--vl-well-border);
}
/* 해 보려 할 때 뜨는 안내(`.vlab-icon-tip` 표면을 그대로 빌린다) — 한 문장이라 폭을 조금 넓힌다. */
.vlab-runlock-tip { max-width: 280px; }

/* ════════ ⑨ 움직임을 줄이는 설정 — 상태는 **즉시** 바뀐다(4차 · 아스트라 §7) ════════
 *   ⚠️ 여기서 끄는 것은 전부 이 파일이 켠 것들이다(카드 hover·선택 겹 · 연필 · 컨트롤 겹 · 포트 확대).
 *      `transform` 을 `none` 으로 되돌리는 것까지 해야 한다 — transition 만 끄면 포트가 **즉시 1.2배**가 된다. */
@media (prefers-reduced-motion: reduce) {
  body.shell-v2 .vlab-node::before,
  body.shell-v2 .vlab-node::after,
  body.shell-v2 .vlab-node .vlab-node-head-btn.vlab-node-rename,
  /* 5차-I 의 호버 테·후광도 여기서 함께 끈다(겹의 transition 만 끄면 테두리·그림자가 혼자 미끄러진다). */
  body.shell-v2 .vlab-node .vlab-node-run,
  body.shell-v2 :is(.vlab-dock, .vlab-topbar) .btn--primary,
  body.shell-v2 .vlab-node .vlab-node-run::before,
  body.shell-v2 .vlab-node .vlab-foot-adv > summary::before,
  body.shell-v2 .vlab-port { transition: none; }
  body.shell-v2 .vlab-port:hover,
  body.shell-v2 .vlab-port.is-target { transform: none; }
  body.shell-v2 .vlab-node::after { transform: none; }
}

/* 선택 도구상자 [이 그룹 실행] 안 크레딧 칩(ui/creditChip.js · 2026-10-16) — 한 줄 · 공용 구분선 토큰 */
body.shell-v2 .vlab-seltool-btn.credit-host { display: inline-flex; align-items: center; gap: 6px; --btn-gap: 6px; white-space: nowrap; }
