/* 영상탭 캔버스 **칠 모드**(2026-10-07 · `.specs/spec_video_lab/canvas-render/contract.md` §1 · §2-4 · `web/ui/canvasPaint/canvasStage.js`).
   ★ 레이아웃은 DOM, 칠은 캔버스 — 카드·그룹·선·격자 DOM 은 그대로 서 있고(측정·핸들러·테스트의 정본) **칠만** 뺀다.
     칠은 `<canvas class="vlab-paint">` 한 장(격자 자리 · 월드 아래)이 숨은 DOM 의 계산된 스타일을 읽어 한다.
   ⚠️ 숨기는 수단은 `visibility:hidden` 이다 — 레이아웃·getBoundingClientRect·ResizeObserver 가 그대로라 잰 크기 소비처
      (선 끝점·마키·도구상자·정렬·미니맵·끌기 스냅)가 한 줄도 안 바뀐다. 대신 숨은 요소는 포인터를 못 받으므로,
      무대가 포인터 아래 카드에 `is-live` 를 **먼저** 붙인다(사용자가 만지는 카드만 DOM 이 칠한다).
   ⚠️ 모든 규칙은 무대가 붙이는 표식 아래다 — 표식이 없으면(`?paint=dom` · `localStorage vlab.paint=dom`) 옛 DOM 칠 그대로.
      선·그룹은 월드의 `is-painted`, 카드는 **카드마다** `is-cv`(캔버스로 넘어온 카드 · 2026-10-07).
   ★ 카드 규칙을 월드 class 에 걸지 않는다 — `.is-painted > .vlab-node … > *` 꼴이면 월드 class 한 번에 월드 아래 8천 요소를 통째로
      다시 계산한다(카드 165 첫 로드 · 넘어오는 프레임 80~100ms). 카드마다면 한 장 ~0.5ms 라 무대가 프레임마다 나눠 넘긴다
      (`canvasStage.js` `CV_CLASS` 머리주석).
   ⚠️ 무대는 스냅샷을 뜨는 동안만 그 카드에 `is-live` 를 잠깐 붙인다(상속된 hidden 과 원래 hidden 을 가르려고) — 그래서 여기에
      `transition` 이 걸린 속성을 바꾸는 규칙을 두지 않는다(붙이고 떼는 사이에 전환이 시작되면 숨은 카드가 비친다).
      캔버스 카드의 전이 끔(아래 `transition: none`)은 이 규칙과 맞물린다 — 되살릴 때 바뀌는 값(바탕·테·그림자·외곽선·visibility)은
      원래 전이 목록(루트 opacity · 포트 transform)에 없고, 떼는 순간에는 전이가 꺼진 쪽이 끝 스타일이라 전이가 시작되지 않는다. */

