
  /* 공통 틀. 바닥 · 벽 · 칸 위치와 기본 외형(MVP UI 디자인 시안의 공장 블록)을 정한다.
     바리에이션은 .v-N 범위에서 장치 · 포트 · 허브 · 벨트 외형만 덮어쓴다. */
  :root {
   /* 58 */






  }

  /* 무대 = 조작할 수 없는 벽 윗면. 보드 왼쪽 위(58, 87)가 격자선에 붙도록 원점을 (0, 29)로 둔다 */
  .var-stage {
    position: relative;
    width: calc(18 * var(--f-cell));
    height: 551px;
    overflow: hidden;
    border-radius: var(--r-window);
    background:
      linear-gradient(90deg, var(--world-grid-line) 1px, transparent 1px) 0 29px / var(--f-cell) var(--f-cell),
      linear-gradient(var(--world-grid-line) 1px, transparent 1px) 0 29px / var(--f-cell) var(--f-cell),
      linear-gradient(rgb(255 255 255 / 12%), rgb(255 255 255 / 12%)),
      var(--world-wallpaper);
    box-shadow: 0 0 0 1px var(--page-line);
    color: var(--os-ink);
    font-family: var(--font-app-body);
  }

  /* 공장 보드 16×7칸: 벽보다 한 단 낮은 바닥 */
  .f-board {
    position: absolute;
    left: var(--f-cell);
    top: 87px;
    width: calc(16 * var(--f-cell));
    height: calc(7 * var(--f-cell));
    background:
      linear-gradient(90deg, var(--f-floor-line) 1px, transparent 1px) 0 0 / var(--f-cell) var(--f-cell),
      linear-gradient(var(--f-floor-line) 1px, transparent 1px) 0 0 / var(--f-cell) var(--f-cell),
      var(--f-floor);
    box-shadow:
      inset -1px -1px 0 var(--f-floor-line),
      inset 0 18px 18px -12px rgb(20 24 32 / 42%),
      inset 14px 0 14px -12px rgb(20 24 32 / 26%),
      inset -14px 0 14px -12px rgb(20 24 32 / 26%),
      inset 0 -8px 10px -8px rgb(20 24 32 / 14%);
  }
  .f-board::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: var(--f-wall-h);
    background: linear-gradient(var(--f-wall-face-top), var(--f-wall-face-bottom));
  }
  .f-board::after {
    content: "";
    position: absolute;
    inset: calc(-1 * var(--f-wall-h)) 0 0;
    pointer-events: none;
    box-shadow: 0 0 0 2px var(--f-rim), 0 0 0 3px rgb(43 45 49 / 10%);
  }

  /* 칸 하나. 바리에이션은 칸(58×58) 밖으로 블록을 내보내지 않는다 */
  .f-t {
    position: absolute;
    left: calc(var(--c) * var(--f-cell));
    top: calc(var(--r) * var(--f-cell));
    width: var(--f-cell);
    height: var(--f-cell);
  }
  .f-label {
    position: absolute;
    left: 50%;
    top: calc(100% + 4px);
    transform: translateX(-50%);
    color: #4a4f63;
    font: 700 11px / 1.2 var(--font-app-body);
    white-space: nowrap;
  }

  /* ── 기본 외형(바리에이션이 덮어쓴다) ── */
  .f-block {
    position: absolute;
    inset: 3px 4px 9px;
    display: grid;
    place-items: center;
    border-radius: 2px;
    font: 400 26px / 1 var(--font-emoji);
  }
  .f-dev {
    background: linear-gradient(#3b3e62, var(--world-device));
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 18%), 0 5px 0 #17182c, 0 8px 10px rgb(20 24 32 / 28%);
  }
  .f-dev.is-idle > .f-glyph { opacity: .45; }
  .f-port, .f-np {
    background: linear-gradient(#fbfcfe, #e1e5ee);
    box-shadow: inset 0 0 0 1px rgb(43 45 49 / 12%), 0 5px 0 #a7aec0, 0 8px 10px rgb(20 24 32 / 20%);
  }
  .f-np { color: var(--app-chat); font: 700 22px / 1 var(--font-os); }
  .f-hopper {
    display: grid;
    place-items: center;
    width: 30px;
    height: 28px;
    border-radius: 1px;
    background: var(--app-chat);
    box-shadow: inset 0 3px 0 rgb(0 0 0 / 35%);
    font-size: 18px;
  }
  .f-block[data-out]::after { content: ""; position: absolute; border-radius: 1px; background: var(--app-soft); }
  .f-port[data-out]::after { background: var(--app-chat); }
  .f-block[data-out="e"]::after { right: -3px; top: calc(50% - 8px); width: 3px; height: 16px; }
  .f-block[data-out="w"]::after { left: -3px; top: calc(50% - 8px); width: 3px; height: 16px; }
  .f-block[data-out="n"]::after { top: -3px; left: calc(50% - 8px); width: 16px; height: 3px; }
  .f-block[data-out="s"]::after { bottom: -8px; left: calc(50% - 8px); width: 16px; height: 3px; }

  .f-belt { position: absolute; }
  .f-belt.e, .f-belt.w {
    inset: 14px 0 15px;
    background: repeating-linear-gradient(90deg, #d9dce3 0 9px, #c8ccd5 9px 11px);
    box-shadow: inset 0 3px 0 #b8bdc8, inset 0 -3px 0 #b8bdc8, 0 3px 0 #a3a9b6;
  }
  .f-belt.n, .f-belt.s {
    inset: 0 14px 0 15px;
    background: repeating-linear-gradient(0deg, #d9dce3 0 9px, #c8ccd5 9px 11px);
    box-shadow: inset 3px 0 0 #b8bdc8, inset -3px 0 0 #b8bdc8, 0 3px 0 #a3a9b6;
  }
  .f-belt::after {
    content: "";
    position: absolute;
    width: 7px;
    height: 7px;
    border: solid rgb(43 45 49 / 38%);
    border-width: 3px 3px 0 0;
  }
  .f-belt.e::after { right: 8px; top: calc(50% - 5px); transform: rotate(45deg); }
  .f-belt.w::after { left: 8px; top: calc(50% - 5px); transform: rotate(-135deg); }
  .f-belt.s::after { bottom: 8px; left: calc(50% - 5px); transform: rotate(135deg); }
  .f-belt.n::after { top: 8px; left: calc(50% - 5px); transform: rotate(-45deg); }
  .f-item {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -58%);
    font: 400 19px / 1 var(--font-emoji);
    filter: drop-shadow(0 2px 0 rgb(43 45 49 / 35%));
  }

  .f-hub {
    position: absolute;
    left: calc(var(--c) * var(--f-cell) + 4px);
    top: calc(var(--r) * var(--f-cell) + 3px);
    width: calc(3 * var(--f-cell) - 8px);
    height: calc(3 * var(--f-cell) - 12px);
    border-radius: 2px;
    background: linear-gradient(#3b3e62, var(--world-device));
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 18%), 0 7px 0 #17182c, 0 12px 18px rgb(20 24 32 / 30%);
  }
  .f-hub-plate {
    position: absolute;
    inset: 18px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    border-radius: 2px;
    background: var(--app-hover);
    box-shadow: inset 0 3px 0 rgb(0 0 0 / 28%), inset 0 -1px 0 rgb(255 255 255 / 10%);
  }
  .f-hub-glyph { font: 400 40px / 1 var(--font-emoji); }
  .f-hub-name { color: var(--app-bright); font: 700 13px / 1 var(--font-app-display); letter-spacing: .08em; }
  .f-intake { position: absolute; border-radius: 1px; background: var(--app-soft); }
  .f-intake.w { left: -3px; top: calc(50% - 10px); width: 3px; height: 20px; }
  .f-intake.e { right: -3px; top: calc(50% - 10px); width: 3px; height: 20px; }
  .f-intake.n { top: -3px; left: calc(50% - 10px); width: 20px; height: 3px; }
  .f-intake.s { bottom: -3px; left: calc(50% - 10px); width: 20px; height: 3px; }

  .f-flag {
    position: absolute;
    left: 50%;
    top: -12px;
    z-index: 2;
    transform: translateX(-50%);
    padding: 2px 6px;
    border-radius: 2px;
    background: rgb(25 26 46 / 90%);
    color: var(--app-soft);
    font: 700 11px / 1.3 var(--font-app-body);
    white-space: nowrap;
  }
  .f-t.is-hl { background: var(--app-accent-wash); box-shadow: inset 0 0 0 1px var(--app-accent); }
  .f-block.is-ghost { background: rgb(137 133 255 / 16%); box-shadow: none; outline: 2px dashed var(--app-accent); outline-offset: -2px; }
  .f-block.is-ghost > .f-glyph { opacity: .6; }

  /* ── 모든 바리에이션 공통 ──
     장치 외곽: 밝은 바닥과 구분되도록 블록 실루엣(앞면·출력 표시 포함) 둘레에 어두운 1px 선을 두른다.
     이모지 외곽: 토이 블록의 흰 스티커 테두리를 이모지(.f-em)에만 씌운다. 상태 필터를 쓰는 바리에이션은 var(--f-edge)를 뒤에 붙인다 */
  .var-stage {
    --f-edge-c: rgb(25 26 46 / 85%);
    --f-edge: drop-shadow(1px 0 0 var(--f-edge-c)) drop-shadow(-1px 0 0 var(--f-edge-c)) drop-shadow(0 1px 0 var(--f-edge-c)) drop-shadow(0 -1px 0 var(--f-edge-c));
    --f-sticker: drop-shadow(1.5px 0 0 #fff) drop-shadow(-1.5px 0 0 #fff) drop-shadow(0 1.5px 0 #fff)
      drop-shadow(0 -1.5px 0 #fff) drop-shadow(0 1.5px 0 rgb(25 26 46 / 35%));
  }
  .var-stage :is(.f-block, .f-hub):not(.is-ghost) { filter: var(--f-edge); }
  .var-stage .f-dev.is-idle { filter: saturate(.5) var(--f-edge); }
  .f-em { display: inline-block; font-style: normal; line-height: 1; filter: var(--f-sticker); }



/* 장치 정본. 입방체(1차 바리에이션 1번)를 조정 값으로 다시 쓴 것이고, 기본값은 2026-10-14에 사용자가 고른 값이다.
   남쪽에서 비스듬히 본 큐브: 윗면(.f-block) + 앞면(::before) + 오른쪽 옆면 그늘 띠 + 장치 둘레 바닥 그림자 */
.v-c {
  
  
  
 
  /* 이모지 그림자 세 겹: 공통(모든 이모지의 흰 테두리 아래) · 장치(생산 장치 이모지와 물류 장치 기호) · 벨트 물건. 진하기 0이면 그 겹이 없다 */
  
  
  
  
  

  /* 위 값에서 나오는 값 */





  /* 흰 테두리는 이모지 그림 공용 파일(에셋/이모지/emoji-texture.js)이 문서에 넣는 SVG 필터 #f-sticker가 그린다. 두께는 --sticker-w이고 조정 스크립트가 공용 파일에 넘긴다.
     가장자리가 투명해지는 폭은 공용 파일이 화면 배율에 맞춰 1기기 픽셀로 정한다.
     테두리(-line)와 공통 그림자(-sh)를 나눠 두어, 이모지 그림(에셋/이모지/emoji-texture.css)이 둘 사이에 질감을 끼운다 */





}

/* 블록 = 윗면 + 앞면 + 바닥 그림자. 윗면 높이는 칸 58 − 위 4 − 앞면 − 그림자 자리 6 */
.v-c .f-block {
  inset: 4px 5px calc(var(--front-h) + 6px);
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--shade), linear-gradient(var(--top));
  box-shadow: inset 0 var(--hl-top-w) 0 var(--lit), inset 0 -1px 0 rgb(255 255 255 / calc(var(--hl-edge-a) * 1%));
}
.v-c .f-block::before {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: var(--fh, var(--front-h));
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--led, none), var(--shade), linear-gradient(var(--front));
}
/* 바닥 그림자: 기계 하우징처럼 윗면 + 앞면 전체 자리(칸 위 4 · 옆 5 · 아래 6)에서 사방으로 번진다.
   칸의 ::before로 블록 뒤에 깔아 장치 외곽선 필터가 그림자에 걸리지 않게 한다 */
.v-c .f-t:has(> .f-dev:not(.is-ghost), > .f-port, > .f-np)::before {
  content: "";
  position: absolute;
  inset: 4px 5px 6px;
  border-radius: var(--radius);
  box-shadow: var(--floor-sh);
}
/* 이모지 그림자: 공통 겹은 --f-sticker 끝에 있고, 장치 이모지와 벨트 물건은 그 위에 한 겹씩 더 진다 */
.v-c .f-glyph { filter: var(--emoji-sh); }
.v-c .f-item { filter: var(--item-sh); }

/* 생산 장치: 앞면 왼쪽의 작은 상태 등 하나로 상태를 보인다. 진행도는 따로 그리지 않는다.
   쉬는 상태 = 회색 등, 작동(.is-on) = 초록 등 + 번짐, 문제 발생(.is-warn) = 주황 등 + 번짐 · 윗면 어둡게.
   등은 앞면 왼쪽 4~12px에 두어 앞면 바닥의 남쪽 벨트 틈(#conveyor)과 겹치지 않는다. 빨강은 카운트 배지 전용(D3)이라 문제 발생은 주황을 쓴다 */
.v-c { }
.v-c .f-dev {
  font-size: var(--emoji);
  --top: var(--dev-top-1), var(--dev-top-2);
  --front: var(--dev-front-1), var(--dev-front-2);
  --led-c: #5d6283;
  --led-glow: transparent;
  --led: linear-gradient(var(--led-c) 0 0) 4px 3px / 8px 4px no-repeat,
    radial-gradient(14px 9px at 8px 5px, var(--led-glow), transparent);
}
.v-c .f-dev.is-on {
  --lit: rgb(255 255 255 / calc(var(--hl-on-a) * 1%));
  --led-c: var(--app-online);
  --led-glow: rgb(35 165 90 / 55%);
}
.v-c .f-dev.is-warn {
  --top: var(--app-hover), var(--app-chat);
  --lit: rgb(255 255 255 / 8%);
  --led-c: var(--warn);
  --led-glow: rgb(240 178 50 / 55%);
}
.v-c .f-dev.is-warn > .f-glyph { opacity: .6; }
.v-c .f-block.is-ghost { background: rgb(137 133 255 / 16%); box-shadow: none; }
.v-c .f-block.is-ghost::before {
  box-sizing: border-box;
  border: 2px dashed var(--app-accent);
  border-top: 0;
  background: rgb(137 133 255 / 34%);
  box-shadow: none;
}

/* 입력 포트: 밝은 강철 큐브, 윗면에 움푹한 호퍼 */
.v-c .f-port { --top: var(--port-top-1), var(--port-top-2); --front: var(--port-front-1), var(--port-front-2); --lit: #fff; }
.v-c .f-hopper {
  width: 30px;
  height: 25px;
  border-radius: 2px;
  box-shadow: inset 0 4px 0 rgb(0 0 0 / 45%), 0 0 0 2px #bfc6d4, 0 2px 0 2px #fff;
}

/* 물류 장치(분배기 · 합류기 · 터널 · 선별기): 생산 장치와 같은 입방체 · 크기 · 그림자. 윗면만 강철 남색이고 상태 등은 없다 */
.v-c .f-np {
  --top: #7a80a5, #61668b;
  --front: var(--app-input), var(--app-side);
  --lit: rgb(255 255 255 / 45%);
  color: #f0f1fa;
  font: 700 21px / 1 var(--font-os);
}
.v-c .f-np > .f-glyph { text-shadow: 0 1px 0 rgb(0 0 0 / 50%); }
.v-c .f-np[data-np="tunnel"] {
  background: radial-gradient(14px 11px at 50% 58%, var(--app-rail) 0 92%, transparent), var(--shade), linear-gradient(var(--top));
}
.v-c .f-np[data-np="tunnel"] > .f-glyph { opacity: 0; }
/* 선별기가 고른 품목 */
.v-c .f-pick {
  position: absolute;
  top: 3px;
  right: 6px;
  display: grid;
  place-items: center;
  width: 16px;
  height: 14px;
  border-radius: 2px;
  background: var(--app-rail);
  font: 400 11px / 1 var(--font-emoji);
}
.v-c .f-np:has(> .f-pick) > .f-glyph { translate: -4px 2px; }


/* 컨베이어 최종안: 쌓는 순서 · 벨트 · 접합부 · 입출력 면. 장치 정본(#cube) 위에 얹는다 */

/* 쌓는 순서: 장면을 행 순서로 쌓고(남쪽 행일수록 위), 같은 행에서는 블록이 벨트 위에 온다.
   그래서 북 · 동 · 서 면에 붙는 벨트는 장치보다 한 단 아래에 깔려 장치 밑으로 들어가고,
   남쪽 면(앞면)에 붙는 벨트는 장치보다 한 단 위에 있어 앞면 틈까지만 온다. n × n칸 허브는 맨 아래 행(r + n − 1)의 블록 높이에 둔다(#hub) */
.v-c .f-t { z-index: calc(var(--r) * 2); }
.v-c .f-t:has(> .f-block) { z-index: calc(var(--r) * 2 + 1); }

/* 벨트: 폭 44px(칸 7~51px) = 강철 테 6px + 고무 32px(8px 간격 홈) + 그늘진 테 6px.
   가로 벨트와 곡선은 아래로 4px 두께를 보인다 */
.v-c {







}
.v-c .f-belt:is(.e, .w) { inset: 7px 0; --across: 180deg; --along: 90deg; }
.v-c .f-belt:is(.n, .s) { inset: 0 7px; --across: 90deg; --along: 180deg; }
.v-c .f-belt:is(.e, .w, .n, .s) {
  background: linear-gradient(var(--across), var(--b-frame)), repeating-linear-gradient(var(--along), var(--b-ribs));
  box-shadow: none;
}
.v-c .f-belt:is(.e, .w) { box-shadow: 0 4px 0 #5d6283, 0 6px 4px -1px rgb(20 24 32 / 30%); }

/* 곡선: 칸 모서리 중심, 반지름 7~51px 고리 1/4 */
.v-c .f-belt.turn {
  inset: 0;
  background: none;
  box-shadow: none;
  filter: drop-shadow(0 4px 0 #5d6283);
  --turn: radial-gradient(circle at var(--at), var(--b-hi) 0 9px, var(--app-soft) 0 13px, transparent 0 45px, var(--app-muted) 0 49px, var(--b-lo) 0),
    repeating-conic-gradient(at var(--at), var(--b-rub) 0 12deg, var(--b-rib) 0 16deg);
}
.v-c .f-belt.turn::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--turn);
  -webkit-mask: radial-gradient(circle at var(--at), transparent 6.5px, #000 7px 51px, transparent 51.5px);
  mask: radial-gradient(circle at var(--at), transparent 6.5px, #000 7px 51px, transparent 51.5px);
}
.v-c .f-belt.turn.ne { --at: 100% 0; }
.v-c .f-belt.turn.nw { --at: 0 0; }
.v-c .f-belt.turn.se { --at: 100% 100%; }
.v-c .f-belt.turn.sw { --at: 0 100%; }

/* 화살 무늬와 벨트 위 물건 */
.v-c .f-belt::after { width: 9px; height: 9px; border-width: 4px 4px 0 0; border-color: rgb(230 233 242 / 70%); }
.v-c .f-belt:is(.e, .w)::after { top: calc(50% - 7px); }
.v-c .f-belt:is(.n, .s)::after { left: calc(50% - 7px); }
.v-c .f-belt.turn[data-out="e"]::after { right: 8px; top: calc(50% - 7px); transform: rotate(45deg); }
.v-c .f-belt.turn[data-out="w"]::after { left: 8px; top: calc(50% - 7px); transform: rotate(-135deg); }
.v-c .f-belt.turn[data-out="n"]::after { top: 8px; left: calc(50% - 7px); transform: rotate(-45deg); }
.v-c .f-belt.turn[data-out="s"]::after { bottom: 8px; left: calc(50% - 7px); transform: rotate(135deg); }
.v-c .f-item { font-size: 25px; }

/* 장치 밑으로 들어가는 끝: 벨트 끝이 블록(장치 · 포트 · 물류 장치 · 허브)에 닿으면 그 쪽 클래스(j-n · j-e · j-s · j-w)가 붙는다.
   동 · 서 · 남 끝은 벨트를 블록 칸 안으로 16px 늘려 블록 밑에 깐다. 16px는 홈 간격 8px의 배수라 무늬가 끊기지 않는다.
   보이는 것은 블록 둘레 바닥 틈(옆 5 · 뒤 4px)뿐이고, 그 위로 블록 외곽선과 바닥 그림자가 진다 */
.v-c .f-belt.j-e { --x-e: -16px; }
.v-c .f-belt.j-w { --x-w: -16px; }
.v-c .f-belt.j-s { --x-s: -16px; }
.v-c .f-belt:is(.j-e, .j-w, .j-s):not(.turn)::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0 var(--x-e, 0) var(--x-s, 0) var(--x-w, 0);
  background: inherit;
  box-shadow: inherit;
}

/* 입출력 면: 장치에 따로 그리지 않는다. 면의 입력 · 출력은 그 면에 붙은 벨트의 방향으로 정해진다
   (장면 스크립트가 .f-io에 in · out을 붙이지만 겉모양은 같다). 북 · 동 · 서 벨트는 장치 밑으로 들어가 그릴 것이 없다 */
.v-c .f-io { position: absolute; }
.v-c .f-io:not(.s) { display: none; }

/* 남쪽 면: 벨트가 장치 앞에 있어 앞면 바닥에 벨트가 드나드는 낮은 틈(3px)을 낸다. 입력 · 출력이 같은 모양이다.
   틈은 상태 등(앞면 위 3~7px) 아래에 있어 등과 겹치지 않는다. 장치가 틈부터 칸 끝까지 벨트 머리를 그려 아래 벨트와 잇는다
   (홈은 칸 끝에서 무늬가 맞도록 1px 내린다). 허브는 앞면 높이와 틈 자리가 다르다(#hub) */
.v-c .f-io.s {
  bottom: calc(-6px - var(--front-h));
  left: calc(50% - 23px);
  width: 46px;
  height: 9px;
  background:
    linear-gradient(90deg, var(--b-pit) 0 1px, transparent 0 45px, var(--b-pit) 0) 0 0 / 100% 3px no-repeat,
    linear-gradient(var(--b-pit) 1px, transparent 6px) 1px 0 / 44px 100% no-repeat,
    linear-gradient(90deg, var(--b-frame)) 1px 0 / 44px 100% no-repeat,
    repeating-linear-gradient(var(--b-ribs)) 1px 1px / 44px 8px repeat-y;
}
.v-c .f-hub > .f-io.s { --front-h: var(--hub-front-h); }

/* 장면 글자: 사례 제목 */
.f-cap {
  position: absolute;
  left: calc(var(--c) * var(--f-cell));
  top: calc(var(--r) * var(--f-cell) + 16px);
  width: calc(var(--span, 1) * var(--f-cell));
  color: #3c4058;
  font: 700 12px / 1.3 var(--font-app-body);
  text-align: center;
  white-space: nowrap;
}
.f-cap small { display: block; color: #6a6f88; font-weight: 500; font-size: 11px; }
.f-cap.left { text-align: left; padding-left: 6px; }


/* 허브 정본. 허브 바리에이션(시안/장치외형/261014-hub-variations.html)의 채택안 3 칸별 계단을 옮겼다.
   물류 장치의 강철 남색 입방체를 n × n칸으로 키운다. n은 홀수 3 · 5 · 7(1~3단계, D-03 DEV-R6)이라 격자 정중앙에 놓인다.
   입방체 구성(윗면 · 앞면 · 오른쪽 그늘 · 위 모서리 빛 · 외곽선 · 바닥 그림자)과 조정 변수는 생산 장치와 같다.
   앞면은 단계마다 생산 장치 앞면의 25%씩 높아진다(10 → 15px). 쌓는 순서는 허브 맨 아래 행(r + n − 1)의 블록 높이다.
   앞면 왼쪽 상태 등은 생산 장치와 같은 자리 · 크기 · 색이다(쉼 회색 · 판매 중 초록 · 한도 도달 주황).
   네 모서리 칸은 입력을 받지 않는다. 윗면 둘레 안쪽에 새긴 선(홈선 테)을 두르고 입력 칸마다 그 선 위에 슬릿을 낸다.
   외곽은 칸별 계단으로 깎는다. --cut은 외곽 깎기 깊이(6px), --rib은 이웃한 입력 칸 경계에 남기는 턱의 폭(8px)이다.
   허브 본체를 네 변에서 --cut만큼 안으로 들이고, 모서리 칸은 블록(f-hub-cut > u)으로 원래 자리까지 채운다.
   그래서 윤곽에서 받는 구간(모서리 칸 사이)만 파여 들어간다. 바닥 그림자(f-hub-sh)는 깎인 윤곽을 따라 진다 */
.v-c :is(.f-hub, .f-hub-sh) {
  --cut: 6px;
  --rib: 8px;
  --hub-front-h: calc(var(--front-h) * (1 + (var(--n) - 3) / 2 * .25));
  --hub-front: var(--app-input), var(--app-side);
}
.v-c .f-hub {
  --hub-top: #7a80a5, #61668b;
  --hub-h: calc(var(--n) * var(--f-cell) - 10px - var(--hub-front-h) - 2 * var(--cut));
  --lit: rgb(255 255 255 / 45%);
  --led-c: #5d6283;
  --led-glow: transparent;
  --led: linear-gradient(var(--led-c) 0 0) 4px 3px / 8px 4px no-repeat, radial-gradient(14px 9px at 8px 5px, var(--led-glow), transparent);
  left: calc(var(--c) * var(--f-cell) + 5px + var(--cut));
  top: calc(var(--r) * var(--f-cell) + 4px + var(--cut));
  width: calc(var(--n) * var(--f-cell) - 10px - 2 * var(--cut));
  height: var(--hub-h);
  z-index: calc((var(--r) + var(--n)) * 2 - 1);
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--shade), linear-gradient(var(--hub-top));
  box-shadow: inset 0 var(--hl-top-w) 0 var(--lit), inset 0 -1px 0 rgb(255 255 255 / calc(var(--hl-edge-a) * 1%));
}
.v-c .f-hub::before {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: var(--hub-front-h);
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--led), var(--shade), linear-gradient(var(--hub-front));
}
.v-c .f-hub.is-on { --led-c: var(--app-online); --led-glow: rgb(35 165 90 / 55%); }
.v-c .f-hub.is-warn { --led-c: var(--warn); --led-glow: rgb(240 178 50 / 55%); }

/* 바닥 그림자 자리: 깎지 않은 n × n칸(칸 위 4 · 옆 5 · 아래 6). 바닥에 깔리므로 벨트보다 아래(z-index 0, 보드에서 맨 먼저 그림)에 두어
   허브에 붙은 벨트에는 그림자가 지지 않는다. 그림자는 ::before, 서 · 동 홈의 벽은 그 위의 ::after에 그린다 */
.v-c .f-hub-sh {
  position: absolute;
  left: calc(var(--c) * var(--f-cell) + 5px);
  top: calc(var(--r) * var(--f-cell) + 4px);
  width: calc(var(--n) * var(--f-cell) - 10px);
  height: calc(var(--n) * var(--f-cell) - 10px);
  z-index: 0;
  border-radius: var(--radius);
}
/* 바닥 그림자: 생산 장치와 같은 값(--sh-y · --sh-blur · --sh-spread · --sh-a)을 깎인 윤곽에 입힌다.
   box-shadow는 사각형 하나라 깎인 홈을 채우지 못하므로, 윤곽 조각(본체 · 모서리 블록 · 턱, 앞면과 벽 포함)을 퍼짐만큼 키워 한 색으로 칠하고
   아래로 옮겨 번짐의 반으로 흐린 뒤 진하기만큼 투명하게 한다(box-shadow와 같은 계산). 상자는 퍼짐만큼 커서 조각 자리도 퍼짐 두 배만큼 크다.
   턱은 칸 경계마다 하나씩 n − 3개다. 북 · 남 턱은 왼쪽이 j칸 + 53 − 턱 폭의 반, 서 · 동 턱은 위가 j칸 + 54 − 턱 폭의 반이다 */
.v-c .f-hub-sh::before {
  --s: var(--sh-spread);
  --k: linear-gradient(rgb(20 24 32) 0 0);
  --ribs: calc((var(--n) - 4) * var(--f-cell) + var(--rib) + 2 * var(--s));
  --rib-x: repeating-linear-gradient(90deg, rgb(20 24 32) 0 calc(var(--rib) + 2 * var(--s)), transparent 0 var(--f-cell));
  --rib-y: repeating-linear-gradient(rgb(20 24 32) 0 calc(var(--rib) + var(--hub-front-h) + 2 * var(--s)), transparent 0 var(--f-cell));
  content: "";
  position: absolute;
  inset: calc(-1 * var(--s));
  translate: 0 var(--sh-y);
  border-radius: calc(var(--radius) + var(--s));
  background:
    var(--k) var(--cut) var(--cut) / calc(100% - 2 * var(--cut)) calc(100% - 2 * var(--cut)) no-repeat,
    var(--k) 0 0 / calc(53px + 2 * var(--s)) calc(54px + var(--hub-front-h) + 2 * var(--s)) no-repeat,
    var(--k) 100% 0 / calc(53px + 2 * var(--s)) calc(54px + var(--hub-front-h) + 2 * var(--s)) no-repeat,
    var(--k) 0 100% / calc(53px + 2 * var(--s)) calc(52px + 2 * var(--s)) no-repeat,
    var(--k) 100% 100% / calc(53px + 2 * var(--s)) calc(52px + 2 * var(--s)) no-repeat,
    var(--rib-x) calc(var(--f-cell) + 53px - var(--rib) / 2) 0 / max(0px, var(--ribs)) calc(var(--cut) + 2 * var(--s)) no-repeat,
    var(--rib-x) calc(var(--f-cell) + 53px - var(--rib) / 2) 100% / max(0px, var(--ribs)) calc(var(--cut) + var(--hub-front-h) + 2 * var(--s)) no-repeat,
    var(--rib-y) 0 calc(var(--f-cell) + 54px - var(--rib) / 2) / calc(var(--cut) + 2 * var(--s)) max(0px, var(--ribs) + var(--hub-front-h)) no-repeat,
    var(--rib-y) 100% calc(var(--f-cell) + 54px - var(--rib) / 2) / calc(var(--cut) + 2 * var(--s)) max(0px, var(--ribs) + var(--hub-front-h)) no-repeat;
  filter: blur(calc(var(--sh-blur) / 2));
  opacity: calc(var(--sh-a) / 100);
}
/* 서 · 동 홈의 남쪽 벽(앞면 띠): 위 모서리 블록 아래(54px)와 턱 아래(턱 가운데 + 4px = 116px부터 한 칸 58px 간격)에 앞면 높이만큼 둔다.
   벨트보다 아래에 깔리므로 벨트가 지나가는 칸에서는 벨트가 벽을 덮어 가려지지 않고, 벨트가 없는 칸에서만 벽이 보인다.
   턱 벽 줄은 아래 모서리 칸까지 이어지지만 아래 모서리 블록이 덮는다. 장치 앞면처럼 바깥 둘레에 외곽선(--f-edge)을 두른다 */
.v-c .f-hub-sh::after {
  --wall: linear-gradient(var(--hub-front));
  --rib-wall: repeating-linear-gradient(var(--app-input) 0, var(--app-side) var(--hub-front-h), transparent 0 var(--f-cell));
  content: "";
  position: absolute;
  inset: 0;
  background:
    var(--wall) 0 54px / var(--cut) var(--hub-front-h) no-repeat,
    var(--wall) 100% 54px / var(--cut) var(--hub-front-h) no-repeat,
    var(--rib-wall) 0 116px / var(--cut) calc(100% - 116px) no-repeat,
    var(--rib-wall) 100% 116px / var(--cut) calc(100% - 116px) no-repeat;
  filter: var(--f-edge);
}
/* 남쪽 틈은 벨트가 붙은 칸(--x번째 열) 앞에 낸다. 깎인 만큼 벨트 머리를 길게 그려 칸 끝의 벨트와 잇는다 */
.v-c .f-hub > .f-io.s {
  left: calc(var(--x) * var(--f-cell) + 1px - var(--cut));
  bottom: calc(-6px - var(--front-h) - var(--cut));
  height: calc(9px + var(--cut));
}

/* 슬릿: 입력 칸마다 하나. 북 · 남은 j번째 열 가운데, 서 · 동은 j번째 행 가운데. 우편함처럼 가는 틈(36 × 6px)이고 위쪽 그늘과 아래 밝은 턱 1px.
   --gap은 가장자리에서 띄운 거리다 */
.v-c .f-hub-in { position: absolute; inset: 0; }
.v-c .f-hub-in > b {
  --along: 36px;
  --depth: 6px;
  --gap: 13px;
  position: absolute;
  border-radius: 3px;
  background: #0e0f1d;
  box-shadow: inset 0 2px 0 rgb(0 0 0 / 55%), 0 1px 0 rgb(255 255 255 / 40%), 0 -1px 0 rgb(25 26 46 / 45%);
}
.v-c .f-hub-in > b:is(.n, .s) { left: calc(var(--j) * var(--f-cell) + 24px - var(--along) / 2 - var(--cut)); width: var(--along); height: var(--depth); }
.v-c .f-hub-in > b:is(.w, .e) { top: calc(var(--j) * var(--f-cell) + 25px - var(--along) / 2 - var(--cut)); width: var(--depth); height: var(--along); }
.v-c .f-hub-in > b.n { top: var(--gap); }
.v-c .f-hub-in > b.s { bottom: var(--gap); }
.v-c .f-hub-in > b.w { left: var(--gap); }
.v-c .f-hub-in > b.e { right: var(--gap); }
/* 홈선 테: 윗면 둘레 16px 안쪽에 새긴 선. 선은 슬릿 뒤를 지난다 */
.v-c .f-hub-in::before {
  content: "";
  position: absolute;
  inset: 16px 16px 15px;
  border: 1px solid rgb(25 26 46 / 40%);
  border-radius: 3px;
  box-shadow: 0 1px 0 rgb(255 255 255 / 22%), inset 0 1px 0 rgb(255 255 255 / 22%);
}

/* 모서리 블록: 깎지 않은 모서리 칸(가로 53px)을 원래 자리까지 채운다. 윗면 그라데이션은 허브 본체에 맞춰 이어지게 놓는다.
   위 모서리는 윗면만, 아래 모서리는 윗면(52px − 앞면)과 앞면을 가진다. 상태 등은 왼쪽 아래 모서리 앞면으로 옮긴다 */
.v-c .f-hub-cut { position: absolute; inset: 0; }
.v-c .f-hub-cut > u { position: absolute; width: 53px; }
.v-c .f-hub-cut > u:is(.nw, .sw) { left: calc(-1 * var(--cut)); }
.v-c .f-hub-cut > u:is(.ne, .se) { right: calc(-1 * var(--cut)); }
.v-c .f-hub-cut > u:is(.nw, .ne) {
  --g: linear-gradient(var(--hub-top)) 0 var(--cut) / 100% var(--hub-h) no-repeat;
  top: calc(-1 * var(--cut));
  height: 54px;
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: inset 0 var(--hl-top-w) 0 var(--lit);
}
.v-c .f-hub-cut > u:is(.sw, .se) {
  --g: linear-gradient(var(--hub-top)) 0 calc(52px - var(--hub-front-h) - var(--hub-h) - var(--cut)) / 100% var(--hub-h) no-repeat;
  bottom: calc(-1 * var(--cut));
  height: calc(52px - var(--hub-front-h));
  box-shadow: inset 0 -1px 0 rgb(255 255 255 / calc(var(--hl-edge-a) * 1%));
}
.v-c .f-hub-cut > u.nw { background: var(--g), #7a80a5; }
.v-c .f-hub-cut > u.ne { background: var(--shade), var(--g), #7a80a5; }
.v-c .f-hub-cut > u.sw { background: var(--g), #61668b; }
.v-c .f-hub-cut > u.se { background: var(--shade), var(--g), #61668b; }
.v-c .f-hub-cut > u:is(.sw, .se)::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: var(--hub-front-h);
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--shade), linear-gradient(var(--hub-front));
}
.v-c .f-hub-cut > u.sw::after { background: var(--led), linear-gradient(var(--hub-front)); }

/* 턱: 이웃한 입력 칸 경계에 깎지 않고 남긴 윤곽(폭 --rib). 입구가 칸마다 갈린다.
   턱은 슬릿 기준으로 그 칸 다음 경계(슬릿 가운데에서 29px)에 둔다. 변의 마지막 칸(.last)은 다음 경계가 모서리 블록이라 턱이 없다.
   북 턱: 허브 위로 솟은 윗면(윗면 맨 위 색 + 위 모서리 빛).
   서 · 동 턱: 허브 옆으로 솟은 윗면(같은 높이의 허브 윗면 그라데이션 + 북쪽 가장자리 빛 1px, 동 턱은 오른쪽 그늘). 그 아래 남쪽 벽은 바닥 그림자 자리에 그린다.
   남 턱: 허브 앞으로 솟은 윗면(윗면 맨 아래 색)과 앞면 */
.v-c .f-hub-in > b:not(.last)::after { content: ""; position: absolute; }
.v-c .f-hub-in > b:is(.n, .s)::after { left: calc(var(--along) / 2 + 29px - var(--rib) / 2); width: var(--rib); }
.v-c .f-hub-in > b:is(.w, .e)::after { top: calc(var(--along) / 2 + 29px - var(--rib) / 2); width: var(--cut); height: var(--rib); }
.v-c .f-hub-in > b.n::after {
  top: calc(-1 * (var(--gap) + var(--cut)));
  height: calc(var(--cut) + 1px);
  border-radius: 2px 2px 0 0;
  background: #7a80a5;
  box-shadow: inset 0 var(--hl-top-w) 0 var(--lit);
}
.v-c .f-hub-in > b:is(.w, .e)::after {
  --g: linear-gradient(var(--hub-top)) 0 calc(var(--rib) / 2 + var(--cut) - 54px - var(--j) * var(--f-cell)) / 100% var(--hub-h) no-repeat;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 30%);
}
.v-c .f-hub-in > b.w::after { left: calc(-1 * (var(--gap) + var(--cut))); background: var(--g), #6d7297; }
.v-c .f-hub-in > b.e::after { right: calc(-1 * (var(--gap) + var(--cut))); background: var(--shade), var(--g), #6d7297; }
.v-c .f-hub-in > b.s::after {
  bottom: calc(-1 * (var(--gap) + var(--cut) + var(--hub-front-h)));
  height: calc(var(--cut) + var(--hub-front-h));
  border-radius: 0 0 2px 2px;
  background: linear-gradient(#61668b 0 0) 0 0 / 100% var(--cut) no-repeat, linear-gradient(var(--hub-front)) 0 var(--cut) / 100% var(--hub-front-h) no-repeat;
}


  /* 합본 화면: 왼쪽에 장면 넷, 오른쪽에 네 장면에 함께 적용되는 조정 값 */
  .page { max-width: 1640px; }
  .page-head { margin-bottom: 20px; }
  .tuner { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 20px; align-items: start; }
  /* 무대 크기는 보드 칸 수(--bw × --bh)를 따른다. 장치 · 컨베이어 장면은 16 × 7칸이다 */
  .var-stage { width: calc((var(--bw) + 2) * var(--f-cell)); height: calc(var(--bh) * var(--f-cell) + 145px); }
  .var-stage .f-board { width: calc(var(--bw) * var(--f-cell)); height: calc(var(--bh) * var(--f-cell)); }
  .view { margin: 0 0 32px; }
  .view > h2 { margin: 0 0 4px; font: 700 20px / 1.3 var(--font-app-display); }
  .view > .lead { margin: 0 0 10px; color: var(--page-muted); font-size: 13px; }
  .view-bar { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; margin: 0 0 12px; font-size: 13px; color: var(--page-muted); }
  .view-bar.is-sticky {
    position: sticky;
    top: 0;
    z-index: 5;
    margin-bottom: 20px;
    padding: 10px 14px;
    border: 1px solid var(--page-line);
    border-radius: 12px;
    background: var(--page-card);
  }
  .grp { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
  .grp > span { margin-right: 4px; }
  .fit { overflow: auto; max-height: calc(100vh - 80px); border-radius: var(--r-window); }
  .sizer { position: relative; }
  .sizer > .var-stage { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
  .var-notes { margin: 12px 0 0; padding-left: 20px; color: var(--page-muted); font-size: 13px; }
  .var-notes li { margin: 2px 0; }
  .panel {
    position: sticky;
    top: 12px;
    max-height: calc(100vh - 24px);
    overflow-y: auto;
    padding: 14px 16px;
    border: 1px solid var(--page-line);
    border-radius: 16px;
    background: var(--page-card);
  }
  .panel h3 { margin: 14px 0 6px; font: 700 14px / 1.3 var(--font-app-display); }
  .panel h3:first-of-type { margin-top: 4px; }
  .panel .hint { margin: -2px 0 8px; color: var(--page-muted); font-size: 12px; line-height: 1.45; }
  .panel-head {
    position: sticky;
    top: -14px;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin: -14px -16px 8px;
    padding: 10px 16px;
    border-bottom: 1px solid var(--page-line);
    background: var(--page-card);
    color: var(--page-muted);
    font-size: 12px;
  }
  .panel h3 { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
  .rs {
    padding: 1px 7px;
    border: 1px solid var(--page-line);
    border-radius: 6px;
    background: var(--page-card);
    color: var(--page-muted);
    font: 600 11px / 1.5 var(--font-app-body);
    cursor: pointer;
  }
  .rs:hover:not(:disabled) { border-color: var(--app-accent); color: var(--page-fg); }
  .rs:disabled { opacity: .4; cursor: default; }
  .rs-all { padding: 4px 10px; font-size: 12px; }
  [data-ctl] > .rs, .ctl .rs { visibility: hidden; }
  .is-changed > .rs, .ctl.is-changed .rs { visibility: visible; }
  .ctl.is-changed output, .color.is-changed label { color: var(--page-fg); font-weight: 700; }
  .ctl { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; margin: 0 0 8px; font-size: 13px; }
  .ctl-val { display: flex; align-items: center; gap: 6px; }
  .ctl output { color: var(--page-muted); font-variant-numeric: tabular-nums; }
  .ctl input[type="range"] { grid-column: 1 / -1; width: 100%; accent-color: var(--app-accent); }
  .colors { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; }
  .colors .color { display: flex; align-items: center; gap: 6px; font-size: 12px; }
  .colors input { width: 30px; height: 24px; padding: 0; border: 1px solid var(--page-line); border-radius: 4px; background: none; }
  .btns { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 0; }
  .btns button, .view-bar button, .view-bar select {
    padding: 5px 10px;
    border: 1px solid var(--page-line);
    border-radius: 8px;
    background: var(--page-card);
    color: var(--page-fg);
    font: 600 12px / 1.3 var(--font-app-body);
    cursor: pointer;
  }
  .btns button:hover, .view-bar button:hover { border-color: var(--app-accent); }
  .view-bar button[aria-pressed="true"] { border-color: var(--app-accent); background: var(--app-side); color: var(--app-bright); }
  .panel textarea {
    width: 100%;
    height: 180px;
    margin-top: 6px;
    padding: 8px;
    border: 1px solid var(--page-line);
    border-radius: 8px;
    background: var(--page-bg);
    color: var(--page-fg);
    font: 12px / 1.45 ui-monospace, "Cascadia Code", Consolas, monospace;
    resize: vertical;
  }
  @media (max-width: 1100px) {
    .tuner { grid-template-columns: minmax(0, 1fr); }
    .panel { position: static; max-height: none; }
  }

