/*
 * 화면 공통 규칙 -- 한 줄로 불러온다.
 *
 * 화면은 이 파일 하나만 걸면 된다. 어디에 무엇이 있는지는 아래 순서가 말한다.
 * 순서를 바꾸면 나중 것이 앞의 것을 덮으므로, 토큰이 가장 앞이어야 한다.
 *
 *   ui-tokens.css  색 · 글자 눈금 · 간격 · 깊이 · 뼈대 치수 (값을 정하는 유일한 자리)
 *   ui-base.css    바탕 · 레이아웃 · 카드 격자 · 코드 예제
 *   ui-shell.css   화면 뼈대 (사이드바 · 머리 · 빵부스러기 · 액션 바)
 *   ui-parts.css   카드 · 정보 나열 · 버튼
 *   ui-dash.css    대시보드의 부품 (계기 타일 · 구성도 · 자주 보는 핀 · 겹쳐 뜨는 창)
 *   ui-plan.css    대시보드의 도면 (시제품에서 그대로 옮긴 것)
 *   ui-drawer.css  서랍함 -- 보기 레일 · 줄기 · 상세칸 · 핀 격자
 *   ui-timeline.css 조작 이력의 시간 줄
 *   ui-sheet.css   구성의 표계산형 -- 이름 붙이기 · 별 달기
 *   ui-map.css     현장 지도 (본사 화면들이 함께 쓴다)
 *   ui-forms.css   폼 · 표 · 목록 · 핀 목록
 *   ui-status.css  상태 표식 · 알림 · 토스트 · 진행 막대 · 로그판
 *   ui-help.css    도움말 · 반응 · 잠금 표시 · 동의 자리
 */
/* ── ui-tokens.css ─────────────────────────────── */
/*
 * 디자인 토큰 -- 이 제품의 색·글자·간격·깊이를 정하는 유일한 자리.
 *
 * 값을 여기서만 정한다. 화면이나 부품 CSS 에 색·간격을 직접 적지 않는다.
 *
 * ══ 2026-10-04 판 — 제로베이스 검토 ═══════════════════════════
 *
 * 앞 판을 지킬 것으로 두지 않고 색·글꼴·모서리를 처음부터 다시 쟀다. 네 가지가 나왔다.
 *
 *   1. **`--accent` 와 `--info` 가 같은 값이었다**(#2f6bff). soft·line 짝까지 같았다.
 *      색은 설비 상태의 뜻으로 쓰기로 해 놓고(아래 <상태> 절) 상태색 하나가 작용색과
 *      겹쳐 있었던 것이다. 그래서 **파란 배지가 누를 수 있는 것처럼** 보였다.
 *      `--info` 를 무채색으로 내렸다. 뜻이 없는 상태는 색도 없어야 한다.
 *
 *   2. **강조색이 AA 최저선에 정확히 걸쳐 있었다.** #2f6bff 는 흰 바탕 대비 4.50 --
 *      본문 기준 4.5 와 소수점까지 같다. 채도는 100%. 가장 자주 나오는 색이 가장 강한
 *      색이면서 동시에 읽힘의 바닥이었다.
 *
 *   3. **브랜드 남색이 화면에 없었다.** `--shell` 갈래 다섯을 선언해 두고 실제로 부르는
 *      곳은 다섯, 그중 셋은 아래 <옛 이름> 의 별명이다. 남는 건 머리단의 23px 네모 하나.
 *
 *   4. **글꼴을 고른 적이 없었다.** 스택에서 Pretendard 가 Segoe UI 뒤에 있어 윈도에서는
 *      걸리지 않았고, Segoe UI 에 한글이 없어 **한글만 맑은 고딕으로** 떨어지고 있었다.
 *
 * 그래서 강조색을 **브랜드 남색에서 끌어올려** 다시 골랐다(#0a1a2f 214° → #1d5aa0 212°).
 * 어디서 가져온 파랑이 아니라 우리 남색을 쓸 수 있는 밝기까지 올린 값이다.
 *
 * 중간에 페트롤 틸(#0e6b78)로 가려다 **버렸다** -- 재 보니 정상 녹색과 색상각이 31° 밖에
 * 안 떨어져, 지금 파랑(63°)보다 오히려 가까웠다. 램프가 녹색인 표 옆에 청록 단추를 두면
 * 같은 가족으로 읽힌다. 작용색은 **녹·황·적 어느 쪽과도 55° 이상** 떨어져야 한다.
 *
 * **무채색 바탕 + 파랑 액센트.** 초록은 설비 상태 전용.
 *
 * ── 앞 판이 낡아 보인 까닭 ───────────────────────────────────
 *
 * 색을 세 번 갈아입혔는데도 "디자인이 그대로" 라는 지적이 이어졌다. 이유는 색이 아니라
 * **짜임새**였다 -- 좌측 아이콘 레일, 숫자 다섯 칸 띠, 그 아래 카드와 표. 그 틀이
 * 2010년대 관리자 화면의 틀이라 토큰을 아무리 바꿔도 같은 화면이 나왔다.
 *
 * 그래서 이번 판은 토큰만 바꾸지 않는다. 뼈대(레일→위 두 줄)와 화면의 형태(도면 · 트리 ·
 * 타임라인 · 표계산)를 함께 바꾼다. 여기서는 그 형태들이 공통으로 쓸 **부품의 값**만 정한다.
 *
 * ── 무엇이 달라졌나 ──────────────────────────────────────────
 *
 *   1. **회색이 무채색이 되었다.** 옛 회색은 푸른 기가 섞여 있어(#f5f8fc) 파랑 옆에서
 *      탁했다. 바탕·선·글자를 전부 중립 회색으로 옮겨, 색이 붙은 것은 **뜻이 있는 것**
 *      뿐이게 한다.
 *
 *   2. **어두운 판독부를 걷었다.** 흰 화면에 남색 띠 하나를 얹으면 붙인 스킨으로 보인다.
 *      값은 상태 한 줄로 훑고, 화면은 조작할 것에 자리를 준다.
 *
 *   3. **모서리를 다시 키웠다(6px → 10px).** 6px 은 정밀해 보이라고 깎은 것이었는데,
 *      선으로만 층을 만드는 화면에서는 각진 상자가 표처럼 보였다. 카드는 10px, 안에 드는
 *      작은 것은 8px.
 *
 * ── 지키는 것 ────────────────────────────────────────────────
 *
 * 색만으로 말하지 않는다. 색을 못 보는 사람에게는 아무 말도 하지 않는 셈이다.
 * 어디서든 색과 함께 글자나 모양을 준다. 손가락으로 누르는 것은 --touch 아래로
 * 내려가지 않는다 -- 밀도가 그것을 이기지 못한다.
 *
 * **폭으로 다른 디자인을 만들지 않는다**(docs/검토지침.md §3). 여기에 폭을 묻는 값은
 * 하나도 없고, 좁아질 때 달라지는 것은 요소의 줄바꿈뿐이다.
 */

/*
 * ── 글꼴 파일 (2026-10-04) ────────────────────────────────
 *
 * Pretendard. 한국어 업무용 화면에서 사실상의 표준이고, 숫자가 폭이 고르다 --
 * 표를 세로로 훑을 때 자릿수가 어긋나지 않는다.
 *
 * ── 왜 직접 깎았나 ────────────────────────────────────────
 *
 * Pretendard 가 내주는 다이내믹 subset 은 조각이 92개인데 **한 화면이 11~14개를 당긴다.**
 * 재 보니 화면당 274~358KB 에 왕복이 열넷이다. 문서→꾸밈을 529ms 에서 102ms 로 줄여 놓은
 * 자리라 그만한 값을 치를 수 없다.
 *
 *   굵기 축   400~700 만 남김 (45~920 전부 두면 309KB → 438KB)
 *   한글      완성형 KS X 1001 2,350자
 *             ※ 유니코드 음절 전체(11,172자)를 담으면 1.7MB 다. 2,350자로 실제 한국어는
 *               거의 다 덮이고, 빠진 음절은 그 **글자 하나만** 시스템 글꼴로 떨어진다
 *   그 밖     라틴 · 문장부호 · ₩ · 화살표 · 네모/동그라미 기호
 *   결과      316KB 한 장. 400·500·600·700 이 이 한 파일에서 나온다
 *
 * 다시 깎으려면 `node tools/make-font.mjs`.
 *
 * ── preload 를 걸지 않는다 ────────────────────────────────
 *
 * 316KB 를 머리에서 당기면 **이 꾸밈 자신과 대역을 다툰다.** 꾸밈은 그려야 멈춤이 풀리는
 * 것이라(hq/console.css 의 await-shell) 그쪽이 밀리면 고쳐 둔 것이 도로 나빠진다.
 * 느린 줄에서 preload 가 화면을 더 늦추는 것이 바로 그 모양이다.
 *
 * `swap` 이다 -- 글자는 **시스템 글꼴로 즉시** 서고, 글꼴이 닿으면 바뀐다. 한 번 받으면
 * 오래 품으므로(edge/worker.mjs 의 KEEP_FONT) 바뀌는 것이 보이는 것은 처음 한 번뿐이다.
 */
@font-face {
  font-family: "Pretendard Variable";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/app/font/pretendard-kr.woff2") format("woff2-variations");
}

:root {
  /*
   * ── 바탕과 면 ───────────────────────────────────────────
   *
   * **바탕이 희다.** 카드도 희다. 둘을 가르는 것은 선이다(--line).
   * 아래 두 겹은 자리를 눌러 두는 데만 쓴다 -- 표 머리, 접힌 자리, 진행 막대의 홈.
   */
  --canvas: #ffffff;
  --surface: #ffffff;
  --surface-2: #f4f6f9;       /* 면 위의 한 겹 아래 (표 머리, 접힌 자리) */
  --surface-3: #eceff4;       /* 눌린 자리, 진행 막대의 홈 */

  /*
   * ── 구조 (사이드바·상단 머리) ───────────────────────────
   *
   * 네이비. 옛 값보다 어둡고 **푸른 쪽**으로 옮겼다 -- 옛 것은 초록이 조금 섞여 있어
   * 라이트 블루와 나란히 두면 탁했다.
   */
  --shell: #0a1a2f;
  --shell-2: #102a4c;         /* 사이드바 안에서 한 단계 밝은 면 */
  --shell-line: #1c3a63;
  --shell-ink: #eaf2fb;
  --shell-ink-2: #8ba6c9;

  /*
   * ── 글자 · 무채색 ──────────────────────────────────────
   *
   * 푸른 기를 뺐다. 옛 값(#46536b · #6a778d)은 파랑 옆에 서면 <흐린 파랑>으로 보여,
   * 액센트가 무엇을 가리키는지 흐려졌다.
   */
  --ink: #11161d;             /* 본문·숫자 */
  --ink-2: #4d5767;           /* 설명 */
  /*
   * 라벨·보조 설명. **두 번 올렸다**(#8a93a3 → #79838f → #636d7c, 2026-10-04).
   *
   * 두 번째 값도 모자랐다. 흰 바탕에서는 3.85 로 아슬아슬했는데, 이 글자가 실제로 서는
   * 자리는 흰 바탕이 아니라 **`--surface-2` 를 깐 띠**다(판독부·표 머리). 거기서는
   * 3.56 까지 떨어진다. 화면에 띄워 재 보고서야 알았다.
   *
   * 지금 값은 흰 바탕 5.24 · 옅은 바탕 4.84 다. 둘 다 본문 기준(4.5)을 넘는다.
   */
  --ink-3: #636d7c;
  /*
   * **비활성과 자리표시 전용이다.** 흰 바탕 대비 2.14 -- 읽으라고 두는 글자에 쓰면
   * 안 된다. 실제로 빈 목록 안내문과 판독부 보조 줄이 이 색이어서 1.98 로 떠 있었다.
   * 읽을 글은 `--ink-3` 이다.
   */
  --ink-4: #aab2be;

  /*
   * ── 선 ─────────────────────────────────────────────────
   * 흰 바탕에서 층을 만드는 것이 이 둘이다. 그림자가 아니라.
   */
  --line: #e4e8ee;            /* 기본 경계 */
  --line-2: #ccd3dc;          /* 강조 경계 (입력칸) -- 카드 테두리를 걷은 뒤로 선이 층을 혼자 만든다 */

  /*
   * ── 라이트 블루 · 이 제품이 사람에게 거는 말 ────────────
   *
   * 두 가지 파랑을 쓴다. 하나로는 안 된다 --
   *
   *   --accent        흰 바탕에 **글자와 단추**로 설 수 있을 만큼 진하다.
   *                   밝은 파랑을 그대로 쓰면 흰 바탕에서 글자가 읽히지 않는다.
   *   --accent-bright 네이비 위에 서는 **라이트 블루.** 사이드바의 켜진 줄, 초점 테두리,
   *                   진행 막대처럼 어두운 데 얹거나 선으로 긋는 자리에 쓴다.
   *
   * ── 값을 고른 기준 (2026-10-04) ────────────────────────
   *
   * 셋을 동시에 만족해야 한다. 하나라도 어기면 다른 색을 고른다.
   *
   *   흰 바탕 대비 4.5 이상   -- 글자로 설 수 있어야 한다
   *   --ok · --warn 과 55° 이상 -- 상태색과 같은 가족으로 읽히면 안 된다
   *   --ink 와 대비 1.6 이상  -- 본문 먹색과 구분돼야 누를 것으로 보인다
   *
   * #1d5aa0 : 6.95 / 56° / 2.61.  색상각 212° 로 --shell(214°)과 같은 줄기다.
   */
  --accent: #1d5aa0;
  --accent-strong: #16477f;   /* 눌렀을 때 */
  --accent-bright: #6ea2ff;   /* 어두운 면 위 · 선 · 초점 */
  --accent-soft: #eaf1f9;     /* 선택된 줄의 바탕 */
  --accent-line: #c2d7ec;

  /*
   * ── 상태 ──────────────────────────────────────────────
   *
   * **설비의 상태에만 쓴다.** 켜졌는가·정상인가·고장인가.
   * 제품이 사람에게 거는 말(누를 것·고른 것·지금 여기)은 파랑이 맡는다 -- 그것을
   * 섞어 두었더니 첫 화면이 초록으로 덮여, 정작 초록이 뜻을 잃었다.
   */
  --ok: #0f7a4f;      --ok-soft: #eaf5ef;   --ok-line: #a9dcc6;
  --warn: #9a5b00;    --warn-soft: #fbf3e6; --warn-line: #f0d5a6;
  --err: #b32424;     --err-soft: #fbecec;  --err-line: #f2c0c0;

  /*
   * `--info` 는 **`--accent` 와 같은 값이었다**(#2f6bff). soft·line 짝까지 똑같았다.
   * 그래서 "참고" 뜻의 배지가 파랗게 떠서, 누를 수 있는 것처럼 보였다 -- 이 콘솔에서
   * 파랑은 <누르시오> 라는 말이다.
   *
   * 무채색으로 내린다. **정보는 상태가 아니다.** 켜졌는지 고장인지를 말하지 않는 것은
   * 색을 가질 까닭이 없고, 색을 빼면 정작 색이 붙은 줄이 눈에 든다.
   */
  --info: #4d5767;    --info-soft: #f1f3f6; --info-line: #d7dbe2;

  /*
   * ── 사람이 붙인 표시 ──────────────────────────────────
   * 자주 보는 핀의 별이 이 색이다. **경고가 아니다** -- 경고의 주황(--warn)을 쓰면
   * 별을 붙인 줄이 전부 "무슨 문제가 있는 줄" 로 읽힌다. 상태가 아니라 사람이 붙인
   * 표시라는 것을 색으로 갈라 둔다.
   */
  --mark: #b58200;   --mark-soft: #fdf8e6;  --mark-strong: #8a6300;

  /*
   * ── 이름표 · **이 창이 무엇인가** ─────────────────────
   *
   * 머리단의 26px 네모 하나에만 쓴다(app/ui-shell.css). 창을 여럿 늘어놓고 훑을 때
   * 이 색이 먼저 걸린다 -- 화면 어디에도 색 띠가 서지 않는 대신, 이 한 자리가 그 일을
   * 전부 맡는다.
   *
   * **네 화면이 각각 제 색을 갖는다.** 한때 셋을 한 색의 밝기로만 갈랐는데, 그러면
   * 나란히 놓고 볼 때 <같은 것의 정도 차이> 로 읽힌다. 이 넷은 정도가 아니라 **다른
   * 자리**다 -- 설비에 붙은 보드와 고객 사무실은 같은 것의 밝고 어두움이 아니다.
   *
   * 값을 여기 둔다. 한때 `ui-shell.css` 안에 #17626f 처럼 직접 적었는데, 그것은 이
   * 파일 머리가 금지한 바로 그 일이다 -- 색이 두 곳에 살면 한쪽만 고쳐진다.
   *
   * ── 고른 기준 ────────────────────────────────────────
   *
   *   1. **상태색을 안 쓴다.** 초록·주황·빨강은 설비 상태의 몫이다. 정체에 끌어 쓰면
   *      멀쩡한 화면이 경고로 읽힌다(그래서 금색 `--mark` 를 여기서 걷었다 -- 주황
   *      계열이라 같은 머리단의 `구독 발급 불가` 와 겹쳤다)
   *   2. **우리 화면은 상징색이다.** 이 제품의 얼굴에 빌려 온 색을 두면 정작 상징색이
   *      아무 데도 안 선다
   *   3. 남색 바탕 위에서 재서 고른다. 그림 글자는 각 바탕에서 읽히는 쪽으로 뒤집는다
   */
  --sign-field: #3fb6c9;              /* 현장 제어기 -- 설비에 붙어 있는 보드. 청록 */
  --sign-center: #2c5282;             /* 중앙 제어기 -- 그 현장의 보드들을 쥔 보드. 네이비 */
  --sign-hq: #a3a9b0;                 /* 고객사 원격 관제 -- 고객 사무실에서 본다. 회색 */
  --sign-vendor: var(--accent-bright); /* iotop 공급 관리 -- **우리 화면.** 상징색 */

  /*
   * ── 네이비 네모는 **테두리가 있어야 보인다** ────────────
   *
   * 머리단 바탕이 네이비다(`--shell`). 그래서 네이비 이름표는 어떤 값을 골라도 바와
   * 안 갈린다 -- 재 보니 네이비라 부를 수 있는 가장 밝은 값도 바 위 대비 2.81 이고,
   * 3 을 넘기려면 그때부터는 네이비가 아니라 파랑이다. 그러면 상징색과 겹친다.
   *
   * 색을 바꾸는 대신 **모양을 세운다.** 옅은 테두리 한 줄이면 네모의 윤곽이 서고,
   * 안의 네이비는 네이비로 남는다. 다른 셋은 바와 충분히 갈리므로 테두리가 없다 --
   * 없어도 되는 데 두르면 그쪽이 더 또렷해져서, 정작 흐린 것이 더 흐려 보인다.
   */
  --sign-edge: rgba(255, 255, 255, .34);


  /*
   * ── 모서리 ─────────────────────────────────────────────
   *
   * 6 → 10 으로 올렸다가 **다시 내렸다(2026-10-04).** 10px 은 각진 상자가 표의 칸으로
   * 보이는 것을 막으려고 키운 값인데, 그 문제는 모서리가 아니라 **카드 테두리**가 만들고
   * 있었다. 테두리를 걷고 머리단에 굵은 선 한 줄을 그으니 구획이 또렷해져, 모서리를
   * 키워 둘 까닭이 없어졌다.
   *
   * 기계를 다루는 화면에 10px 은 물렁하다. 이 제품이 붙는 곳은 배전반이다.
   */
  --r-sm: 2px;       /* 카드 안에 드는 것 (단추·입력칸·배지) */
  --r: 3px;          /* 카드 · 구획 */
  --r-lg: 5px;       /* 떠 있는 것 (겹쳐 뜨는 창) */
  --r-pill: 999px;

  /*
   * ── 깊이 ──────────────────────────────────────────────
   *
   * **거의 쓰지 않는다.** 층은 선이 만든다. 그림자는 <이것은 다른 것 위에 떠 있다> 를
   * 말할 때만 -- 겹쳐 뜨는 창, 열린 목록. 카드에는 한 겹의 아주 옅은 것만 둔다.
   */
  --sh-xs: 0 1px 1px rgba(13, 17, 23, .04);
  --sh-sm: 0 1px 2px rgba(13, 17, 23, .06);
  --sh-md: 0 4px 12px rgba(13, 17, 23, .08);
  --sh-lg: 0 16px 40px rgba(13, 17, 23, .16);

  /*
   * ── 초점 ──────────────────────────────────────────────
   * 키보드로 다니는 사람에게 **지금 어디인가**를 말한다. 한 곳에서 정해 두어야
   * 화면마다 다른 테두리가 뜨지 않는다.
   */
  --focus: 0 0 0 3px rgba(29, 90, 160, .26);
  /*
   * 어두운 면 위의 초점(머리단). 위의 것은 남색 위에서 남색이라 **안 보인다** --
   * 키보드로 다니는 사람이 머리단에 들어서는 순간 자기가 어디 있는지를 잃는다.
   */
  --focus-bright: 0 0 0 3px rgba(110, 162, 255, .45);

  /* ── 간격 ──────────────────────────────────────────────── */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 20px; --s-6: 24px; --s-8: 32px; --s-10: 40px;

  /*
   * ── 글자 눈금 ─────────────────────────────────────────
   *
   * 본문은 그대로 두고 **큰 것만 키웠다.** 이 제품에서 사람이 멀리서 읽는 것은
   * 숫자다 -- 몇 대가 붙었나, 몇 개가 켜졌나. 본문을 줄이면 키오스크에서 읽히지 않는다.
   *
   * **반 픽셀을 걷었다(2026-10-04).** 11.5 / 12.5 / 13.5 은 세 단이 서로 1px 차이라
   * 나란히 두면 구분이 안 됐다 -- 눈금이 셋인데 보이기는 하나였다. 정수로 내리고
   * 본문을 13.5 → 14 로 한 칸 올린다. 하루 종일 표를 읽는 화면이다.
   */
  --t-xs: 12px;     /* 라벨 · 배지 */
  --t-sm: 13px;     /* 보조 설명 */
  --t-md: 14px;     /* 본문 · 표 */
  --t-lg: 16px;     /* 카드 제목 */
  --t-xl: 24px;     /* 화면 제목 */
  --t-2xl: 32px;    /* 숫자 강조 -- 어두운 판독부를 걷은 뒤로 쓰는 자리가 적다 */

  /*
   * 글꼴.
   *
   * **Pretendard 를 맨 앞으로 옮겼다(2026-10-04).** 전에는 Segoe UI 뒤에 있었다. 그러면
   * 윈도에서는 영영 걸리지 않고, 게다가 Segoe UI 에는 한글이 없어서 **한글만 맑은 고딕으로**
   * 떨어진다 -- 영문과 한글이 서로 다른 글꼴로 나란히 서 있었다는 뜻이다. 고른 글꼴이
   * 아니라 남은 글꼴이었다.
   *
   * ── 인터넷에서 받아 오지 않는다. **우리가 들고 있다** ──
   *
   * 한때 이 자리에 <웹 글꼴을 받아 오지 않는다 -- 현장에는 인터넷이 없다> 고 적혀 있었다.
   * 맞는 걱정인데 결론이 한 칸 어긋나 있었다. 막아야 하는 것은 **남의 서버에서 받아 오는
   * 것**이지 글꼴 파일 자체가 아니다.
   *
   * 그래서 깎아서 **제품에 넣는다**(app/font/). 보드가 제 디스크에서 스스로 내주므로
   * 인터넷이 끊긴 현장에서도 그대로 서고, 공급 관리 콘솔·고객사 관제·보드 키오스크가
   * **한 글꼴로** 선다. 같은 화면이 어디서 열리느냐에 따라 두 모습이 되지 않는다.
   *
   * 아래 @font-face 가 그 파일을 건다. 왜 이만큼만 깎았는지는 거기 적어 두었다.
   */
  --font: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont,
          "Segoe UI Variable Text", "Segoe UI", "Noto Sans KR", "Malgun Gothic",
          "Apple SD Gothic Neo", sans-serif;
  /*
   * 고정폭 짜임의 **끝에 Pretendard 를 둔다.**
   *
   * 글꼴 되돌림은 글자 하나씩 일어난다 -- 영문·숫자는 앞의 고정폭 글꼴에서 찾히므로
   * 칸이 그대로 맞고, 한글만 뒤로 밀려 Pretendard 로 떨어진다.
   *
   * 이 줄이 없었을 때 보드의 **조작 패널 편집기가 통째로 네모**였다. 우리가 싣는 글꼴은
   * Pretendard 하나뿐인데 그것은 `--font` 에만 걸려 있었고, 어플라이언스 이미지에는
   * 한글 글꼴이 한 벌도 없을 수 있다 -- 실제로 검증한 기계에 0개였다. 그 화면은
   * 한글 주석으로 가득한 코드를 보여 주는 화면이라, 쓸 수가 없는 화면이 된다.
   *
   * 칸이 어긋나는 것과 글자가 안 보이는 것 중에서는 어긋나는 쪽이 낫다.
   */
  --mono: ui-monospace, "Cascadia Mono", SFMono-Regular, Consolas, "D2Coding",
          "Pretendard Variable", Pretendard, monospace;

  /* ── 뼈대 치수 ─────────────────────────────────────────── */
  /*
   * ── 명령 레일 ─────────────────────────────────────────
   *
   * 216px 짜리 목록이었다. 구획이 다섯인데 그 다섯을 세로로 늘어놓느라 화면 가로의
   * 1/6 을 늘 쓰고 있었고, 정작 그 자리에 적히는 것은 두세 글자였다.
   *
   * 레일은 **아이콘과 아주 작은 글자**를 세로로 쌓는다. 아이콘만 두지 않는 이유는
   * 이 제품을 처음 만지는 사람이 설치기사이기 때문이다 -- 그림만으로는 <관제> 와
   * <현장 제어기> 를 가를 수 없다.
   */
  --nav-w: 76px;
  --nav-w-min: 76px;   /* 접히지 않는다. 이미 가장 좁다 */
  --head-h: 60px;      /* 위 머리. 문맥 제목이 서므로 한 단계 높다 */
  --bar-h: 48px;       /* 액션 바 */
  --touch: 36px;       /* 손가락으로 누르는 것의 최소 높이. 밀도가 이것을 이기지 못한다 */
  --page-max: 1440px;
  /*
   * 본문 좌우 여백. **상단바 · 머리 · 형제 줄 · 본문이 같은 선에 서게 하는 값**이다.
   * 쓰는 곳은 ui-base.css 의 .page 와 ui-shell.css 의 세 자리다 -- 한 곳만 고치면
   * 딱 그만큼 어긋나고, 어긋난 선은 맞춘 것보다 눈에 더 걸린다.
   */
  /*
   * 본문 좌우 여백. **폭을 따라 저절로 준다** -- 좁은 화면에서 여백을 줄이던 갈림목을
   * 걷은 자리다(app/ui-help.css 의 주석). 넓으면 24px, 390px 짜리 화면에서는 15px 쯤.
   */
  --page-pad: min(var(--s-6), 4vw);

  /*
   * ── 움직임 ────────────────────────────────────────────
   * 빠르게 시작해 부드럽게 선다. 기계가 반응하는 느낌이지 애니메이션이 아니다.
   */
  --ease: cubic-bezier(.16, 1, .3, 1);
  --fast: .12s;
  --med: .2s;
}

/* 움직임을 원치 않는 사람에게는 움직이지 않는다. 현장에서 어지럼을 호소하는 경우가 있다. */
@media (prefers-reduced-motion: reduce) {
  :root { --fast: 0s; --med: 0s; }
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/*
 * ── 옛 이름 ────────────────────────────────────────────────
 * 화면과 부품이 아직 옛 이름으로 색을 부른다. 한꺼번에 바꾸면 무엇이 깨졌는지 알 수 없으므로
 * 여기서 새 값으로 이어 준다. 화면을 하나씩 뼈대로 옮기면서 이 묶음도 함께 줄여 나간다.
 */
:root {
  --navy-900: var(--shell);
  --navy-800: var(--shell);
  --navy-700: var(--shell-2);
  --navy-600: var(--shell-line);

  --blue-700: var(--accent-strong);
  --blue-600: var(--accent);
  --blue-100: var(--accent-line);
  --blue-50:  var(--accent-soft);

  --gray-50:  var(--surface-2);
  --gray-100: var(--surface-2);
  --gray-200: var(--line);
  --gray-300: var(--line-2);
  --gray-400: var(--ink-4);
  --gray-500: var(--ink-3);
  --gray-600: var(--ink-2);
  --gray-700: var(--ink);

  --white: var(--surface);
  --black: var(--ink);

  --ok-700: var(--ok);       --ok-50: var(--ok-soft);
  --warn-700: var(--warn);   --warn-50: var(--warn-soft);
  --err-700: var(--err);     --err-50: var(--err-soft);

  /*
   * `--amber-50` 은 이 갈래에서 빠져 있었다. 본사 화면의 주의 알림 하나가 그 이름을
   * 쓰면서 `var(--amber-50, #fffbeb)` 처럼 **뒤에 색을 하나 더 적어** 버티고 있었는데,
   * 그 순간 그 알림만 팔레트 바깥의 노랑이 된다 -- 이름이 없어서 생긴 색이다.
   */
  --amber-50: var(--warn-soft);

  /*
   * `--r-md` 는 **정의된 적이 없었다.** 아홉 자리가 그 이름으로 모서리를 정하고 있었고
   * (ui-forms.css · ui-shop.css · hq/console.css), 없는 이름은 조용히 무시되므로 그
   * 아홉 자리만 **각진 모서리**로 떴다 -- 같은 화면 안에서 어떤 상자는 둥글고 어떤 것은
   * 각져 있었다. 눈금은 셋뿐이므로(sm · 기본 · lg) 가운데 이름은 기본에 붙인다.
   */
  --r-md: var(--r);

  --radius: var(--r);
  --radius-sm: var(--r-sm);
  --shadow-sm: var(--sh-xs);
  --shadow: var(--sh-sm);
}

/* ── ui-base.css ─────────────────────────────── */
/*
 * 공용 디자인 시스템
 *
 * 업무용 도구의 톤앤매너를 유지한다. 현장에서 오래 들여다보는 화면이라
 * 장식보다 밀도와 가독성이 먼저다.
 *
 *   색   네이비(구조) · 그레이(면과 경계) · 블루(주요 동작에만) · 화이트(작업면) · 블랙(본문)
 *   여백 4px 배수
 *   모서리 4~6px 로 절제
 *   강조는 큰 색면이 아니라 작은 배지와 굵기로
 *
 * setup / slave / master / front 가 모두 이 한 벌을 쓴다.
 */

/* 색·간격·글자 눈금은 app/ui-tokens.css 한 곳에 있다. 여기서는 쓰기만 한다. */

* { box-sizing: border-box; }

/*
 * hidden 속성이 항상 이기게 한다.
 * 아래에서 .badge 나 .check 처럼 display 를 지정하면 그 규칙이 브라우저 기본
 * [hidden] { display: none } 보다 우선해, 감춰야 할 요소가 그대로 보인다.
 */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--gray-50);
  color: var(--black);
  font-family: var(--font);
  font-size: var(--t-md);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;

  /*
   * ── 한글은 **낱말 안에서 끊지 않는다** (2026-10-04) ────────
   *
   * CSS 의 기본값(`normal`)은 한글을 **아무 글자 사이에서나** 끊는다. 좁은 칸에 들어간
   * 한 줄이 <회선 없 / 음> 처럼 갈라진다. 실제로 보드 대시보드의 중앙 제어기 카드가
   * 그랬다 -- 148px 칸에서 <없음> 이 두 줄로 쪼개져 있었다.
   *
   * 이 규칙은 저장소가 이미 알고 있었다. 다만 **스물두 곳에 손으로 하나씩** 적혀 있어서,
   * 눈에 띈 자리만 고쳐져 있었다. 한 곳에 두면 새로 만드는 화면도 저절로 따른다.
   *
   * 짝으로 `overflow-wrap` 을 둔다. 낱말을 안 끊기로 했으니 긴 식별자(현장 번호 ·
   * 주소)가 칸을 넘칠 수 있는데, 넘치는 것보다는 끊는 편이 낫다. `break-word` 를
   * 쓰는 이유는 `anywhere` 가 flex·grid 의 최소 폭 셈을 바꿔 짜임을 흔들기 때문이다.
   *
   * 일부러 끊어야 하는 자리는 제 규칙으로 덮는다(ui-dash.css 의 `break-all`).
   */
  word-break: keep-all;
  overflow-wrap: break-word;
}

/* ── 상단바 ─────────────────────────────────────────────── */
.topbar {
  background: var(--navy-800);
  color: var(--white);
  border-bottom: 1px solid var(--navy-900);
}
.topbar-inner {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 0 20px;
  height: 52px;
  display: flex;
  align-items: center;
  gap: 12px;
}
.topbar .brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--t-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
}
/* 뜻은 글자가 갖는다. 마크는 같은 색·같은 굵기로 옆에 서기만 한다. */
.topbar .brand svg {
  width: 17px; height: 17px;
  flex: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  fill: none;
}
.topbar .brand-sub {
  font-size: var(--t-xs);
  color: #9db4cc;
  font-weight: 400;
  margin-left: 2px;
}
.topbar .spacer { flex: 1; }

/* ── 액션 바 ────────────────────────────────────────────── */
/*
 * 이동과 주요 동작만 담는 한 줄. 제목과 설명은 카드가 갖는다.
 * 스크롤해도 따라오므로 긴 화면에서도 저장이 손에 닿는다.
 */
.toolbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--white);
  border-bottom: 1px solid var(--gray-200);
  box-shadow: var(--shadow-sm);
}
.toolbar-inner {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 9px 20px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  min-height: 48px;
}
.toolbar .spacer { flex: 1; }

/*
 * **폭을 묻지 않는다**(docs/검토지침.md §3).
 *
 * 좁을 때만 줄을 바꾸게 해 두었는데, 줄바꿈은 좁을 때만 쓸 일이 아니다 -- 단추가 많은
 * 화면에서는 넓어도 넘친다. 늘 바꿀 수 있게 두면 갈림목이 없어도 같은 결과가 나온다.
 * 여백은 한 값으로 둔다. 720px 언저리에서 14px 과 20px 이 튀는 것을 볼 이유가 없다.
 */

/* ── 레이아웃 ───────────────────────────────────────────── */
/*
 * ── 좌우 여백은 **한 값이다** ─────────────────────────────
 *
 * 여기가 20px, 머리와 형제 줄은 24px(--s-6), 상단바 두 줄은 아예 다른 셈법이었다.
 * 그래서 넓은 화면에서 제품 이름은 x=43, 화면 제목은 x=67, 카드는 x=63 에 섰다 --
 * **세 개의 왼쪽 선**이 20px 과 4px 씩 어긋난 채 겹쳐 있었다.
 *
 * 값을 `--page-pad` 하나로 모은다. 이 값을 쓰는 곳은 넷이다.
 *
 *   .page                본문
 *   .head-inner          화면 제목 줄        (ui-shell.css)
 *   .subnav-inner        형제 줄             (ui-shell.css)
 *   .nav-top · .nav-tabs 상단바 두 줄         (ui-shell.css -- 여기만 셈이 다르다)
 *
 * 상단바는 **띠가 화면 끝까지 그어져야** 하므로 상한을 두고 가운데로 모을 수 없다.
 * 대신 남는 폭의 절반에 이 값을 더해, 결과가 같은 자리에 오게 한다.
 */
.page {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: var(--s-5) var(--page-pad) 64px;
}
/*
 * 구획 사이. **여기가 그 값을 쥔다** (2026-10-05).
 *
 * 20px 이라고 적혀 있었는데 app/ui-dash.css 가 30px 로 덮고 있었다. 그 파일은
 * 이름대로라면 대시보드의 것인데 `.section` 에는 자리를 안 매어 두어서, 결국
 * **모든 화면이 30px** 이었다 -- 여기 적힌 20px 는 한 번도 안 먹었다.
 *
 * 지금 보이는 값을 그대로 둔다(30px). 바꾸는 것은 이 고침의 일이 아니다.
 */
.section { margin-bottom: 30px; }