/* 칠 판 — 격자 다음 · 월드 앞(무대가 그 자리에 끼운다). 포인터는 캔버스(빈 곳 클릭·마키·우클릭)의 몫이다. */
.vlab-paint {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* 격자 — 캔버스가 같은 간격·같은 색 토큰으로 그린다(`gridBackgroundOf` 가 정본). 토큰은 무대가 이 레이어에서 읽으므로 요소는 남긴다. */
.vlab-canvas.vlab-paint-on > .vlab-grid { visibility: hidden; }

/* 카드 — live 가 아니면 칠하지 않는다.
   ★ 제목 편집 칸은 **곧바로** 보인다 — 칸은 렌더 안에서 `focus()` 를 받는데 숨은 요소는 초점을 못 받는다(무대의 live 표식은
     다음 프레임이다). 칸 **자신**만 `visibility:visible` 로 되살린다(상속된 hidden 은 자손이 덮어쓸 수 있다) — 그 한 프레임은
     캔버스가 칠한 카드 위에 칸만 DOM 이 칠하고, 다음 프레임에 무대가 카드를 live 로 세운다(`canvasStage.readCardState` edit 사유).
   ★ `:has()` 를 쓰지 않는다(2026-10-08) — 옛 규칙은 카드 루트마다 `:not(:has(input.vlab-node-title-input))` 를 달았다. 카드 안 요소가
     늘고 줄 때마다 브라우저가 그 조건을 다시 따져야 하는 모양이라 칸 자신만 되살리는 규칙으로 바꿨다(같은 뜻).
     ⚠️ 실측으로는 고르기/Esc 가 가끔 느린 세션(850~1075ms)의 원인이 아니었다 — 원인은 아래 전이 규칙 머리주석. */
/* ★★ 카드 **루트는 보이는 채로 칠만 0** 으로 만든다(리드 2026-10-07) — 루트까지 hidden 이면 포인터를 못 받아 hover 로 live 가
     되기 전의 누름(줌 직후 바로 누르기 · 터치 탭 · 선 잇기 첫 이동의 `elementFromPoint`)이 빈 캔버스에 떨어진다
     → 빈 곳 클릭 = 선택 해제 · 마키 시작. 루트가 히트 대상으로 남아 있으면 그 누름이 지금처럼 카드(`closest('[data-node-id]')`)로 간다.
   ⚠️ 루트의 transition 은 opacity 뿐이다(canvas-contrast.css) — 배경·테두리·그림자·외곽선을 끄는 이 규칙은 전환 없이 즉시 바뀐다.
      그 목록에 다른 속성이 들어오면 live 전환 때 페이드가 비친다. */
.vlab-world > .vlab-node.is-cv:not(.is-live) {
  background: none !important;
  border-color: transparent !important;
  box-shadow: none !important;
  outline-color: transparent !important;
}
.vlab-world > .vlab-node.is-cv:not(.is-live) > *,
.vlab-world > .vlab-node.is-cv:not(.is-live)::before,
.vlab-world > .vlab-node.is-cv:not(.is-live)::after { visibility: hidden; }
.vlab-world > .vlab-node.is-cv:not(.is-live) input.vlab-node-title-input { visibility: visible; }

/* ★★ 캔버스로 넘어온 카드는 CSS 전이를 끈다(2026-10-08 · 카드 165 고르기/Esc 실측) — 고르기 한 번에 흐림 층(opacity 150ms)·선택 링
     전이가 카드 165장에서 돈다. 옛 DOM 칠에서는 합성기가 돌려 메인 스레드가 놀지만, 칠 무대는 프레임마다 스냅샷·opacity 를 읽으므로
     읽을 때마다 돌고 있는 전이 전부의 스타일을 메인 스레드에서 다시 맞췄다(스냅샷 한 장 30~46ms · 고르기 뒤 40ms 프레임 5~6장).
     이 카드들은 DOM 이 칠하지 않으니 전이가 보일 데도 없다 — opacity 곡선은 무대가 같은 길이·이징으로 칠한다(`canvasStage.fadeTimingOf`).
     live 카드(DOM 이 칠한다)는 전이 그대로다. */
.vlab-world > .vlab-node.is-cv:not(.is-live),
.vlab-world > .vlab-node.is-cv:not(.is-live)::before,
.vlab-world > .vlab-node.is-cv:not(.is-live)::after,
.vlab-world > .vlab-node.is-cv:not(.is-live) * { transition: none !important; }

/* 선 — 층째 칠하지 않고 **히트 경로만** 살린다(투명 획이라 칠할 것이 없다 · 클릭·hover·`<title>` 툴팁은 그대로).
   ⚠️ hover 의 인접 선택자(`.vlab-edge-hit:hover + …`)는 숨은 형제의 계산된 스타일을 바꾼다 — 무대가 그 값을 읽어 칠한다. */
.vlab-world.is-painted > .vlab-edges { visibility: hidden; }
.vlab-world.is-painted > .vlab-edges .vlab-edge-hit { visibility: visible; }

/* 그룹 — 상자·몸통은 캔버스가 칠한다. 포인터를 받는 제목바·손잡이만 live 일 때 DOM 이 칠한다(무대가 그 자리를 캔버스에서 비운다).
   ★ 이름 칸이 선 그룹도 칸 자신은 곧바로(카드 제목 칸과 같은 이유 — `focus()` · `:has()` 없이 칸 자신만 되살린다). */
.vlab-world.is-painted .vlab-group { visibility: hidden; }
.vlab-world.is-painted .vlab-group.is-live > :is(.vlab-group-title, .vlab-group-resize) { visibility: visible; }
.vlab-world.is-painted .vlab-group input.vlab-group-title-input { visibility: visible; }

/* ★ 포트 — live 가 아닌 카드의 포트도 **히트 대상으로 남긴다**(칠 0 · 2026-10-07 조작 대조 차이 1).
   옛 DOM 에서 포트(`z-index:1`)는 겹친 뒤 형제 카드보다 위라 그 포트를 잡으면 선 초안이 시작됐다. 숨기면 포인터가 뒤 카드로 떨어져
   그 카드가 끌린다. 보이는 채로 칠만 빼 두면 포인터가 포트에 닿고, 무대가 그 주인을 live 로 세워 DOM 이 칠한다.
   ⚠️ 포트의 transition 은 transform 뿐이다(canvas-contrast.css) — 아래 값들은 전환 없이 바뀐다. */
.vlab-world > .vlab-node.is-cv:not(.is-live) > .vlab-port {
  visibility: visible;
  background: transparent !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