/*
 * ── 넓은 화면이 앉는 판 ───────────────────────────────────
 *
 * 도면(.plan) · 서랍함(.dw) · 시간 줄(.tl) 셋은 제 안을 **테두리로 나눈다** -- 검사칸의
 * 왼쪽 선, 줄기 칸의 오른쪽 선, 옆칸의 왼쪽 선. 그래서 바깥 테두리가 하나 더 필요하다.
 * 없으면 안쪽 선들이 허공에서 시작해 허공에서 끝난다.
 *
 * 카드(.card)를 쓰지 않는 이유는 여백이다. 카드는 안쪽에 여백을 두는데, 이 셋은 제 칸이
 * 각자 여백을 들고 있어 두 겹이 된다 -- 실제로 그렇게 떴다.
 */
.screen {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  /* 안쪽 칸의 배경(캔버스 · 보기 레일)이 둥근 모서리 밖으로 비어져 나온다. */
  overflow: hidden;
}
/* 생김새는 ui-parts.css 가 정한다(.card-title 과 한 획). 여기서는 아래 여백만 둔다. */
.section-title { margin: 0 0 var(--s-3); }
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.row-end { justify-content: flex-end; }

/*
 * 남는 자리를 밀어내는 빈 칸. 상단바·액션 바뿐 아니라 카드 머리말에서도 쓴다.
 * 여기서 전역으로 두지 않으면, 쓰는 곳마다 규칙을 따로 적어야 하고
 * 빠뜨리면 오른쪽으로 가야 할 배지가 제목 옆에 붙어 버린다.
 */
.spacer { flex: 1 1 auto; }

/*
 * 두 칸 배치 -- 왼쪽에 목록, 오른쪽에 고른 항목의 내용.
 * 기기가 늘어도 세로로만 길어지지 않도록 화면 폭이 되는 만큼은 가로를 쓴다.
 * 좁아지면 위아래로 쌓는다. 가로 스크롤은 만들지 않는다.
 */
.cols {
  /*
   * 목록 칸은 이름과 상태만 담으므로 좁아도 된다. 남는 폭은 실제 작업면인 오른쪽에 준다.
   * 오른쪽이 넓어야 핀 목록이 두 열로 접혀 세로 길이가 반으로 준다.
   */
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
}
/*
 * 상한을 넉넉히 준다. 300px 로 묶어 두었더니 넓은 화면에서 설비 이름이 "컨베…" 로
 * 잘렸다 -- 좁은 휴대폰에서는 온전히 보이는데 데스크톱에서 잘리는, 거꾸로 된
 * 반응형이었다. 관제에서 가장 먼저 읽어야 하는 것이 설비 이름이다.
 */
.cols > * { min-width: 0; }
.cols > *:first-child { flex: 1 1 240px; max-width: 26rem; }
/* 남는 폭은 실제 작업면인 오른쪽이 가져간다. 넓어야 핀 목록이 두 열로 접힌다. */
.cols > *:last-child { flex: 999 1 320px; }
/* 칸이 하나뿐이면 그 하나가 폭을 다 쓴다 -- 26rem 에 묶이면 안 된다. */
.cols > *:only-child { flex: 1 1 100%; max-width: none; }
/* 칸 사이 간격은 격자가 gap 으로 준다. 카드가 자기 여백까지 들고 있으면 아래가 두 배로 벌어진다. */
.cols > .card { margin-bottom: 0; }
/*
 * ── `is-flat` 은 **없앴다.** 다시 만들지 말 것 ──────────────
 *
 * 장치가 적을 때 이 두 칸을 한 칸으로 접는 모드가 있었다(관제 상세에서 장치 셋 이하).
 * 왼쪽 카드가 짧아 화면이 비어 보인다는 이유였는데, 값보다 대가가 컸다.
 *
 *   · 같은 일을 하는 두 화면(master.html 의 상세 · slave.html)이 **서로 다른 배치**로 떴다.
 *     원격으로 만지던 사람이 현장에 가면 다시 익혀야 한다.
 *   · 접으면서 `.rowlist` 를 격자에서 flex 로 바꾸는데, 그러면 `.rowgroup` 의
 *     `grid-column: 1 / -1` 이 무력해진다. 구획 머리(출력·쓰기 / 입력·읽기)가 핀과
 *     한 줄에 늘어서고 좁은 상자에 갇힌 글자가 잘렸다. 장치 셋 이하가 대부분이라
 *     **그 깨진 모습이 곧 기본 모습**이었다.
 *
 * 비어 보이는 것은 배치를 갈아서 풀 일이 아니다. 현장 제어기 화면도 같은 모양이고
 * 그쪽은 문제로 여겨지지 않았다 -- 두 화면을 같게 두는 쪽이 먼저다.
 */

/*
 * ── 두 칸은 **스스로 접힌다** ─────────────────────────────
 *
 * 900px 아래에서 한 칸으로 바꾸는 갈림목이 있었다(지침 §3 이 금지한 것). 격자를
 * 줄바꿈 상자로 바꾸면 갈림목 없이 같은 일이 된다 -- 늘어나는 몫을 오른쪽에 몰아
 * 두면(999 대 1) 넓은 화면의 모습은 그대로고(목록 26rem 까지), 좁아지면 목록이
 * 스스로 위로 올라가 폭을 다 쓴다. 어느 폭에서 접힐지는 두 칸의 최소 폭이 정한다.
 */

/*
 * 카드 격자 -- 무엇이 몇 개 있는지를 먼저 보여 주고, 하나를 고르면 그 안으로 들어간다.
 * 목록이 길어질수록 한 화면에 전부 늘어놓는 것보다 이쪽이 머리에 덜 부담이다.
 */
.cardgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 16px;
  /* 카드마다 갖고 있던 아래 여백을 격자가 대신 갖는다. 없으면 다음 카드와 맞붙는다. */
  margin-bottom: 16px;
}
.cardgrid .card { margin-bottom: 0; }
/*
 * 이 보드 자신을 가리키는 카드. 남의 보드와 같은 모양이면 어느 것이 눈앞의 기기인지 알 수 없다.
 * 색면을 넓게 쓰지 않고 테두리와 옅은 바탕으로만 구분한다.
 */
/* 이 보드 자신. 상자를 없앴으므로 바탕이 아니라 **왼쪽 띠**로 말한다. */
.card.is-self { border-left: 3px solid var(--accent); padding-left: var(--s-3); }
/*
 * **고르는 대상은 상자로 남는다.**
 *
 * 제어기 카드처럼 눌러서 고르는 것은 하나하나가 만질 수 있는 물건이다. 경계가 없으면
 * 어디까지가 하나인지 알 수 없고, 누를 자리도 흐려진다 -- 구획(.card)과는 성질이 다르다.
 */
.card-click {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s-3) var(--s-4);
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.card-click:hover { border-color: var(--accent); background: var(--accent-soft); }
.card-click:focus-visible { outline: none; box-shadow: var(--focus); }

/* 카드 안의 숫자 요약. 값이 먼저 읽히고 이름은 거든다. */
.statrow { display: flex; gap: 20px; flex-wrap: wrap; }
/*
 * 비율 막대 -- 목록 카드에서 "지금 얼마나 돌고 있나" 를 한눈에.
 *
 * 숫자 두 개(켜짐 9 / 꺼짐 36)로는 비율이 읽히지 않고, 카드끼리 비교하려면 매번 머리로 나눠야 한다.
 * 막대는 길이가 카드마다 같아서 눈으로 바로 비교된다. 남는 폭도 막대가 가져가므로
 * 자리를 채우겠다고 배지를 늘릴 이유가 없어진다.
 */
.meter { display: flex; align-items: center; gap: 10px; }
.meter + .meter { margin-top: 9px; }
.meter-k {
  flex: none; width: 26px;
  font-size: var(--t-xs); font-weight: 600; color: var(--gray-500);
}
/*
 * 막대. **모서리를 깎지 않는다.**
 *
 * 둥근 끝을 가진 막대는 앱의 진행 표시이고, 계기의 눈금은 각지다. 게다가 둥근 끝은
 * 값이 아주 작을 때 **실제보다 커 보인다** -- 3px 짜리 채움이 6px 짜리 알약이 된다.
 * 값을 읽는 자리에서 그것은 거짓말이다.
 */
.meter-bar {
  display: flex;
  flex: 1 1 auto; min-width: 0;
  height: 6px;
  background: var(--surface-3);
  overflow: hidden;
}
/*
 * 막대는 왼쪽부터 채워진다: 켜짐(측정) → 켜짐(강제) → 꺼짐(강제) → 남은 자리(꺼짐, 측정).
 * 그래서 색이 찬 왼쪽 길이가 곧 "켜짐" 이고, 파란 구간은 사람이 정해 둔 값이다.
 * 강제는 켜짐/꺼짐과 다른 축이라 색으로만 구분한다 -- 숫자(9/45)는 건드리지 않는다.
 */
.meter-fill { flex: none; height: 100%; background: var(--ok-700); }
.meter-fill.on-forced { background: var(--blue-600); }
.meter-fill.off-forced { background: var(--blue-100); }
.meter-v {
  flex: none;
  font-size: var(--t-xs); color: var(--gray-600);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.meter-none { flex: 1 1 auto; font-size: var(--t-xs); color: var(--ink-3); }

/* 색이 무슨 뜻인지 설명할 때. 글자로 색 이름을 적는 것보다 색 자체를 보여 주는 편이 빠르다. */
.swatch {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 2px;
  vertical-align: -1px;
  margin-right: 6px;
  background: var(--gray-300);
}
.swatch.on { background: var(--ok-700); }
.swatch.forced { background: var(--blue-600); }
.swatch.forced-off { background: var(--blue-100); }
.stat-v {
  font-size: 20px; font-weight: 600; line-height: 1.2;
  color: var(--navy-800);
  font-variant-numeric: tabular-nums;
}
.stat-k {
  font-size: var(--t-xs); font-weight: 600; color: var(--gray-500);
  text-transform: uppercase; letter-spacing: .05em;
}

/*
 * 조작 타일 -- front 의 본체.
 *
 * front 는 현장에서 장갑 낀 손으로 누르는 화면이다. 표의 작은 버튼이 아니라
 * 이름이 크게 보이고 손가락으로 눌리는 덩어리여야 한다.
 * 무엇을 띄울지는 front/front-logic.mjs 의 HIGHLIGHTS 가 정한다.
 */
.tiles {
  display: grid;
  /* 장갑 낀 손가락이 목표다. 손끝(약 9mm)보다 넉넉해야 오조작이 없다. */
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
}
.tile {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 18px;
  min-height: 116px;
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  background: var(--white);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color .12s, background .12s;
}
.tile:hover:not(:disabled) { border-color: var(--blue-600); }
.tile:focus-visible { outline: none; box-shadow: var(--focus); }
.tile:disabled { cursor: default; }
.tile.is-on { border-color: var(--ok-700); background: var(--ok-50); }
.tile.is-forced { border-color: var(--blue-600); background: var(--blue-50); }
.tile.is-missing { border-style: dashed; background: var(--gray-50); }
/* 멀리서도 읽혀야 한다. 이 화면은 손에 쥐고 보는 게 아니라 벽에 붙어 있다. */
.tile-name {
  font-size: var(--t-lg); font-weight: 600; color: var(--navy-800);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tile-foot { display: flex; align-items: center; gap: 8px; margin-top: auto; }
.tile-hint { font-size: var(--t-xs); color: var(--gray-500); }

/* ── 코드 예제 ──────────────────────────────────────────── */
/*
 * 문서 안의 코드. 로그판과 같은 네이비 바탕을 써서 "이건 기계의 말" 이라는 결을 맞춘다.
 * 코드는 줄을 바꾸면 오히려 읽기 어려우므로 이 블록 안에서만 가로로 넘긴다.
 * 페이지 자체는 절대 가로로 넘기지 않는다.
 */
.code {
  margin: 0;
  padding: 12px 14px;
  background: var(--navy-900);
  color: #cbd5e1;
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: var(--t-xs);
  line-height: 1.75;
  white-space: pre;
  overflow-x: auto;
}
.code .c { color: #64748b; }          /* 주석 */
.code .s { color: #86efac; }          /* 문자열 */
.code .f { color: #93c5fd; }          /* 함수 이름 */

/* 예제 한 덩어리: 설명 → 코드 → 실행 → 결과 */
.demo + .demo { margin-top: 18px; border-top: 1px solid var(--gray-100); padding-top: 18px; }
.demo-title { font-size: var(--t-sm); font-weight: 600; color: var(--navy-800); margin: 0 0 4px; }
.demo-desc { font-size: var(--t-xs); color: var(--gray-500); margin: 0 0 8px; }
.demo-run { display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.demo-out {
  flex: 1 1 200px; min-width: 0;
  font-family: var(--mono); font-size: var(--t-xs); color: var(--gray-600);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.demo-out.err { color: var(--err-700); }

/*
 * 사람이 읽는 글이 아니라 **기계가 지은 값** -- 현장 번호, 주문 번호, 토큰.
 *
 * 규칙이 없어서 오래 본문과 같은 모양으로 서 있었다(rest.html · network.html ·
 * master/alerts.mjs). 이런 값은 한 글자가 다르면 다른 값인데, 비례 글꼴에서는
 * l 과 1, O 와 0 이 같은 자리에서 같아 보인다. 옮겨 적을 때 그 차이가 드러난다.
 */
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* ── 단계 탭 ────────────────────────────────────────────── */
/* 한 번에 한 단계만 보여 준다. 지나온 단계는 눌러서 되돌아갈 수 있다. */
.steps {
  display: flex;
  align-items: stretch;
  background: var(--white);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 20px;
}
.step {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  border: 0;
  border-right: 1px solid var(--gray-200);
  background: var(--white);
  font-family: inherit;
  text-align: left;
  min-width: 0;
  cursor: default;
}
.step:last-child { border-right: 0; }
.step[data-enabled="1"] { cursor: pointer; }
.step[data-enabled="1"]:hover { background: var(--gray-50); }
.step:focus-visible { outline: none; box-shadow: var(--focus); }
.step-num {
  flex: none;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--gray-200);
  color: var(--gray-600);
  font-size: var(--t-xs);
  font-weight: 600;
  display: flex; align-items: center; justify-content: center;
}
.step-label {
  font-size: var(--t-sm); color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.step.done .step-num { background: var(--ok-700); color: var(--white); }
.step.done .step-label { color: var(--gray-700); }
.step.active { background: var(--blue-50); box-shadow: inset 0 -2px 0 var(--blue-600); }
.step.active .step-num { background: var(--blue-600); color: var(--white); }
.step.active .step-label { color: var(--navy-800); font-weight: 600; }

/* ── 단계 화면 ──────────────────────────────────────────── */
.panel { display: none; }
.panel.active { display: block; }


/*
 * `body.wide` 를 걷었다.
 *
 * 관제만 넓은 판(1560px)을 쓰던 규칙이다. 그러자 제어기 한 대를 보는 자리도 함께
 * 넓어져, 보드 앞의 같은 화면(slave.html)과 폭이 달라졌다 -- 오가는 사람에게는 그것이
 * 다른 화면으로 읽힌다. 제품의 폭은 --page-max 한 벌로 둔다(app/ui-tokens.css).

/* ── ui-shell.css ─────────────────────────────── */
/*
 * 화면 뼈대의 모습.
 *
 * 위에 구획 줄 하나, 그 아래 얇은 머리, 액션 바, 그리고 본문.
 *
 * ── 왼쪽 레일을 걷었다 (2026-09-12) ───────────────────────
 *
 * 오래 왼쪽에 세로 목록을 두었다. 근거는 <구획이 늘어나도 규칙이 변하지 않는다> 였고,
 * 그 자체는 지금도 맞다. 그런데 이 제품의 구획은 여섯에서 늘지 않았고, 76px 짜리 레일은
 * **화면 가로의 1/6 을 늘 쓰면서** 거기 적히는 것은 두세 글자였다.
 *
 * 무엇보다 그 레일이 화면을 낡아 보이게 하는 첫째 이유였다. 색을 세 번 갈아입혀도
 * "그대로" 라는 지적이 이어졌는데, 바뀐 것이 색뿐이고 **짜임새가 2010년대 관리자 화면의
 * 틀** 그대로였기 때문이다.
 *
 * 그래서 세로를 가로로 눕힌다. 이름은 그대로 온전히 서고(아이콘만 두지 않는다는 규칙은
 * 유지된다), 가로가 모자라면 **줄이 바뀐다** -- 숨지 않는다. 폭을 묻는 갈림목은 없다
 * (docs/검토지침.md §3).
 */

/* ── 틀 ─────────────────────────────────────────────────── */
body.has-shell {
  margin: 0;
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-md);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
/* 한 열이다. 레일이 없으므로 나눌 것이 없다. */
.shell { display: block; min-height: 100vh; }
.shell-main { display: flex; flex-direction: column; min-width: 0; }
.shell-body { flex: 1 1 auto; min-width: 0; }

/* ── 위 구획 줄 ─────────────────────────────────────────── */
/*
 * 흰 줄이다. 어두운 레일이었을 때는 **이 띠가 화면에서 가장 진한 덩어리**였고, 그래서
 * 눈이 늘 그리로 먼저 갔다 -- 정작 볼 것은 본문인데.
 *
 * 화면마다 색을 섞던 것(`--screen`)도 함께 걷었다. 흰 줄에 파비콘 색을 섞으면 여덟
 * 화면이 여덟 가지 옅은 색으로 물들어, 같은 제품으로 보이지 않는다. 창을 가르는 일은
 * 탭 제목과 파비콘이 이미 하고 있다.
 */
/*
 * ── **이 창이 무엇인지**는 이름표가 말한다 ─────────────────
 *
 * 한때 화면 맨 위에 색을 꽉 채운 띠를 둘렀다. 다섯 창을 가르는 데는 성공했지만, 그
 * 모양이 **배포 환경 경고 배너**였다 -- 개발 판이나 점검 중인 서버가 쓰는 그 언어라,
 * 멀쩡한 제품이 <지금 무언가 이상하다> 고 말하는 꼴이 됐다. 36px 을 매 화면에서
 * 가져가는 것도 한 번만 읽으면 되는 값치고는 비쌌다.
 *
 * 이름표가 그 일을 한다. **제품 이름 옆에 역할을 적고**, 그 아래 줄에 어디인지를 적는다 --
 * `iotop 중앙 제어기 / 한빛공장 3동`. 띠가 아니라 **이 화면의 이름**이다.
 *
 * 색은 마크 하나에만 쓴다(26px). 창을 늘어놓고 훑을 때 색이 먼저 눈에 걸리되, 화면
 * 어디에도 색 띠가 서지 않는다.
 */

/*
 * ── 붙어 있는 것이 **셋이다** ─────────────────────────────
 *
 * 구획 줄(.nav) · 머리(.head) · 그 아래 탭(.subnav). 셋 다 스크롤을 따라오는데,
 * **둘이 같은 자리(top: 0)에 붙어 있었다.** 스크롤을 내리는 순간 머리가 구획 줄 위로
 * 올라타서 제품 이름과 탭 윗줄을 덮었다 -- 화면 맨 위에 <대시보드>가 있고 그 아래
 * 구획 탭이 서는, 안 내렸을 때와 차례가 뒤집힌 모양이 된다.
 *
 * 아래 `.subnav` 는 처음부터 `top: var(--head-h)` 로 **쌓을 생각이었다.** 구획 줄이
 * 붙어 있다는 것만 그 셈에서 빠져 있었다.
 *
 *   .nav      0
 *   .head     구획 줄 높이
 *   .subnav   구획 줄 + 머리
 *
 * 구획 줄 높이는 고정값이 아니다 -- 탭이 많거나 좁은 화면에서는 줄이 접힌다. 그래서
 * 뼈대가 재서 `--nav-h` 에 적어 둔다(app/shell.mjs). 여기 적힌 90px 은 그 값이 아직
 * 없을 때의 기본값이고, 두 줄짜리 구획 줄의 실제 높이다.
 */
:root { --nav-h: 90px; }

/*
 * ── 머리단은 **남색**이다 (2026-10-04) ────────────────────
 *
 * 흰 바탕에 흰 머리단이었다. 틀과 내용을 가르는 것이 선 한 줄뿐이라, 화면을 훑으면
 * 어디까지가 길이고 어디부터가 일감인지가 흐렸다.
 *
 * 그보다 큰 것은 이것이다 -- `--shell` 갈래 다섯 색을 선언해 두고 **실제로 쓰는 곳이
 * 다섯, 그중 셋은 옛 이름 별명**이었다(app/ui-tokens.css 의 맨 아래). 화면에 남아
 * 있던 것은 26px 짜리 이름표 네모 하나다. 선언만 있고 보이지 않는 색은 없는 색이다.
 *
 * 머리단을 그 색으로 깐다. 틀과 내용이 갈리고, 브랜드색이 처음으로 화면에 선다.
 *
 * **아래 이 구획의 글자색을 전부 함께 옮겼다.** 바탕만 어둡게 하면 `--ink-2` 로 적힌
 * 글자들이 남색 위의 남색이 된다 -- 안 보이는 글자는 깨진 것보다 찾기 어렵다.
 */
.nav {
  position: sticky; top: 0; z-index: 21;
  display: block;
  background: var(--shell);
  border-bottom: 0;
  color: var(--shell-ink);
}
/*
 * 윗줄 -- 여기가 어느 제품인가. 오른쪽 끝에 읽는 글.
 * 아랫줄 -- 어디로 갈 수 있는가.
 *
 * 한 줄에 몰았더니 제품 이름이 **첫 탭처럼** 보였고, 탭 밑줄이 이름 옆에서 시작해
 * 어디까지가 길인지 흐렸다.
 */
/*
 * ── 본문과 **같은 선에 선다** ─────────────────────────────
 *
 * 두 줄을 `padding: 4px 20px` 로 두었더니, 1440px 보다 넓은 화면에서 위 줄만 왼쪽 끝에
 * 붙고 본문(`.page`·`.head-inner`)은 가운데로 모였다 -- 24인치에서 제품 이름과 화면
 * 제목이 190px 어긋나 있었다.
 *
 * 띠는 화면 끝까지 긋되(아래 선이 full-bleed 여야 한다), **속은 본문과 같은 자리**에
 * 서야 한다. 그래서 상한을 두고 가운데로 모으는 대신 **여백을 계산해서** 준다 --
 * 화면이 좁으면 --s-6, 넓으면 남는 폭의 절반. 폭을 묻는 갈림목이 아니라 값 하나다.
 */
.nav-top, .nav-tabs {
  /*
   * 남는 폭의 절반에 **본문 여백을 더한다.** 더하지 않으면 상단바는 본문 상자의
   * 바깥선에, 본문은 그 안쪽 여백만큼 들어간 자리에 서서 20px 이 어긋난다 --
   * 실제로 제품 이름이 카드보다 20px 왼쪽에 있었다.
   */
  padding-inline: max(var(--page-pad),
                      calc((100% - var(--page-max)) / 2 + var(--page-pad)));
}
.nav-top {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-3);
  min-height: 44px; padding-block: 4px;
  border-bottom: 1px solid var(--shell-line);
}
.nav-tabs {
  display: flex; align-items: center; flex-wrap: wrap; gap: 2px;
  min-height: 42px;
}
/*
 * 브랜드는 **글리프 하나**다. 제 화면을 보는 사람에게 제품 이름은 매번 읽을 것이 아니다 --
 * 한 번 알면 끝이다. 이름은 문 앞(로그인)과 문서에서 온전히 선다.
 */
/*
 * 제품 표기. 마크 옆에 **이름을 적는다.**
 *
 * 레일이었을 때는 76px 안에 글자가 들어가지 않아 마크만 남기고 이름은 읽어 주는 기계에만
 * 두었다. 가로줄에는 자리가 있고, 이름이 서면 그 줄이 <제품의 줄>인 것이 분명해진다.
 */
.nav-brand {
  display: flex; align-items: center; gap: 9px;
  flex: none; height: 38px;
  color: var(--shell-ink); text-decoration: none;
  border-radius: var(--r-sm);
}
/*
 * 마크. **이 창이 무엇인지를 색으로 말하는 유일한 자리**다.
 *
 * 26px 짜리 하나뿐이라 화면을 덮지 않는다. 창을 여럿 늘어놓고 훑을 때는 이 사각형의
 * 색이 먼저 걸리고, 한 창 안에서는 옆의 글자가 그것을 다시 말해 준다.
 *
 * 상태색(초록·주황·빨강)은 쓰지 않는다. 그 셋은 설비의 상태에만 쓰는 색이라
 * (app/ui-tokens.css), 정체를 나타내는 데 끌어 쓰면 멀쩡한 화면이 경고로 읽힌다.
 */
/*
 * 머리단이 남색이 되면서 **바탕과 글자가 뒤집혔다.** 전에는 흰 머리단 위의 색 네모라
 * 네모가 색을 갖고 글자가 희었다. 남색 위에서는 그 반대다 -- 네모가 밝고 그림이 어둡다.
 * 특히 `hq` 는 바탕과 **같은 색**이라 그대로 두면 사라진다.
 */
.nav-brand-mark {
  display: grid; place-items: center; flex: none;
  width: 26px; height: 26px; border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .15); color: var(--shell-ink);
}
.nav-brand-mark svg { width: 18px; height: 18px; display: block; }
/*
 * ── 네 화면, **각각 제 색** ───────────────────────────────
 *
 *   field    현장 제어기 -- 설비에 붙어 있는 보드
 *   center   중앙 제어기 -- 그 현장의 보드들을 쥔 보드
 *   hq       고객사 원격 관제 -- 고객 사무실에서 제 공장들을 본다
 *   vendor   iotop 공급 관리 -- **우리 화면.** 상징색이 여기 붙는다
 *
 * 값은 팔레트에 있다(app/ui-tokens.css 의 `--sign-*`). 왜 그 색들인지도 거기 적었다.
 * 여기서는 **어느 자리에 어느 이름을 거는지**만 정한다 -- 색을 여기 적으면 팔레트와
 * 두 곳에 살게 되고, 그러면 한쪽만 고쳐진다.
 *
 * 그림 글자만 여기서 뒤집는다. 바탕의 밝기에 따라 달라지는 값이라 색과 짝으로 다닌다 --
 * 어두운 바탕에는 흰 그림, 밝은 바탕에는 남색 그림.
 *
 * ※ `field` 에는 규칙이 아예 없어 기본값으로 떨어지고 있었다. 떨어진 값이 마침 쓸
 *   만해서 아무도 몰랐는데, 그러면 기본값을 고치는 날 현장 제어기가 같이 바뀐다.
 */
.nav-brand-mark[data-role="field"]  { background: var(--sign-field); color: var(--shell); }
.nav-brand-mark[data-role="hq"]     { background: var(--sign-hq); color: var(--shell); }
.nav-brand-mark[data-role="vendor"] { background: var(--sign-vendor); color: var(--shell); }
/*
 * 네이비는 바탕과 같은 색이라 **테두리로 윤곽을 세운다**(app/ui-tokens.css 의 까닭).
 * 그림도 흰 쪽이다 -- 어두운 네모 위에서 남색 그림은 안 보인다.
 */
.nav-brand-mark[data-role="center"] {
  background: var(--sign-center); color: var(--shell-ink);
  box-shadow: inset 0 0 0 1px var(--sign-edge);
}

.nav-brand-text { display: block; line-height: 1.2; min-width: 0; }
/*
 * 제품 이름과 **역할이 한 덩어리**다. 이름이 굵고 역할은 그 뒤에 한 단계 가볍게 선다 --
 * `iotop 중앙 제어기`. 배지로 떼어 놓으면 이름 옆에 붙은 꼬리표가 되지만, 이어 적으면
 * **이 화면의 이름**이 된다.
 */
.nav-brand-name { display: block; font-size: var(--t-lg); font-weight: 700; letter-spacing: -.03em;
                  white-space: nowrap; }
.nav-brand-name em { font-style: normal; font-weight: 500; color: var(--shell-ink-2); }
/* 어디인가. 이름 아래 한 줄 -- 현장 이름이거나, 본사에서는 보는 범위다. */
.nav-brand-where {
  display: block; font-size: var(--t-xs); color: var(--shell-ink-2); font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 42ch;
}
/* 읽어 주는 기계에만 남기는 글. 매번 읽을 것이 아니다. */
.nav-brand-sub {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.nav-brand:focus-visible { outline: none; box-shadow: var(--focus-bright); }

.nav-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: center; flex-wrap: wrap; gap: 2px;
  min-width: 0;
}
/*
 * 구획 한 칸.
 *
 * **아이콘을 걷고 글자만 남겼다.** 아이콘을 둔 근거는 <처음 만지는 사람이 설치기사라
 * 그림만으로는 관제와 현장 제어기를 가를 수 없다> 였는데, 그 결론이 가리키던 것은
 * 실은 **글자를 두라**는 것이었다. 가로줄에서는 글자가 온전히 서므로 그림이 할 일이 없다.
 * (본사 관제도 같은 이유로 아이콘이 없다 -- 거기 서는 것은 설비가 아니라 장부다.)
 *
 * 손가락으로 누르는 자리라 높이는 --touch 아래로 내려가지 않는다.
 */
.nav-item {
  display: flex; align-items: center; gap: 6px;
  min-height: var(--touch);
  padding: 0 11px;
  border-radius: var(--r-sm);
  color: var(--shell-ink-2);
  text-decoration: none;
  font-size: var(--t-md); font-weight: 500; letter-spacing: -.01em;
  white-space: nowrap;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
  position: relative;
}
.nav-item:hover { background: var(--shell-2); color: #fff; }
.nav-item:focus-visible { outline: none; box-shadow: var(--focus-bright); }
/*
 * 지금 있는 자리.
 *
 * 색만으로 말하지 않는다 -- 글자를 굵게 하고 **아래에 띠**를 둔다. 색을 못 보는 사람도
 * 어디에 있는지 알아야 한다. 띠는 액센트가 아니라 글자색이다 -- 파랑은 <누를 것>의
 * 색이고, 지금 서 있는 자리는 누를 것이 아니다.
 */
.nav-item.is-here { color: var(--shell-ink); font-weight: 700; }
/*
 * 띠는 **머리단의 아래 끝에 딱 붙어야** 한다.
 *
 * -8px 이었다. 그때는 머리단이 희고 아래에 1px 선이 있어서, 띠가 그 선 위에 얹혀
 * 자연스러웠다. 머리단을 남색으로 깔고 그 선을 걷자(위 `.nav`) 띠만 **남색 밖
 * 흰 바탕으로 5px 떨어져** 떴다 -- 탭과 이어지지 않은 짧은 막대 하나가 됐다.
 *
 * 칸 아래와 머리단 아래 사이는 3px 이다(탭 줄 42px, 칸 36px). 그만큼만 내린다.
 */
.nav-tabs .nav-item.is-here::after {
  content: ''; position: absolute; left: 11px; right: 11px; bottom: -3px;
  height: 2px; border-radius: 2px 2px 0 0;
  background: var(--shell-ink);
}
/*
 * ── 머리단 안의 상태 표식 ─────────────────────────────────
 *
 * 상태색은 **흰 바탕에 맞춰 고른 값**이다(app/ui-tokens.css). 남색 위에 그대로 얹으면
 * 안 읽힌다 -- 안 읽은 편지 수가 대비 3.2 로 떠 있었다(기준 4.5).
 *
 * 머리단 안에서만 옅은 쪽으로 바꿔 단다. 뜻은 같고 바탕만 다르다. 값을 새로 짓지 않고
 * 이미 있는 `--*-line` 을 쓴다 -- 그 셋은 원래 어두운 데 긋는 선의 색이다.
 */
.nav .badge-ok   { color: var(--ok-line); }
.nav .badge-warn { color: var(--warn-line); }
.nav .badge-err  { color: var(--err-line); }
.nav .badge-info,
.nav .badge-mute { color: var(--shell-ink-2); }

/* 아이콘은 그리지 않는다. 표시는 shell.mjs 가 계속 붙이지만 화면에는 서지 않는다. */
.nav-ico { display: none; }
.nav-label { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }

/*
 * 어디의 화면인가 -- **이름표 안으로 들어갔다**(.nav-brand-where).
 *
 * 제품 이름 옆에 따로 서 있었다. 그러면 `iotop` · `한빛공장 3동` · 배지 셋이 한 줄에
 * 나란히 서서, 어느 것이 제품이고 어느 것이 자리인지가 크기로만 갈렸다.
 * 이름표 한 덩어리로 묶으면 위가 <무엇>, 아래가 <어디>로 층이 생긴다.
 *
 * 본사 화면들은 아직 이 이름을 쓴다(hq/shell.mjs). 그쪽도 이름표로 옮기면 함께 걷는다.
 */
.nav-where { display: flex; align-items: center; gap: 7px; min-width: 0; }
.nav-site { font-size: var(--t-md); font-weight: 600; color: var(--shell-ink);
            overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* 문서는 오른쪽 끝으로. 매일 쓰는 것과 읽는 글을 같은 줄의 양끝으로 갈라 둔다. */
.nav-foot { margin-left: auto; display: flex; align-items: center; gap: 2px; }
/* 탭 줄 안에서 오른쪽으로 밀어내는 빈 칸. */
.nav-gap { flex: 1 1 auto; }

/*
 * 들어와 있는 계정. **오른쪽 끝**이다.
 *
 * 탭 줄에 두지 않는 까닭은 하나다 -- 그 줄은 <무엇을 보는가>를 고르는 자리이고,
 * 계정은 보는 대상이 아니라 **보고 있는 사람**이다. 업무용 도구가 대개 이 자리에
 * 사람을 두는 것도 같은 이유다.
 *
 * 글자는 계정 아이디로 바뀐다(ui/account.mjs). 주소가 길어도 줄을 밀지 않게 잘라 둔다.
 */
.nav-user {
  margin-left: auto; max-width: 28ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 5px 10px; border-radius: var(--r-sm);
  font-size: var(--t-sm); font-weight: 600; color: var(--shell-ink-2); text-decoration: none;
}
.nav-user:hover { background: var(--shell-2); color: #fff; }
.nav-user.is-here { background: var(--shell-2); color: var(--accent-bright); }

/*
 * 나가기. **계정 이름 바로 옆**이다 -- 이름을 확인한 자리에서 끊는 것이 사람이 찾는 차례다.
 *
 * 이름보다 **약하게** 그린다. 매일 누르는 단추가 아니고, 관제 화면에서 실수로 눌리면
 * 보던 값이 통째로 사라진다. 눈에 띄되 손이 먼저 가지는 않아야 한다.
 */
.nav-out {
  flex: none; margin-left: 2px;
  padding: 5px 9px; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--shell-ink-2);
  font-family: inherit; font-size: var(--t-xs); font-weight: 600; cursor: pointer;
}
.nav-out:hover { background: var(--shell-2); color: var(--shell-ink); }
.nav-out:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 1px; }
/*
 * 묶음 이름. 가로줄에는 글자로 설 자리가 없다 -- **선 하나**로 나눈다.
 * 이름은 읽어 주는 기계에만 남긴다. 눈으로는 선이, 귀로는 이름이 나눈다.
 */
.nav-group {
  width: 1px; height: 16px; margin: 0 var(--s-2);
  background: var(--line-2);
  font-size: 0; color: transparent; padding: 0; overflow: hidden;
  flex: none;
}
.nav-group:first-child { display: none; }   /* 맨 앞에는 나눌 것이 없다 */

/* 새 창으로 열린다는 표시. 글자와 겨루지 않게 흐리고 작게. */
.nav-ext { font-size: 10px; color: var(--shell-ink-2); opacity: .75; }

/* 문서 줄도 손가락으로 누른다. 글자만 한 단계 작다. */
.nav-item-sm { font-size: var(--t-sm); color: var(--shell-ink-2); }

/* 머리단은 app/ui-head.css · 마법사 단계 띠는 app/ui-wizard.css 로 나갔다. */
/* ── ui-head.css ─────────────────────────────── */
/*
 * **머리단** -- 화면 제목과 되돌아갈 길, 그리고 한 대상 안의 형제 화면들.
 *
 * 뼈대(app/ui-shell.css)에서 떼어 냈다. 한 파일이 766줄이었다.
 *
 * **걸리는 차례가 원본과 같아야 한다.** 그래서 ui-shell(틀·구획 줄) 바로 뒤,
 * ui-wizard 앞에 선다(app/ui.css). 펼친 결과를 전후로 맞대어 규칙이 차례까지
 * 같은 것을 확인했다.
 */

/* ── 위 머리 ─────────────────────────────────────────────── */
/*
 * 띠는 화면 끝까지, **속은 본문과 같은 폭.**
 *
 * 둘을 한 요소로 두었더니 단추가 화면 맨 끝으로 밀려, 가운데 모인 본문과 최대 868px
 * 어긋났다(3440px 화면 기준). 바탕과 아래 선은 끝까지 가야 하므로 겉과 속을 나눈다.
 */
.head {
  /* 구획 줄 **아래에** 붙는다. 같은 자리에 붙어 있던 것이 위 `.nav` 머리말의 그 일이다. */
  position: sticky; top: var(--nav-h); z-index: 20;
  min-height: var(--head-h);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  /*
   * ── 상태 척추 ────────────────────────────────────────────
   *
   * 머리 아래에 라이트 블루 한 줄. **이 화면이 살아 있는 계기라는 표시**다.
   * 정지 화면과 계기를 가르는 것은 결국 <지금 값인가> 하나이고, 그 말을 늘 하고 있는
   * 자리가 화면에 없었다 -- 배지는 읽어야 알고, 이 줄은 눈에 그냥 들어온다.
   *
   * 회선이 끊기면 `body.is-offline` 이 붙어 흐려진다(app/shell.mjs).
   */
  box-shadow: inset 0 -1px 0 0 var(--accent-bright);
}
body.is-offline .head { box-shadow: inset 0 -1px 0 0 var(--line-2); }
.head-inner {
  display: flex; align-items: center; gap: var(--s-3);
  min-height: var(--head-h);
  /*
   * 본문(.page)과 같은 상한·같은 좌우 여백이라 두 오른쪽 끝이 한 선에 선다.
   * 좌우는 아래 `.has-shell .page` 의 --s-6 과 **같은 값**이어야 한다 -- 다르면 딱 그만큼
   * 카드와 어긋나고, 4px 어긋난 것은 맞춘 것보다 눈에 더 걸린다.
   */
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--s-2) var(--page-pad);
  flex-wrap: wrap;
}
/* 제목이 서는 자리라 상태 배지와 동작은 **아래 선에 맞춰** 가운데 정렬한다. */
.head-state, .head-actions { align-self: center; }
/* 머리의 단추가 둘 이상이면 붙지 않는다(조작 이력: CSV 내려받기 · 새로고침). */
.head-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--s-2); }
.head-gap { flex: 1 1 auto; }
.head-menu {
  display: none; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex: none;
  border: 1px solid var(--line-2); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink-2); cursor: pointer;
}
.head-menu svg { width: 18px; height: 18px; }
.head-state { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }

/* ── 한 대상 안의 화면들 ────────────────────────────────── */
/*
 * 제어기 한 대 안의 화면을 오가는 줄(app/shell.mjs 의 setTabs).
 *
 * **머리에 붙어 함께 따라 올라온다.** 따로 떼어 두면 핀이 서른두 개인 목록에서 스크롤을
 * 조금만 내려도 사라지고, 그러면 다른 화면으로 가려고 맨 위까지 되돌아가야 한다.
 * `top` 은 머리 높이와 **같은 값**이라야 둘 사이가 벌어지지 않는다.
 *
 * 밑줄로 지금 자리를 말한다. 칸을 통째로 칠하면 이 줄이 액션 바처럼 읽혀,
 * 방금 머리에서 걷어낸 그 혼동이 한 칸 아래에서 되살아난다.
 */
/*
 * 머리와 같이 **두 겹**이다. 바깥이 배경과 아래 선을 화면 끝까지 긋고, 안쪽이 본문과
 * 같은 상한·같은 여백으로 가운데 선다. 한 겹으로 두면 넓은 모니터에서 선이 본문 폭까지만
 * 그어져 머리와 어긋난다.
 */
.subnav {
  /* 구획 줄과 머리를 **둘 다** 지나야 한다. 머리 하나만 세고 있었다. */
  position: sticky; top: calc(var(--nav-h) + var(--head-h)); z-index: 19;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
/*
 * ── 탭과 **동작이 한 줄**이다 ─────────────────────────────
 *
 * 탭이 있는 화면에서는 동작 단추가 이 줄로 내려온다(app/shell.mjs 의 _placeActions).
 * 왼쪽이 <어느 화면인가>, 오른쪽이 <여기서 무엇을 하는가> 다.
 *
 * 머리에 두었을 때는 단추가 탭보다 **위**에 서서, 저장이 이 제어기 전체의 일인지 지금
 * 선 탭의 일인지 화면에서 읽히지 않았다.
 *
 * ── 바깥 띠를 건드리지 않는다 ─────────────────────────────
 *
 * 한 번 `.subnav` 자체를 flex 로 만들고 max-width·margin:auto 를 줬다가 줄이 통째로
 * 463px 으로 오므라들었다. 세로 flex 안의 칸에 **가로 auto 여백**을 주면 늘어나던 것이
 * 내용 폭으로 줄어든다 -- 띠가 화면 끝까지 그어지지 않아 머리와 어긋났다.
 *
 * 그래서 단추는 **안쪽 줄에** 들어간다. 바깥은 선을 긋는 일만 계속한다.
 */
.subnav-inner > .head-actions { margin-left: auto; flex: none; padding-block: var(--s-2); }
.subnav[hidden] { display: none; }
.subnav-inner {
  display: flex; align-items: stretch; gap: var(--s-1);
  /* 좌우는 `.head-inner`·`.has-shell .page` 와 **같은 값**이어야 한다. */
  max-width: var(--page-max);
  margin-inline: auto;
  padding: 0 var(--page-pad);
  /*
   * 가로로 밀어 볼 수는 있게 두되 막대는 감춘다. 칸이 넷뿐이라 좁은 화면에서만
   * 넘치고, 그때 막대가 뜨면 줄 높이가 흔들린다.
   */
  overflow-x: auto;
  scrollbar-width: none;
}
.subnav-inner::-webkit-scrollbar { display: none; }
.subnav-item {
  flex: none;
  display: flex; align-items: center;
  min-height: var(--touch);
  padding: 0 var(--s-3);
  border: 0; border-bottom: 2px solid transparent; background: none;
  font: inherit; font-size: var(--t-md); font-weight: 600;
  color: var(--ink-3); text-decoration: none; cursor: pointer;
  white-space: nowrap;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.subnav-item:hover { color: var(--ink); }
.subnav-item:focus-visible { outline: none; box-shadow: var(--focus); }
.subnav-item.is-here {
  color: var(--accent-strong);
  border-bottom-color: var(--accent);
  cursor: default;
}

/* ── 빵부스러기 ─────────────────────────────────────────── */
/*
 * 위치와 되돌아갈 길을 **한 줄**이 함께 갖는다. 되돌아가기 버튼을 따로 두지 않는다.
 * 마디는 키오스크에서 손가락으로 누를 수 있는 크기여야 한다.
 */
/*
 * ── 지금 어디인가 -- **사슬이 아니라 제목** ────────────────
 *
 * `관제 › 1라인 제어기 › 장치 등록 [3단계]` 처럼 마디를 이어 붙였다. 그런데 그 사슬의
 * 첫 마디는 이미 **레일이 말하고 있고**(켜진 칸), 마지막 마디는 이 화면의 제목이다.
 * 가운데만 새 정보인데, 셋을 같은 크기로 늘어놓으니 정작 <여기가 어디인가> 가
 * 작은 글자 셋 사이에 묻혔다. 깊이를 숫자로 적어 두던 배지는 그 증상이었다 --
 * 눈으로 세기 어려우니 세어 준 것이다.
 *
 * 이제 **마지막이 제목**이고, 그 위에 <어디에서 왔는가> 한 줄이 작게 선다.
 * 깊이 배지는 걷었다. 셀 것이 없다.
 */
.crumb { display: flex; flex-direction: column; justify-content: center; min-width: 0; gap: 1px; }
.crumb-link {
  border: 0; background: none; font: inherit; cursor: pointer;
  color: var(--ink-3); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .04em;
  padding: 0; text-align: left;
  display: inline-flex; align-items: center; gap: 3px;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 되돌아가는 길이라는 것을 화살표가 말한다. 글자만으로는 눌러도 되는지 알 수 없다. */
.crumb-link::before { content: '‹'; font-size: var(--t-md); line-height: 1; }
/*
 * 어디에 속한 화면인지만 말하는 윗마디. 누를 수 없으므로 화살표를 붙이지 않는다 --
 * 붙이면 눌러도 아무 일이 없는 것이 되고, 그것은 고장으로 읽힌다.
 * 생김새는 되돌아가는 길과 같다. 다른 것은 손가락 모양과 화살표뿐이다.
 */
.crumb-up {
  color: var(--ink-3); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .04em;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.crumb-link:hover { color: var(--accent); }
.crumb-link:focus-visible { outline: none; box-shadow: var(--focus); border-radius: var(--r-sm); }
/* 지금 자리 = 이 화면의 제목. 크고 조인다. */
.crumb-here {
  font-size: var(--t-xl); font-weight: 680; letter-spacing: -.02em;
  color: var(--ink); line-height: 1.15;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 사슬의 가운데 마디와 구분자는 걷었다. 제목과 되돌아갈 곳 둘이면 족하다. */
.crumb-sep { display: none; }
.crumb-link:not(:first-of-type) { display: none; }
/* 줄이 넘치면 앞쪽 마디부터 접는다. 지금 자리와 깊이는 끝까지 남는다. */
.crumb { flex-wrap: nowrap; }
.crumb-link, .crumb-here { min-width: 0; }
/* 넘치면 말줄임은 위 .crumb-link 가 이미 건다. 여기서 또 적지 않는다. */


/* ── ui-wizard.css ─────────────────────────────── */
/*
 * **마법사 단계 띠** -- 초기 설정이 몇 걸음이고 지금 어디인가.
 *
 * 뼈대에서 떼어 냈다. 선택자가 이 구획 안에서만 돈다.
 */

/* ── 마법사 단계 띠 ─────────────────────────────────────── */
/*
 * 단계를 **위에 가로로** 세운다. 지금 어디인지, 무엇이 남았는지가 한눈에 보여야
 * 계속할지 그만둘지 판단할 수 있다.
 *
 * ── 왜 옆이 아니라 위인가 ────────────────────────────────
 *
 * 옆에 세로로 세워 두었다. 그러면 폭에 따라 **모양이 두 벌**이 된다 -- 넓으면 옆에
 * 세로로, 좁으면 위에 가로로. 같은 제품인데 기기마다 다른 화면이 되고, 그 사이
 * 어중간한 폭에서는 레일이 200px 을 붙들어 카드를 좁히는 일까지 났다.
 *
 * 위로 올리면 모양이 **한 벌**이다. 스마트폰이든 27인치든 같은 띠가 같은 자리에 선다.
 * 걸음이 셋뿐이라 가로로 세워도 좁아지지 않고, 본문은 폭을 통째로 쓴다.
 *
 * ── 폭에 숫자를 박지 않는다 ──────────────────────────────
 *
 * 갈림목(@media)이 하나도 없다. 칸은 늘 셋이고 저마다 1fr 이라, 폭이 얼마든 같은 비율로
 * 줄고 는다. 글자만 좁은 자리에서 두 줄이 될 뿐이다.
 */
.wizard-page {
  display: grid;
  gap: clamp(var(--s-4), 2.5vw, var(--s-6));
}
/*
 * **판의 폭 = 카드의 폭.** 레일이 옆에서 비켜났으니 더 넓힐 이유가 없다.
 * 가운데로 보내는 `margin: 0 auto` 는 .page 가 이미 갖고 있다(ui-base.css).
 *
 * 숫자는 고른 것이 아니라 **더한 것**이다 -- 본문 상한 + 안여백(40).
 */
.wizard-page { max-width: 900px; }
/*
 * 계정 연결은 묻는 것이 계정과 비밀번호 둘뿐이라 더 좁다(680 + 40).
 * 지금 서 있는 단계를 :has 로 본다 -- 셋 중 하나만 .active 다.
 * :has 를 모르는 브라우저에서는 900 으로 서는데, 그래도 깨지지 않고 조금 넓을 뿐이다.
 */
.wizard-page:has(.panel.active[data-view="link"]) { max-width: 720px; }

.wizard-main { min-width: 0; }
/*
 * 카드가 판보다 좁아지는 일이 있으면 **그 안에서도 가운데** 선다.
 * 한쪽에만 붙으면 위의 띠는 꽉 찼는데 카드만 치우쳐 줄이 어긋나 보인다.
 */
.wizard-main > .panel { max-width: 860px; margin-inline: auto; }
.wizard-main > .panel[data-view="link"] { max-width: 680px; }

/*
 * ── 단계를 넘기는 단추 ─────────────────────────────────────
 *
 * 머리 오른쪽에 있었다. 넓은 화면에서 카드는 가운데 있는데 단추는 화면 맨 끝이라
 * **거기 있는 줄도 모르는** 자리였다 -- 1920px 에서 둘 사이가 1000px 넘게 벌어진다.
 *
 * 읽기가 끝나는 자리에 둔다. 카드와 **같은 폭·같은 축**이라, 카드 오른쪽 아래에서
 * 눈이 자연스럽게 다음 단추에 닿는다.
 */
.wizard-act {
  display: flex; align-items: center; gap: var(--s-2);
  /* 카드와 같은 자리에 선다. 위 .panel 과 같은 상한·같은 가운데맞춤. */
  max-width: 860px; margin-inline: auto;
  /* 카드에서 조금 떼어 둔다. 붙으면 카드의 일부로 읽힌다. */
  margin-top: calc(var(--s-2) * -1);
  flex-wrap: wrap;
  /* 주 동작은 오른쪽 끝. 눈이 카드를 다 읽고 내려오는 자리다. */
  justify-content: flex-end;
}
.wizard-page:has(.panel.active[data-view="link"]) .wizard-act { max-width: 680px; }

/* <이전> 만 왼쪽으로 민다. 뒤로 가는 것은 앞으로 가는 것과 같은 자리에 있으면 안 된다. */
.wizard-act .act-back { margin-right: auto; }

/*
 * 머리에 있을 때는 작은 단추(btn-sm)였다. 여기서는 이 화면의 주 동작이므로 제 크기를 준다.
 * 44px 은 손가락으로 눌러도 빗나가지 않는 높이다 -- 카드 속 단추와 같은 값.
 */
.wizard-act .btn { min-height: 44px; padding: 0 var(--s-5); font-size: var(--t-md); }
.wizard-act .btn-primary { font-weight: 600; }

/*
 * 칸은 **늘 셋**이다. auto-fit 으로 접지 않는다 -- 접히는 순간 좁은 화면만 다른 모양이
 * 되고, 그게 바로 걷어내려던 것이다.
 */
.rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}
.rail-step {
  /* 잇는 선을 이 칸 안에 그린다. */
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-2);
  border: 0; background: none; text-align: center;
  border-radius: var(--r-sm); font: inherit; color: var(--ink-3);
}
/*
 * 걸음을 **선으로 잇는다.** 동그라미만 셋 떨어뜨려 두면 나란한 세 개일 뿐,
 * 하나를 지나야 다음이 온다는 것이 보이지 않는다.
 *
 * 선은 동그라미 한가운데 높이에 건다 -- 위여백(12) + 동그라미 반지름(12).
 * 양옆 18px 을 비워 동그라미에 닿지 않게 한다.
 */
.rail-step::before, .rail-step::after {
  content: ''; position: absolute; top: calc(var(--s-3) + 12px);
  height: 1px; background: var(--line);
}
.rail-step::before { left: 0; right: 50%; margin-right: 18px; }
.rail-step::after { right: 0; left: 50%; margin-left: 18px; }
/* 첫 걸음 왼쪽과 마지막 걸음 오른쪽에는 이어질 것이 없다. */
.rail-step:first-child::before, .rail-step:last-child::after { display: none; }

button.rail-step { cursor: pointer; }
button.rail-step:hover { background: var(--surface-2); }
.rail-no {
  flex: none; width: 24px; height: 24px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--t-xs); font-weight: 700; font-variant-numeric: tabular-nums;
  border: 1px solid var(--line-2); color: var(--ink-3); background: var(--surface);
  /* 잇는 선이 동그라미 뒤로 지나가지 않게 한 겹 위에 올린다. */
  position: relative; z-index: 1;
}
.rail-body { display: grid; gap: 2px; min-width: 0; }
/*
 * 이름은 좁은 자리에서 두 줄이 된다. 낱말 단위로만 끊는다 --
 * "아두이 / 노 등록" 이 되면 읽는 데 한 박자가 더 든다.
 */
.rail-name { font-size: var(--t-sm); word-break: keep-all; line-height: 1.4; }
.rail-why { font-size: var(--t-xs); color: var(--ink-3); word-break: keep-all; }

/* 지나온 것 · 지금 것 · 남은 것을 색만이 아니라 모양으로도 구분한다. */
.rail-step.is-done .rail-no { background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok); }
.rail-step.is-done .rail-name { color: var(--ink-2); }
/*
 * 지금 걸음에 바탕을 칠했었다. 칸이 세로로 길쭉하던 시절의 모양이라, 가로 띠에서는
 * 파란 덩어리 하나가 놓인 꼴이 된다. 동그라미와 글자 굵기로만 말한다.
 */
.rail-step.is-now .rail-no { background: var(--accent); border-color: var(--accent); color: #fff; }
.rail-step.is-now .rail-name { color: var(--ink); font-weight: 650; }
.rail-step.is-locked .rail-name { text-decoration: line-through; }
/*
 * 지나왔지만 이루지 못한 걸음. 초록 ✓ 를 주면 안 된다 -- 장치 없이 넘어간 사람에게
 * 띠는 "끝났다", 배너는 "등록된 장치가 없습니다" 라고 서로 반대로 말하게 된다.
 * 번호를 그대로 두고 주의 색만 입혀, 돌아갈 곳이 있다는 것을 보인다.
 */
.rail-step.is-skip .rail-no { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }
.rail-step.is-skip .rail-name { color: var(--ink-2); }
/* 지나온 구간은 잇는 선도 함께 지나왔다. */
.rail-step.is-done::before, .rail-step.is-done::after { background: var(--ok-line); }

/*
 * 여기 있던 `@media (max-width: 1000px)` 를 걷었다. 레일이 세로로 설지 가로로 누울지는
 * 이제 위의 auto-fit 이 제 폭을 보고 정한다 -- 창 크기를 물어볼 일이 없어졌다.
 */

/*
 * ── 품긴 화면 ──────────────────────────────────────────────
 *
 * 다른 화면 안에 품길 때(mountShell 의 bare) 자기 틀을 벗는다.
 *
 * 벗기지 않았더니 카드가 **네 겹**으로 겹쳤다 -- 마법사 카드 안에 장치 추가 카드,
 * 그 안에 값 상자 둘, 또 로그판. 테두리가 네 번 그어지면 어디까지가 한 덩어리인지
 * 읽히지 않고, 안여백이 네 번 겹쳐 정작 내용이 들어갈 자리가 좁아진다.
 *
 * 품는 쪽(마법사)이 이미 카드 한 겹을 그렸으므로, 안쪽은 **선 없이 내용만** 낸다.
 */
/*
 * 바탕을 **html 까지** 지운다.
 *
 * body 만 지웠더니 회색 덩어리가 그대로 남았다 -- 바탕색은 html 에 걸려 있고, 그것이
 * 흰 카드 안에서 회색 상자로 보인다. 품긴 화면은 자기 바탕을 갖지 않아야 한다.
 */
html:has(> body.is-bare) { background: transparent; }
.is-bare { background: transparent; }
.is-bare .page { padding: 0; max-width: none; }
.is-bare .card {
  border: 0; box-shadow: none; background: transparent;
  margin: 0 0 var(--s-5);
}
.is-bare .card-head { padding: 0 0 var(--s-3); border-bottom: 0; }
/*
 * 품긴 화면의 제목은 **곁제목**이다.
 *
 * 품는 쪽이 이미 이 걸음의 제목을 달았다(<1. 아두이노 등록>). 안쪽 제목까지 같은 크기로
 * 서면 둘 중 무엇이 이 화면의 이름인지 흐려진다. 다만 지우지는 않는다 -- 흐름이 나아가면
 * 이 제목이 <장치 추가>에서 <장치 구성>으로 바뀌며 지금 어디인지를 말한다.
 */
.is-bare .card-title { font-size: var(--t-md); color: var(--ink-2); }
.is-bare .card-desc { font-size: var(--t-sm); }
.is-bare .card-body { padding: 0; }

/* 동작은 맨 위. 누르는 것부터 시작하는 화면이다. */
/*
 * 동작은 맨 위, **왼쪽부터**.
 *
 * 머리에 있을 때는 오른쪽 끝에 붙는 것이 맞았다(.head-actions 의 flex-end). 그런데 여기서는
 * 머리가 아니라 **내용의 첫 줄**이다. 오른쪽에 붙여 두었더니 왼쪽이 통째로 비어, 눈이
 * 글을 읽던 자리에서 단추까지 화면을 가로질러야 했다.
 */
.is-bare .bare-actions {
  display: flex; gap: var(--s-2); flex-wrap: wrap;
  justify-content: flex-start;
  margin-bottom: var(--s-4);
}

/*
 * **아직 안 채워진 값 상자는 접는다.**
 *
 * 포트를 고르기 전에는 대상 장치도 칩 시그니처도 알 수 없다. 그 자리에 `—` 를 다섯 개
 * 늘어놓으면 처음 보는 사람에게는 고장으로 읽힌다 -- 값이 비어 있는 것과 아직 물어보지
 * 않은 것은 다른 말인데 화면은 같게 보여 준다.
 *
 * setFact 가 값을 넣을 때 muted 를 뗀다(app/setup/screen.mjs). 그러니 muted 아닌 칸이
 * 하나도 없는 상자는 아직 아무것도 모르는 상자다.
 */
.is-bare .facts:not(:has(.fact-v:not(.muted))) { display: none; }

/*
 * ── 품는 쪽이 이미 말한 것은 **두 번 말하지 않는다** ───────
 *
 * 마법사의 아두이노 등록 걸음을 재어 보니 430px 중 실제 표는 148px 이었다. 나머지는
 * 겹친 머리와 로그판이었다. 그중 이 둘이 순전한 되풀이였다.
 *
 *   안쪽 목록의 카드 머리   <연결된 장치 / USB 에 연결하면 이 보드가 알아서 잡습니다>
 *                          바깥이 이미 적었다 -- <아두이노 등록 / 아두이노 한 대를 USB 로
 *                          연결해 등록합니다>. 같은 말이 두 크기로 두 번 섰다.
 *   <새로고침> 단추         이 화면은 스스로 계속 물어본다(app/setup/screen.mjs 의
 *                          setInterval(pull)). 안 눌러도 뜨는 것을 누르라고 내놓고 있었다.
 *
 * **목록 걸음에서만** 걷는다. 핀을 정하는 걸음으로 넘어가면 안쪽 제목이 <장치 구성> 으로
 * 바뀌어 지금 어디인지를 말하고, 단추도 <저장>·<되돌리기> 로 바뀐다 -- 그것은 되풀이가
 * 아니라 그 걸음에만 있는 것이라 그대로 둔다.
 */
.is-bare .panel[data-view="list"] .card-head { display: none; }
.is-bare:has(.panel.active[data-view="list"]) .bare-actions { display: none; }



/* ── ui-parts.css ─────────────────────────────── */
/*
 * 조각 -- 카드 · 정보 나열 · 버튼
 *
 * 업무용 도구의 화면 규칙을 한곳에 둔다. 색·간격·글자 크기를 화면마다 다시 정하면
 * 같은 제품 안에서 화면마다 다른 인상이 된다.
 *
 * 어느 화면에나 서는 것만 여기 둔다. 대시보드에만 서는 부품은 app/ui-dash.css 로
 * 갈랐다 -- 한 장이 852줄이 되어 한 번에 읽히지 않았다(목표 400 · 상한 800).
 * 전체 차례는 app/ui.css 가 적어 둔다.
 */
/* ── 구획 ───────────────────────────────────────────────── */
/*
 * ── 카드는 **상자가 아니라 구획**이다 ─────────────────────
 *
 * 모든 묶음이 테두리·모서리·그림자를 두른 상자였다. 그런데 그 안에 든 것이 이미 제
 * 판을 갖는다 -- 표(.table), 핀 격자(.rowlist), 정보 나열(.facts). **판 안에 판이
 * 겹치면** 테두리가 두 겹으로 보이고, 안쪽 것이 무엇의 일부인지가 흐려진다.
 *
 * 그래서 기본을 뒤집었다. 구획은 **제목 + 선 + 내용**이고, 판은 내용이 필요할 때만
 * 스스로 갖는다. 화면이 넓어지고, 테두리가 한 겹으로 줄어든다.
 *
 * 상자로 남는 것은 **고르는 대상**뿐이다(.card-click) -- 제어기 카드처럼 눌러서 고르는
 * 것은 하나하나가 만질 수 있는 물건이라, 경계가 있어야 어디까지가 하나인지 안다.
 */
.card {
  margin-bottom: var(--s-6);
}
/*
 * 알아 두어야 하는 카드.
 *
 * 왼쪽에 굵은 선 하나만 세운다. 통째로 칠하면 그 카드가 화면에서 가장 세게 보여,
 * 정작 눌러야 하는 자리(연결 요청·설정)보다 앞서 읽힌다 -- 알림은 **알리는 것**이지
 * 시키는 것이 아니다.
 */
/*
 * 알아 두어야 하는 구획. 왼쪽 띠 하나 -- 화면 어디서나 상태를 말하는 그 자리, 그 굵기다.
 * 띠를 세우려면 그만큼 안쪽으로 물러나야 한다.
 */
.card.card-warn { border-left: 3px solid var(--warn); padding-left: var(--s-3); }

/*
 * 구획 머리. 주요 동작은 여기 둔다 -- 긴 화면에서 아래까지 스크롤하지 않아도 닿는다.
 * 아래 선이 **구획을 가르는 선**이 된다. 상자를 없앤 지금 이것이 유일한 경계다.
 */
/*
 * **선 아래로 한 칸 띄운다.**
 *
 * 선만 긋고 여백을 안 줬더니, 구독 화면의 <약정>·<값> 처럼 머리 바로 밑에서 내용이
 * 시작하는 구획에서 첫 줄이 선에 붙어 보였다 -- 카드 테두리가 사라진 판에서 이 선이
 * 유일한 경계이므로, 붙으면 경계가 아니라 밑줄로 읽힌다.
 *
 * 머리와 내용 사이가 이미 벌어져 있던 구획은 저마다 위쪽 여백을 갖고 있었다. 그것을
 * 지우지 않고 여기에 얹으면 그 자리만 두 배가 되므로, 간격은 **여기 한 곳에서만** 준다.
 */
.card-head {
  padding: 0 0 var(--s-3);
  margin-bottom: var(--s-3);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}
.card-head > div:not(.spacer) { min-width: 0; }
/*
 * 구획 이름.
 *
 * 12px 소문자 대문자꼴(.section-title)과 14px 반굵은 글자(.card-title)가 따로 있었다.
 * 같은 뜻(여기서부터 다른 이야기다)을 두 모양으로 그리던 것이라 **한 획으로 모았다** --
 * 두 이름을 다 받는 것은 쓰는 자리가 열한 곳이라, 이름을 갈아 끼우는 대신 여기서 잇는다.
 *
 * 판독부의 값이 40px 을 넘으므로 이 글자는 작아도 밀리지 않는다. 작게 두되 **또렷하게**
 * 둔다 -- 흐린 회색 대문자꼴은 제목이 아니라 꼬리표처럼 읽힌다.
 */
.card-title, .section-title {
  font-size: var(--t-lg); font-weight: 700; letter-spacing: -.012em;
  color: var(--ink); margin: 0;
  text-transform: none;
}
/* 구획 이름 옆의 셈. 이름과 같은 줄에 서되 제목의 무게를 가져가지 않는다. */
.section-count {
  margin-left: var(--s-2);
  font-size: var(--t-sm); font-weight: 500; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/*
 * 카드 제목 앞의 별. <자주 보는 핀> 카드가 쓴다.
 *
 * 줄 맨 앞의 별과 **같은 획, 같은 색**이라 "여기 모인 것이 그 별을 붙인 것들" 이
 * 말 없이 읽힌다. 제목 글자보다 조금 작게 두어 제목을 밀어내지 않는다.
 */
.title-star {
  width: 14px; height: 14px; vertical-align: -2px; margin-right: 6px;
  fill: var(--mark); stroke: var(--mark); stroke-width: 1.7; stroke-linejoin: round;
}
.card-desc { font-size: var(--t-xs); color: var(--gray-500); margin: 2px 0 0; }
/*
 * 좌우 여백을 두지 않는다. 안에 든 판(표·격자)이 **구획 머리와 같은 선에서** 시작해야
 * 한 덩어리로 읽힌다 -- 여백을 두면 판이 구획 안에서 한 번 더 들여쓰기 된다.
 */
.card-body { padding: var(--s-4) 0 0; }
/*
 * ── 카드 한 장 안의 묶음 ──────────────────────────────────
 *
 * 다루는 것이 둘인데 그 둘이 **한 가지 일**일 때가 있다. 구성 화면이 그렇다 --
 * 이 보드에 붙은 장치와, 화면이 이 보드를 부를 이름. 따로 볼 일이 없고 저장도 한 번이다.
 *
 * 그런 것을 카드 두 장으로 세우면 머리(제목·설명·테두리·그림자)가 두 벌 서고, 정작
 * 안에 든 것은 입력칸 하나다. 실제로 구성 화면이 그랬다 -- 칸 하나를 담으려고 90px
 * 짜리 카드가 서 있었고, 카드 사이의 빈 띠가 "여기서 저기로 넘어간다" 고 말하는 바람에
 * 한 번에 저장되는 것들이 서로 남처럼 보였다.
 *
 * 카드는 한 장으로 두고 **안에서** 나눈다. 선 하나와 작은 제목이면 족하다.
 */
.card-part { display: grid; gap: 12px; margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--line); }
.card-part:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* 제목과 설명은 한 덩어리다. 사이를 묶음 간격(12px)으로 벌리면 둘이 남처럼 보인다. */
.part-head { display: grid; gap: 3px; }
.part-title { font-size: var(--t-sm); font-weight: 600; color: var(--ink-2); margin: 0; }
.part-desc { font-size: var(--t-xs); color: var(--ink-3); margin: 0; max-width: 70ch; }
/* 칸 사이는 격자가 벌린다. 칸이 제 아래 여백까지 들고 있으면 마지막 줄만 두 배로 뜬다. */
.card-part .field-grid .field { margin-bottom: 0; }
.card-foot {
  padding: var(--s-3) 0 0;
  margin-top: var(--s-4);
  border-top: 1px solid var(--line);
  display: flex; gap: var(--s-2); align-items: center;
}

/* ── 정보 나열 ──────────────────────────────────────────── */
/*
 * 항목 수가 열 수의 배수가 아닐 때가 대부분이다. 그때 마지막 줄에 홀로 남은 항목이
 * 가로로 늘어나 버리면 만들다 만 화면처럼 보인다(6개짜리에서 실제로 그랬다).
 *
 * auto-fit 은 **전체에서 비는 칸만** 접는다. 그래서
 *   항목이 적어 첫 줄도 못 채우면  -> 빈 칸이 접히고 항목이 고르게 늘어난다
 *   항목이 많아 줄이 넘어가면      -> 칸 너비가 고정되어 마지막 항목도 제 폭을 지킨다
 * 두 경우 모두 빈 칸이 회색으로 남지 않는다. 칸에 따로 배경이나 선을 주지 않기 때문이다.
 */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  background: var(--white);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-sm);
  padding: 4px 0;
}
/* 묶음이 둘 이상 이어지면 서로 붙어 한 덩어리로 보인다. 사이를 띄워 구분한다. */
.facts + .facts { margin-top: 12px; }
.fact { min-width: 0; padding: 10px 16px; }
.fact-k {
  font-size: var(--t-xs); color: var(--gray-500);
  text-transform: uppercase; letter-spacing: .05em; font-weight: 600;
}
.fact-v { font-size: var(--t-md); color: var(--black); margin-top: 3px; font-weight: 500; word-break: break-all; }
.fact-v.muted { color: var(--gray-400); font-weight: 400; }

/* ── 버튼 ───────────────────────────────────────────────── */
/*
 * 단추. **기계의 단추**다.
 *
 * 옛것은 흰 바탕에 회색 테두리, 500 굵기였다 -- 웹 양식의 기본값이고, 그래서 어느
 * 제품에나 있는 모양이었다. 셋을 바꿨다.
 *
 *   · 바탕을 비운다. 흰 판 위의 흰 단추는 테두리로만 서 있어 있으나 마나다.
 *   · 글자를 굵힌다(600). 누르는 것은 읽는 것보다 무거워야 한다.
 *   · 모서리를 4px 로 깎는다. 둥근 단추는 부드럽고, 이 제품은 설비를 켠다.
 */
.btn {
  appearance: none;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  padding: 7px 13px;
  font-family: inherit;
  font-size: var(--t-md);
  font-weight: 600;
  line-height: 1.3;
  cursor: pointer;
  background: transparent;
  color: var(--ink);
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
  /* 화면 이동 버튼은 <a> 로 만든다. 밑줄이 남으면 다른 버튼과 따로 논다. */
  text-decoration: none;
  /* <a> 는 inline 이라 padding 만으로는 <button> 과 높이가 어긋난다. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
/*
 * 버튼 안의 마크. 화면 이동 버튼은 목적지 화면과 같은 마크를 달아,
 * 상단바에서 본 그림과 버튼의 그림이 같다는 것만으로 어디로 가는지 알게 한다.
 */
.btn svg {
  width: 14px; height: 14px;
  flex: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  margin-left: -2px;
}
/*
 * 마크를 감싸는 칸(app/shell.mjs 의 btn-ico). 규칙이 없어 `inline` 으로 서 있었고,
 * 그러면 글꼴의 밑줄 자리만큼 그림이 아래로 내려앉아 글자와 한 줄로 안 맞는다.
 */
.btn-ico { display: inline-flex; align-items: center; }
.btn-sm svg { width: 13px; height: 13px; }
.btn:hover:not(:disabled) { background: var(--surface-2); border-color: var(--ink-4); }
.btn:focus-visible { outline: none; box-shadow: var(--focus); }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* 주 동작. 채워서 무게를 준다 -- 화면에 이런 단추는 하나뿐이다(app/shell.mjs). */
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-primary:hover:not(:disabled) { background: var(--accent-strong); border-color: var(--accent-strong); }

/* 링크는 disabled 속성이 없다. 굽는 중처럼 나가면 안 되는 순간에 쓴다. */
.btn.is-disabled { pointer-events: none; opacity: .5; }
.btn-danger { background: transparent; color: var(--err); border-color: var(--err-line); }
.btn-danger:hover:not(:disabled) { background: var(--err-soft); border-color: var(--err); }

/*
 * 곁딸린 동작. 바로 위에 주 동작이 있고, 이것은 <그게 아니라면> 쪽이다.
 *
 * 기본 흰 단추 그대로 두면 파란 단추와 나란히 서서 **둘 중 무엇을 눌러야 하는지**를
 * 묻게 된다. 마법사의 <계정 연결> 아래 <회원가입> 이 그 자리다 -- 하나는 오늘 할 일이고
 * 하나는 계정이 없을 때만 가는 길인데, 무게가 같으면 그 차이가 안 보인다.
 *
 * 바탕을 비우고 테두리와 글자를 한 단계 흐리게 한다. 지워 버리지는 않는다 --
 * 정말 계정이 없는 사람에게는 이것이 유일한 길이다.
 */
.btn-sub {
  background: transparent;
  color: var(--ink-2);
  border-color: var(--line);
  font-weight: 500;
}
.btn-sub:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }
/* 마크까지 또렷하면 흐리게 만든 글자보다 마크가 먼저 읽힌다. */
.btn-sub svg { opacity: .7; }

.btn-sm { padding: 5px 10px; font-size: var(--t-xs); }
.btn-icon { padding: 5px 9px; font-size: var(--t-xs); }


/*
 * ── 문서 안에서 **건너뛰는 줄** ───────────────────────────
 *
 * 긴 문서(REST API 안내)는 창 높이의 세 배쯤 된다. 장을 여섯으로 나누고 번호까지 붙여
 * 두었는데 건너뛸 자리가 없으면, 찾는 것이 뒤쪽에 있을 때 계속 굴려야 한다. 연동
 * 개발자는 이 문서를 처음부터 읽지 않는다 -- 한 군데를 확인하러 온다.
 *
 * 이 줄이 `.doc-toc` 을 빌려 쓰고 있었다. 그런데 그것은 **사용 설명서 전용 스타일시트**에
 * 있고(app/ui-doc.css), 그 파일은 설명서 한 장만 건다. 그래서 이 화면에서는 아무 규칙도
 * 안 붙은 채로 파란 밑줄 링크 여섯 개가 한 줄에 늘어서 있었다 -- 문서의 차례가 아니라
 * 본문에 섞여 든 링크 뭉치로 읽힌다.
 *
 * 여기 두는 것은 이 파일이 모든 화면에 실리기 때문이다. 설명서의 차례는 옆에 세로로
 * 붙어 따라 내려가는 것이고(그쪽은 두 칸 짜임새다), 이것은 본문 위에 가로로 한 줄
 * 눕는 것이다 -- 같은 이름을 쓸 것이 아니었다.
 */
.jump {
  display: flex; flex-wrap: wrap; gap: 2px;
  margin: 0 0 var(--s-5);
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.jump a {
  padding: 5px 10px; border-radius: var(--r-sm);
  color: var(--ink-2); text-decoration: none;
  font-size: var(--t-sm); font-weight: 500; white-space: nowrap;
}
.jump a:hover { background: var(--surface); color: var(--ink); }
.jump a:focus-visible { outline: none; box-shadow: var(--focus); }

/*
 * ── 불러 주는 코드 한 장 ───────────────────────────────────
 *
 * 본사 관제의 가입 코드가 이것이다. 이 값은 **사람이 전화로 불러 주거나 옆자리에서
 * 받아 적는 값**이라, 입력칸에 담아 두면 안 된다 -- 고쳐 쓰는 값으로 읽히고, 글자가
 * 작아 O 와 0 을 되묻게 된다(코드 자체에는 그 글자가 없지만 보는 쪽은 모른다).
 *
 * 그래서 칸이 아니라 **판**이다. 크게, 가운데, 자간을 벌려 한 글자씩 끊어 읽히게 둔다.
 */
.joincode {
  margin: 0 auto;
  padding: 22px 16px;
  max-width: 420px;
  border: 1px dashed var(--line-strong, var(--line));
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink-1, var(--ink));
  text-align: center;
  font-size: 34px; font-weight: 700; letter-spacing: .16em;
  /* 한 글자씩 짚어 읽는 값이라 줄이 접히면 안 된다. 좁은 화면에서는 글자를 줄인다. */
  white-space: nowrap; overflow-x: auto;
  user-select: all;
}
@media (max-width: 520px) { .joincode { font-size: 26px; letter-spacing: .1em; } }
.joincode-s {
  margin: 10px 0 0; text-align: center;
  font-size: var(--t-xs); color: var(--ink-3);
}

/*
 * ── 장치 카드 ─────────────────────────────────────────────
 *
 * 제어기를 누르면 가장 먼저 보는 목록이다(관제 · 현장 제어기 둘 다). 한 줄짜리였는데,
 * 그 줄로는 무엇이 붙어 있는지(기종)도 지금 몇 개가 켜져 있는지도 알 수 없어 결국
 * 하나씩 눌러 보게 됐다. 옆의 핀 격자·조작 판은 카드인데 이 자리만 줄이라 층도 끊겼다.
 *
 * 누르는 것이므로 `button` 이다 -- 키보드로도 짚히고, 눌리는 것임이 모양으로도 남는다.
 * 좁아지면 한 칸으로 접힌다. 가로로 넘치게 두지 않는다.
 */
.devcards { display: grid; gap: var(--s-3);
            grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
.devcard {
  display: flex; flex-direction: column; gap: 6px;
  width: 100%; padding: 14px 16px; text-align: left;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); color: inherit; font: inherit; cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.devcard:hover { border-color: var(--line-2); background: var(--surface-2); }
.devcard:focus-visible { outline: none; box-shadow: var(--focus); }
/* 고른 것은 **테두리와 띠**로 말한다 -- 색만으로는 못 보는 사람이 있다. */
.devcard.is-selected {
  border-color: var(--accent); background: var(--accent-soft);
  box-shadow: inset 3px 0 0 var(--accent);
}
.devcard-head { display: flex; align-items: center; gap: var(--s-2); }
.devcard-head b { font-size: var(--t-lg); font-weight: 650; letter-spacing: -.01em;
                  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.devcard-head .badge { margin-left: auto; flex: none; }
.devcard-sub { font-size: var(--t-xs); color: var(--ink-3); }
/* 셈 셋. 값이 앞이고 이름이 뒤로 물러난다 -- 훑을 때 읽는 것은 숫자다. */
.devcard-facts { display: flex; flex-wrap: wrap; gap: var(--s-3);
                 margin-top: 2px; font-size: var(--t-sm); color: var(--ink-2); }
.devcard-facts s { text-decoration: none; color: var(--ink-3); margin-right: 5px; }

/* ── ui-dash.css ─────────────────────────────── */
/*
 * 대시보드의 부품 -- 계기 타일 · 시스템 구성 · 손봐야 할 것 · 자주 보는 핀 · 겹쳐 뜨는 창
 *
 * `ui-parts.css` 한 장이 852줄이었다. 이 저장소는 파일 하나를 **한 번에 읽을 수 있는
 * 범위**로 둔다(목표 400줄 · 상한 800줄). 그 파일이 이미 갖고 있던 결을 따라 둘로 갈랐다 --
 * 어느 화면에나 서는 것(카드 · 정보 나열 · 버튼)과, **대시보드에만 서는 것**.
 *
 * 순서를 바꾸지 않았다. app/ui.css 가 둘을 원래 차례대로 부른다.
 */

/* ── 대시보드 ───────────────────────────────────────────── */
/*
 * 이 화면이 답해야 하는 물음은 셋이다 -- 지금 괜찮은가, 손봐야 할 것이 있는가, 어디로 가는가.
 * 예전에는 세 번째만 있어서 링크 목록처럼 보였고, 무엇이 잘못됐는지는
 * 각 화면에 들어가 봐야 알 수 있었다.
 */
/*
 * ── 값을 직접 적지 않고 토큰으로 옮겼다 ──────────────────────
 *
 * 이 묶음만 `#fff`·`10px`·`11.5px` 처럼 값을 직접 들고 있었다. 토큰 파일이 머리말에서
 * 금지한 바로 그것이다(app/ui-tokens.css). 낱개로는 멀쩡한데, 모서리가 카드는 8px
 * 타일은 10px 이라 **나란히 놓으면 어긋난다** -- 그것이 "만들다 만 것" 으로 보이는 이유다.
 */
/*
 * ══ 판독부 -- **상태 한 줄** ═══════════════════════════════
 *
 * 화면 맨 위에 서서 이 자리가 지금 어떤가를 말하는 줄이다.
 *
 * ── 어두운 띠를 걷었다 (2026-09-12) ───────────────────────
 *
 * 전에는 이 자리가 남색 그러데이션 띠였다. 관제실 계기를 흉내 낸 것이었는데, 흰 화면에
 * **어두운 면 하나만** 얹히니 계기가 아니라 붙인 스킨으로 보였다. 글로우·유리 눈금·
 * 흐르는 발광선까지 얹혀 2010년대 대시보드의 관용구가 그대로 남아 있었다.
 *
 * 지금은 **한 줄**이다.
 *
 *   · 값은 크지 않다. 이 줄이 하는 일은 <읽게> 가 아니라 <훑게> 다
 *   · 첫 칸은 늘 회선이다. 붙어 있지 않으면 그 뒤의 값은 전부 옛날 것이다
 *   · 자리를 적게 먹으므로, 그만큼이 **조작할 것**에 간다. 그것이 이 화면의 일이다
 *
 * 큰 수치가 필요한 자리가 없어진 것은 아니다. 그런 곳(분석 · 구독)은 화면이 제 카드로
 * 크게 적는다. 맨 위의 띠가 그 일까지 겸하지 않는다.
 *
 * ── 옛 이름을 함께 받는다 ─────────────────────────────────
 *
 * 여덟 화면이 `.stats` / `.stat` 이라는 이름으로 값을 세운다. 화면마다 이름을 갈아
 * 끼우면 한 곳을 고칠 때마다 다른 곳이 남고, 그것이 두더지잡기다. 여기서 한 번 잇는다.
 *
 * ── 폭을 묻지 않는다 ──────────────────────────────────────
 *
 * 여기 있던 `@media (max-width: 760px)` 를 걷었다. 칸이 몇 개 서는지는 **내용의 길이**가
 * 정할 일이지 창의 폭이 정할 일이 아니다(docs/검토지침.md §3). 좁아지면 줄이 바뀐다.
 */
.stats, .readout {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2) var(--s-5);
  margin: 0 0 var(--s-5);
  padding: 11px var(--s-5);
  background: var(--surface-2);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  font-size: var(--t-md);
}

/*
 * 칸 하나. 이름과 값이 **한 줄에** 선다.
 *
 * 세로로 쌓으면(이름 위, 값 아래) 다섯 칸이 두 줄 높이를 먹는다. 옆으로 세우면 한 줄이고,
 * 넘치면 그때 줄이 바뀐다.
 */
.stat, .readout-cell {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
}
/* 칸 사이는 옅은 세로선으로 가른다. 줄이 바뀌면 그 줄의 첫 칸에는 선이 없다. */
.stat + .stat, .readout-cell + .readout-cell {
  padding-left: var(--s-5);
  border-left: 1px solid var(--line-2);
}

.stat-k, .readout-k {
  font-size: var(--t-sm); color: var(--ink-3);
  white-space: nowrap;
}
/*
 * 값.
 *
 * 숫자 폭을 고정한다(tabular). 이 값은 몇 초마다 다시 그려지는데, 폭이 들쭉날쭉하면
 * 바뀌지 않은 숫자도 흔들려 보이고 -- 현장에서 그 흔들림은 고장 신호로 읽힌다.
 */
.stat-v, .readout-v {
  font-size: var(--t-md); font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* 상태는 **값 자체가** 말한다. 띠를 두르지 않는다. */
.stat-ok   .stat-v, .readout-ok   .readout-v { color: var(--ok); }
.stat-warn .stat-v, .readout-warn .readout-v { color: var(--warn); }
.stat-err  .stat-v, .readout-err  .readout-v { color: var(--err); }
.stat-info .stat-v, .readout-info .readout-v { color: var(--accent); }
/* 볼 이유가 없는 값(0 · 없음)은 물러선다. 색은 볼 것에만 준다. */
.stat-mute .stat-v, .readout-mute .readout-v { color: var(--ink-3); }

/*
 * 부연.
 *
 * 값 옆에 작게 붙는다. 이 줄에서 가장 약한 글자이고, 좁아지면 가장 먼저 다음 줄로 간다 --
 * 지우지 않는 이유는 <모두 장치가 보고한 값> 같은 말이 그 숫자를 어떻게 읽을지를 정하기
 * 때문이다.
 */
.stat-sub, .readout-sub {
  font-size: var(--t-sm); color: var(--ink-3); line-height: 1.4;
  word-break: keep-all; min-width: 0;
}
.stat-sub b, .readout-sub b { color: var(--warn); font-weight: 600; }

/*
 * ── **눌리는 칸** ─────────────────────────────────────────
 *
 * 몇 칸은 눌러서 그 일을 처리하는 자리로 간다(<대기 중인 신청> → 거래).
 * 그것을 `<button>` 으로 세우는 것은 맞다 -- 눌리는 것은 단추여야 키보드로도 닿고
 * 읽어 주는 도구도 그렇게 읽는다.
 *
 * 그런데 벗기지 않으면 **브라우저가 제 단추 옷을 입힌다** -- 테두리 · 회색 바탕 ·
 * 모서리 · 제 글꼴까지. 이 줄의 다른 칸은 상자가 없으므로, 읽는 줄 한가운데에
 * 상자 하나가 박힌 꼴이 된다. 옆칸과 가르는 세로선과 겹쳐 선이 두 줄로 보이기까지 한다.
 *
 * 그래서 옷을 벗기고, **부연에 밑줄 하나**로만 눌린다고 말한다. 이 줄은 숫자를 읽는
 * 줄이라 단추를 하나 놓으면 그것부터 보이고, 정작 읽어야 할 숫자가 뒤로 밀린다.
 *
 * 칸 사이 세로선(`.stat + .stat`)은 그대로 산다 -- 선택자가 더 세서 여기 `border: 0`
 * 이 그 줄만은 덮지 못한다. 일부러 그렇게 둔다.
 */
button.stat {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
button.stat .stat-sub {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
button.stat:hover .stat-sub { color: var(--accent-strong); }
/* 키보드로 왔을 때. 칸 자체를 감싸야 어디에 서 있는지 보인다. */
button.stat:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

/* ── 시스템 구성 ────────────────────────────────────────── */
/*
 * 숫자 타일이 답하지 못하는 것 하나 -- **어디까지 이어져 있는가.**
 * 끊긴 자리가 모양으로 먼저 읽혀야 하므로, 상태를 칸 색이 아니라 이음선에 싣는다.
 *
 * 좁은 화면에서는 줄이 바뀐다. 가로로 밀어 보게 두지 않는다 -- 벽에 걸린 장비에서
 * 가로 스크롤은 손가락으로 찾아 헤매는 일이 된다.
 */
.chain {
  display: flex; align-items: stretch; flex-wrap: wrap; gap: 6px;
  margin: 18px 0 26px;
}
.chain-node {
  flex: 1 1 9.5rem; min-width: 0;
  /*
   * 상태는 **왼쪽 띠**로 말한다. 위쪽 굵은 선은 칸마다 색 모자를 씌운 꼴이라,
   * 넷이 나란히 서면 색 띠 넷이 먼저 읽히고 정작 값이 뒤로 밀렸다.
   * 계기 띠·핀 격자와 같은 자리, 같은 굵기다.
   */
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  box-shadow: inset 3px 0 0 var(--line-2);
  padding: 11px 14px 12px; text-decoration: none; color: inherit;
  display: flex; flex-direction: column; gap: 2px;
}
a.chain-node:hover { border-color: var(--blue-600); background: var(--blue-50); }
a.chain-node:focus-visible { outline: none; box-shadow: var(--focus); }
.chain-name { font-size: var(--t-xs); color: var(--gray-500); letter-spacing: .02em; }
.chain-value {
  font-size: var(--t-lg); font-weight: 650; color: var(--gray-700); line-height: 1.25;
  letter-spacing: -.01em;
}
.chain-sub { font-size: var(--t-xs); color: var(--gray-500); line-height: 1.45; word-break: keep-all; }
.chain-node.chain-ok   { box-shadow: inset 3px 0 0 var(--ok); }
.chain-node.chain-warn { box-shadow: inset 3px 0 0 var(--warn); }
.chain-node.chain-err  { box-shadow: inset 3px 0 0 var(--err); }
.chain-node.chain-info { box-shadow: inset 3px 0 0 var(--accent); }
/* 모르는 칸은 흐리게. 고장과 구분되어야 한다 -- 모르는 것은 나쁜 것이 아니다. */
.chain-node.chain-mute { opacity: .62; }

.chain-link {
  flex: 0 0 auto; align-self: center;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 0 2px; min-width: 34px;
}
.chain-line { display: block; width: 100%; height: 2px; border-radius: 2px; background: var(--gray-300); }
.chain-label { font-size: 10.5px; color: var(--gray-500); letter-spacing: .02em; line-height: 1; }
.chain-link.chain-ok .chain-line { background: var(--ok-700); }
.chain-link.chain-err .chain-line { background: var(--err-700); }
.chain-link.chain-err .chain-label { color: var(--err-700); font-weight: 700; font-size: var(--t-sm); }
.chain-link.chain-mute { opacity: .5; }

/* ── 손봐야 할 것 ───────────────────────────────────────── */
/*
 * 알리기만 하고 끝내지 않는다. 줄마다 **무엇을 누르면 되는지**를 함께 둔다 --
 * "없습니다" 로 끝내면 사람은 어디로 가야 하는지 모른 채 화면을 닫는다.
 */
.todos { display: grid; gap: 10px; margin-bottom: 26px; }
.todo {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--gray-200);
  border-left: 3px solid var(--gray-300); border-radius: var(--r); padding: 13px 16px;
}
.todo-err  { border-left-color: var(--err-700);  background: var(--err-50); }
.todo-warn { border-left-color: var(--warn-700); background: var(--warn-50); }
.todo-info { border-left-color: var(--blue-600); background: var(--blue-50); }
.todo-body { flex: 1 1 22rem; min-width: 0; }
.todo-title { font-size: 14.5px; font-weight: 650; color: var(--gray-700); }
.todo-why { font-size: var(--t-sm); color: var(--gray-600); margin-top: 3px; line-height: 1.5; word-break: keep-all; }
.todo .btn { flex: 0 0 auto; }

/* ── 자주 보는 핀 (대시보드에서 바로 다룬다) ─────────────── */
/*
 * **목록의 줄이 아니라 판이다.**
 *
 * 관제와 현장 제어기의 핀은 줄로 선다(.rowitem). 수십 개를 훑어야 하는 자리라 그것이 맞다.
 * 그런데 그 줄에서 누르는 자리는 오른쪽 끝의 작은 단추 하나다 -- 매일 스무 번 누르는
 * 자리로는 좁고, 이 제품의 주 사용 환경은 터치스크린이다.
 *
 * 여기는 별을 붙인 서넛만 서는 자리이므로 줄을 아낄 이유가 없다. 판 하나에 핀 하나를 두고
 * **단추가 판의 아래를 가득 채운다.** 손가락이 겨눌 곳을 찾지 않아도 된다.
 */
.pinpad {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  gap: var(--s-3);
  margin-bottom: var(--s-6);
}
.pintile {
  display: flex; flex-direction: column; gap: var(--s-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--sh-xs);
  padding: var(--s-3) var(--s-4) var(--s-4);
  position: relative; overflow: hidden;
}
/*
 * 상태는 **왼쪽 띠**로 말한다. 숫자 타일(.stat)과 같은 규칙이다 -- 판 전체를 물들이면
 * 이름이 안 읽히고, 판이 여럿 나란히 서면 화면이 색판이 된다.
 */
.pintile::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--line-2);
}
.pintile.is-on::before      { background: var(--ok); }
.pintile.is-forced::before  { background: var(--info); }
.pintile.is-unconfirmed::before { background: var(--warn); }
.pintile.is-pending         { opacity: .72; }

.pintile-head { display: flex; align-items: center; gap: var(--s-2); }
.pintile-name {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--t-lg); font-weight: 650; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 사람이 붙인 이름이 아니면 흐리게. 목록에서 지키는 구분을 여기서도 지킨다. */
.pintile-name[data-unnamed] { color: var(--ink-3); font-weight: 600; }
.pintile-head .badge { flex: none; }
.pintile-meta {
  font-size: var(--t-sm); color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/*
 * 단추. **판의 아래를 가득 채우고 손가락 높이를 지킨다**(--touch).
 * 켜져 있으면 <끄기> 이므로 눈에 띄어야 하고, 꺼져 있으면 <켜기> 라 조용해도 된다.
 */
.pintile-act {
  width: 100%; min-height: var(--touch); margin-top: auto;
  font-size: var(--t-md); font-weight: 650;
}
.pintile-act.is-on {
  border-color: var(--ok-line); background: var(--ok-soft); color: var(--ok);
}
.pintile-act.is-on:hover:not(:disabled) { border-color: var(--ok); }
/* 입력 핀에는 단추가 없다. 그 자리에 **왜 없는지**를 적는다. */
.pintile-foot {
  min-height: var(--touch); margin-top: auto;
  display: flex; align-items: center;
  font-size: var(--t-sm); color: var(--ink-3);
  border-top: 1px dashed var(--line); padding-top: var(--s-2);
}

/* ── 구획과 바로가기 ────────────────────────────────────── */
/* 여백은 app/ui-base.css 가 쥔다. 여기서는 **눌러서 온 자리**만 띄운다. */
.section { scroll-margin-top: 90px; }
/*
 * 구획 머리도 **딱지**다. 제목처럼 크게 두면 그 아래 카드의 제목과 겨루고,
 * 화면에 굵은 글자가 두 겹으로 쌓인다. 작고 넓게 벌려 <여기서부터 다른 이야기> 만 말한다.
 */
.section-head {
  font-size: var(--t-xs); font-weight: 600; color: var(--ink-3);
  letter-spacing: .09em; text-transform: uppercase;
  margin: 0 0 var(--s-3);
}
.section-desc { font-size: var(--t-sm); color: var(--gray-500); margin: 0 0 12px; max-width: 70ch; }

.tilegrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 12px;
}
/*
 * 되돌릴 수 없는 동작은 사이를 띄워 따로 둔다. 매일 누르는 것들 사이에 끼워 두면
 * 언젠가 손이 미끄러진다 -- 액션 바에서 지키는 규칙과 같다(app/shell.mjs).
 * 비어 있을 때는 자리를 먹지 않는다.
 */
.tilegrid-apart:not(:empty) {
  margin-top: 18px; padding-top: 18px; border-top: 1px dashed var(--gray-300);
}
.ltile {
  display: flex; flex-direction: column; gap: 6px;
  background: var(--surface); border: 1px solid var(--gray-200); border-radius: var(--r-lg);
  padding: 15px 16px; text-decoration: none; color: inherit; cursor: pointer;
  transition: border-color .12s, box-shadow .12s;
}
.ltile:hover { border-color: var(--blue-600); box-shadow: 0 1px 3px rgba(15,23,42,.07); }
.ltile:focus-visible { outline: none; box-shadow: var(--focus); }
.ltile-head { display: flex; align-items: center; gap: 8px; }
.ltile-name { font-size: var(--t-lg); font-weight: 650; color: var(--gray-700); flex: 1 1 auto; min-width: 0; }
.ltile-why { font-size: var(--t-sm); color: var(--gray-500); margin: 0; line-height: 1.55; word-break: keep-all; }
.ltile-act { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.ltile.is-make { cursor: default; }
.ltile.is-make:hover { border-color: var(--gray-200); box-shadow: none; }

/* 아래는 app/ui-overlay.css 로 나갔다. */
/* ── ui-overlay.css ─────────────────────────────── */
/*
 * **겹쳐 뜨는 창** -- 서랍 · 모달 · 그 안의 부품.
 *
 * app/ui-dash.css 가 692줄이었다. 이 구획이 348줄이고 선택자가 겹치지 않아 그대로 나온다.
 *
 * **걸리는 차례가 원본과 같아야 한다.** app/ui-dash.css 바로 뒤에 선다(app/ui.css).
 * 펼친 결과를 전후로 맞대어 규칙이 차례까지 같은 것을 확인했다.
 */

/* ── 겹쳐 뜨는 창 ───────────────────────────────────────── */
/*
 * 그 자리에서 끝날 일은 화면에 늘어놓지 않는다.
 * 대시보드에 만들기·문서·선택지를 전부 펼쳐 두면 "지금 무엇이 어떤가" 가 그 속에 묻힌다.
 */
.modal-back {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(15, 23, 42, .45);
  display: flex; align-items: flex-start; justify-content: center;
  /*
   * 바깥 여백이 **폭을 따라 준다.** 좁은 화면에서는 0 에 수렴해 창이 화면을 꽉 채운다 --
   * 갈림목으로 `padding: 0` 을 주던 자리다.
   */
  padding: min(6vh, 48px) min(16px, 2vw) min(16px, 2vw);
  overflow-y: auto;
}
.modal {
  background: var(--surface);
  /*
   * 좁으면 **스스로 전면이 된다.** 폭이 화면을 넘지 않으므로 모서리도 그때만 펴면 되는데,
   * 그 갈림목을 두지 않으려고 모서리는 늘 둔다 -- 여백이 0 에 가까워지면 둥근 모서리가
   * 화면 모서리와 맞물려 거의 안 보인다.
   */
  width: min(720px, 100%);
  box-shadow: 0 12px 40px rgba(15, 23, 42, .22);
  border-radius: var(--r-lg);
  display: flex; flex-direction: column;
  /* 여백이 줄면 창이 그만큼 길어진다. 갈림목으로 100vh 를 주던 자리다. */
  max-height: calc(100vh - min(6vh, 48px) - min(16px, 2vw));
}
.modal-head {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 18px 20px 14px; border-bottom: 1px solid var(--gray-200);
}
.modal-head > div { flex: 1 1 auto; min-width: 0; }
.modal-title { font-size: var(--t-lg); font-weight: 650; color: var(--gray-700); margin: 0; }
.modal-desc { font-size: var(--t-sm); color: var(--gray-500); margin: 4px 0 0; line-height: 1.55; word-break: keep-all; }
.modal-x {
  flex: 0 0 auto; width: 32px; height: 32px; border-radius: var(--r);
  border: 1px solid var(--gray-200); background: var(--surface); color: var(--gray-500);
  font-size: var(--t-md); cursor: pointer; line-height: 1;
}
.modal-x:hover { background: var(--gray-50); color: var(--gray-700); }
.modal-x:focus-visible { outline: none; box-shadow: var(--focus); }
.modal-body { padding: 16px 20px 20px; overflow-y: auto; }

/* 창 안에서 고르는 목록. 타일과 같은 규칙을 쓰되 좁은 폭에 맞춘다. */
.modal .tilegrid { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
.pick-list { display: grid; gap: 8px; }
.pick {
  display: flex; align-items: center; gap: 12px; width: 100%;
  background: var(--surface); border: 1px solid var(--gray-200); border-radius: var(--r);
  padding: 12px 14px; cursor: pointer; text-align: left; font: inherit;
}
.pick:hover:not(:disabled) { border-color: var(--blue-600); }
.pick:focus-visible { outline: none; box-shadow: var(--focus); }
.pick:disabled { cursor: default; background: var(--blue-50); border-color: var(--blue-600); }
.pick-name { font-size: 14.5px; font-weight: 600; color: var(--gray-700); flex: 1 1 auto; }
.pick-why { font-size: var(--t-xs); color: var(--gray-500); }

/*
 * ── 좁으면 **스스로 전면이 된다** ─────────────────────────
 *
 * 640px 아래에서 모서리를 펴고 화면을 꽉 채우는 갈림목이 있었다(지침 §3 이 금지한 것).
 * 같은 일을 값 하나로 한다 -- 폭을 `min(560px, 100%)` 으로 두면 좁은 화면에서 저절로
 * 화면 폭이 되고, 바깥 여백을 `min(16px, 2vw)` 로 두면 그때 여백도 함께 사라진다.
 * 갈림목이 없으므로 그 언저리에서 화면이 튀지 않는다.
 */

/* 화면 맨 아래의 곁들이는 줄. 자주 쓰지 않는 것은 여기로 내린다. */
.page-foot {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 26px 0 0; padding-top: 16px; border-top: 1px solid var(--gray-200);
  font-size: var(--t-sm); color: var(--gray-500);
}
.page-foot-why { color: var(--ink-3); }
.dot-sep { color: var(--gray-300); }
.linklike {
  background: none; border: 0; padding: 0; margin: 0; font: inherit;
  color: var(--blue-700); cursor: pointer; text-decoration: underline;
  text-underline-offset: 2px;
}
.linklike:focus-visible { outline: none; box-shadow: var(--focus); }

/* 되돌릴 수 없는 조작을 확인받는 창. 무엇이 지워지고 무엇이 남는지 나란히 적는다. */
.modal-lead { margin: 0 0 14px; font-size: var(--t-md); color: var(--err-700); font-weight: 600; }
.pair { margin: 0 0 12px; }
.pair h3 { margin: 0 0 6px; font-size: var(--t-xs); font-weight: 700; color: var(--gray-500);
  letter-spacing: .02em; }
.pair ul { margin: 0; padding-left: 18px; font-size: var(--t-sm); color: var(--gray-600); line-height: 1.75; }
.modal-act { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px;
  padding-top: 14px; border-top: 1px solid var(--gray-200); }
.page-foot a { color: var(--blue-700); text-decoration: underline; text-underline-offset: 2px; }
.page-foot .spacer { flex: 1 1 auto; }

/*
 * 바닥글의 글자 링크도 손가락으로 누른다. 작게 보이되 누르는 자리는 줄이지 않는다.
 * 키오스크에서 옆의 것이 눌리면 그것이 사고가 되는 화면들이다.
 */
.page-foot a, .page-foot .linklike {
  display: inline-flex; align-items: center;
  min-height: var(--touch); padding: 0 2px;
}

/*
 * ── 계정 연결의 두 갈래 ────────────────────────────────────
 *
 * 마법사의 <계정 연결> 하나만 이것을 쓴다 -- 왼쪽은 계정이 있는 사람, 오른쪽은 없는 사람.
 *
 * ── 무엇이 잘못돼 있었나 ─────────────────────────────────
 *
 * 곁길에 회색 바탕을 깔고 테두리로 감싸 <여기가 곁길> 이라고 말하려 했다. 결과는
 * 정반대였다. **채워진 상자는 빈 자리보다 무겁다.** 게다가 그 안에 168px QR 을
 * 세웠으니, 화면에서 가장 큰 물건이 <하지 말아야 할 쪽> 이 됐다. 눈은 큰 것부터
 * 보므로, 설치기사는 매번 QR 을 먼저 보고 나서 왼쪽으로 돌아와야 했다.
 *
 * 축도 어긋나 있었다. 왼쪽은 왼쪽맞춤, 오른쪽은 가운데맞춤이라 두 칸의 글이 서로 다른
 * 선에서 시작했다. 거기에 align-items:start 라 짧은 칸 아래가 허옇게 남아, 카드 바닥이
 * 한쪽만 뚝 끊긴 모양이 됐다.
 *
 * ── 지금 ─────────────────────────────────────────────────
 *
 * 상자를 걷고 **머리카락 선 하나**로만 가른다. 곁길은 채우지 않는다 -- 비어 있는 것이
 * 곧 가볍다는 뜻이고, 이건 색이 아니라 밀도로 하는 말이라 색을 못 보는 사람에게도 통한다.
 * 글자는 한 단계 작고 흐리게, 두 칸의 글은 **같은 축(왼쪽)** 에 세운다.
 * 칸끼리 높이를 맞춰(stretch) 가르는 선이 카드 바닥까지 곧게 내려가게 한다.
 *
 * 좁아지면 위아래로 쌓인다. 그때는 세로선이 뜻을 잃으므로 가로선으로 바꾼다.
 *
 * ── 폭에 숫자를 박지 않는다 ──────────────────────────────
 *
 * 언제 쌓일지를 `@media (max-width: 760px)` 로 정해 두었었다. 그건 **창**의 폭이다.
 * 이 칸이 실제로 얼마나 받았는지와는 다른 이야기라, 옆 목록이 펼쳐져 이 칸만 좁아지면
 * 화면은 아직 넓은 줄 알고 두 칸을 나란히 밀어 넣었다.
 *
 * 이제 폭을 아무에게도 묻지 않는다. 칸마다 바닥값만 주고 **안 들어가면 줄이 바뀌게**
 * 둘 뿐이다. 줄바꿈은 같은 배치가 자리에 맞춰 접히는 것이지 다른 배치가 아니다.
 */
.split {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  /* 좁으면 붙고 넓으면 벌어진다. % 는 이 칸의 폭을 기준으로 재므로 창 크기와 무관하다. */
  gap: clamp(var(--s-5), 4%, var(--s-8));
}

/*
 * 늘어나는 힘을 6 대 1 로 준다. 남는 폭은 대부분 본길이 가져가므로, 나란히 설 때
 * 본길이 넉넉하고 곁길은 제 바닥값 언저리에 머문다 -- 무게가 형태로 드러난다.
 *
 * 두 바닥값을 더한 것(300 + 200)이 곧 **줄이 바뀌는 지점**이다. 그 지점을 어디에도
 * 따로 적지 않는다 -- 적는 순간 두 곳이 어긋날 수 있고, flex 가 이미 알고 있다.
 */
.split-main { flex: 6 1 300px; min-width: 0; }
.split-aside { flex: 1 1 200px; min-width: 0; }

/* 본길. 칸도 단추도 이 폭을 그대로 쓴다 -- 왼쪽 끝과 오른쪽 끝이 모두 한 선에 선다. */
.split-main .input { max-width: none; }
/*
 * 한글을 아무 데서나 끊지 않는다. 칸 폭이 좁아 "폐기합니 / 다." 처럼 낱말이 두 줄에
 * 걸리는 일이 실제로 났다. 낱말 단위로만 끊는다.
 */
.split-main .field-hint, .split-main .field-error { word-break: keep-all; }

/*
 * 곁길. 바탕도 테두리도 없이 **가르는 선 하나**만 둔다.
 * 채우지 않는다 -- 비어 있는 것이 곧 가볍다는 뜻이고, 색이 아니라 밀도로 하는 말이라
 * 색을 못 보는 사람에게도 통한다.
 */
.split-aside {
  /*
   * 가르는 선은 **늘 가로선 하나**다.
   *
   * 폭을 물어(@container) 나란히 설 때는 세로선, 쌓일 때는 가로선으로 바꿨었다.
   * 걷었다 -- 그건 폭으로 **다른 디자인을 만드는 일**이다. 같은 화면이 기기마다
   * 다른 모양이 되면, 한쪽에서 익힌 것이 다른 쪽에서 안 통한다.
   *
   * 가로선 하나로 두 자리를 다 감당한다. 쌓였을 때는 둘을 가르고, 나란히 섰을 때는
   * 곁길 머리 위의 줄이 되어 <여기부터는 딸린 이야기> 라고 말한다. 어느 쪽이든 뜻이 산다.
   */
  border-top: 1px solid var(--line);
  padding-top: clamp(var(--s-4), 3%, var(--s-5));
}

/*
 * ── 손가락과 마우스를 함께 받는다 ──────────────────────────
 *
 * `@media (pointer: coarse)` 로 손가락일 때만 칸을 키웠었다. 걷었다 -- 그것도 화면을
 * 두 벌로 만드는 일이다. **한 벌이 둘 다 감당하게** 값을 정한다.
 *
 *   44px  손가락으로 눌러도 빗나가지 않는 높이. 마우스에는 조금 넉넉할 뿐 해롭지 않다
 *   16px  이 아래로 두면 iOS 가 칸을 누를 때마다 화면을 확대한다. 한 번 확대되면
 *         사람이 손으로 되돌려야 하고, 그 사이 단추는 화면 밖에 있다
 *
 * 이 화면에서 가장 중요한 칸 둘이다. 밀도보다 정확히 누르는 것이 앞선다.
 */
.split-main .input { min-height: 44px; padding: 10px 12px; font-size: var(--t-lg); }

/*
 * 본길 첫 줄. 카드 머리가 <왜 하는가> 를 말했으니 여기는 <누구 계정인가> 만 말한다.
 * 제목이 아니라 안내문이라 굵기를 주지 않는다 -- 굵히면 걷어 낸 제목이 되살아난다.
 */
.split-lead {
  margin: 0 0 var(--s-5);
  font-size: var(--t-md); line-height: 1.55; color: var(--ink-2);
}

/*
 * 곁길의 머리. 카드 제목과 겨루지 않도록 작고 흐리게, 대신 자간을 벌려
 * <이건 딸린 안내다> 라는 인상을 준다.
 */
.split-head {
  margin: 0 0 var(--s-4);
  font-size: var(--t-xs); font-weight: 600; letter-spacing: .04em; color: var(--ink-3);
}

/* 곁길의 잔글씨. 읽지 않아도 되지만 읽으면 안심이 되는 것들. */
.split-note {
  margin: var(--s-3) 0 0;
  font-size: var(--t-sm); line-height: 1.6; color: var(--ink-3);
  word-break: keep-all;
}
.split-note b { color: var(--ink-2); font-weight: 600; }

/*
 * 이 칸의 주 동작은 하나뿐이다. 폭을 채워 그것이 본길임을 형태로 말한다.
 *
 * **높이도 함께 준다.** 폭만 늘리면 430px x 30px 짜리 파란 띠가 되어 단추가 아니라
 * 진행 막대처럼 보였다. 누르는 것은 누르는 것처럼 보여야 한다.
 */
.btn-wide {
  width: 100%; justify-content: center;
  /* 44 는 손가락으로 눌러도 빗나가지 않는 높이다. 위 .split-main .input 과 같은 값. */
  min-height: 44px; font-size: var(--t-md); font-weight: 600;
  margin-top: var(--s-2);
}

/*
 * QR 자리. **흰 바탕을 여기서도 깐다** -- 어두운 화면에서 검은 눈금이 묻히지 않게.
 * 그림 자체에도 바탕이 있지만(app/qr.mjs), 그리기 전의 빈 자리도 흰색이어야
 * 뜨는 순간 네모가 번쩍이지 않는다.
 *
 * 그림자는 걷었다. 가르는 선이 머리카락 한 올인데 그 옆의 QR 만 떠 있으면,
 * 다시 곁길이 본길보다 무거워진다.
 */
.qr {
  /*
   * **칸 가운데** 세운다.
   *
   * inline-block 이라 글자처럼 왼쪽에 붙어 있었고, 제 폭(146)이 칸(216)보다 좁아
   * 오른쪽에 70px 이 남았다. 둘레의 글은 칸을 꽉 채우는데 그림만 한쪽으로 쏠려,
   * 자리를 잘못 잡은 것처럼 보였다.
   *
   * 글을 가운데로 돌리지는 않는다 -- 두 칸의 글이 같은 축에 서 있어야 눈이 한 칸에서
   * 다음 칸으로 곧장 간다. 움직이는 것은 **그림 하나**뿐이다.
   */
  display: block;
  /*
   * **칸을 꽉 채운다.** 146px 짜리 그림이 216px 칸 안에 앉아 오른쪽에 70px 을 남기고
   * 있었다. 둘레의 글은 칸을 다 쓰는데 그림만 작아, 자리를 잘못 잡은 것처럼 보였다.
   *
   * 상한을 두는 이유는 하나다 -- 자리가 좁아 두 칸이 위아래로 쌓이면 이 칸이 카드 폭을
   * 통째로 받는다. 그때 100% 를 그대로 두면 QR 하나가 500px 이 되어 화면을 잡아먹는다.
   * 상한에 걸리면 가운데 선다.
   */
  width: 100%; max-width: 280px;
  margin-inline: auto;
  background: var(--surface); padding: 8px; border-radius: var(--r-sm);
  border: 1px solid var(--line); line-height: 0;
}
/*
 * 그림도 함께 늘어난다. app/qr.mjs 가 붙여 둔 width/height 를 덮어쓴다 --
 * viewBox 가 있어 어느 크기로 늘려도 눈금이 뭉개지지 않는다.
 */
.qr svg { display: block; width: 100%; height: auto; }
/*
 * 주소를 글자로도 적는다. 카메라가 없거나 안 잡히는 자리에서 손으로 칠 수 있어야 한다.
 * 줄이 길어 넘칠 수 있으므로 어디서든 끊는다 -- 카드를 옆으로 밀어내면 안 된다.
 *
 * 글꼴을 여기서 직접 준다. 전에는 표시에 `mono` 를 함께 달아 두었는데 그런 규칙이
 * 이 제품에 없어서, 붙여 놓고도 고정폭이 아니었다.
 */
.qr-url {
  margin: var(--s-2) 0 0;
  font-family: var(--mono);
  font-size: var(--t-xs); color: var(--ink-3); word-break: break-all; line-height: 1.5;
}

/*
 * ── 다 된 걸음 ─────────────────────────────────────────────
 *
 * 붙고 나면 물어볼 것이 없어진다. 그렇다고 칸만 감추면 카드 속이 텅 빈 흰 띠로 남아,
 * 다 된 것인지 덜 그려진 것인지 알 수 없다. 끝났다는 말을 그 자리에 놓는다.
 *
 * 초록으로 통째로 칠하지 않는다. 이건 축하가 아니라 **사실 한 줄**이고,
 * 이 화면에서 가장 세게 보여야 하는 것은 여전히 다음으로 가는 단추다.
 */
/*
 * 이름을 `.done` 으로 지었다가 걷었다. 그 낱말은 이 제품에서 이미 **덧붙이는 표시**로
 * 쓰이고 있다 -- `.progress-fill.done`, `.step.done`. 맨 `.done` 에 규칙을 걸면
 * 진행 막대의 채워진 부분까지 이 규칙을 뒤집어쓴다.
 */
.stepdone {
  display: flex; align-items: flex-start; gap: var(--s-3);
}
.stepdone-mark {
  flex: none;
  width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--ok-soft); border: 1px solid var(--ok-line); color: var(--ok);
}
.stepdone-mark svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
}
.stepdone-body { min-width: 0; }
.stepdone-title {
  margin: 4px 0 0;
  font-size: var(--t-md); font-weight: 600; color: var(--ink);
}
.stepdone-sub {
  margin: var(--s-1) 0 0;
  font-size: var(--t-sm); line-height: 1.6; color: var(--ink-2); word-break: keep-all;
}
.stepdone-sub b { color: var(--ink); font-weight: 600; }
/*
 * 끝난 걸음에서도 할 일이 남을 때가 있다 -- 본사 관제 걸음은 주소가 바뀌면 다시
 * 알려야 하고, 붙은 곳을 끊을 수도 있다. 그 단추는 **끝났다는 말 아래**에 둔다.
 * 다시 해야 하는 일이 아니라 <필요하면 여기> 라는 뜻이라, 작게 나란히 세운다.
 */
.stepdone-act { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-3); }

/*
 * ── 품은 화면 ──────────────────────────────────────────────
 *
 * 마법사가 장치 등록 화면을 그대로 품는다(wizard.html 의 setupFrame). 테두리를 두르지
 * 않는다 -- 카드 안에 또 상자가 생기면 화면 안에 화면이 든 꼴로 읽힌다.
 *
 * 높이를 넉넉히 잡는다. 안쪽 화면이 핀 표까지 펴면 길어지는데, 그때 안에서 또 굴리게
 * 하면 바깥 굴림과 겹쳐 어느 쪽이 움직이는지 알 수 없다.
 */
.embed {
  display: block; width: 100%;
  /* 높이는 화면이 내용에 맞춰 잡는다(app/setup/wizard.mjs 의 fitSetupFrame). */
  min-height: 160px;
  border: 0; margin-top: var(--s-2);
}


/* ── ui-plan.css ─────────────────────────────── */
/*
 * 대시보드의 **도면** -- 시제품(scratchpad/mix/screens.css)에서 **그대로** 옮긴 것.
 *
 * ── 왜 옮겨 적는가 ────────────────────────────────────────
 *
 * 한 번 <같은 뜻으로> 다시 썼다가 되돌렸다. 값을 제품 토큰으로 바꿔 적었더니
 * (램프 22px → --touch 36px, 여백 18px → --s-6, 검사칸을 카드로) 같은 화면이 아니게
 * 되었다. 확정된 것은 뜻이 아니라 **그 화면**이므로, 여기서는 시제품의 값을 그대로 쓴다.
 *
 * 값을 고칠 일이 생기면 시제품 파일과 **함께** 고친다. 두 곳이 갈리면 다음 대조에서
 * 또 <다르다> 가 나온다.
 *
 * ── 토큰 이름만 바꿔 적었다 ───────────────────────────────
 *
 * 시제품은 `--ink3`·`--line2` 를, 제품은 `--ink-3`·`--line-2` 를 쓴다. **값은 같다.**
 * 이름만 제품 것으로 옮겼고 숫자는 하나도 건드리지 않았다.
 */

/*
 * 검사칸이 292px 에 못 박혀 있어 좁은 화면에서 도면이 가로로 밀렸다. 줄바꿈으로 푼다 --
 * 늘어나는 무게를 도면에 몰아 두면 넓은 화면의 모습은 그대로고(검사칸 292), 좁아지면
 * 검사칸이 도면 아래로 내려가 폭을 다 쓴다. 폭을 묻는 갈림목은 두지 않는다(지침 §3).
 */
.plan { display: flex; flex-wrap: wrap; align-items: stretch;
        min-height: calc(100vh - 158px); }
.plan > .canvas { flex: 999 1 560px; min-width: 0; }
.plan > .insp { flex: 1 1 292px; }

.plan .canvas { container-type: inline-size; position: relative; padding: 16px 18px 24px; overflow: auto; background: #fbfcfd; }
.plan .canvas::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image: linear-gradient(90deg, rgba(13, 17, 23, .05) 1px, transparent 1px),
                    linear-gradient(180deg, rgba(13, 17, 23, .05) 1px, transparent 1px);
  background-size: 26px 26px;
}
.plan .tools { position: relative; display: flex; align-items: center; gap: 6px; margin-bottom: 16px; }
.plan .tools .z { margin-left: auto; color: var(--ink-3); font-size: var(--t-xs); }

/*
 * ── 가로로 흐른다 ───────────────────────────────────────────
 *
 * 제어기 하나가 한 줄을 통째로 쓰고 아래로만 자라면, 24인치 와이드에서 오른쪽 절반이
 * 비고 스크롤이 생긴다 -- 도면이 자리를 쓰지 못하면 도면일 이유가 없다.
 *
 *   .plant  제어기 묶음(.chain)이 **흐르며 접힌다**
 *   .chain  `flex:1 1 620px` -- 자리가 되면 둘이 나란히, 좁으면 아래로
 *   .devs   장치가 `auto-fit`. 남는 폭만큼 옆으로 벌어진다
 *   .lamps  `auto-fill` -- 넓으면 16개가 한 줄, 좁으면 8개씩 접힌다
 *
 * 어느 눈금에서 접힐지를 픽셀로 못 박지 않는다(docs/검토지침.md §3).
 */
.plan .plant { position: relative; display: flex; flex-wrap: wrap; align-items: flex-start;
               gap: 20px 26px; }
.plan .chain { display: flex; flex-wrap: wrap; align-items: flex-start; flex: 1 1 620px; min-width: 0; }
.plan .devs { flex: 1; min-width: 0; display: grid; gap: 12px;
              grid-template-columns: repeat(auto-fit, minmax(min(226px, 100%), 1fr)); align-items: start; }

.plan .node { border: 1.5px solid #2c3746; border-radius: 12px; background: #fff;
              padding: 11px 13px 13px; box-shadow: 0 2px 0 rgba(13, 17, 23, .05); min-width: 0; }
.plan .node.ctlbox { flex: none; width: 176px; }
/*
 * 장치가 넷 이상인 제어기는 한 줄을 통째로 가져간다 -- 카드가 여럿이라 자리가 필요하다.
 * **카드 크기는 건드리지 않는다.** 늘어난 것은 줄이 받는다.
 */
.plan .chain.wide { flex: 1 1 100%; }
.plan .node h4 { font-size: var(--t-sm); font-weight: 700; margin: 0; }
.plan .node > s { text-decoration: none; display: block; color: var(--ink-3); font-size: var(--t-xs);
                  margin: 1px 0 10px; }
.plan .node.ctl { border-color: var(--accent); background: #f7faff; }

/*
 * ══ 이 그림에서 **내가 어디인가** ═════════════════════════════
 *
 * 도면은 현장 전체를 그린다 -- 본사 · 중앙 제어기 · 제어기들 · 장치들. 그 안에서 지금
 * 보고 있는 사람이 **어느 보드 앞에 서 있는지**가 안 보였다. 이름만으로는 못 가린다:
 * 중앙 제어기를 겸하는 보드는 같은 이름이 위(중앙 제어기 자리)와 아래(현장 제어기 묶음)
 * 두 곳에 서기 때문에, 처음 보는 사람은 보드가 두 대인 줄 안다.
 *
 * 그래서 **맡은 자리마다 테두리를 두른다.** 두 자리에 같은 테두리가 서면 그것은
 * <한 보드가 두 자리를 맡았다> 로 읽힌다 -- 겸하지 않는 보드는 아래 한 곳만 둘러진다.
 *
 * ── 왜 테두리이고 왜 채우지 않는가 ────────────────────────
 *
 * 파랑은 이미 두 가지로 쓰인다 -- 지금 **짚은** 장치(`.node.dev.is-picked`)와 누를 수
 * 있다는 표시다. 같은 색으로 세 번째 뜻을 더하려면 모양이 갈려야 한다.
 *
 *   짚은 것   칸 하나를 **채운다**(accent-soft). 잠깐이고, 누르면 옮겨 간다
 *   이 보드   묶음을 **두른다**. 안 바뀌는 사실이고, 배경을 먹지 않는다
 *
 * 채우지 않는 쪽을 고른 까닭이 하나 더 있다. 이 테두리 안에는 설비 상태를 말하는 색이
 * 들어 있다(램프의 초록 · 확인 중의 주황). 바탕을 칠하면 그 색들이 한 겹 위에 뜨면서
 * 대비가 무너진다 -- 이 화면에서 가장 비싼 값이 그것이다.
 */
/*
 * 층이 둘이라 두르는 대상이 갈린다. **표시는 같다** -- 층을 오가는 사람이 같은 테두리를
 * 같은 뜻으로 읽어야 한다.
 *
 *   보드의 도면   이 보드가 맡은 자리 (중앙 제어기 상자 · 제 제어기 묶음)
 *   본사의 도면   그 현장에서 **관제를 맡은 보드** (prod/opt/lis-hq/ui/plan.mjs)
 *
 * 본사에는 <나> 가 없으니, 그 자리를 대신하는 물음이 <누가 관제를 맡았는가> 다.
 */
.plan .node.ctlbox.is-me,
.plan .node.dev.is-me { border-width: 2px; border-color: var(--accent); }
.plan .slave-grp.is-me {
  border: 2px solid var(--accent);
  border-radius: 12px;
  /* 위쪽은 머리글이 이미 여백을 가지므로 조금 덜 준다. */
  padding: 9px 12px 12px;
}
/* 두른 묶음의 머리글은 흐리지 않는다 -- 테두리가 가리키는 곳이 여기다. */
.plan .slave-grp.is-me > .slave-head { color: var(--ink); }
.plan .node.add { border: 1.5px dashed #c3cbd6; color: var(--ink-3); display: grid;
                  place-items: center; min-width: 150px; font-size: var(--t-sm); box-shadow: none;
                  background: transparent; text-align: center; }

/* ── 접힌 장치 ──────────────────────────────────────────────
 * 이름 · 켜짐 수 · 6px 상태 띠만. **누르는 자리가 아니라 읽는 자리**다.
 */
.plan .node.dev { display: grid; gap: 0; align-content: start; }
.plan .node .nh { display: flex; align-items: center; gap: 6px; }
.plan .node .nh h4 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.plan .node .fold { margin-left: auto; border: 0; background: transparent; color: var(--ink-3);
                    font-size: var(--t-md); line-height: 1; padding: 2px 4px; cursor: pointer; }
.plan .node.shut { border-color: var(--line-2); }
.plan .node.shut:hover { border-color: #2c3746; }

/*
 * 지금 고른 장치. **도면과 검사칸이 같은 것을 가리킨다**(app/dash/plan.mjs).
 * 도면에서 고른 것이 위에서 열리는데 도면 쪽에 아무 표시가 없으면, 한 화면 안에서
 * 두 자리가 서로 다른 것을 보고 있는 것처럼 읽힌다.
 */
.plan .node.dev.is-picked { border-color: var(--accent); background: var(--accent-soft); }
/* 누를 수 있는 칸이라는 표시. 도면의 장치 칸은 고르는 자리다. */
.plan .node.dev[data-dev] { cursor: pointer; }
.plan .node.dev[data-dev]:hover { border-color: var(--accent); }
.plan .strip { display: flex; gap: 3px; margin: 9px 0 7px; }
.plan .strip i { flex: 1; height: 6px; border-radius: 2px; background: #e4e8ee; font-style: normal;
                 min-width: 4px; }
.plan .strip i.on { background: var(--ok); }
.plan .shutline { font-size: var(--t-xs); color: var(--ink-3); }
/* 들어오는 주소 -- 본사 관제의 <열기 →> 와 같은 부품(.go)을 줄마다 쓴다. */
.plan .ways { display: flex; flex-direction: column; align-items: flex-start; }
.plan .node.ctlbox .ways .go { margin-top: 6px; }
.plan .node.ctlbox .ways .go:first-child { margin-top: 0; }
/*
 * 현장 이름을 고치는 줄.
 *
 * 상자가 176px 로 좁다(위 .ctlbox). 기본 칸 크기를 그대로 쓰면 글자 칸만으로 줄이
 * 차서 단추가 아래로 밀려 내려간다 -- 그러면 두 줄짜리 상자가 되고, 구성도에서
 * 현장 상자만 유독 길어진다. 이 줄에서만 작게 쓴다.
 */
.plan .rename { display: flex; gap: 4px; align-items: center; }
.plan .rename input { flex: 1 1 auto; min-width: 0; padding: 4px 6px;
                      font-size: var(--t-xs); border: 1px solid var(--line-strong);
                      border-radius: 6px; background: #fff; color: var(--ink); }
.plan .rename .btn { flex: none; padding: 4px 8px; font-size: var(--t-xs); }
/* 제어기 노드의 <맡은 일> 배지들. 줄이 좁으므로 접힌다. */
.plan .plan-pills { display: flex; gap: 5px; flex-wrap: wrap; }

.plan .wire { flex: none; width: 44px; height: 2px; background: #c3cbd6; margin-top: 52px;
              position: relative; }
.plan .wire s { position: absolute; top: -16px; left: 2px; font-size: 10.5px; color: var(--ink-3);
                text-decoration: none; white-space: nowrap; }

.plan .grp { margin-bottom: 10px; }
.plan .grp:last-child { margin-bottom: 0; }
.plan .grp b { display: block; font-size: 10.5px; font-weight: 700; color: var(--ink-3);
               letter-spacing: .05em; margin-bottom: 6px; }
.plan .lamps { display: grid; grid-template-columns: repeat(auto-fill, 22px); gap: 6px; }
/*
 * 램프 22px.
 *
 * 손가락으로 누르는 것은 36px 아래로 내려가지 않는다는 규칙이 있는데, **이 자리는 예외**다.
 * 도면은 사무실 화면에서 훑는 자리이고(현장에서 손으로 누르는 것은 조작 패널이다),
 * 22px 이어야 열여섯 개가 한 줄에 서서 <몇 개가 켜졌나> 가 한눈에 읽힌다.
 * 실제로 켜고 끄는 단추는 오른쪽 검사칸의 44px 짜리 둘이다.
 */
.plan .lamp { width: 22px; height: 22px; border-radius: 6px; border: 1.5px solid #c3cbd6;
              background: #f1f3f6; cursor: pointer; padding: 0; position: relative; }
.plan .lamp:hover { border-color: var(--ink-3); }
.plan .lamp.on { background: var(--ok); border-color: #0c6647; }
/* 강제된 값은 켜진 것과 **다른 모습**이다. 배선이 아니라 화면이 만든 값이다. */
.plan .lamp.forced { background: var(--warn-soft); border-color: var(--warn); }
.plan .lamp.in { border-radius: 99px; cursor: default; }
.plan .lamp.in.on { background: #7fd0b2; border-color: #4fae8c; }
.plan .lamp.sel { outline: 2px solid var(--accent); outline-offset: 2px; }
/*
 * 번호. 관제 격자(ui-drawer.css 의 `.lamp-n`)와 같은 규칙이다 -- 평소에는 물러나 있되 읽히는
 * 선(대비 4.7)까지 올리고, 켜진 칸에서 또렷해진다. 입력은 바탕이 연초록이라 흰 글자가 사라지므로
 * 어두운 글자를 쓴다. 번호가 눌림을 가로채지 않게 한다.
 */
.plan .lamp { display: flex; align-items: center; justify-content: center; }
.plan .lamp .lamp-n { font-size: 10px; line-height: 1; font-weight: 600; color: var(--ink-3);
                      font-variant-numeric: tabular-nums; pointer-events: none; }
.plan .lamp.on .lamp-n { color: #fff; }
.plan .lamp.in.on .lamp-n { color: var(--ink); }
.plan .lamp.sel .lamp-n { color: var(--accent); }
/* 켜진 칸에서는 선택이 글자색을 덮지 않는다(ui-drawer.css 의 같은 자리 주석). */
.plan .lamp.on.sel .lamp-n { color: #fff; }
.plan .lamp.in.on.sel .lamp-n { color: var(--ink); }
/*
 * 다른 제어기의 램프. **보는 값이고 누르는 것이 아니다.**
 *
 * 도면은 현장 전체를 훑는 자리라 옆 라인의 값까지 편다(app/dash/plan.mjs). 그런데 거기서
 * 누르면 그 보드에 원격으로 명령을 내리는 일이 되고, 그것은 구독이 가리는 관제의 일이다.
 *
 * 그래서 `<span>` 으로 낸다 -- 키보드로 짚히지도, 눌리지도 않는다. 손 모양도 바꾸지
 * 않는다. 값은 색으로 그대로 읽힌다.
 */
.plan .lamp.is-view { cursor: default; }
.plan .lamp.is-view:hover { border-color: #c3cbd6; }


/* ── 검사칸 ─────────────────────────────────────────────── */
.plan .insp { border-left: 1px solid var(--line); padding: 16px; display: flex;
              flex-direction: column; }
.plan .insp .nm { display: flex; align-items: center; gap: 8px; }
.plan .insp .nm h3 { font-size: var(--t-lg); font-weight: 600; letter-spacing: -.02em; margin: 0; }
.plan .insp .nm button { margin-left: auto; border: 0; background: transparent; color: var(--ink-4);
                         font-size: var(--t-lg); cursor: pointer; }
.plan .insp .nm button.on { color: var(--mark); }
.plan .insp .m { color: var(--ink-3); font-size: var(--t-xs); margin: 2px 0 14px; }
.plan .insp .kv div { display: flex; justify-content: space-between; gap: 10px; padding: 9px 0;
                      border-top: 1px solid var(--line); font-size: var(--t-sm); }
.plan .insp .kv s { text-decoration: none; color: var(--ink-3); }
.plan .insp .two { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 16px 0 0; }
.plan .insp .two button { height: 44px; border: 1px solid var(--line-2); border-radius: 10px;
                          background: #fff; font-size: var(--t-md); font-weight: 600; cursor: pointer; }
.plan .insp .two button.on { background: var(--ok); border-color: var(--ok); color: #fff; }
/*
 * 다른 화면으로 가는 길. **단추 줄**이다.
 *
 * 파란 글 두 줄이었다(`.go`). 누르는 것과 읽는 것이 모양으로 갈리지 않았고, 손가락으로
 * 짚기에는 표적이 글자 높이뿐이었다. 이 칸의 다른 누를 것들이 전부 단추라 더 그랬다.
 *
 * 좁아지면 접힌다. 가로로 넘치게 두지 않는다.
 */
.plan .insp .insp-go {
  display: flex; flex-wrap: wrap; gap: var(--s-2);
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
}

/*
 * ── 사슬의 **세 번째 층** ────────────────────────────────
 *
 * 도면이 네 층이 되었다 -- 본사 관제 ─ 중앙 제어기 ─ **현장 제어기들** ─ 장치들.
 * 오래 가운데 두 층을 건너뛰고 <이 보드와 제 장치> 만 그렸는데, 그러면 현장에 제어기가
 * 셋이어도 도면에는 하나만 선다(app/dash/plan.mjs 의 draw).
 *
 * 제어기마다 제 장치를 품는다. 칸을 나란히 늘어놓고 선으로 잇는 대신 **품게** 하는
 * 것은, 어느 장치가 어느 제어기의 것인지가 자리로 드러나야 하기 때문이다 -- 선으로
 * 이으면 제어기가 셋만 되어도 선이 엉킨다.
 */
.plan .slaves { flex: 1 1 280px; min-width: 0; display: grid; gap: 14px; }
/*
 * 제어기 박스는 **안의 장치 카드보다 좁아지지 않는다.** 여기에 `min-width: 0` 이 있었다 -- 박스가
 * 남은 폭(207px)까지 줄어드는데 카드 칸은 최소 260px 이라, 카드가 박스(중앙 제어기는 파란 테두리)
 * 밖으로 삐져나와 잘렸다. 좁은 화면에서는 박스째 옆으로 밀려 도면의 가로 스크롤이 받는다.
 */
/* 2026-10-21: min-content 를 걷었다 -- 모바일에서 박스가 카드째 밀려 도면이 잘렸다.
 * 대신 카드 칸이 박스 폭을 넘지 않게(min(260px,100%)) 하고, 사슬은 좁으면 줄바꿈한다. */
.plan .slave-grp { min-width: 0; }
.plan .slave-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px;
  margin-bottom: 8px; color: var(--ink-2);
}
.plan .slave-head b { font-size: var(--t-sm); font-weight: 700; }
.plan .slave-head s { text-decoration: none; color: var(--ink-3); font-size: var(--t-xs); }
/* 제어기 하나가 품은 장치들. 바깥 `.devs` 와 같은 규칙이라 칸 크기가 같다. */
.plan .slave-grp > .devs { display: grid; gap: 12px;
                           grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }
/* 사슬 맨 위의 본사 관제로 가는 문. */
.plan .node.ctlbox .go { display: inline-block; margin-top: 10px; font-size: var(--t-xs);
                         color: var(--accent); font-weight: 500; text-decoration: none; }
.plan .node.ctlbox .go:hover { text-decoration: underline; }

/*
 * ── 세로 폰: 사슬이 위에서 아래로 선다 ──────────────────────
 *
 * 가로로 늘어놓은 사슬(본사 ─ 중앙 ─ 제어기)은 줄바꿈만으로는 선이 어긋난다. 창이 아니라
 * **도면 칸**의 폭을 묻는다(container query) -- 검사칸이 옆에 붙어 칸이 좁아진 넓은 창에서도
 * 같은 규칙이 선다. 박스는 칸을 다 쓰고, 연결선은 세로로 눕는다.
 */
@container (max-width: 560px) {
  .plan .chain { flex-direction: column; align-items: stretch; flex-basis: 100%; }
  .plan .node.ctlbox { width: auto; }
  .plan .wire { width: 2px; height: 30px; margin: 0 0 0 28px; }
  .plan .wire s { top: 6px; left: 12px; }
  .plan .slaves { flex: none; }
}

/* ── ui-drawer.css ─────────────────────────────── */
/*
 * 서랍함 -- 시제품(scratchpad/mix/screens.css)에서 **그대로** 옮긴 것.
 *
 * 관제와 현장 제어기가 같은 부품을 쓰고, 루트만 다르다.
 *
 *   관제        제어기 › 장치   (핀은 목록에 없다. 장치를 고르면 상세에 격자로 펼쳐진다)
 *   현장 제어기  장치 › 핀      (이 보드 하나. 핀이 목록의 주인공이다)
 *
 * 값을 고칠 일이 생기면 시제품 파일과 **함께** 고친다. 토큰 이름만 제품 것으로 옮겼고
 * (`--ink3`→`--ink-3`, `--soft`→`--surface-2`) 숫자는 하나도 건드리지 않았다.
 */

/*
 * ── 세 칸은 **접힌다** ──────────────────────────────────────
 *
 * `grid-template-columns: 176px 336px 1fr` 이었다. 앞의 둘이 못이라 **512px 아래에서는
 * 접힐 수가 없었고**, 좁은 화면에서 화면이 통째로 가로로 밀렸다 -- 이 제품이 금지한 것이
 * 바로 그것이다(docs/검토지침.md §3).
 *
 * 폭을 묻는 갈림목(@media)을 두지 않고 **줄바꿈**으로 푼다. 늘어나는 무게를 상세칸에
 * 몰아 두면(999) 넓은 화면에서는 앞의 둘이 제 크기(176 · 336)에 서고 남는 폭은 전부
 * 상세칸이 가져간다 -- 지금과 같은 모습이다. 좁아지면 들어가지 못하는 칸부터 아래로
 * 내려가고, 혼자 남은 줄에서는 그 칸이 폭을 다 쓴다.
 */
.dw { display: flex; flex-wrap: wrap; align-items: stretch; align-content: flex-start;
      container-type: inline-size; }
/* 높이는 서랍함이 아니라 **칸**이 갖는다 -- 서랍함이 min-height 를 가지면 줄바꿈된 줄들이 남는
 * 높이를 나눠 가져, 좁은 화면에서 맨 위 줄(보기)만 수백 px 로 늘어났다. */
.dw > * { min-height: calc(100vh - 158px); }
/*
 * ── 아래에 더 있으면 **화면을 다 먹지 않는다** ────────────
 *
 * 서랍함이 창 높이를 통째로 차지한다. 그것이 이 화면의 전부일 때는 맞다 -- 세 칸이
 * 화면 끝까지 서야 도구처럼 보인다.
 *
 * 그런데 아래에 더 있는 화면이 생겼다(현장 제어기의 도면). 장치가 하나뿐인 판에서
 * 서랍함은 300px 을 쓰고 400px 을 비워 두는데, 그 빈 자리를 지나 굴려야 도면이 나온다 --
 * **화면이 끝난 줄 알고 아무도 안 내린다.**
 *
 * 마크업이 <아래에 더 있다> 고 말한다. CSS 가 스스로 알 수 있는 사실이 아니다 --
 * `:last-child` 로는 안 된다(관제에서는 숨은 구획들이 뒤에 남아 있다).
 *
 * **지금은 쓰는 화면이 없다.** 현장 제어기 아래에 도면을 두었다가 걷었기 때문이다
 * (대시보드가 같은 그림을 그려서 한 화면을 두 번 보여 주는 꼴이었다). 규칙을 남겨
 * 두는 것은 서랍함 아래에 무언가를 붙이는 화면이 또 생길 것이기 때문이고, 그때
 * 이 함정을 다시 밟지 않게 하려는 것이다.
 */
.dw.has-below, .dw.has-below > * { min-height: 0; }
.dw > .rail { flex: 1 1 176px; max-width: 176px; }
.dw > .list, .dw > .panel.list { flex: 1 1 336px; max-width: 336px; }
.dw > .det, .dw > .panel.det { flex: 999 1 460px; }

/*
 * ── 세 칸은 **함께 선다** ────────────────────────────────
 *
 * 화면 전환은 `.panel.active` 하나가 정하는데(app/ui-base.css), 줄기와 옆칸만은 동시에
 * 서야 한다 -- 제어기를 열면 목록이 사라져 옆 제어기를 보려고 한 번 나갔다 다시
 * 들어가야 했다. 여는 쪽이 `.is-both` 를 붙이고, 그때만 줄기가 `.active` 없이도 보인다.
 */
.dw.is-both > .list { display: flex; }

/*
 * ── 줄기 묶음 **밖의** 화면이 서면 서랍함은 통째로 물러난다 ─
 *
 * 장치 등록 · 구성 · 조작 이력은 서랍함이 아니라 제 화면이고, 마크업에서도 이 묶음
 * **바깥**에 있다(master.html). 그런데 레일은 `.panel` 이 아니어서 늘 서 있었고 옆칸도
 * 안내를 적은 채 남아, <관제 › 장치 등록> 을 열면 화면 위쪽을 **빈 서랍함 한 통**이
 * 차지하고 정작 장치 등록은 그 아래로 밀렸다 -- 한 화면이 통째로 비어 보였다.
 *
 * `is-both` 가 이미 그 구분을 들고 있다(app/master/screen.mjs 의 show).
 */
.dw:not(.is-both) { display: none; }

/*
 * 옆칸을 아직 안 열었으면 그 자리에 안내만 선다 -- **빈 칸은 고장으로 읽힌다.**
 *
 * 기준은 `is-both` 가 아니라 <이 칸이 열렸는가>(.active) 다. 줄기가 선 순간 `is-both` 는
 * 붙으므로(제어기를 아직 안 골랐어도), 그것으로 가르면 관제를 처음 열었을 때 오른쪽이
 * 그냥 흰 칸이 된다 -- 실제로 그렇게 떴다.
 *
 * `.panel` 인 옆칸에만 건다. 현장 제어기 화면의 옆칸은 갈아 끼우는 구획이 아니라
 * 늘 서 있는 칸이라 `.panel` 을 붙이지 않았고(slave.html), 그쪽은 제 안내를 스스로 적는다.
 */
.dw.is-both > .panel.det { display: block; }
.dw.is-both > .panel.det:not(.active) > * { display: none; }
/*
 * 아직 아무것도 안 고른 칸.
 *
 * 한 줄만 적어 두었더니 화면의 절반이 **문장 하나만 떠 있는 흰 칸**이 됐다. 이 칸이
 * 화면에서 가장 넓은데, 거기 적힌 것이 <고르십시오> 한 줄이면 무엇을 고르면 무엇이
 * 나오는지는 눌러 봐야 안다.
 *
 * 그래서 **무엇이 서는 자리인지**를 먼저 적고, 그 다음에 무엇을 하라고 적는다.
 * 제목과 설명 두 줄이라 `::before` 와 `::after` 로 나눈다.
 */
.dw.is-both > .panel.det:not(.active) {
  padding: 22px;
  color: var(--ink-3);
}
.dw.is-both > .panel.det:not(.active)::before {
  content: '제어기 하나';
  display: block; color: var(--ink); font-size: var(--t-lg); font-weight: 650;
}
.dw.is-both > .panel.det:not(.active)::after {
  content: '왼쪽에서 제어기나 장치를 고르면 그 핀이 여기 격자로 섭니다 — 지금 값을 보고, 여기서 켜고 끕니다.';
  display: block; margin-top: 6px; max-width: 46ch;
  font-size: var(--t-sm); line-height: 1.6;
}
/* 이 묶음 밖의 화면(장치 등록 · 구성 · 조작 이력)은 제 폭을 다 쓴다. */
.dw > .panel:not(.list):not(.det) { flex: 1 1 100%; }

/* ── 왼쪽 보기 ─────────────────────────────────────────── */
/*
 * `display: block` 을 **반드시 적는다.**
 *
 * 마법사의 단계 표시도 `.rail` 이라는 이름을 쓰고 있고, 그쪽은 `display: grid` 에
 * `grid-auto-flow: column` 이다(app/ui-shell.css). 여기서 display 를 정하지 않으면 그 규칙이
 * 남아, 보기 네 줄이 **세로 글자 네 칸**으로 늘어선다. 실제로 그렇게 떴다.
 */
.dw .rail { display: block; border-right: 1px solid var(--line); padding: 12px 0;
            background: var(--surface-2); }
.dw .rail .g { padding: 14px 14px 4px; font-size: 10.5px; font-weight: 700; color: var(--ink-3);
               letter-spacing: .06em; }
.dw .rail a { display: flex; align-items: center; gap: 8px; padding: 7px 14px; font-size: var(--t-sm);
              color: var(--ink-2); cursor: pointer; text-decoration: none; }
.dw .rail a:hover { color: var(--ink); }
.dw .rail a .n { margin-left: auto; color: var(--ink-3); font-size: var(--t-xs); }
.dw .rail a.on { background: #fff; color: var(--ink); font-weight: 600;
                 box-shadow: inset 2px 0 0 var(--accent); }

/* ── 줄기 ──────────────────────────────────────────────── */
.dw .list { border-right: 1px solid var(--line); display: flex; flex-direction: column;
            min-width: 0; }
.dw .list .f { padding: 8px 12px; border-bottom: 1px solid var(--line); }
/*
 * 검색칸은 **줄 옆에 서는 도구**다 -- 목록보다 눈에 띄면 안 된다. 일반 입력칸(회색 바탕 · 안쪽
 * 그림자 · 굵은 글자)을 그대로 쓰면 목록에서 혼자 튀었다. 바탕을 걷고 그림자를 없애고, 돋보기를
 * 앞에 두어 <입력칸> 이 아니라 <찾기> 로 읽히게 한다. 높이도 목록 줄(.it)에 맞춘다.
 */
.dw .list .f .input {
  height: 34px; padding: 0 10px 0 32px; font-size: var(--t-sm);
  background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238a94a3' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='6.5'/%3E%3Cpath d='m20 20-4-4'/%3E%3C/svg%3E") no-repeat 10px 50% / 16px;
  border-color: var(--line); border-radius: 17px; box-shadow: none; max-width: none;
}
.dw .list .f .input::placeholder { color: var(--ink-3); }
.dw .list .f .input:focus { background-color: #fff; border-color: var(--accent); }
.dw .list .items { overflow: auto; }
.dw .it { position: relative; padding: 10px 12px; border-bottom: 1px solid var(--line);
          display: grid; gap: 2px; cursor: pointer; }
.dw .it:hover { background: var(--surface-2); }
.dw .it.on { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
.dw .it .a { display: flex; align-items: center; gap: 7px; min-width: 0; }
.dw .it .a b { font-size: var(--t-sm); font-weight: 600; overflow: hidden; text-overflow: ellipsis;
               white-space: nowrap; }
.dw .it .a s { text-decoration: none; margin-left: auto; color: var(--ink-3); font-size: var(--t-xs);
               flex: none; }
.dw .it .b { color: var(--ink-3); font-size: var(--t-xs); padding-left: 23px; }

/*
 * 계층을 들여쓰기로 그린다. 제어기 › 장치 › 핀.
 * 보드에 장치가 둘이면 **핀 번호가 겹치므로**, 들여쓰기가 없으면 같은 <핀 32> 가
 * 두 번 서서 어느 것인지 알 수 없다.
 */
.dw .it.d1 { padding-left: 26px; }
.dw .it.d2 { padding-left: 42px; }
.dw .it.d3 { padding-left: 58px; }
.dw .it.d1::before, .dw .it.d2::before, .dw .it.d3::before {
  content: ''; position: absolute; top: 0; bottom: 0; border-left: 1px solid var(--line);
}
.dw .it.d1::before { left: 16px; }
.dw .it.d2::before { left: 32px; }
.dw .it.d3::before { left: 48px; }
.dw .it.d0 .a b { font-weight: 700; }
.dw .it.d2 .a b { font-weight: 500; }
.dw .it.d3 .a b { font-weight: 500; }

/* 가지를 여는 단추. **줄 전체가 아니라 이 단추만** 여닫는다 -- 고르기와 펼치기는 다른 일이다. */
.dw .it .tw { width: 16px; height: 16px; flex: none; border: 0; background: transparent;
              padding: 0; color: var(--ink-3); font-size: var(--t-xs); line-height: 16px;
              text-align: center; cursor: pointer; }
.dw .it .tw:hover { color: var(--ink); }
/*
 * 화살표는 16px 라 **겨냥하기 어렵다.** 눈에 보이는 크기는 두고 눌리는 자리만 넓힌다 --
 * 안 보이는 가장자리를 눌러도 펼쳐진다. 위아래는 줄 안에서, 왼쪽은 들여쓰기 쪽으로만 넓혀
 * 옆 줄이나 옆 단추를 가로채지 않는다.
 */
.dw .it .tw { position: relative; }
.dw .it button.tw::after { content: ''; position: absolute; inset: -12px -6px -12px -14px; }
/*
 * 갈래 글리프. `--ink-4` 라 대비 1.88 이었다 -- 보이라고 둔 표시가 안 보이면
 * 그 자리는 빈칸과 같다. 읽히는 선까지 올린다(비활성용 색은 글자에 쓰지 않는다).
 */
.dw .it .ki { color: var(--ink-3); font-size: var(--t-xs); flex: none; }
/*
 * ── 표시등은 **네모다** (2026-10-04) ──────────────────────
 *
 * 7px 짜리 상태 표시등이 세 곳에서 `border-radius: 99px` -- 동그라미였다. 그런데 같은
 * 뜻의 표시등이 `.dot` 에서는 네모다(app/ui-status.css). 거기 적어 둔 까닭은 이것이다 --
 * **동그라미는 소비자 앱의 모양이고, 계기의 램프는 네모이거나 네모에 가깝다.**
 *
 * 한 화면 안에서 같은 뜻이 두 모양이면 사람은 그 둘이 다른 것이라고 읽는다. 맞춘다.
 *
 * ※ 입력 핀(`.lamp.in`)의 동그라미는 그대로 둔다. 거기서는 모양이 **누를 수 있는가**를
 *   말한다 -- 네모는 누르는 것, 동그라미는 읽기만 하는 것이다. 뜻이 다르다.
 */
.dw .it i { width: 7px; height: 7px; border-radius: 1px; background: var(--line-2);
            font-style: normal; flex: none; }
.dw .it i.on { background: var(--ok); }
.dw .it i.er { background: var(--err); }
/*
 * 손봐야 할 것. **이 줄이 없었다** -- 줄기가 `warn` 을 보내도 칠할 규칙이 없어 기본
 * 회색으로 떨어졌고, 확인 중인 장치와 고정해 둔 값이 정상인 줄과 똑같아 보였다.
 * 지도 범례는 그것을 주황이라고 적어 두고 있었다.
 */
.dw .it i.wr { background: var(--warn); }

/* ── 오른쪽 상세칸 ─────────────────────────────────────── */
.dw .det { padding: 20px 22px; min-width: 0; }
.dw .det .nm { display: flex; align-items: center; gap: 10px; }
.dw .det .nm h3 { font-size: 19px; font-weight: 600; letter-spacing: -.02em; margin: 0; }
.dw .det .nm button { border: 0; background: transparent; color: var(--ink-4); font-size: var(--t-lg);
                      cursor: pointer; }
.dw .det .nm button.on { color: var(--mark); }
.dw .det .m { color: var(--ink-3); font-size: var(--t-sm); margin: 3px 0 18px; }
.dw .det .two { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; max-width: 320px; }
.dw .det .two button { height: 46px; border: 1px solid var(--line-2); border-radius: 10px;
                       background: #fff; font-size: var(--t-md); font-weight: 600; cursor: pointer; }
.dw .det .two button.on { background: var(--ok); border-color: var(--ok); color: #fff; }
.dw .det .kv { margin-top: 20px; max-width: 460px; }
.dw .det .kv div { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0;
                   border-top: 1px solid var(--line); font-size: var(--t-sm); }
.dw .det .kv s { text-decoration: none; color: var(--ink-3); white-space: nowrap; }
.dw .det .kv .input { height: 28px; max-width: 220px; }

/*
 * 관제의 상세칸에 서는 핀 격자.
 *
 * **관제에서 핀이 서는 자리는 여기 하나다.** 목록에 핀을 늘어놓지 않는 대신, 장치를
 * 고르면 그 장치의 핀이 여기 펼쳐진다 -- 회선 건너편 보드도 이 자리에서 조작된다.
 */
.dw .det .pins { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }
.dw .det .pins .grp + .grp { margin-top: 12px; }
.dw .det .pins .grp > b { display: block; font-size: 10.5px; font-weight: 700;
                          color: var(--ink-3); letter-spacing: .05em; margin-bottom: 7px; }
/*
 * ── 여덟 칸씩 접는다 ──────────────────────────────────────
 *
 * `auto-fill` 로 두었더니 폭에 들어가는 만큼 채웠다 -- 열여섯 개가 **14 + 2** 로 접혀
 * 마지막 줄에 두 개만 남았다. 배선은 여덟씩 묶여 있고(단자대도 그렇다) 사람도 그렇게
 * 센다. 폭을 따라 접는 자리가 매번 달라지면 같은 장치가 창 크기마다 다른 모양이 된다.
 *
 * `minmax(0, 28px)` 인 것은 **가로로 넘치지 않게** 하기 위해서다 -- 좁은 창에서는 칸이
 * 줄어들 뿐 줄이 밀려나거나 가로 스크롤이 생기지 않는다.
 */
.dw .det .lamps { display: grid; grid-template-columns: repeat(8, minmax(0, 28px)); gap: 6px; }
.dw .det .lamp { width: 100%; aspect-ratio: 1; border-radius: 7px; border: 1.5px solid #c3cbd6;
                 background: #f1f3f6; padding: 0; cursor: pointer;
                 display: flex; align-items: center; justify-content: center; }
/*
 * 번호. 평소에는 물러나 있고 켜진 칸에서만 또렷해진다(app/master/pingrid.mjs 의 주석).
 *
 * ── 물러난 것과 **안 보이는 것은 다르다** (2026-10-04) ──────
 *
 * `--ink-4` 였다. 꺼진 칸 바탕(#f1f3f6) 위에서 대비 **1.92** 다 -- 10px 글자가
 * 그 대비면 물러난 게 아니라 없는 것이다. 띄워 놓고 재 보고 알았다.
 *
 * 핀 번호는 **읽으라고 적어 둔 값**이다. 설비를 만지는 사람이 <41번이 어디인가> 를
 * 찾는 자리이고, 못 찾으면 옆 핀을 누른다. 물러나 있되 읽히는 선(4.71)까지 올린다.
 */
.dw .det .lamp .lamp-n { font-size: 10px; line-height: 1; font-weight: 600;
                         color: var(--ink-3); font-variant-numeric: tabular-nums; }
.dw .det .lamp.on { background: var(--ok); border-color: #0c6647; }
.dw .det .lamp.on .lamp-n { color: #fff; }
.dw .det .lamp.sel .lamp-n { color: var(--accent); }
/*
 * 선택(sel)이 켜짐(on)의 글자색을 **덮어쓰면 안 된다.** 바로 위 규칙이 뒤에 와서, 켜진 출력 핀을
 * 짚으면 짙은 초록 바탕에 파란 번호가 섰다(대비 1.9). 바탕이 켜진 색이면 글자도 켜진 색의 짝이다 --
 * 선택은 바깥 테두리(outline)가 말한다.
 */
.dw .det .lamp.on.sel .lamp-n { color: #fff; }
.dw .det .lamp.in.on.sel .lamp-n { color: var(--ink); }
/* 강제된 값은 켜진 것과 **다른 모습**이다. 배선이 아니라 화면이 만든 값이다. */
.dw .det .lamp.forced { background: var(--warn-soft); border-color: var(--warn); }
/*
 * 입력 핀. **동그라미인 것은 누를 수 없다는 뜻**이다 -- 출력은 네모, 입력은 동그라미.
 * 모양이 손을 안내한다(상태 표시등을 네모로 맞춘 것과는 다른 이야기다, 위 주석).
 */
.dw .det .lamp.in { border-radius: 99px; cursor: default; }
.dw .det .lamp.in.on { background: #7fd0b2; border-color: #4fae8c; }
/*
 * 켜진 **입력** 핀의 번호는 어두워야 한다 (2026-10-04).
 *
 * 위의 `.lamp.on .lamp-n` 이 흰 글자를 준다. 출력은 켜지면 바탕이 짙은 초록(`--ok`)이라
 * 맞는데, 입력은 연초록이라 **흰 글자가 대비 1.82** 로 사라진다 -- 16개가 켜진 줄에서
 * 번호를 하나도 못 읽었다. 같은 `on` 이라도 바탕이 다르면 글자도 달라야 한다.
 */
.dw .det .lamp.in.on .lamp-n { color: var(--ink); }
.dw .det .lamp.sel { outline: 2px solid var(--accent); outline-offset: 2px; }

/* 격자 아래에 붙는 <짚은 핀> 칸. 자리를 옮기지 않고 조작하는 곳이다. */
.dw .det .picked { position: sticky; bottom: 0; margin-top: 14px; border: 1px solid var(--line-2);
                   border-radius: 10px; padding: 12px 14px; background: #fff;
                   box-shadow: 0 -4px 16px rgba(13, 17, 23, .06); }
.dw .det .picked .ph { display: flex; align-items: center; gap: 8px; }
.dw .det .picked b { font-size: var(--t-md); font-weight: 600; }
.dw .det .picked s { text-decoration: none; color: var(--ink-3); font-size: var(--t-xs); }
.dw .det .picked .mk { margin-left: auto; border: 0; background: transparent; color: var(--ink-4);
                       font-size: var(--t-lg); cursor: pointer; }
.dw .det .picked .mk.on { color: var(--mark); }
.dw .det .picked .two { margin-top: 10px; max-width: 320px; }
.dw .det .picked .rn { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.dw .det .picked .rn s { text-decoration: none; color: var(--ink-3); font-size: var(--t-xs); }
.dw .det .picked .rn .input { height: 28px; max-width: 220px; }
.dw .det .empty { color: var(--ink-3); font-size: var(--t-sm); padding: 16px 0; }

/*
 * ── 짚은 핀을 **조작하는 판** ─────────────────────────────
 *
 * 이 자리에 목록 줄을 세우고 있었다. 줄은 여럿을 훑어보라고 만든 모양이라, 넓은 빈칸에
 * 한 줄만 떠 있으면 무엇을 누르라는 자리인지 읽히지 않는다 -- 화면에서 가장 큰 결과가
 * 나는 자리인데 가장 작게 서 있었다. 그리는 코드는 app/master/pinpanel.mjs 한 곳이다.
 */
.pinpanel {
  margin-top: 14px; padding: 14px 16px;
  border: 1px solid var(--line-2); border-radius: 12px; background: var(--surface);
}
/* 강제된 값은 판 전체가 다른 얼굴이다 -- 배선이 아니라 화면이 만든 값이다. */
.pinpanel.is-forced { border-color: var(--warn-line); background: var(--warn-soft); }
.pp-head { display: flex; align-items: flex-start; gap: var(--s-2); }
.pp-name { min-width: 0; }
.pp-name b { display: block; font-size: var(--t-lg); font-weight: 700; color: var(--ink); }
.pp-name s { display: block; margin-top: 2px; text-decoration: none;
             font-size: var(--t-xs); color: var(--ink-3); }
.pp-star { margin-left: auto; }
.pp-act { display: flex; align-items: center; gap: var(--s-2); margin-top: 14px; }
.pp-gap { flex: 1; }
.pp-do .btn { min-width: 108px; }
/* 지금 값. 판에서 가장 먼저 읽혀야 하는 한 마디다. */
.pp-state { display: inline-flex; align-items: center; gap: 7px;
            font-size: var(--t-md); font-weight: 600; color: var(--ink-3); }
.pp-state .dot { width: 9px; height: 9px; border-radius: var(--r-pill);
                 background: var(--gray-300); }
.pp-state.is-on { color: var(--ok); }
.pp-state.is-on .dot { background: var(--ok); }
.pp-why { margin: 10px 0 0; font-size: var(--t-xs); color: var(--ink-3); }

/*
 * ══ 본사 관제의 **제어 대상 설정** ═══════════════════════════
 *
 * 층마다 <이 단위로 등록> 단추가 서고, 핀은 칩으로 눕는다(prod/opt/lis-hq/setup.html).
 *
 * 체크상자를 늘어놓다가 갈았다. 체크상자는 <목록에서 여럿을 고르는> 말인데, 여기서 하는
 * 일은 **<이 층으로 산다>**는 선택이다. 그리고 핀 서른둘이 체크 줄로 서면 그 화면은
 * 목록이 되고, 정작 <어느 층에서 살까>가 그 속에 묻힌다.
 */

/* ── 계량 넉 장 ─────────────────────────────────────────── */
.rg-meters { display: grid; gap: var(--s-3);
             grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
/*
 * 한 장짜리. 계정 화면의 <관제 계정> 이 그렇다.
 *
 * 위 규칙은 넉 장이 나란히 설 때를 위한 것이라, 한 장만 두면 그 한 장이 화면 폭을 다
 * 차지한다. 숫자 하나에 1,000px 짜리 상자가 붙으면 그 값이 실제보다 큰 일로 읽힌다.
 */
.rg-meters-one { grid-template-columns: minmax(200px, 300px); }
.rg-meter { padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--r-md);
            background: var(--surface); }
.rg-meter-k { font-size: var(--t-xs); color: var(--ink-3); font-weight: 600; }
.rg-meter-v { display: flex; align-items: baseline; gap: 5px; margin: 4px 0 8px; }
.rg-meter-v b { font-size: 22px; font-weight: 700; color: var(--ink);
                font-variant-numeric: tabular-nums; }
.rg-meter-v s { text-decoration: none; font-size: var(--t-xs); color: var(--ink-3); }
.rg-meter-s { margin-top: 6px; font-size: var(--t-xs); color: var(--ink-3); }
.rg-meter.is-full .rg-meter-s { color: var(--warn); }
.rg-meter.is-over { border-color: var(--err-line); background: var(--err-soft); }
.rg-meter.is-over .rg-meter-s { color: var(--err); font-weight: 600; }
.rg-meter.is-full .lic-meter > i { background: var(--warn); }
.rg-meter.is-over .lic-meter > i { background: var(--err); }

/* ── 층 ─────────────────────────────────────────────────── */
.rg-site + .rg-site { margin-top: var(--s-5); padding-top: var(--s-4);
                      border-top: 1px solid var(--line); }
.rg-site.is-off { color: var(--ink-3); }
.rg-row { display: flex; align-items: center; gap: var(--s-3); padding: 7px 0; }
.rg-t { min-width: 0; }
.rg-t b { display: block; font-size: var(--t-md); font-weight: 600; color: var(--ink); }
.rg-t s { display: block; margin-top: 1px; text-decoration: none;
          font-size: var(--t-xs); color: var(--ink-3); }
.rg-top .rg-t b { font-size: var(--t-lg); font-weight: 700; }
/* 그 아래에 등록된 것이 몇인가. 층 줄의 오른쪽 끝에 곁들인다. */
.rg-n { margin-left: auto; font-size: var(--t-xs); color: var(--accent); font-weight: 600; }
.rg-row .rg-take { margin-left: auto; }
.rg-n + .rg-take { margin-left: var(--s-2); }
.rg-body { padding-left: var(--s-3); }
.rg-ctl { padding: 2px 0 2px var(--s-3); border-left: 2px solid var(--line); }
.rg-mod { padding-left: var(--s-3); }

/*
 * 등록 단추. **켠 것은 채워서** 말한다 -- 이 화면에서 켜짐과 꺼짐은 곧 돈이라,
 * 테두리 색만으로 가르면 훑을 때 안 보인다.
 */
.rg-take { flex: none; border: 1px solid var(--line-2); border-radius: var(--r-pill);
           background: var(--surface); color: var(--ink-2);
           padding: 5px 12px; font-size: var(--t-xs); font-weight: 600; cursor: pointer; }
.rg-take:hover { border-color: var(--accent); color: var(--accent); }
.rg-take.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }
.rg-take.is-covered { border-style: dashed; border-color: var(--line-2);
                      color: var(--ink-3); font-weight: 500; cursor: default; }
.rg-take.is-covered:hover { border-color: var(--line-2); color: var(--ink-3); }

/* ── 핀 칩 ──────────────────────────────────────────────── */
/*
 * 번호가 먼저, 이름이 그다음. 현장에서 사람이 찾는 것은 대개 번호 하나이고,
 * 이름은 그 번호가 무엇인지 확인하는 값이다(핀 격자와 같은 규칙).
 */
.rg-pins { display: grid; gap: 6px; padding: 4px 0 10px;
           grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); }
.pinchip { display: flex; align-items: center; gap: 6px; min-width: 0;
           padding: 6px 9px; border: 1px solid var(--line); border-radius: var(--r-sm);
           background: var(--surface); cursor: pointer; text-align: left; }
.pinchip b { font-size: var(--t-xs); font-weight: 700; color: var(--ink-3);
             font-variant-numeric: tabular-nums; flex: none; }
.pinchip span { font-size: var(--t-xs); color: var(--ink-2); overflow: hidden;
                text-overflow: ellipsis; white-space: nowrap; }
.pinchip:hover { border-color: var(--accent); }
.pinchip.is-on { background: var(--accent-soft); border-color: var(--accent-line); }
.pinchip.is-on b, .pinchip.is-on span { color: var(--accent-strong); }
/* 출력과 입력은 왼쪽 띠로 가른다 -- 색만으로 말하지 않는다. */
.pinchip.is-out { box-shadow: inset 2px 0 0 var(--ink-4); }
.pinchip.is-on.is-out { box-shadow: inset 2px 0 0 var(--accent); }
.pinchip.is-covered { background: var(--gray-50); border-style: dashed; cursor: default; }
.pinchip.is-covered b, .pinchip.is-covered span { color: var(--ink-4); }

/*
 * 본사 관제의 **화면 머리 한 줄** -- 지금 어디이고, 여기서 갈 수 있는 곳은 어디인가.
 *
 * 윗줄 탭은 <무엇을 보는가>를 고르는 자리다. 그 줄에 설정까지 세우면 성질이 다른 것이
 * 나란히 서서 <또 다른 화면>으로 읽힌다 -- 설정은 이 관제가 무엇을 다룰지 정하는 일이라
 * 관제 안쪽에 있어야 한다.
 */
.hqbar { display: flex; align-items: center; gap: var(--s-3);
         padding: 0 0 var(--s-3); margin-bottom: var(--s-3);
         border-bottom: 1px solid var(--line); }
.hqbar b { font-size: var(--t-lg); font-weight: 700; color: var(--ink); }
.hqbar s { display: block; margin-top: 2px; text-decoration: none;
           font-size: var(--t-xs); color: var(--ink-3); }
.hqbar .crumb { display: block; font-size: var(--t-xs); color: var(--ink-3); text-decoration: none; }
.hqbar .crumb:hover { color: var(--accent); }

/*
 * 글자처럼 생긴 단추. kv 칸의 <이 제어기의 설정에서> 가 그것이다.
 *
 * 현장 제어기 화면에서는 같은 자리가 링크(`<a href>`)다 -- 그쪽은 갈 주소가 있다.
 * 관제에서는 주소가 아니라 이 문서 안의 구획을 바꾸는 일이라 단추여야 하고,
 * 그래도 **읽히기는 링크처럼** 읽혀야 한다 -- 같은 자리의 같은 말이기 때문이다.
 */
.linkbtn { border: 0; background: transparent; padding: 0; cursor: pointer;
           color: var(--accent); font: inherit; text-decoration: underline; }
.linkbtn:hover { color: var(--accent-strong); }

/*
 * ── 좁아지면 **단계로** 접는다 (2026-10-21) ─────────────────
 *
 * 창이 아니라 이 서랍함의 폭을 묻는다. 폭 176 · 336 · 460 으로 줄바꿈만 시키면 보기(rail)나
 * 줄기가 한 줄을 혼자 차지하고 옆이 빈다 -- 세로 태블릿(3:4)이 그랬다.
 *
 *   넓다   보기 · 줄기 · 상세 세 칸
 *   중간   보기가 위로 올라 알약 줄이 되고, 아래에 줄기 | 상세 두 칸 (세로 태블릿)
 *   좁다   전부 위에서 아래로 (세로 폰). 가로 스크롤은 두지 않는다.
 */
@container (max-width: 960px) {
  .dw > * { min-height: 0; }
  .dw > .rail { flex: 1 1 100%; max-width: none; border-right: 0; border-bottom: 1px solid var(--line);
                display: flex; flex-wrap: wrap; align-content: flex-start; align-items: center;
                gap: 6px; padding: 10px 14px; }
  .dw .rail .g { flex: 1 1 100%; padding: 4px 2px 0; }
  .dw .rail a { padding: 8px 12px; border: 1px solid var(--line-2); border-radius: 99px;
                background: #fff; box-shadow: none; min-height: 36px; }
  .dw .rail a.on { background: var(--accent-soft); border-color: var(--accent); box-shadow: none; }
  .dw > .list, .dw > .panel.list { flex: 1 1 280px; max-width: 340px; }
  .dw > .det, .dw > .panel.det { flex: 999 1 340px; }
}
@container (max-width: 640px) {
  .dw > .list, .dw > .panel.list { flex: 1 1 100%; max-width: none; border-right: 0;
                                   border-bottom: 1px solid var(--line); }
  .dw > .det, .dw > .panel.det { flex: 1 1 100%; }
  .dw .det { padding: 16px 14px; }
}

/* ── ui-timeline.css ─────────────────────────────── */
/*
 * 조작 이력의 **시간 줄** -- 시제품(scratchpad/mix/screens.css)에서 그대로 옮긴 것.
 *
 * 표를 걷고 시간의 줄로 세웠다. 얻는 것은 **거절이 눈에 띄는 것**이다 -- 표에서는 거절
 * 한 줄이 마흔네 줄 사이에 묻혔다. 여기서는 판 하나를 통째로 쓴다.
 *
 * 토큰 이름만 제품 것으로 옮겼고(`--ink3`→`--ink-3`) 숫자는 하나도 건드리지 않았다.
 */

/*
 * 옆칸이 272px 에 못 박혀 있어 좁은 화면에서 줄이 가로로 밀렸다. 줄바꿈으로 푼다 --
 * 넓은 화면의 모습은 그대로고, 좁아지면 옆칸이 줄 아래로 내려간다(지침 §3).
 */
.tl { display: flex; flex-wrap: wrap; align-items: stretch;
      min-height: calc(100vh - 158px); }
.tl > .stream { flex: 999 1 420px; min-width: 0; }
.tl > .side { flex: 1 1 272px; }
.tl .stream { padding: 16px 20px 28px; }
.tl .fx { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; align-items: flex-end; }
.tl .day { font-size: var(--t-xs); font-weight: 700; color: var(--ink-3); letter-spacing: .06em;
           margin: 18px 0 10px; }
.tl .day:first-of-type { margin-top: 0; }

.tl .ev { display: grid; grid-template-columns: 58px 1fr; gap: 14px; }
.tl .ev .t { text-align: right; color: var(--ink-3); font-size: var(--t-xs);
             font-variant-numeric: tabular-nums; padding-top: 11px; }
/* 기둥. 줄과 줄을 잇는 선이고, 그 위에 사건의 점이 놓인다. */
.tl .ev .c { position: relative; border-left: 2px solid var(--line); padding: 0 0 14px 20px; }
.tl .ev:last-child .c { border-left-color: transparent; }
.tl .ev .c::before { content: ''; position: absolute; left: -6px; top: 14px; width: 10px;
                     height: 10px; border-radius: 99px; background: #fff;
                     border: 2px solid var(--line-2); }
.tl .ev.on .c::before { background: var(--ok); border-color: var(--ok); }
.tl .ev.err .c::before { background: var(--err); border-color: var(--err); }

.tl .ev .box { border: 1px solid var(--line); border-radius: 10px; padding: 10px 13px; }
.tl .ev .box b { font-size: var(--t-md); font-weight: 600; }
.tl .ev .box p { color: var(--ink-3); font-size: var(--t-xs); margin-top: 2px; }
.tl .ev .box .acts { display: flex; gap: 6px; margin-top: 9px; }
/* 거절은 **판이 물든다.** 이 화면을 여는 이유의 절반이 <왜 안 켜졌나> 이다. */
.tl .ev.err .box { border-color: var(--err-line); background: var(--err-soft); }

.tl .side { border-left: 1px solid var(--line); padding: 16px; }
.tl .side h4 { font-size: var(--t-xs); font-weight: 600; color: var(--ink-3); margin: 0 0 8px; }
.tl .side h4 + .p { border-top: 0; }
.tl .side .p { display: flex; align-items: center; gap: 9px; padding: 8px 0;
               border-top: 1px solid var(--line); font-size: var(--t-sm); }
.tl .side .p i { width: 7px; height: 7px; border-radius: 1px; background: var(--line-2);
                 font-style: normal; flex: none; }
.tl .side .p i.on { background: var(--ok); }
.tl .side .p s { text-decoration: none; margin-left: auto; color: var(--ink-3); font-size: var(--t-xs); }
/*
 * 세는 값이 아니라 **사실을 적는 줄**. 숫자 줄보다 한 단계 물러선다.
 * 화면 코드가 줄마다 `style="color:var(--ink-3)"` 를 달고 있었다 -- 같은 뜻을 네 곳에
 * 적어 두면 색을 한 번 고칠 때 네 곳을 찾아야 한다.
 */
.tl .side .p.dim { color: var(--ink-3); }
/* 이름은 잘리고 셈(`건`)은 남는다. 272px 안에서 먼저 읽어야 하는 것이 셈이다. */
.tl .side .p .nm { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/*
 * 주체 줄은 **누를 수 있다** -- 누르면 위의 거르개가 그 주체로 채워진다.
 * 시제품에 없던 줄이라 모양도 여기서 정한다(제품 전체에 쓰는 규칙이 아니다).
 * 단추이지만 단추처럼 보이지 않아야 한다 -- 옆줄과 글자 크기·줄 높이가 같아야 셈이 읽힌다.
 */
.tl .side .as-link { border: 0; background: none; padding: 0; font: inherit; color: inherit;
                     cursor: pointer; text-align: left; }
.tl .side .as-link:hover { color: var(--accent); text-decoration: underline; }

/* ── ui-sheet.css ─────────────────────────────── */
/*
 * 구성의 **표계산형** -- 시제품(scratchpad/mix/screens.css)에서 그대로 옮긴 것.
 *
 * ── 왜 여기만 표인가 ──────────────────────────────────────
 *
 * 구성에서 하는 일은 **여럿을 한꺼번에**다 -- 핀 열여섯 개에 이름을 붙이고, 넷을 골라
 * 별을 달고, 무엇이 어떤 갈래인지 훑는다. 그 일에는 줄이 촘촘한 표가 맞다.
 *
 * **조작 화면에는 이 형태를 쓰지 않는다.** 줄 높이 32px 는 장갑 낀 손가락이 못 누르고,
 * 잘못 누른 줄이 설비를 켠다. 그래서 이 표에는 켜고 끄는 단추가 아예 없다.
 *
 * 토큰 이름만 제품 것으로 옮겼다(`--soft`→`--surface-2`, `--accent-ink`→`--accent-strong`).
 * 숫자는 하나도 건드리지 않았다.
 */

/* 고른 줄이 있을 때 맨 위에 서는 띠. 무엇을 한꺼번에 할 수 있는지 여기서 말한다. */
.sheet .bulk {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 9px 16px; background: var(--accent-soft);
  border-bottom: 1px solid var(--accent-line); font-size: var(--t-sm);
}
.sheet .bulk b { color: var(--accent-strong); }
.sheet .bulk .r { margin-left: auto; color: var(--ink-3); }

/*
 * 거르개 줄. 장치 고르기 · 갈래 고르기 · 그 장치가 무엇인지.
 *
 * ── **두 묶음이 한 줄에 선다** ────────────────────────────
 *
 * 장치를 고르는 단추와 갈래를 고르는 단추가 같은 모양으로 나란히 서 있었고, 사이를
 * 가르는 것이 빈 10px 하나였다. 둘 다 고른 것이 파랗게 차 있으므로 **한 묶음에서 둘을
 * 고른 것처럼** 보인다 -- 실제로는 서로 다른 축이다(어느 장치인가 / 어떤 핀을 볼 것인가).
 *
 * 묶음마다 이름을 붙이고 사이에 선을 하나 긋는다. 이름이 있으면 그 단추들이 무엇을
 * 고르는 것인지가 눌러 보기 전에 읽힌다.
 */
.sheet .fx {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  padding: 9px 16px; border-bottom: 1px solid var(--line);
}
.sheet .fx-k {
  font-size: var(--t-xs); font-weight: 600; color: var(--ink-3);
  letter-spacing: .02em;
}
/* 묶음 사이의 선. 여백만으로는 두 묶음이 한 묶음으로 읽힌다. */
.sheet .fx-sep {
  width: 1px; align-self: stretch; margin: 0 4px;
  background: var(--line);
}
/* 오른쪽 끝에 붙는 그 장치의 정체. 고르는 것이 아니라 읽는 값이다. */
.sheet .fx-what { margin-left: auto; color: var(--ink-3); font-size: var(--t-xs); }

.sheet table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.sheet th {
  text-align: left; font-size: var(--t-xs); font-weight: 600; color: var(--ink-3);
  padding: 7px 10px; background: var(--surface-2);
  border-bottom: 1px solid var(--line-2); white-space: nowrap;
}
.sheet td { padding: 0 10px; height: 32px; border-bottom: 1px solid var(--line); white-space: nowrap; }
.sheet td.n, .sheet th.n { text-align: right; font-variant-numeric: tabular-nums; }
.sheet tr.sel td { background: var(--accent-soft); }

/* 지금 값. 표에서는 배지가 아니라 점 하나다 -- 열여섯 줄에 배지가 서면 표가 안 읽힌다. */
.sheet .st { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; }
.sheet .st i { width: 7px; height: 7px; border-radius: 1px; background: var(--line-2); font-style: normal; }
.sheet .st.on { color: var(--ok); }
.sheet .st.on i { background: var(--ok); }
/* 배선이 아니라 화면이 만든 값. 켜진 것과 **다른 모습**이어야 한다. */
.sheet .st.forced { color: var(--warn); }
.sheet .st.forced i { background: var(--warn); }

/*
 * 이름 칸. **평소에는 표의 한 칸처럼 보이고** 마우스를 얹으면 고칠 수 있다는 것이 드러난다.
 * 늘 입력칸으로 두면 열여섯 줄이 전부 상자가 되어, 무엇이 값이고 무엇이 칸인지 흐려진다.
 */
.sheet .nm input {
  width: 100%; height: 25px; border: 1px solid transparent; border-radius: 5px;
  padding: 0 6px; font: inherit; font-size: var(--t-sm); background: transparent; color: var(--ink);
}
.sheet .nm input:hover { border-color: var(--line-2); background: var(--surface); }
.sheet .nm input:focus { border-color: var(--accent); background: var(--surface); outline: none; }
/* 아직 안 적은 이름. 기계가 붙인 것이라는 표시다. */
.sheet .nm input::placeholder { color: var(--ink-4); }

/*
 * 별. 표에서는 그림이 아니라 **글자**를 쓴다(★ ☆) -- 줄 높이가 32px 라 svg 를 얹으면
 * 칸이 그만큼 벌어진다. 목록의 별(app/favorites.mjs)이 그림인 것과 다른 까닭이 이것이다.
 * 두 글자의 폭 차이로 줄이 흔들리지 않도록 폭을 못 박는다.
 */
.sheet .mk {
  border: 0; background: transparent; color: var(--ink-4); font-size: var(--t-sm);
  cursor: pointer; padding: 0; width: 22px; text-align: center; line-height: 1;
}
.sheet .mk.on { color: var(--mark); }
.sheet .mk:hover { color: var(--mark-strong); }

.sheet .foot {
  padding: 9px 16px; color: var(--ink-3); font-size: var(--t-xs);
  border-top: 1px solid var(--line); display: flex; gap: 18px; flex-wrap: wrap;
}

/*
 * 칸이 일곱이라 좁은 화면에서는 표 자체가 가로로 밀린다. **표만** 밀리게 가둔다 --
 * 화면이 통째로 밀리면 위의 거르개와 아래 셈이 함께 사라진다(docs/검토지침.md §3).
 */
.sheet .wrap { overflow-x: auto; }

/* ── ui-map.css ─────────────────────────────── */
/*
 * 현장 **지도** -- 본사 화면들이 함께 쓴다.
 *
 * ── 왜 제품 쪽으로 왔나 ──────────────────────────────────
 *
 * 오래 `hq/console.css` 안에만 있었다. 그때는 지도를 쓰는 화면이 iotop 본사의 현장
 * 목록 하나뿐이었다. 이제 **고객사 본사 관제**(prod/opt/lis-hq)도 같은 지도를 쓴다 --
 * 그쪽은 다른 서버라 그 파일을 가져갈 수 없다.
 *
 * 그리는 코드도 함께 왔다(app/map.mjs). 두 곳이 같은 부품을 쓰면 한 번만 고치면 된다.
 *
 * 바탕 그림은 밖에서 받아 오지 않는다. 해안선을 함께 싣고 캔버스에 그린다
 * (app/geo-world.mjs -- 왜 그렇게 했는지는 그 파일 머리에 있다).
 */

/*
 * 지도.
 *
 * 그림이 넓게 깔리는 자리라 제품의 카드 안에서 튀지 않아야 한다 -- 모서리와 테두리를
 * 카드와 맞추고, 색은 점에만 쓴다. 바닥 그림은 흑백에 가까운 것이 아니므로
 * 점이 묻히지 않도록 테두리를 흰색으로 두른다.
 */
.map {
  position: relative; overflow: hidden;
  height: 460px; border: 1px solid var(--gray-200); border-radius: var(--r-md);
  background: var(--surface-2); cursor: grab; user-select: none;
  /*
   * 손가락으로는 위아래로 넘기게 두고(pan-y), 좌우로 끄는 것만 지도가 가져간다.
   * none 으로 두면 태블릿에서 지도 위에 손이 닿는 순간 페이지가 굳는다 --
   * 휠을 가져가지 않기로 한 것과 같은 이유다.
   */
  touch-action: pan-y;
}
.map.is-dragging { cursor: grabbing; }
/*
 * 바닥은 캔버스 한 장이다. 타일 `<img>` 를 수십 장 붙였다 떼던 자리를 대신한다
 * (hq/map.mjs -- 왜 바꿨는지는 그 파일 머리에 있다).
 */
.map-base, .map-pins { position: absolute; inset: 0; }
.map-pins { pointer-events: none; }
.map-pin {
  position: absolute; transform: translate(-50%, -100%); pointer-events: auto;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  background: none; border: 0; padding: 0 0 4px; cursor: pointer;
}
.map-pin-dot {
  width: 15px; height: 15px; border-radius: 99px; border: 2.5px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .45); background: var(--gray-400);
}
/*
 * 겹칠 때 이름표만 아래로 내린다(app/map.mjs 의 placeLabel). 기본값은 0 이라
 * 안 겹치는 이름표는 예전 그대로 점 위에 붙는다.
 */
.map-pin { --label-drop: 0px; }
.map-pin-label {
  transform: translateY(var(--label-drop));
  font-size: var(--t-xs); font-weight: 600; color: var(--gray-700); white-space: nowrap;
  background: rgba(255, 255, 255, .88); padding: 0 5px; border-radius: var(--r-sm);
}
.map-pin-ok .map-pin-dot { background: var(--ok-700); }
.map-pin-warn .map-pin-dot { background: var(--warn-700); }
.map-pin-err .map-pin-dot { background: var(--err-700); }
.map-pin-info .map-pin-dot { background: var(--blue-600); }
/* 고른 현장은 크게. 목록에서 고른 것을 지도에서 눈으로 찾게 두지 않는다 */
.map-pin.is-on .map-pin-dot { width: 21px; height: 21px; box-shadow: 0 0 0 4px rgba(37, 99, 235, .35); }
.map-zoom { position: absolute; right: 10px; top: 10px; z-index: 2; display: grid; gap: 4px; }
.map-zoom button {
  width: 30px; height: 30px; border: 1px solid var(--gray-300); border-radius: var(--r-sm);
  background: #fff; color: var(--gray-700); font-size: var(--t-lg); line-height: 1; cursor: pointer;
}
.map-credit {
  position: absolute; right: 4px; bottom: 3px; z-index: 2;
  font-size: 10px; color: var(--gray-600); background: rgba(255, 255, 255, .75); padding: 0 4px;
}
.map-hint {
  position: absolute; left: 10px; bottom: 8px; z-index: 2;
  font-size: var(--t-xs); color: var(--gray-600); background: rgba(255, 255, 255, .9);
  padding: 3px 8px; border-radius: var(--r-sm);
}
/* 거르개. 고른 것 하나만 파랗게 남는다 */
/* 눌러서 열리는 줄. 손이 올라가면 그 줄이 대상임이 보여야 한다 */
.table tr.is-clickable { cursor: pointer; }
.table tr.is-clickable:hover td { background: var(--surface-2); }
.table tr.is-clickable.is-on:hover td { background: var(--blue-50); }

/* 거르개. 두 축을 나란히 두고, 걸려 있으면 파랗게 남는다 */
.filters { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 14px; }
.filter { display: flex; flex-direction: column; gap: 4px; }
.filter-k { font-size: var(--t-xs); color: var(--gray-500); }
.filter select { min-width: 148px; }
/* 찾기 칸도 거르개 한 칸이다. 옆의 고르개들과 폭이 어긋나면 한 줄로 안 읽힌다. */
.filter input[type="search"] { min-width: 200px; max-width: 260px; }
.filter select.is-on { border-color: var(--blue-600); background: var(--blue-50); color: var(--blue-700); font-weight: 600; }

/*
 * 점을 누르면 뜨는 말풍선.
 *
 * 지도 위에 떠 있으므로 바닥 그림과 겹쳐도 글이 읽혀야 한다 -- 흰 바탕에 그림자를 준다.
 */
.map-pop {
  position: absolute; z-index: 4; width: 268px; transform: translate(-50%, -100%);
  background: #fff; border: 1px solid var(--gray-200); border-radius: var(--r-md);
  box-shadow: 0 8px 28px rgba(15, 23, 42, .22); padding: 12px 14px 13px;
  display: flex; flex-direction: column; gap: 7px;
}
.map-pop.is-below { transform: translate(-50%, 0); }
.pop-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.pop-head b { font-size: 14.5px; font-weight: 650; color: var(--gray-700); }
.pop-sub { color: var(--gray-500); margin-top: -4px; }
.pop-row { display: flex; align-items: baseline; gap: 8px; font-size: var(--t-sm); color: var(--gray-700); }
.pop-row > span:first-child { flex: 0 0 34px; color: var(--gray-500); font-size: var(--t-xs); }
/* 창 안에서는 이름표가 길어진다("사업자등록번호"). 좁게 두면 두 줄로 접힌다 */
.modal .pop-row { align-items: baseline; padding: 3px 0; }
.modal .pop-row > span:first-child { flex: 0 0 86px; }
.pop-why { font-size: var(--t-xs); color: var(--gray-500); border-top: 1px solid var(--gray-100); padding-top: 7px; }
.map-pop .btn { width: 100%; }

.map-legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px; font-size: var(--t-xs); color: var(--gray-500); }
.map-legend .lg { display: inline-block; width: 9px; height: 9px; border-radius: 99px; margin-right: 5px; }
.lg-ok { background: var(--ok-700); }
.lg-warn { background: var(--warn-700); }
.lg-err { background: var(--err-700); }
.lg-info { background: var(--blue-600); }
.lg-mute { background: var(--gray-400); }
/* 대략 잡은 자리는 테두리만. 정확한 점과 같은 모양이면 그 값을 믿게 된다 */
.lg-rough { background: #fff; box-shadow: inset 0 0 0 2px var(--gray-400); }
.map-pin.is-rough .map-pin-dot { background: #fff !important; box-shadow: inset 0 0 0 3px var(--gray-500), 0 1px 4px rgba(0,0,0,.35); }
.map-pin.is-rough .map-pin-label { opacity: .75; font-style: italic; }
/*
 * 구독이 없어 못 쓰는 자리.
 *
 * **보이되 죽어 있다.** 빼 버리면 자기 현장이 사라져 없어진 줄 알고, 멀쩡히 두면
 * 눌러도 안 열려 고장으로 읽힌다. 색을 빼고 커서를 바꿔 그 중간을 만든다.
 */
.map-pin.is-locked { cursor: default; filter: grayscale(1); opacity: .45; }
/* 이 범례가 지도만의 것이 아니라는 한 마디. 점을 쓰지 않으므로 색 칸 없이 선다. */
.map-legend-note { margin-left: auto; color: var(--ink-3); }

/* ── ui-forms.css ─────────────────────────────── */
/*
 * 입력과 목록 -- 폼 · 표 · 핀 목록 · 토스트
 *
 * 업무용 도구의 화면 규칙을 한곳에 둔다. 색·간격·글자 크기를 화면마다 다시 정하면
 * 같은 제품 안에서 화면마다 다른 인상이 된다.
 *
 * 폼과 표와 목록만 여기 둔다. 상태 표식·알림·토스트는 app/ui-status.css 로, 도움말과
 * 잠금 표시는 app/ui-help.css 로 갈랐다 -- 한 장이 1113줄이 되어 한 번에 읽히지
 * 않았다(목표 400 · 상한 800). 전체 차례는 app/ui.css 가 적어 둔다.
 */
/* ── 폼 ─────────────────────────────────────────────────── */
.field { margin-bottom: 14px; }
.field-label {
  display: flex;
  align-items: center;
  font-size: var(--t-xs); font-weight: 600; color: var(--gray-600);
  /*
   * 강제 대문자를 쓰지 않는다. 한글 라벨과 섞이면 읽기 불편하고,
   * "DuckDNS" 같은 상표 표기가 "DUCKDNS" 로 틀어진다.
   */
  letter-spacing: .02em;
  margin-bottom: 5px;
}
/*
 * 칸 아래 도움말. **낱말 안에서 자르지 않는다**(word-break: keep-all).
 *
 * 좁은 칸에서 한글이 아무 데서나 잘렸다 -- `기계 이름이 그대로 섭니` / `다.` 처럼
 * 마지막 줄에 글자 하나가 남는다. 읽는 사람은 그것을 먼저 오타로 본다.
 */
.field-hint {
  font-size: var(--t-xs); color: var(--gray-500); margin-top: 4px;
  line-height: 1.55; word-break: keep-all;
}
.field-error { font-size: var(--t-xs); color: var(--err-700); margin-top: 4px; }

/*
 * 값을 적는 칸.
 *
 * **적는 자리는 눌러 들어간 것처럼 보여야 한다.** 흰 판 위에 흰 칸을 얹고 테두리만
 * 두르면 그것이 읽는 자리인지 적는 자리인지 알 수 없다 -- 화면이 온통 흰 지금은 더 그렇다.
 * 바탕을 한 겹 낮추고 위쪽에 얇은 그림자 한 줄을 둔다. 종이가 아니라 **홈**이다.
 *
 * 초점은 한 곳에서 정한 고리를 쓴다(--focus). 칸마다 다른 테두리를 만들지 않는다.
 */
.input, .select {
  width: 100%;
  min-height: 36px;
  padding: 7px 12px;
  font-family: inherit;
  font-size: var(--t-sm);
  line-height: 1.4;
  color: var(--ink);
  background-color: #fff;
  border: 1px solid var(--line-2);
  border-radius: 8px;
  box-shadow: none;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
/*
 * 2026-10-21: 회색 바탕 + 안쪽 그림자(<홈>)를 걷었다. 화면이 온통 흰 지금은 그 회색 칸이 목록 ·
 * 표보다 먼저 눈에 걸렸고, 날짜 · 고르기 칸이 줄마다 다른 덩어리로 보였다. 흰 바탕에 얇은
 * 테두리 -- 읽는 자리와 적는 자리는 테두리로 가르고, 초점일 때 고리가 켜진다(아래).
 * 고르기 칸은 브라우저 기본 화살표 대신 같은 모양의 화살표를 쓴다.
 */
.input::placeholder { color: var(--ink-3); }
.select, select.input {
  appearance: none; -webkit-appearance: none;
  padding-right: 32px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238a94a3' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center; background-size: 16px;
}
.select, select.input { min-height: 36px; }
.input:hover:not(:disabled):not(:focus), .select:hover:not(:disabled):not(:focus) { border-color: var(--ink-4); }
.input:focus, .select:focus {
  outline: none;
  background-color: #fff;
  border-color: var(--accent);
  box-shadow: var(--focus);
}
/*
 * ── 잠긴 칸은 **한눈에 달라야 한다** ──────────────────────
 *
 * `background: var(--gray-100)` 였다. 그런데 그 토큰은 `--surface-2` 를 가리킨다 --
 * **보통 칸의 바탕과 같은 값**이다(app/ui-tokens.css). 그래서 잠긴 칸이 멀쩡한 칸처럼
 * 보였고, 사람은 눌러 보고 나서야 알았다. 관제의 설정 화면에서 남의 보드 줄이 통째로
 * 그랬다.
 *
 * 세 가지로 말한다 -- 바탕을 빼고(눌린 느낌을 없앤다), **테두리를 점선**으로 바꾸고,
 * 글자를 흐린다. 점선은 색을 못 보는 사람에게도 남는다.
 */
.input:disabled, .select:disabled {
  background: var(--surface);
  color: var(--ink-3);
  border-style: dashed;
  border-color: var(--line-2);
  box-shadow: none;
  cursor: not-allowed;
}
/* 이름표도 함께 물러난다. 칸만 흐리면 <읽을 것> 과 <못 고치는 것> 이 섞인다. */
.field:has(.input:disabled) .field-label,
.field:has(.select:disabled) .field-label { color: var(--ink-3); }
.input.invalid { border-color: var(--err-700); }

/*
 * ── 제어기와 장치를 **반씩 나눈다** ────────────────────────
 *
 * 설정 화면의 한 묶음이 이 모양이다.
 *
 *   왼쪽   제어기 이름 하나
 *   오른쪽 그 제어기에 붙은 장치들 -- **세로로** 쌓인다
 *
 * 한 격자에 다 넣었더니 장치가 둘 셋일 때 제어기 이름 옆으로 칸이 줄줄이 붙어,
 * 어느 칸까지가 이 제어기의 것인지가 가로로 흘렀다. 층이 다른 둘을 같은 줄에 세우면
 * 늘 그렇게 된다 -- 층을 가르는 것은 세로이고, 짝을 이루는 것은 가로다.
 *
 * 좁아지면 한 칸으로 접힌다(auto-fit). 폭을 묻는 갈림목을 두지 않는다.
 */
.field-split {
  display: grid; gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
/* 오른쪽 칸. 접혀서 아래로 내려가도 **딸린 것**임이 선으로 남는다. */
.field-side-sub {
  padding-left: var(--s-4);
  border-left: 2px solid var(--line);
}

.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
}

/* 짧은 거르개(날짜 · 이름) -- 칸이 줄 전체로 늘어나지 않고 값만큼만 선다. 체크는 같은 줄 아래선에 맞춘다. */
.field-grid.tight { grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); align-items: end; max-width: 1040px; }
.field-grid .field { min-width: 0; }
/*
 * 격자 안의 칸은 **자기 칸을 다 쓴다.** 상한(위의 26rem)은 격자 밖에서 혼자 서는 칸을 위한 것이다 --
 * 격자 칸은 이미 폭이 정해져 있어(auto-fit) 상한이 더 줄 이유가 없고, 폰에서는 그 상한이 글꼴에 따라
 * 칸을 폭의 70% 로 깎아 오른쪽을 비웠다. 넓은 화면에서 지나치게 길어지는 것은 격자 폭이 받는다
 * (.tight 는 1040px).
 */
.field-grid .input, .field-grid .select { min-width: 0; max-width: none; }
.field-grid.tight .check { min-height: 36px; }

/*
 * ── 입력칸의 폭 ───────────────────────────────────────────
 *
 * 짧은 값에 긴 칸을 주지 않는다. 조사에서 나온 지적 가운데 눈에 가장 먼저 걸린 것이
 * "주소 한 줄에 1000px 짜리 칸" 이었다. 칸의 길이는 사람에게 **얼마나 긴 값을 적어야 하는지**
 * 를 말한다. 값보다 열 배 긴 칸은 거짓말이다.
 *
 * 값의 성격으로 폭을 정한다. 화면마다 눈으로 맞추지 않는다.
 */
/* 2026-10-21: 상한을 `ch` 에서 rem 으로. ch 는 글꼴(과 글자 크기)에 따라 달라져, 폰의 삼성 브라우저에서
 * 칸이 카드 폭의 4분의 3만 쓰고 오른쪽이 비었다. 칸은 늘 `min(칸 폭, 26rem)` 이다. */
.input, .select { max-width: min(100%, 26rem); }
.input-url { max-width: 62ch; }      /* ws://host:port · https://… */
.input-id { max-width: 26ch; }       /* 아두이노 ID · 제어기 ID */
.input-name { max-width: 32ch; }     /* 사람이 붙이는 이름 */
.input-token { max-width: min(100%, 26rem); }
.input-port { max-width: 10ch; }
.input-num { max-width: 12ch; }
/*
 * 사람이 불러 주는 **짧은 코드** -- 가입 코드 · 연결 코드.
 *
 * 값이 아홉 자다. 칸을 이름·주소용 기본 폭(44ch)이나 ID 폭(26ch)으로 두면, 빈 칸이
 * 값보다 넓어서 <더 적을 것이 있나> 로 읽힌다. 칸의 폭도 값이 무엇인지를 말한다.
 */
.input-shortcode { max-width: 14ch; letter-spacing: .08em; }
/* 검색칸은 예외다. 걸린 결과를 옆에 함께 보여 주므로 줄 전체를 쓴다. */
.searchbar .input, .filterbar .input { max-width: none; }

/*
 * 코드 편집칸.
 *
 * 입력칸의 기본 폭(44ch)은 사람이 이름·주소를 적는 칸에 맞춘 값이다. 코드를 그 폭에
 * 넣으면 줄이 자꾸 접혀 무엇을 고치는지 알 수 없다 -- 조작 패널을 고치는 자리가 그렇다.
 */
.input-code {
  max-width: none;
  min-height: 340px;
  /* 짜임을 따로 적지 않는다 -- 한글 되돌림이 --mono 에만 들어 있다(ui-tokens.css).
     여기 손으로 적어 두었을 때 이 칸의 한글 주석이 전부 네모였다. 코드를 고치는 칸인데
     그 코드의 주석이 한글이다. */
  font-family: var(--mono);
  font-size: var(--t-sm);
  line-height: 1.55;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}

/*
 * 제목에서 떼어 낸 도움말. 제목 바로 옆에 서되 제목의 글자에 섞이지 않는다.
 * 뜻은 aria-describedby 로 잇는다 -- 눈에는 붙어 있고 읽어 주는 도구에도 이어진다.
 */
.hint-loose { display: inline-flex; vertical-align: middle; margin-left: var(--s-1); }

/*
 * 제목과 그 도움말을 **한 줄에** 세운다.
 *
 * 제목은 블록(.section-title)이라 뒤에 오는 도움말이 다음 줄로 떨어졌다 -- 화면에는
 * <핀 구성> 아래에 `?` 하나가 홀로 서 있었다. 제목 안에 넣으면 제목의 이름이 도움말
 * 문단을 통째로 달고 다니므로(master.html 의 주석), 밖에 둔 채로 줄만 묶는다.
 */
.section-head { display: flex; align-items: center; gap: var(--s-1); }
.section-head .section-title { margin-bottom: 0; }
.section-head .hint-loose { margin-left: 0; }

/* 가려 둔 값과 그것을 펴는 단추. 단추가 칸 옆에 붙어 한 벌로 읽힌다. */
.peekrow { display: flex; align-items: center; gap: var(--s-2); max-width: 44ch; }
.peekrow .input { flex: 1 1 auto; max-width: none; }
.peekrow .btn { flex: none; }

.check { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-sm); color: var(--gray-700); }
/*
 * 체크 상자의 **치수는 여기서 안 정한다** (2026-10-05).
 *
 * 15px 이라고 적혀 있었는데 app/ui-help.css 의 <손가락으로 누르는 최소 크기>
 * 자리가 18px 로 덮고 있었다 -- 그 값이 맞는 값이다. 두 곳이 다른 말을 하면
 * 파일만 봐서는 어느 쪽이 사는지 알 수 없다.
 */
.check input { accent-color: var(--blue-600); }

/* ── 표 ─────────────────────────────────────────────────── */
/*
 * 표는 **제 판을 갖는다.**
 *
 * 구획이 상자를 내려놓으면서(app/ui-parts.css 의 .card) 표를 감싸던 테두리가 없어졌다.
 * 그러면 표가 화면에 떠 있는 글자 뭉치가 되고, 어디까지가 한 표인지 흐려진다.
 * 판은 **내용이 필요할 때 스스로 갖는다** -- 핀 격자(.rowlist)와 같은 규칙이다.
 */
.table-wrap {
  width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--r);
  /*
   * 넘치면 **이 판 안에서만** 민다.
   *
   * `overflow: hidden` 이었다. 둥근 모서리 밖으로 내용이 비어져 나오는 것을 막으려는
   * 것이었는데, 그러면 좁은 화면에서 오른쪽 열이 잘려 **보이지 않는 채로 사라진다.**
   * 좁을 때 표를 세로로 쌓던 갈림목이 그 잘림을 가리고 있었다(app/ui-help.css).
   *
   * 화면이 통째로 밀리는 것과 표 하나가 제 안에서 밀리는 것은 다른 일이다. 앞엣것은
   * 이 제품이 금지한 것이고, 뒤엣것은 견주어 읽는 판을 지키는 길이다.
   */
  overflow-x: auto;
  overflow-y: hidden;
}
/*
 * 표의 숫자는 **자리를 고정한다.**
 *
 * 이 제품의 표에는 시각·핀 번호·개수·금액이 들어간다. 비례폭 숫자로 두면 `1` 이 좁아
 * 시각 열이 줄마다 들쭉날쭉하고, 그러면 위아래를 견주어 읽을 수가 없다 -- 표를 쓰는
 * 이유가 바로 그 견주기다. 계기와 장부가 예외 없이 고정폭 숫자를 쓰는 이유다.
 */
.table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.table th {
  text-align: left;
  font-size: var(--t-xs); font-weight: 600; color: var(--gray-500);
  text-transform: uppercase; letter-spacing: .05em;
  padding: 8px 10px;
  border-bottom: 1px solid var(--gray-200);
  background: var(--gray-50);
  white-space: nowrap;
}
.table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
/* 마지막 줄의 아래 선은 판의 테두리와 겹친다. 겹친 선은 2px 로 굵어 보인다. */
.table tbody tr:last-child td { border-bottom: 0; }
.table .num, .table .shrink, .table .col-action { white-space: nowrap; }
/*
 * 줄바꿈만 막고 **폭은 안 정한다.**
 *
 * `shrink` 는 둘을 같이 한다(`width: 1%` + nowrap). 그래서 사람이 붙인 이름이 들어가는
 * 열에 그것을 주면, 값이 짧은 판에서는 열이 최소 폭으로 쪼그라들고 남는 폭이 마지막 열에
 * 통째로 몰린다 -- 조작 이력이 그랬다(동작과 결과 사이가 300px 넘게 벌어졌다).
 *
 * 이 열들은 **줄이 늘어나면 안 되지만 넓어지는 것은 좋다.** 남는 폭을 여기가 받아 가면
 * 현장에서 붙인 긴 이름(`1라인 충전 제어기` · `REST API (192.168.0.50)`)이 그대로 선다.
 */
.table .nowrap { white-space: nowrap; }
.table tr:last-child td { border-bottom: 0; }
.table tbody tr:not(.is-selected):hover { background: var(--gray-50); }

/* 목록에서 지금 보고 있는 항목. 눌러서 고르는 표에만 쓴다. */
.table tbody tr.clickable { cursor: pointer; }
.table tbody tr.is-selected > td { background: var(--blue-50); }
.table tbody tr.is-selected > td:first-child { box-shadow: inset 3px 0 0 var(--blue-600); }
.table .num { font-variant-numeric: tabular-nums; }

/*
 * 칸 안의 **둘째 줄** -- 이름 밑의 주소, 기한 밑의 날짜, 번호.
 *
 * 첫 줄과 같은 크기로 서면 두 값이 같은 무게로 읽혀서, 한 칸에 두 가지가 들어 있다는
 * 것만 보이고 무엇이 그 칸의 제목인지는 안 보인다.
 */
.cell-sub { font-size: var(--t-xs); color: var(--ink-3); margin-top: 2px; }
.table .shrink { width: 1%; white-space: nowrap; }
/*
 * ── 장치 등록 표는 **폭을 비율로 나눈다** ──────────────────
 *
 * 다섯 열에 `shrink`(width:1%)를 붙여 두었더니 남는 폭이 전부 **이름 한 열로** 몰렸다 --
 * 실기에서 표 1014px 중 이름이 541px 이었고 그 안에 든 글자는 일곱 자였다. 나머지 열은
 * 최소 폭으로 다닥다닥 붙고 가운데만 허허벌판이 된다. 판정 아래 사유 한 줄은 좁은 칸에서
 * 두세 줄로 접혔다 -- 옆에 빈 자리가 한참 남아 있는데도.
 *
 * `shrink` 자체는 잘못이 아니다. 그것은 **한 열만** 늘어나는 표를 위한 것이고, 이 표는
 * 늘어나도 되는 열이 여럿이다(이름 · 상태). 그런 표는 열마다 제 몫을 주는 편이 맞다.
 *
 * `fixed` 라 값이 길면 그 열 안에서 접힌다 -- **가로로 넘치지 않는다**(docs/검토지침.md §3).
 * 긴 자리 문자열(윈도우의 `USB 0000.0014…`)이 칸을 밀어내지 않도록 끊어 준다.
 *
 * 관제와 현장 제어기가 **같은 표**를 쓴다(master.html · setup.html). 폭이 갈리면 같은
 * 것을 보는 두 화면이 다른 모양이 된다.
 */
.table.dev-table { table-layout: fixed; }
.table.dev-table td { overflow-wrap: anywhere; }
.dev-table .col-port  { width: 13%; }
.dev-table .col-id    { width: 12%; }
.dev-table .col-name  { width: 26%; }
.dev-table .col-board { width: 14%; }
.dev-table .col-pin   { width: 6%; }
.dev-table .col-state { width: 21%; }
.dev-table .col-action { width: 8%; }
/* 동작 열은 오른쪽에 붙인다. 왼쪽 정렬로 두면 버튼 오른쪽에 빈 공간이 남는다. */
.table .col-action { text-align: right; white-space: nowrap; }
/*
 * 동작 열의 단추끼리는 붙이지 않는다.
 *
 * 붙여 두면 누르려던 것 **옆의 것**을 누른다 -- 이 줄에는 <구성> 옆에 되돌릴 수 없는
 * 동작이 함께 서므로 그것이 곧 사고다. 터치스크린에서는 손가락이 단추보다 넓어 더 그렇다.
 *
 * 자리마다 `style.marginLeft` 로 적던 것을 여기로 모았다. 표가 늘 때마다 그 줄을 베끼면
 * 언젠가 한 곳을 빠뜨리고, 빠뜨린 자리는 눈으로만 드러난다.
 */
.table .col-action > .btn + .btn { margin-left: 6px; }

.empty {
  padding: 22px;
  text-align: center;
  color: var(--ink-3);
  font-size: var(--t-sm);
}
/*
 * 표 아래에 접어 두는 곁말. 지금은 장치 등록의 <건너뛴 자리> 가 쓴다.
 *
 * 표의 줄로 세우면 볼 일 없는 것이 봐야 할 것 사이에 낀다. 그렇다고 지우면 신원을 못
 * 알아본 아두이노가 아무 데도 안 보이게 된다 -- 접어 두는 것이 그 사이다.
 */
.skipnote { font-size: var(--t-xs); color: var(--gray-500); }
.skipnote > summary { cursor: pointer; user-select: none; color: var(--gray-500); }
.skipnote > summary:hover { color: var(--navy-800); }
.skipnote > div { margin: 4px 0 0 14px; font-family: var(--mono); font-size: var(--t-xs); }


/* ── 목록 검색 ──────────────────────────────────────────── */
/*
 * 기기가 열 대를 넘으면 눈으로 훑는 것보다 이름을 치는 게 빠르다.
 * 표 바로 위에 두어, 무엇을 거르고 있는지가 결과와 같은 시야에 들어오게 한다.
 */
.filterbar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.filterbar .input { flex: 1 1 auto; min-width: 0; padding: 7px 10px; font-size: var(--t-sm); }
.filterbar .count { flex: none; font-size: var(--t-xs); color: var(--gray-500); white-space: nowrap; }

/*
 * 화면 전체에 걸리는 검색. 카드 안이 아니라 카드들 위에 둔다.
 * 카드 안에 넣으면 그 카드만 걸러진다고 읽히는데, 실제로는 기기와 핀을 함께 거른다.
 */
/*
 * 통합 검색. 화면에서 가장 먼저 서는 칸이라 한 단계 크게 둔다.
 * 폭 제한을 풀어 두는 것은 위(.searchbar .input)에서 이미 하고 있다.
 */
.searchbar { margin-bottom: var(--s-5); }
.searchbar .input { padding: 9px 12px; font-size: var(--t-md); }

/* 아래는 app/ui-pinlist.css 로 나갔다. */
/* ── ui-pinlist.css ─────────────────────────────── */
/*
 * **핀 목록** -- 제어기 아래 늘어서는 줄들.
 *
 * app/ui-forms.css 가 669줄이었다. 이 구획이 323줄이고 폼·표와 선택자가 겹치지 않는다.
 *
 * **걸리는 차례가 원본과 같아야 한다.** app/ui-forms.css 바로 뒤에 선다(app/ui.css).
 * 펼친 결과를 전후로 맞대어 규칙이 차례까지 같은 것을 확인했다.
 */

/* ── 핀 목록 ────────────────────────────────────────────── */
/*
 * 핀이 20개를 넘으면 표는 가로 공간을 반도 쓰지 않으면서 세로로만 끝없이 길어진다.
 * 그래서 핀 하나를 '한 줄짜리 행' 으로 만들고, 그 행을 가로로 채워 넣는다.
 * 넓은 화면에서는 서너 열로 접히고, 좁은 화면에서는 한 열이 된다.
 * 어느 쪽이든 핀 하나가 한 줄이라 세로 길이가 핀 수에 비례해 폭발하지 않는다.
 */
/*
 * ── 상자 스무 개가 아니라 **격자 한 판** ──────────────────
 *
 * 줄마다 테두리를 두르고 사이를 8px 벌려 두었다. 핀이 서른둘이면 **떠 있는 상자가
 * 서른둘**이고, 계기가 아니라 카드 더미로 읽힌다.
 *
 * 판 하나에 칸을 나누고 사이는 실선 한 줄이다. 선을 긋는 방법은 상태 타일과 같다
 * (app/ui-parts.css 의 .stats) -- 바탕을 선 색으로 깔고 틈을 1px 만 준다.
 *
 * **왜 테두리로 긋지 않는가.** 이 줄에는 안전에 관한 표시가 테두리와 그림자를 쓰고
 * 있다 -- 강제된 값의 파란 띠, 확인되지 않은 명령의 주황 띠. 칸마다 테두리를 주면
 * 그 자리를 뺏고, 맞닿은 선이 2px 로 겹친다. 틈으로 긋는 방식은 칸의 테두리와 그림자를
 * **통째로 비워 둔다.**
 *
 * `auto-fill` 그대로 둔다. 남는 칸에도 선이 그어지지만, 이 목록은 판 안이 늘 채워져
 * 있고(핀은 열여섯·서른둘 단위로 온다) 빈 칸이 격자로 보이는 편이 오히려 계기답다.
 */
.rowlist {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  background: var(--surface);
}
/*
 * 선은 **칸이 스스로 긋는다** -- 오른쪽과 아래.
 *
 * 처음에는 판의 바탕을 선 색으로 깔고 틈을 1px 만 주었다. 선은 정확했지만 **채워지지
 * 않은 칸까지 선 색으로 칠해져**, 열여섯 개가 세 열에 들어가면 마지막 줄에 회색 덩어리가
 * 남았다.
 *
 * `box-shadow` 로 그으면 자리를 차지하지 않아 맞닿은 선이 겹치지 않는다. 대신 이 자리를
 * 쓰던 상태 띠(강제·미확인·고름)는 **가짜 요소로 옮겼다** -- 아래를 보라.
 */
.rowitem {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 7px 10px;
  background: var(--surface);
  position: relative;
  box-shadow: inset -1px 0 0 var(--line), inset 0 -1px 0 var(--line);
}
/*
 * 상태 띠. 화면 어디서나 같은 자리, 같은 굵기다(계기 띠·구성도 칸과 같다).
 * 가짜 요소에 두는 이유는 위의 `box-shadow` 가 이미 선을 긋고 있기 때문이다 --
 * 한 성질에 둘을 담으면 나중에 붙는 쪽이 앞의 것을 지운다.
 */
.rowitem.is-on::before,
.rowitem.is-forced::before,
.rowitem.is-unconfirmed::before,
.rowitem.is-selected::before,
.rowitem.is-danger::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
}
/*
 * 핀 번호.
 *
 * **기계가 정한 값**이다. 사람이 붙인 이름과 나란히 서므로 글꼴로 갈라 둔다 -- 고정폭은
 * 자릿수를 세어야 하는 값의 것이고, 그래서 `32` 와 `7` 이 같은 폭으로 선다.
 * 알약이 아니라 네모다. 값이지 표식이 아니다.
 */
.row-chip {
  flex: none;
  min-width: 28px;
  padding: 2px 5px;
  text-align: center;
  font-family: var(--mono);
  font-size: var(--t-xs); font-weight: 600;
  color: var(--ink-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 2px;
}
/* 이름은 길이가 제각각이다. 남는 폭을 가져가되 넘치면 줄여서 행 높이를 지킨다. */
.row-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--t-sm);
  color: var(--black);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/*
 * 읽기와 쓰기는 하는 일이 다르다. 쓰기는 사람이 누르는 것이고 읽기는 보기만 하는 것이다.
 * 줄마다 작은 글자로 "읽기/쓰기" 를 적어 두면 스무 줄 중에서는 구분이 되지 않으므로,
 * 아예 묶어서 나누고 핀 번호 칩 색으로 한 번 더 표시한다.
 */
.rowgroup {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 10px;
  margin-top: 10px;
  font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .05em; text-transform: uppercase;
  color: var(--gray-500);
}
.rowgroup:first-child { margin-top: 0; }
/*
 * 줄이 아닌 것도 **제 바탕을 갖는다.** 판의 바탕이 선 색이라, 바탕이 없으면 그 자리가
 * 통째로 선으로 비친다. 좌우 여백은 줄과 맞춘다.
 */
.rowgroup, .rowlist > .lock-note { background: var(--surface); padding: var(--s-2) 10px; }
/*
 * 구획 머리 말고도 **줄이 아닌 것**이 이 목록에 들어온다. 잠금 안내가 그렇다.
 *
 * 목록은 격자다. 칸을 채우는 것이 기본이라, 안내가 그냥 들어가면 **칸 하나를 차지하고**
 * 그 옆에 핀 줄이 나란히 선다 -- 실제로 <입력·읽기> 구획이 그렇게 두 칸으로 갈라져,
 * 안내는 왼쪽 칸에 갇히고 센서 줄은 오른쪽으로 밀려 카드가 부서져 보였다.
 *
 * 줄이 아닌 것은 줄 전체를 쓴다. 머리(.rowgroup)와 같은 규칙이다.
 */
.rowlist > .lock-note { grid-column: 1 / -1; margin: 2px 0 6px; }
.rowgroup .rule { flex: 1; height: 1px; background: var(--gray-200); }
.rowgroup .n { color: var(--ink-3); font-weight: 500; }
/* 읽기 핀은 보기만 하는 값이라 기본으로 접어 둔다. 머리말을 눌러 편다. */
.rowgroup.toggle { cursor: pointer; user-select: none; }
.rowgroup.toggle:hover { color: var(--navy-800); }
.rowgroup .caret { color: var(--gray-400); font-size: 10px; }
.rowlist.hide-read .rowitem.is-read { display: none; }

.rowitem.is-write .row-chip { background: var(--blue-50); color: var(--blue-700); }
/*
 * 아직 사지 않은 줄. **지우지 않고 불만 끈다** -- 없는 것으로 두면 그 기능이 있다는
 * 사실조차 모르고, 살 이유도 알지 못한다. 흐리게 두어 산 줄과 한눈에 갈린다.
 */
/*
 * 눌러서 고르는 줄. 포트 목록이 이것을 쓴다 -- 줄 전체가 누르는 자리여야
 * 손가락으로도 빗나가지 않는다.
 */
.rowitem.clickable {
  width: 100%; text-align: left; cursor: pointer;
  font: inherit; color: inherit;
}
.rowitem.clickable:hover { background: var(--accent-soft); }
.rowitem.clickable:focus-visible { outline: none; box-shadow: var(--focus); }

/*
 * 꺼진 줄. 점선 테두리로 말하던 자리다 -- 줄에 테두리가 없어졌으므로 바탕으로만 말한다.
 * 색에만 기대지 않는다: 배지가 <꺼짐> 이라고 글자로 적고 있다.
 */
.rowitem.is-off { background: var(--surface-2); }
.rowitem.is-off .row-name { color: var(--ink-3); }

.rowitem.is-read { background: var(--gray-50); }
/*
 * 강제된 값(오버드라이브). 기기가 측정한 값이 아니라 사람이 정해 둔 값이라,
 * 한눈에 구분되지 않으면 고장 난 센서를 정상으로 착각한다.
 */
.rowitem.is-forced::before { background: var(--accent); }
/*
 * ── **나가 있는 줄** ──────────────────────────────────────
 *
 * 켜진 핀은 켜져 보여야 한다.
 *
 * 예전에는 줄 오른쪽의 배지 하나가 그 말을 다 했다. 열여섯 줄이 나란히 서 있으면 어느
 * 것이 나가 있는지 알려고 배지를 하나씩 읽어야 했고 -- 현장에서 그것은 안 읽는다는 뜻이다.
 *
 * 왼쪽 띠를 세우고, 줄 바닥을 아주 옅게 물들이고, 띠에 빛을 조금 준다. 흰 목록 위에서
 * **나가 있는 줄만 떠 보이는** 정도면 된다. 더 칠하면 목록이 얼룩이 된다.
 */
.rowitem.is-on {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--ok) 9%, var(--surface)) 0%,
    var(--surface) 34%);
}
.rowitem.is-on::before {
  background: var(--ok);
  box-shadow: 0 0 10px color-mix(in srgb, var(--ok) 70%, transparent);
}
/* 나가 있는 줄의 이름은 조금 더 세게. 목록을 훑을 때 이름이 먼저 걸린다. */
.rowitem.is-on .row-name { font-weight: 600; color: var(--ink); }
/*
 * 값이 가려진 줄 (<읽기 핀 보기> 를 안 산 현장).
 *
 * 핀 번호와 이름은 이 현장이 스스로 적어 둔 자기 설정이라 그대로 읽혀야 하고,
 * 가려진 것은 **값 하나**다. 그래서 줄 전체를 흐리게 하지 않는다 -- 흐린 줄은
 * "고장 났거나 꺼진 것" 으로 읽히고, 그러면 배선을 확인하러 현장에 나간다.
 */
.rowitem.is-veiled .badge { border-style: dashed; }
/*
 * 명령을 보냈으나 결과가 돌아오지 않은 줄.
 *
 * 실패한 것이 아니라 **모르는 것**이다. 빨강(오류)으로 칠하면 사람이 "안 됐구나" 로 읽고
 * 다시 누른다. 실제로는 이미 움직였을 수도 있으므로 그 오해가 더 위험하다.
 */
/*
 * 이름을 아직 적지 않은 자리.
 *
 * 그 자리를 비워 두면 스무 줄 중에서 어느 것이 몇 번 핀인지 알 수 없다. 사람이 적은
 * 이름은 아니라는 것만 흐리게 표시하고, 기계가 아는 이름은 그대로 보여 준다.
 */
.row-name .dim { color: var(--gray-500); }

/* 시간이 걸리는 일을 하는 중. 두 번 눌러 겹쳐 돌지 않게 잠근다. */
.rowitem.is-busy { opacity: .6; pointer-events: none; cursor: progress; }
.rowitem.is-unconfirmed { background: var(--warn-soft); }
.rowitem.is-unconfirmed::before { background: var(--warn); }
.rowitem .badge { flex: none; min-width: 58px; justify-content: center; }
/*
 * 읽기 핀에는 버튼이 없다. 자리까지 없애면 옆 행과 배지 위치가 어긋나 표처럼 읽히지 않는다.
 * 빈 자리를 남겨 세로줄을 맞춘다.
 *
 * ── 폭은 못 박지 않는다 ───────────────────────────────────
 *
 * `width: 52px` 로 못 박아 두었더니, 단추가 둘인 줄(강제된 핀의 <오버라이드 해제> + 켜기)
 * 에서 내용이 칸 밖으로 삐져나가 옆 칸을 덮었다. 아래 규칙(`flex-wrap: wrap`)이 넘치는 것을
 * 다음 줄로 접어 주게 두고, 여기서는 **가장 좁을 때의 폭**만 정한다.
 *
 * 높이도 여기서 잡는다. 단추가 있는 줄과 없는 줄의 높이가 달라(44px / 39px) 목록이
 * 톱니처럼 보였다 -- 빈 칸도 단추와 같은 높이를 차지한다.
 */
.row-act {
  flex: 0 0 auto;
  min-width: 52px; min-height: 28px;
  display: flex; align-items: center; justify-content: flex-end; gap: 6px;
}
.row-act .btn { flex: 1 1 auto; }

/* 목록으로 쓸 때 -- 카드 폭이 좁아 두 열로 나눌 수 없으면 한 열로 둔다. */
.rowlist.one { grid-template-columns: minmax(0, 1fr); }
/* 곁들이는 값이므로 자리가 모자라면 이쪽이 먼저 줄어든다. 이름이 먼저 잘리면 안 된다. */
.row-meta {
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--t-xs); color: var(--gray-500);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 곁들이는 값 안에서 눈에 띄어야 하는 것(강제된 핀 수 등) */
.row-meta .em { color: var(--blue-700); font-weight: 600; }

/*
 * ── 자주 보는 핀 묶음의 줄 ────────────────────────────────
 *
 * 이 목록만 **여러 아두이노의 핀이 섞여 있다.** 다른 목록은 장치를 하나 고르고 그 장치의
 * 핀만 보는 자리라 장치 이름이 필요 없지만, 여기서는 그것을 모르면 줄을 읽을 수가 없다 --
 * 아두이노가 둘이면 `릴레이1` 이라는 이름은 두 곳에 있을 수 있다.
 *
 * ── 그래서 장치 이름이 **줄의 머리**다 ────────────────────
 *
 * 처음에는 핀 이름 옆에 곁들이는 작은 회색으로 두었다. 300px 칸에서 그것은 22px 로 눌려
 * `tes…` 가 됐다 -- 있으나 마나 한 자리에 있으나 마나 한 크기로 있었다.
 *
 * 한 단 위로 올려 제 줄을 준다. 위에서 아래로 <어느 아두이노의 · 몇 번 핀> 으로 읽히고,
 * 폭이 얼마든 눌리지 않는다. 창을 넓혀야 보이는 것은 없다.
 */
.rowlist.fav .row-meta {
  order: -1;                           /* 핀 줄보다 먼저 */
  flex: 1 1 100%;                      /* 제 줄을 쓴다 */
  margin-left: 0;                      /* 별과 왼쪽을 맞춘다 -- 줄 전체의 머리다 */
  font-size: var(--t-sm); font-weight: 600;
  color: var(--ink-2);
}
/*
 * `.rowitem.clickable` 의 모양은 **위에 한 번만** 적는다 (2026-10-05).
 *
 * 여기에 `cursor: pointer` 와 회색 hover 가 한 벌 더 있었다. 뒤에 오므로 그 회색이
 * 이겼고, 위에서 <눌러서 고르는 줄> 이라고 상징색(--accent-soft)으로 칠해 둔 뜻이
 * 조용히 무르고 있었다. 고르는 자리와 그냥 지나가는 자리가 같은 회색이 된다.
 */
/* 줄 전체가 누르는 자리다. 링크로 만들어도 글자에 밑줄이 그이면 안 된다. */
a.rowitem { text-decoration: none; color: inherit; }
/* 되돌릴 수 없는 동작을 한 번 더 묻는 동안. 색으로만 알리고 자리는 그대로 둔다. */
.rowitem.is-danger { background: var(--err-soft); }
.rowitem.is-danger::before { background: var(--err); }
.rowitem.is-danger .row-name { color: var(--err-700); font-weight: 600; }
/* 고른 줄. 강제·미확인 표시와 같은 자리, 같은 굵기다. */
.rowitem.is-selected { background: var(--accent-soft); }
.rowitem.is-selected::before { background: var(--accent); }
.rowitem.is-selected:hover { background: var(--blue-50); }

/*
 * ── `.rowlist.links` 를 **걷었다** (2026-10-05) ──────────────
 *
 * 진입점 목록의 설명을 어디에 둘 것인가로 설계가 **두 벌** 들어 있었다.
 * 하나는 이름 아래로 내리고(flex: 1 1 100%), 하나는 오른쪽에 두고 넘치면 접는다
 * (flex: 0 1 auto). 뒤의 것이 이겼으므로 앞의 결정은 글로만 남아 있었다.
 *
 * 그런데 **그 마크업을 아무도 안 만든다.** `class="rowlist links"` 를 그리는 데가
 * 저장소에 한 곳도 없다(화면에서도 0개). 두 설계가 다 죽은 채로 모든 화면에
 * 실려 나가고 있었다.
 *
 * 걷는다. 다시 필요해지면 그때 **마크업과 함께** 한 벌만 적는다.
 */

/*
 * 설명이 한 문장인 목록(대시보드의 화면 진입점).
 *
 * 여기 설명은 곁들이는 값이 아니라 **"왜 여기로 가는가"** 다. 다른 목록처럼 한 줄로 잘라
 * 말줄임을 붙이면 정작 읽어야 할 뒷부분이 사라진다. 그래서 이 목록에서만 줄을 접는다.
 *
 * 글줄 폭은 62자로 묶는다. 넓은 화면에서 폭을 다 쓰면 한 줄이 길어져 다음 줄의 시작을
 * 눈이 놓친다 -- 대시보드는 훑어보는 화면이라 그 한 번이 곧 다시 읽기다.
 */
/*
 * 진입점 목록의 설명은 이름 **아래**에 둔다.
 *
 * 오른쪽 끝에 붙여 두었더니 줄마다 시선이 좌우로 튀고, 두 줄이 되는 항목은 줄 끝이
 * 어긋나 보였다. 이름 아래에 두는 것이 목록의 표준 형태다.
 */

/*
 * 좁은 화면에서는 이름과 설명을 한 줄에 나란히 둘 수 없다. 그대로 두면 설명이 폭을 차지해
 * **이름이 통째로 말줄임으로 사라진다** -- 정작 눌러야 할 것이 안 보인다.
 * 그래서 좁아지면 설명을 아랫줄로 내리고 이름은 온전히 남긴다.
 */
/*
 * ── 줄바꿈은 **늘 허용한다** ──────────────────────────────
 *
 * 760px 아래에서만 줄을 바꾸게 해 두었다(지침 §3 이 금지한 갈림목). 줄바꿈은 좁을
 * 때만 쓸 일이 아니다 -- 이름이 긴 줄은 넓은 화면에서도 넘친다. 늘 바꿀 수 있게 두면
 * 갈림목 없이 같은 결과가 나오고, 넘치지 않는 줄은 원래대로 한 줄에 선다.
 */
/* 곁의 값은 자리가 있으면 오른쪽 끝에, 없으면 다음 줄로 내려가 왼쪽에서 시작한다. */


/* ── ui-status.css ─────────────────────────────── */
/*
 * 상태 표식 · 알림 · 토스트 · 진행 막대 · 로그판
 *
 * `ui-forms.css` 한 장이 1113줄이었다. 이 저장소는 파일 하나를 **한 번에 읽을 수 있는
 * 범위**로 둔다(목표 400줄 · 상한 800줄). 그래서 그 파일이 이미 갖고 있던 결을 따라
 * 셋으로 갈랐다 -- 폼과 표 / 상태와 알림 / 도움말과 그 밖.
 *
 * **순서를 바꾸지 않았다.** 뒤엣것이 앞엣것을 덮는 규칙이 있어, 이어 붙이면 가르기 전과
 * 글자 그대로 같아야 한다. app/ui.css 가 이 셋을 원래 차례대로 부른다.
 */

/* ── 상태 표식 -- **알약이 아니라 표시등** ───────────────── */
/*
 * 둥근 알약에 색 바탕과 테두리를 두르고 그 안에 점을 찍었다. 웹 관리자 화면의 표식이고,
 * 스무 줄짜리 목록에서는 **알약 스무 개가 색 덩어리로 늘어선다** -- 정작 어느 것이
 * 다른지가 그 속에 묻힌다.
 *
 * 계기는 그렇게 말하지 않는다. **표시등 하나와 글자**다. 바탕도 테두리도 없다.
 * 색 덩어리가 사라지니 켜진 것과 꺼진 것이 한눈에 갈리고, 줄도 가벼워진다.
 *
 * 표시등이 **네모**인 이유. 동그라미는 부드럽고 친근한 모양이라 소비자 앱의 것이다.
 * 계기의 램프는 네모이거나 네모에 가깝다 -- 정밀한 것으로 읽힌다.
 */
.badge {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--t-xs); font-weight: 600; letter-spacing: .01em;
  color: var(--ink-2);
  white-space: nowrap;
}
.badge-ok    { color: var(--ok); }
.badge-warn  { color: var(--warn); }
.badge-err   { color: var(--err); }
/*
 * `--accent` 를 쓰고 있었다 (→ 2026-10-04).
 *
 * 그때는 `--info` 와 `--accent` 가 **같은 값**이어서 어느 쪽을 적든 같았다. 그래서
 * 아무도 틀린 줄 몰랐다. 두 이름을 가른 지금은 틀린 쪽이다 -- 이 콘솔에서 파랑은
 * <누르시오> 이고, 참고 뜻의 표시등이 파랗게 켜지면 누를 것으로 읽힌다.
 */
.badge-info  { color: var(--info); }
.badge-mute  { color: var(--ink-3); }

/*
 * 표시등. 켜진 것(정상·경고·오류)은 **빛난다** -- 그 빛이 이 화면을 계기로 만든다.
 * 꺼진 것(mute)은 빛나지 않고 테두리만 남는다. 색을 못 보는 사람에게는 그 차이가
 * 밝기가 아니라 **속이 찼는가**로 읽힌다.
 */
.dot {
  width: 7px; height: 7px; flex: none;
  border-radius: 1px;
  background: currentColor;
  box-shadow: 0 0 6px currentColor;
}
.badge-mute .dot { background: transparent; border: 1.5px solid currentColor; box-shadow: none; }

/* ── 알림 ───────────────────────────────────────────────── */
.notice {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 11px 14px;
  border: 1px solid var(--gray-200);
  border-left-width: 3px;
  border-radius: var(--radius-sm);
  font-size: var(--t-sm);
  background: var(--white);
  margin-bottom: 14px;
}
/*
 * 글자색이 **팔레트 밖의 값**이었다(#065f46 · #92400e · #991b1b → 2026-10-04).
 * 토큰 파일이 색을 정하는 유일한 자리라고 적어 두고(app/ui-tokens.css 머리) 정작
 * 알림 셋이 저마다 제 색을 들고 있었다. 상태색을 갈아도 여기만 옛 색으로 남는다.
 */
.notice-info { border-left-color: var(--info); background: var(--info-soft); color: var(--ink); }
.notice-ok   { border-left-color: var(--ok);   background: var(--ok-soft);   color: var(--ok); }
.notice-warn { border-left-color: var(--warn); background: var(--warn-soft); color: var(--warn); }
.notice-err  { border-left-color: var(--err);  background: var(--err-soft);  color: var(--err); }

/* ── 토스트 ─────────────────────────────────────────────── */
/* 지나가는 알림. 화면 상태를 설명하는 .notice 와 역할이 다르다. */
.toasts {
  position: fixed;
  /* 왼쪽도 함께 잡되 여백은 폭을 따라 준다. 좁으면 저절로 화면 폭이 된다. */
  right: min(20px, 3vw); left: min(20px, 3vw); bottom: min(20px, 3vw);
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
  z-index: 100;
  pointer-events: none;
}
.toast {
  display: flex; align-items: center; gap: 9px;
  /* 넓으면 380px 에 멈추고, 좁으면 남는 폭을 다 쓴다. */
  min-width: min(220px, 100%); max-width: min(380px, 100%);
  padding: 11px 14px;
  background: var(--navy-800);
  color: var(--white);
  border-radius: var(--radius);
  border-left: 3px solid var(--blue-600);
  box-shadow: 0 6px 20px rgba(11, 18, 32, .22);
  font-size: var(--t-sm);
  line-height: 1.45;
  pointer-events: auto;
  animation: toast-in .16s ease-out;
}
.toast.ok  { border-left-color: #34d399; }
.toast.warn{ border-left-color: #fbbf24; }
.toast.err { border-left-color: #f87171; }
.toast.leaving { animation: toast-out .16s ease-in forwards; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes toast-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(4px); }
}

/* 깜빡임 같은 진행 중 상태를 나타내는 작은 점 */
.toast .pulse {
  width: 8px; height: 8px; border-radius: 50%;
  background: currentColor; flex: none;
  animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }

/*
 * 좁은 화면에서 화면 폭을 쓰게 하던 갈림목을 걷었다(지침 §3).
 * `left` 를 늘 두고 폭 상한을 `min()` 으로 두면 같은 일이 갈림목 없이 일어난다 --
 * 넓으면 오른쪽 아래에 제 크기로 서고, 좁으면 저절로 양쪽 여백만 남기고 늘어난다.
 */

/* ── 진행 막대 ──────────────────────────────────────────── */
.progress {
  height: 6px;
  background: var(--gray-200);
  border-radius: 999px;
  overflow: hidden;
}
.progress-fill {
  height: 100%; width: 0;
  background: var(--blue-600);
  transition: width .15s ease-out;
}
.progress-fill.done { background: var(--ok-700); }
.progress-meta {
  display: flex; justify-content: space-between;
  font-size: var(--t-xs); color: var(--gray-500); margin-top: 6px;
  font-variant-numeric: tabular-nums;
}

/* ── 로그판 ─────────────────────────────────────────────── */
/* 로그판은 본문과 성격이 다른 덩어리다. 앞의 것이 무엇이든 붙지 않게 자기 여백을 갖는다. */
.logpanel {
  margin-top: 16px;
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  background: var(--white);
}
.logpanel summary {
  padding: 10px 16px;
  cursor: pointer;
  font-size: var(--t-xs); font-weight: 600; color: var(--gray-600);
  text-transform: uppercase; letter-spacing: .05em;
  list-style: none;
}
.logpanel summary::-webkit-details-marker { display: none; }
.logpanel summary::before { content: '▸ '; color: var(--gray-400); }
.logpanel[open] summary::before { content: '▾ '; }
.logpanel summary:hover { background: var(--gray-50); }
.logbar {
  display: flex; align-items: center; gap: 12px;
  flex-wrap: wrap;
  padding: 0 16px 10px;
  border-bottom: 1px solid var(--gray-200);
  margin-bottom: -1px;
}
.logbar .spacer { flex: 1; }
.log {
  margin: 0;
  padding: 12px 16px;
  background: var(--navy-900);
  color: #cbd5e1;
  border-radius: 0 0 var(--radius) var(--radius);
  font-family: var(--mono);
  font-size: var(--t-xs);
  line-height: 1.65;
  max-height: 320px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-all;
}
/*
 * 로그판의 시각·곁글. **팔레트 밖의 값**이었고(#64748b · #7c8aa0), 남색 위에서 대비가
 * 3.5 안팎이라 본문 기준(4.5)에 못 미쳤다. 이 판은 12px 고정폭이 빽빽한 자리라
 * 안 읽히면 로그를 안 읽게 된다. 어두운 면 위의 글자색은 이미 토큰에 있다.
 */
.log .t { color: var(--shell-ink-2); }
.log .ok   { color: #6ee7b7; }
.log .warn { color: #fcd34d; }
.log .err  { color: #fca5a5; }

/* 상세 기록은 처음부터 담아 두고 감춰만 둔다. 켜는 순간 지난 내역까지 드러난다. */
.log .detail { display: none; color: var(--shell-ink-2); }
.log.verbose .detail { display: block; }

/* ── ui-help.css ─────────────────────────────── */
/*
 * 도움말 · 반응 · 잠금 표시 · 동의 자리
 *
 * `ui-forms.css` 한 장이 1113줄이었다. 이 저장소는 파일 하나를 **한 번에 읽을 수 있는
 * 범위**로 둔다(목표 400줄 · 상한 800줄). 그래서 그 파일이 이미 갖고 있던 결을 따라
 * 셋으로 갈랐다 -- 폼과 표 / 상태와 알림 / 도움말과 그 밖.
 *
 * **순서를 바꾸지 않았다.** 뒤엣것이 앞엣것을 덮는 규칙이 있어, 이어 붙이면 가르기 전과
 * 글자 그대로 같아야 한다. app/ui.css 가 이 셋을 원래 차례대로 부른다.
 */

/* ── 도움말 아이콘 + 툴팁 ───────────────────────────────── */
/*
 * 설명을 줄글로 깔면 읽지 않는다. 필요한 사람만 펼쳐 보도록 아이콘 뒤에 둔다.
 * 안쪽은 항목별로 끊어 쓴다.
 */
.hint { position: relative; display: inline-flex; vertical-align: middle; margin-left: 5px; }
/*
 * 도움말. 동그라미는 소비자 앱의 것이다 -- 네모로 둔다.
 *
 * **크기는 여기서 안 정한다** (2026-10-05). 16×16 이라고 적혀 있었는데 아래의
 * <손가락으로 누르는 최소 크기> 자리가 24×24 로 덮고 있었다 -- 그 값이 맞는
 * 값이다(터치 화면에서 작은 자리는 옆의 것이 눌린다). 두 값이 다른 말을 하면
 * 파일만 봐서는 어느 쪽이 사는지 알 수 없으므로, 크기는 한 곳에만 둔다.
 */
.hint-btn {
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: 2px;
  background: transparent;
  color: var(--ink-3);
  font-family: inherit;
  font-weight: 700; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: help;
}
.hint-btn:hover { border-color: var(--accent); color: var(--accent); }
.hint-btn:focus-visible { outline: none; box-shadow: var(--focus); }

.hint-body {
  position: absolute;
  left: 0;
  top: calc(100% + 7px);
  z-index: 30;
  /* 화면 밖으로 나가지 않는다. 좁으면 저절로 줄어 아래로 옮길 이유가 없어진다. */
  width: min(340px, calc(100vw - 28px));
  padding: 11px 13px;
  background: var(--navy-800);
  color: #cbd5e1;
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgba(11, 18, 32, .25);
  font-size: var(--t-xs);
  font-weight: 400;
  line-height: 1.65;
  letter-spacing: normal;
  text-transform: none;
  white-space: normal;
  /*
   * 글은 늘 왼쪽부터. 이 창은 어디에나 붙을 수 있어서, 붙은 자리의 정렬을 물려받으면
   * 같은 말이 자리마다 다르게 선다 -- 조작 이력의 <결과> 칸이 오른쪽 정렬이라
   * 거부 사유가 통째로 오른쪽에 붙어 있었다.
   */
  text-align: left;
  opacity: 0;
  /*
   * 닫혀 있을 때는 자리에서 아예 빼야 한다.
   * visibility 만 감추면 절대 위치라도 문서의 스크롤 범위에는 그대로 남아,
   * 오른쪽 끝에 붙은 아이콘의 툴팁 때문에 열지도 않았는데 가로 스크롤이 생긴다.
   */
  display: none;
  visibility: hidden;
  transition: opacity .12s;
}
/*
 * 눌러서 여는 것이 기본이다. 이 화면은 터치스크린에서도 쓰이는데 거기에는 hover 가 없다.
 * 마우스가 있는 환경에서만 hover 를 덤으로 동작시킨다.
 */
.hint.open .hint-body { display: block; opacity: 1; visibility: visible; }
@media (hover: hover) {
  .hint:hover .hint-body { display: block; opacity: 1; visibility: visible; }
}
/*
 * 아이콘이 화면 오른쪽 끝에 있으면 왼쪽 기준으로 펼친 툴팁이 넘쳐 가로 스크롤이 생긴다.
 * 넘칠 때만 반대쪽에 붙인다. 판단은 app/ui.mjs 의 bindHints() 가 열릴 때 재서 한다.
 */
.hint-body.flip { left: auto; right: 0; }
.hint-body b { color: var(--white); font-weight: 600; }
.hint-body div + div { margin-top: 5px; }
.hint-body code {
  font-family: var(--mono);
  background: rgba(255, 255, 255, .1);
  padding: 1px 4px;
  border-radius: 3px;
  color: #e2e8f0;
}

/* ── 도움말 ─────────────────────────────────────────────── */
/*
 * 각주. 구획 안에서 **덧붙이는 말**이다.
 *
 * 폭을 다 쓰는 선을 위에 두고 있었다. 카드가 상자이던 때는 그 선이 <이 상자 안의 아랫단>
 * 으로 읽혔는데, 구획이 상자를 내려놓은 지금은 **구획을 가르는 선**과 똑같이 생겼다 --
 * 그래서 제목 없는 구획이 하나 시작된 것처럼 보인다.
 *
 * 각주 선은 짧다. 인쇄물이 수백 년 그렇게 해 왔고, 이유도 같다 -- 나누는 선이 아니라
 * **여기부터는 곁말**이라는 표시이기 때문이다.
 */
.help {
  font-size: var(--t-sm); color: var(--ink-3);
  padding-top: var(--s-3); margin-top: var(--s-5);
  position: relative;
}
.help::before {
  content: ''; position: absolute; top: 0; left: 0;
  width: 40px; height: 1px; background: var(--line-2);
}
/*
 * ── **곁말이 없으면 선도 없다** ───────────────────────────
 *
 * 화면이 채우는 각주 자리는 처음엔 비어 있다(`<p class="help" id="…"></p>`). 그런데
 * 선은 글이 아니라 상자에 붙어 있어서, **빈 칸에 40px 짜리 막대만 남았다.**
 *
 * 구독의 <값> 칸이 그랬다 -- 신청 단추 위에 하나, 아래에 하나. 각주가 아니라 **끊다 만
 * 구분선**으로 읽힌다. 위아래 여백(20px + 8px)까지 그대로 잡고 있어서 단추가 뜬금없이
 * 멀어지기도 했다.
 *
 * 공백만 들어 있는 것은 `:empty` 가 아니다. 화면이 `textContent = ''` 로 비우므로
 * 여기 걸린다 -- 글이 들어오는 순간 선도 함께 돌아온다.
 */
.help:empty { display: none; }
.help code {
  font-family: var(--mono);
  background: var(--gray-100);
  padding: 1px 5px;
  border-radius: 3px;
  color: var(--gray-700);
}

/* ── 좁은 화면 ──────────────────────────────────────────── */
/*
 * ── 좁은 화면을 **갈림목 없이** 받는다 ───────────────────
 *
 * 여기 있던 `@media (max-width: 760px)` 를 걷었다(docs/검토지침.md §3). 그 블록이 하던
 * 일 넷을 갈림목 없이 같은 결과로 옮겼다.
 *
 *   여백 줄이기      `--page-pad` 를 `min()` 으로 -- 폭을 따라 저절로 준다
 *   칸 한 줄로        `.field-grid` 는 이미 auto-fit 이라 스스로 접힌다. 덧쓸 것이 없었다
 *   도움말을 아래로   폭 상한을 화면 안으로 묶는다. 넘칠 일이 없으면 옮길 이유도 없다
 *   표를 세로로 쌓기  아래를 보라
 *
 * ── 표는 **쌓지 않고 밀어 본다** ─────────────────────────
 *
 * 좁은 화면에서 표를 줄 단위 블록으로 쌓고 머리글을 감추던 코드였다. 그런데 지침이
 * 금지한 것이 바로 그것이다 -- <선의 방향이 바뀌거나 **요소가 사라지면** 위반>.
 * 머리글이 사라지고 칸이 세로로 서면서, 같은 표가 폭에 따라 **다른 물건**이 됐다.
 * 칸마다 `data-label` 을 달아 이름을 다시 만들어 주어야 했던 것이 그 증거다.
 *
 * 표는 원래 **견주어 읽는 판**이다. 세로로 쌓으면 그 견주기가 사라진다. 그래서 판은
 * 그대로 두고, 좁을 때는 **그 판만** 가로로 밀어 본다(.table-wrap 의 overflow-x).
 * 화면이 통째로 밀리는 것과 표 하나가 제 안에서 밀리는 것은 다른 일이다.
 */


/* ── 잠금과 경로 표시 ───────────────────────────────────── */
/*
 * 잠긴 이유는 **화면에 글로** 적는다. title(툴팁)은 보조일 뿐이다 --
 * 이 제품의 주 사용 환경은 터치스크린이고, 거기에는 hover 가 없다.
 * 이유를 볼 방법이 없는 비활성 버튼은 현장에서 그냥 "고장" 으로 읽힌다.
 */
.lock-note {
  margin: 6px 0 10px; padding: 8px 12px;
  background: var(--gray-50); border-left: 3px solid var(--gray-300);
  font-size: var(--t-sm); color: var(--gray-600); line-height: 1.5;
  /* 낱말 안에서 자르지 않는다. `신청합니 / 다.` 처럼 끝줄에 글자 하나가 남았다(.field-hint 와 같은 이유). */
  word-break: keep-all;
}
/*
 * 사면 열리는 잠금에는 갈 곳이 함께 붙는다(app/master/lock.mjs 의 lockNote).
 * 이유만 적힌 안내는 막다른 길이다 -- 키오스크에는 주소창이 없어, 화면 이름만 알려 주면
 * 읽은 사람은 그 화면을 찾아 헤맨다.
 */
.lock-note a, .field-hint a, .card-desc a { color: var(--blue-700); font-weight: 600; }
.lock-note a:hover, .field-hint a:hover, .card-desc a:hover { text-decoration: underline; }
.btn.is-locked { cursor: not-allowed; }

/* 목록 머리말의 곁들이는 설명. 무엇을 보고 있는지 한마디로 말한다. */
.rowgroup .hint-text { font-size: var(--t-xs); color: var(--gray-500); font-weight: 400; margin-left: 8px; }

/* 지금 어디에 있는가. 제품 이름을 바꾸는 대신 이 줄이 위치를 말한다. */
.crumbs {
  display: flex; align-items: center; flex-wrap: wrap; gap: 2px 6px;
  max-width: var(--page-max, 1080px); margin: 0 auto; padding: 10px 16px 0;
  font-size: var(--t-sm); color: var(--gray-500);
}
.crumbs .crumb {
  background: none; border: 0; padding: 2px 4px; margin: 0;
  font: inherit; color: var(--blue-700); cursor: pointer; border-radius: 4px;
}
.crumbs .crumb:hover { background: var(--gray-100); }
.crumbs .crumb:focus-visible { outline: none; box-shadow: var(--focus); }
.crumbs .crumb.is-here { color: var(--gray-700); font-weight: 600; cursor: default; }
.crumb-sep { color: var(--gray-400); }

/* ID 가 겹쳐 명령이 거부되는 중인 카드. 정상과 같은 색으로 두면 지나친다. */
.card.is-clash { border-color: #fca5a5; }

/* 조작 이력에서 거절된 줄. 성공과 같은 무게로 두면 훑을 때 걸리지 않는다. */
.table tr.is-refused td { background: var(--err-50); }

/* 확인용 두 번째 누름을 기다리는 버튼 */
.btn.is-danger { border-color: #fca5a5; background: var(--err-50); color: var(--err-700); }

/* 체크 상자의 모양은 app/ui-forms.css 가 쥔다. 여기 한 벌 더 있던 것을 걷었다 -- 글자까지 같았다. */

/*
 * 해당 없는 단계.
 *
 * 감추지 않고 흐리게 두어 "그런 단계가 있고 지금은 내 몫이 아니다" 를 보이게 한다.
 * 감추면 나중에 "외부 접속은 어디서 하느냐" 는 물음이 그대로 남는다.
 */
.card-body.is-locked-step { opacity: .45; pointer-events: none; }

/*
 * 첫 화면 고르기.
 *
 * 줄을 누르면 그 자리에서 목록이 열린다. 다른 화면으로 옮기지 않는 이유는,
 * 이 설정을 바꾸러 왔다가 어디로 갔는지 잊는 일이 실제로 생기기 때문이다.
 */
.home-pick {
  flex: 1 1 100%;
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--gray-200);
}

/* 그냥 두면 안 되는 값. 회색으로 두면 정상과 구분되지 않는다. */
.warn-text { color: var(--warn-700, #b45309); font-weight: 600; }

/*
 * 줄 안에서 무엇이 먼저 자리를 얻는가.
 *
 * 설비 이름이 가장 먼저 읽혀야 한다. 예전에는 배지가 자리를 다 먹어 이름이
 * "컨베이어 제…" 로 잘렸다 -- 관제에서 그 이름을 못 읽으면 카드를 열어 볼 이유가 없다.
 * 좁으면 배지를 아랫줄로 내리고, 이름은 끝까지 지킨다.
 */
/*
 * 진입점 목록(.links)은 위에서 이미 "이름 위, 설명 아래" 로 정해 두었다.
 * 여기서 같은 특이도로 다시 정하면 **뒤에 쓴 이것이 이겨** 설명이 다시 오른쪽으로 붙는다.
 * 실제로 그렇게 되돌아갔다. 그래서 이 규칙은 진입점 목록을 건드리지 않는다.
 */
.rowlist.one:not(.links) .rowitem { flex-wrap: wrap; row-gap: 4px; }
.rowlist.one:not(.links) .row-name { flex: 1 1 8rem; min-width: 6rem; }
.rowlist.one:not(.links) .badge { flex: 0 0 auto; }

/* 버튼 글자는 접지 않는다. 세 줄로 접힌 버튼은 누르는 자리로 보이지 않는다. */
.row-act .btn, .rowitem > .btn { white-space: nowrap; }

/* 핀 줄은 넘치기 전에 접는다. 칸 밖으로 삐져나간 버튼은 눌리지 않는다. */
.rowlist:not(.links) .rowitem { flex-wrap: wrap; row-gap: 4px; }
.rowlist:not(.links) .rowitem .row-name { flex: 1 1 6rem; min-width: 4.5rem; }

/*
 * 비어 있는 자리는 "없다" 로 끝내지 않는다. 그래서 한 줄이 아니라 묶음이 된다 --
 * 무엇이 없는지, 왜 없는지, 무엇을 누르면 되는지.
 */
.empty-title { font-size: var(--t-md); font-weight: 600; color: var(--gray-600); }
.empty-why {
  font-size: var(--t-sm); color: var(--gray-500); margin: 6px auto 0;
  max-width: 46ch; line-height: 1.6; word-break: keep-all;
}
.empty .btn { margin-top: 14px; }
/* 표 칸 안에 들어가면 칸의 가운데 정렬 규칙과 겹친다. 여기서 다시 세운다. */
.table td .empty { padding: 26px 12px; }

/* 줄이 많은 표. 대시보드처럼 훑어보는 자리에서 쓴다. */
.table-tight th, .table-tight td { padding: 7px 12px; }
.table-tight td.num { font-variant-numeric: tabular-nums; color: var(--ink-3); }

/* 표의 줄 안에서 되돌릴 수 없는 동작을 떼어 놓는 사이. */
.act-gap { display: inline-block; width: 18px; }

/*
 * ── 손가락으로 누르는 최소 크기 ──────────────────────────
 *
 * 이 제품은 터치스크린 키오스크에서 돈다. 누르는 자리가 작으면 옆의 것이 눌리고,
 * 설비를 다루는 화면에서 그것은 사고다. 밀도를 올리는 것이 이 판의 큰 일이지만
 * **밀도가 이 규칙을 이기지는 못한다.**
 *
 * 검사(test/test-shell.mjs)가 24px 를 밑도는 것을 찾아 지적한다.
 */
.check {
  min-height: var(--touch);
  padding: 0 var(--s-1);
  cursor: pointer;
}
.check input { width: 18px; height: 18px; }

.hint-btn {
  width: 24px; height: 24px;
  font-size: var(--t-xs);
}

/* ── 읽고 동의하는 자리 ─────────────────────────────────── */
/*
 * 약관을 그대로 펼치면 아무도 읽지 않는다. **뒤에 문제가 되는 조항**만 추려
 * 짧은 목록으로 두고, 전문은 링크로 연다.
 */
.bullets { margin: 0; padding-left: 18px; display: grid; gap: 8px; }
.bullets li { font-size: var(--t-md); color: var(--gray-700); line-height: 1.6; word-break: keep-all; }
.bullets b { font-weight: 650; }

/*
 * 동의 체크는 크게 둔다. 작은 네모 하나로 두면 손가락으로 누르는 화면에서 빗나가고,
 * 빗나간 사람은 "동의했는데 안 넘어간다" 로 읽는다.
 */
.agree {
  display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
  border: 1px solid var(--gray-200); border-radius: var(--r-md); padding: 14px 16px;
  font-size: var(--t-md); color: var(--gray-700); line-height: 1.55;
}
.agree input { margin: 2px 0 0; flex: 0 0 auto; width: 18px; height: 18px; }
.agree:has(input:checked) { border-color: var(--blue-600); background: var(--blue-50); }

/*
 * ── 별(자주 보는 핀) ──────────────────────────────────────
 *
 * ── 왜 줄 맨 앞인가 ───────────────────────────────────────
 *
 * 한동안 동작 칸(.row-act)에 넣어 두었다. 켜기 단추와 **한 칸을 나눠 쓰게** 되어 셋이
 * 어긋났다: 별이 칸 밖으로 삐져나가고, 단추가 없는 읽기 핀에서는 별이 43px 오른쪽에
 * 혼자 서고, 단추가 있는 줄만 5px 더 높아 목록이 톱니처럼 보였다.
 *
 * 자리를 나눠 쓰는 대신 **제 칸을 준다. 그리고 그 칸은 맨 앞이다.**
 *
 *   · 별은 상태도 동작도 아니고 **사람이 붙인 표시**다. 표시는 줄 머리에 선다
 *     (메일함의 별과 같다). 눈이 왼쪽 끝만 훑어도 어느 것이 표시된 줄인지 보인다.
 *   · 설비를 움직이는 단추(켜기·끄기)에서 **멀어진다.** 별을 누르려다 빗나가면
 *     컨베이어가 돈다 -- 나란히 둘 이유가 없는 두 가지다.
 *
 * 평소에는 흐리다. 매일 누르는 것은 켜기·끄기이고 별은 한 번 붙여 두면 그만이라,
 * 같은 무게로 서 있으면 안 된다. 줄에 손을 올리면 조금 진해져 "누를 수 있는 것" 이 되고,
 * 붙인 것만 색이 든다.
 */
.star {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: var(--r-sm);
  background: none; cursor: pointer;
  /*
   * 안 붙인 별의 색. **--line-2(선 색)까지 내렸다가 되돌렸다** -- 회색 줄 바탕(읽기 핀)
   * 위에서 거의 사라져, 별을 붙일 수 있다는 것 자체가 안 보였다. 이 화면에서 별을
   * 붙이는 법을 말해 주는 것은 이 별 하나뿐이다.
   */
  color: var(--ink-4);
  transition: color var(--fast) var(--ease), background-color var(--fast) var(--ease);
}
/*
 * 두 상태의 크기가 같다. 채우기만 든다 -- 글자 ★ ☆ 를 갈아 끼우던 때는 폭이 달라
 * 누를 때마다 줄이 밀렸다(app/favorites.mjs).
 */
.star svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor;
  stroke-width: 1.7; stroke-linejoin: round;
}
.rowitem:hover .star { color: var(--ink-4); }
.star:hover, .star:focus-visible { color: var(--mark-strong); background: var(--mark-soft); }
.star:focus-visible { outline: none; box-shadow: var(--focus); }
.star.is-on, .rowitem:hover .star.is-on { color: var(--mark); }
.star.is-on svg { fill: currentColor; }
.star.is-on:hover, .star.is-on:focus-visible { color: var(--mark-strong); }

