  /* ==================================================================
     Module Manager 콘솔의 디자인 토큰을 따른다.
     파스텔 시맨틱 색을 자기 틴트 위 글자로 쓰면 WCAG AA에 미달하므로
     "채움색"(--success)과 "글자색"(--success-text)을 분리해 쓴다.
     ================================================================== */
  :root {
    color-scheme: light;
    --bg: #f9f9fc;
    --surface: #ffffff;
    --surface-2: #f8f9fa;
    --border: #eaedf1;
    --border-input: #d8dfe7;

    --heading: #1c252e;
    --body: #5d7186;
    --muted: #617488;

    --primary: #604ae3;
    --primary-text: #5f49e1;
    --primary-soft: rgba(96, 74, 227, .11);
    --on-primary: #ffffff;

    --success: #5cc184;  --success-text: #397852;
    --danger:  #e96767;  --danger-text:  #aa4b4b;
    --warning: #f0934e;  --warning-text: #975d31;
    --info:    #45c5cd;  --info-text:    #2b7a7f;

    /*
     * 그래프 카드의 폭. 모듈 노드(292px)보다 넓은 쪽이고, **세로로 쌓은 모듈 단이
     * 이 폭에 맞춰진다**(`.g-children.col`) — 세 곳이 같은 숫자를 따로 적으면
     * 한 곳만 고쳐지는 날 줄기가 카드를 빗나간다.
     */
    --g-card-w: 332px;

    --radius: 5.6px;
    --radius-lg: 8px;
    --shadow: 0 2px 4px rgba(0, 0, 0, .05);
    --shadow-lg: 0 4px 16px rgba(28, 37, 46, .1);

    --sans: "Figtree", system-ui, -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
    --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

    --menu-w: 232px;
    --menu-w-condensed: 76px;
    --topbar-h: 70px;
  }
  @media (prefers-color-scheme: dark) {
    :root:where(:not([data-theme="light"])) {
      color-scheme: dark;
      --bg: #141a21; --surface: #1c252e; --surface-2: #2f3943;
      --border: #29333d; --border-input: #3a4653;
      --heading: #dee2e6; --body: #aab8c5; --muted: #96a2b0;
      --primary-text: #9688ed; --primary-soft: rgba(96, 74, 227, .22);
      --success-text: #6ec892; --danger-text: #ef9595;
      --warning-text: #f3a56c; --info-text: #45c5cd;
      --shadow: 0 2px 4px rgba(0,0,0,.35); --shadow-lg: 0 6px 20px rgba(0,0,0,.5);
    }
  }
  :root[data-theme="dark"] {
    color-scheme: dark;
    --bg: #141a21; --surface: #1c252e; --surface-2: #2f3943;
    --border: #29333d; --border-input: #3a4653;
    --heading: #dee2e6; --body: #aab8c5; --muted: #96a2b0;
    --primary-text: #9688ed; --primary-soft: rgba(96, 74, 227, .22);
    --success-text: #6ec892; --danger-text: #ef9595;
    --warning-text: #f3a56c; --info-text: #45c5cd;
    --shadow: 0 2px 4px rgba(0,0,0,.35); --shadow-lg: 0 6px 20px rgba(0,0,0,.5);
  }

  * { box-sizing: border-box; }
  body {
    margin: 0; background: var(--bg); color: var(--body);
    font-family: var(--sans); font-size: 14px; line-height: 1.6;
    -webkit-font-smoothing: antialiased;
  }
  h1, h2, h3, h4 { color: var(--heading); }
  .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: .92em; }
  .muted { color: var(--muted); }

  /* ================= 앱 셸 ================= */
  .app-menu {
    position: fixed; inset: 0 auto 0 0; z-index: 40; width: var(--menu-w);
    background: var(--surface); border-right: 1px solid var(--border);
    display: flex; flex-direction: column; transition: width .2s ease;
  }
  .logo-box {
    height: var(--topbar-h); flex: none; display: flex; align-items: center; gap: 11px;
    padding: 0 20px; border-bottom: 1px solid var(--border);
  }
  .logo-mark {
    width: 36px; height: 36px; flex: none; border-radius: var(--radius-lg);
    background: var(--primary); color: var(--on-primary); display: grid; place-items: center;
  }
  .logo-text { min-width: 0; }
  .logo-text .name { display: block; font-weight: 700; font-size: 16px; color: var(--heading); line-height: 1.15; letter-spacing: -.01em; }
  .logo-text .sub { display: block; font-size: 11px; color: var(--muted); }

  .menu-scroll { flex: 1; overflow-y: auto; padding: 14px 12px 20px; }
  .menu-title {
    font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
    color: var(--muted); padding: 14px 10px 7px; white-space: nowrap;
  }
  .menu-item {
    display: flex; align-items: center; gap: 11px; width: 100%;
    padding: 9px 11px; margin-bottom: 2px; border: 0; border-radius: var(--radius-lg);
    background: none; font: inherit; font-size: 14px; font-weight: 500; color: var(--body);
    text-align: left; cursor: pointer; white-space: nowrap; transition: background .15s, color .15s;
  }
  .menu-item[hidden] { display: none; }   /* display:flex 가 UA 의 [hidden] 을 이긴다 */
  .menu-item > svg { width: 19px; height: 19px; flex: none; stroke-width: 1.7; }
  .menu-item:hover { background: var(--surface-2); color: var(--heading); }
  .menu-item.active { background: var(--primary-soft); color: var(--primary-text); font-weight: 600; }
  .menu-item .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .menu-badge {
    font-size: 11px; font-weight: 600; line-height: 1.5; min-width: 21px; text-align: center;
    padding: 0 6px; border-radius: 999px; background: var(--surface-2);
    color: var(--muted); border: 1px solid var(--border);
  }
  .menu-item.active .menu-badge { background: var(--surface); color: var(--primary-text); border-color: var(--primary-soft); }
  .menu-badge.soon { background: transparent; border-style: dashed; color: var(--muted); font-weight: 500; }
  .menu-item.is-soon { color: var(--muted); }
  .menu-item.is-soon > svg { opacity: .7; }

  :root[data-menu="condensed"] .app-menu { width: var(--menu-w-condensed); }
  :root[data-menu="condensed"] .logo-box { padding: 0; justify-content: center; }
  :root[data-menu="condensed"] .logo-text,
  :root[data-menu="condensed"] .menu-item .label,
  :root[data-menu="condensed"] .menu-badge,
  :root[data-menu="condensed"] .menu-title { display: none; }
  :root[data-menu="condensed"] .menu-item { justify-content: center; padding: 11px 0; }
  :root[data-menu="condensed"] .app-shell { padding-left: var(--menu-w-condensed); }

  .app-shell { padding-left: var(--menu-w); transition: padding .2s ease; min-height: 100vh; }
  .topbar {
    position: sticky; top: 0; z-index: 30; height: var(--topbar-h);
    display: flex; align-items: center; gap: 12px; padding: 0 24px;
    background: var(--surface); border-bottom: 1px solid var(--border);
  }
  .icon-btn {
    width: 38px; height: 38px; flex: none; display: grid; place-items: center;
    border: 0; border-radius: var(--radius-lg); background: none; color: var(--body);
    cursor: pointer; position: relative; transition: background .15s, color .15s;
  }
  .icon-btn:hover { background: var(--surface-2); color: var(--heading); }
  .icon-btn svg { width: 20px; height: 20px; stroke-width: 1.7; }
  .topbar .spacer { flex: 1; }
  /*
   * 상단바의 환경 범위 칩. 고른 칸만 등급 색으로 채운다 — 전부 칠하면 상단바가
   * 신호등이 되어, 정작 색으로 말해야 할 것(지금 어디를 보고 있나)이 묻힌다.
   */
  #env-scope { margin-left: 4px; }
  #env-scope[hidden] { display: none; }   /* inline-flex 가 UA 의 [hidden] 을 이긴다 */
  #env-scope .seg-btn { padding: 5px 12px; font-size: 12px; white-space: nowrap; }
  #env-scope .seg-btn.active.env-prod { color: var(--danger-text);  background: color-mix(in srgb, var(--danger) 14%, transparent); }
  #env-scope .seg-btn.active.env-stg  { color: var(--warning-text); background: color-mix(in srgb, var(--warning) 16%, transparent); }
  #env-scope .seg-btn.active.env-dev  { color: var(--info-text);    background: color-mix(in srgb, var(--info) 14%, transparent); }
  /* 범위 밖의 문제. 배지 모양이지만 누르면 전체로 넓어지므로 버튼이다. */
  #env-elsewhere { cursor: pointer; border: 0; font: inherit; }
  #env-elsewhere[hidden] { display: none; }
  /*
   * 좁아지면 칩의 여백부터 줄인다. **범위 밖 경고는 끝까지 남긴다** — 자리가 없다고
   * 숨기면 그건 "화면이 좁을 때는 운영이 넘어져도 말하지 않는다"는 뜻이 된다.
   */
  @media (max-width: 1100px) {
    #env-scope .seg-btn { padding: 5px 9px; font-size: 11.5px; }
  }
  .avatar {
    width: 36px; height: 36px; border-radius: 50%; flex: none;
    background: var(--primary-soft); color: var(--primary-text);
    display: grid; place-items: center; font-weight: 700; font-size: 13px;
  }
  .page { padding: 24px; }

  /* ================= 페이지 헤더 ================= */
  .page-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px 20px; flex-wrap: wrap; margin-bottom: 20px;
  }
  .page-head h2 { font-size: 19px; font-weight: 700; margin: 0; letter-spacing: -.01em; }
  .page-head .sub { display: block; font-size: 13px; color: var(--muted); font-weight: 400; margin-top: 3px; }
  .crumb { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); flex-wrap: wrap; }
  .crumb .cur { color: var(--heading); font-weight: 600; }
  .crumb svg { width: 14px; height: 14px; stroke-width: 2; }
  .crumb .route { font-family: var(--mono); font-size: 11.5px; color: var(--muted); background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: 2px 7px; }

  /* ================= 카드 ================= */
  .card {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius-lg); box-shadow: var(--shadow);
    margin-bottom: 20px; overflow: hidden;
  }
  .card-head { display: flex; align-items: center; gap: 10px 14px; flex-wrap: wrap; padding: 16px 20px 12px; }
  .card-head h3 { font-size: 15.5px; font-weight: 600; margin: 0; }
  .card-head .hint { font-size: 12.5px; color: var(--muted); }
  .card-head .spacer { flex: 1; }
  .card-body { padding: 8px 20px 20px; }
  .card-body + .card-body { border-top: 1px solid var(--border); padding-top: 16px; }
  .card-head + .card-body { padding-top: 0; }
  .card > .tbl-wrap + .card-body { border-top: 1px solid var(--border); padding-top: 16px; }
  /*
   * 카드 본문을 2단으로 가른다. 모듈 개요가 이걸 쓴다 — 왼쪽은 **사람이 정한 값**
   * (콘솔의 장부), 오른쪽은 **에이전트가 관측한 값**(PM2). 세로로 쌓으면 21줄이라
   * 등록한 경로를 보려면 내리고 CPU 를 보려면 또 내려야 했고, 그동안 오른쪽
   * 절반은 내내 비어 있었다.
   *
   * 칸 사이는 세로선, 칸 **안**의 구분은 기존 `.card-body + .card-body` 의 가로선이
   * 그대로 맡는다 — 두 방향을 한 규칙으로 섞지 않는다.
   */
  .split2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .split2 > .split-col + .split-col { border-left: 1px solid var(--border); }
  /* 칸의 첫 줄은 위 여백을 지운다 — 카드 머리 바로 아래에 붙는 자리다 */
  .split-col > .card-body:first-child { padding-top: 0; }
  /* 위에 다른 본문(경고 배너 등)이 있으면 그때는 선을 긋고 여백을 준다 */
  .card-body + .split2 { border-top: 1px solid var(--border); }
  .card-body + .split2 .split-col > .card-body:first-child { padding-top: 16px; }
  /*
   * 좁아지면 한 단으로 떨어진다. 순서는 등록 정보 → 프로세스 → 자원, 즉 전과 같다.
   * 값 칸이 `.kv` 의 dt 148px 를 빼고 250px 아래로 내려가면 경로가 너무 잘게 접힌다.
   */
  @media (max-width: 1080px) {
    .split2 { grid-template-columns: minmax(0, 1fr); }
    .split2 > .split-col + .split-col { border-left: 0; border-top: 1px solid var(--border); }
    .split2 > .split-col + .split-col > .card-body:first-child { padding-top: 16px; }
  }

  /* ==================================================================
     모듈 상세 오른쪽 레일 — 형제 모듈 목록

     상세를 보다가 다음으로 보고 싶은 것은 대개 **같은 호스트의 옆 모듈**이다.
     전에는 그때마다 [모듈]로 돌아가 표에서 다시 찾아야 했고, 돌아가면 정렬과
     필터가 그대로라 "내가 어디를 보고 있었나"를 매번 다시 맞춰야 했다.

     왼쪽 대메뉴에 넣지 않은 이유는 `#mod-scope` 레일과 같다 — 대메뉴가 모듈 수만큼
     길어지면 그건 더 이상 메뉴가 아니다. 이 목록은 이 화면의 것이므로 이 화면 안에 둔다.
     ================================================================== */
  .detail-split { display: grid; grid-template-columns: minmax(0, 1fr) 250px; gap: 18px; align-items: start; }
  .rail {
    position: sticky; top: calc(var(--topbar-h) + 18px);
    display: flex; flex-direction: column; min-height: 0;
    max-height: calc(100vh - var(--topbar-h) - 42px);
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius-lg); box-shadow: var(--shadow);
  }
  .rail-head { padding: 12px; border-bottom: 1px solid var(--border); }
  .rail-head .seg { display: flex; width: 100%; }
  .rail-head .seg-btn { flex: 1; padding: 6px 8px; text-align: center; white-space: nowrap; }
  .rail-find {
    width: 100%; margin-top: 8px; padding: 6px 9px; font: inherit; font-size: 12.5px;
    border: 1px solid var(--border-input); border-radius: var(--radius);
    background: var(--surface); color: var(--heading);
  }
  .rail-find::placeholder { color: var(--muted); }
  /* 위 여백을 0 으로 두는 것은 아래 sticky 머리글이 스크롤포트에 딱 붙게 하려는 것이다. */
  .rail-list { flex: 1; min-height: 0; overflow-y: auto; padding: 0 8px 8px; }
  .rail-title {
    position: sticky; top: 0; z-index: 1; background: var(--surface);
    display: flex; align-items: center; gap: 6px;
    /*
     * 대문자로 변형하지 않는다 — 이 줄은 머리글이 아니라 **에이전트 이름**, 즉 데이터다.
     * 대메뉴의 `.menu-title`(준비 중 같은 라벨)과 성격이 다르다. 호스트 이름을 화면이
     * 멋대로 바꿔 적으면 등록된 이름과 눈으로 대조할 수 없다.
     */
    font-size: 11.5px; font-weight: 700; color: var(--muted); padding: 10px 8px 5px;
  }
  .rail-title > span:first-of-type { overflow: hidden; text-overflow: ellipsis; }
  .rail-title .n { margin-left: auto; font-weight: 600; letter-spacing: 0;
    font-variant-numeric: tabular-nums; opacity: .75; }
  .rail-title.off { opacity: .62; }
  .rail-title svg { width: 13px; height: 13px; flex: none; }
  .rail-item {
    display: flex; align-items: center; gap: 9px; width: 100%;
    padding: 7px 9px; margin-bottom: 2px; border: 0; border-radius: var(--radius-lg);
    background: none; font: inherit; font-size: 13px; font-weight: 500; color: var(--body);
    text-align: left; cursor: pointer; transition: background .15s, color .15s;
  }
  .rail-item:hover { background: var(--surface-2); color: var(--heading); }
  .rail-item.active { background: var(--primary-soft); color: var(--primary-text); font-weight: 600; }
  .rail-item .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /*
   * 상태는 점 하나다. 레일은 **훑는** 자리이지 읽는 자리가 아니다 —
   * 배지를 넣으면 250px 안에서 이름이 먼저 잘린다. 자세한 것은 본문에 있다.
   */
  .rail-dot { width: 7px; height: 7px; flex: none; border-radius: 50%; background: var(--muted); }
  .rail-dot.ok { background: var(--success); }
  .rail-dot.warn { background: var(--warning); }
  .rail-dot.crit { background: var(--danger); }
  .rail-dot.missing { background: transparent; border: 1.5px dashed var(--border-input); }
  .rail-empty { padding: 16px 10px; font-size: 12.5px; color: var(--muted); text-align: center; }
  /*
   * 좁아지면 레일을 **감춘다**. 긴 목록을 본문 위아래에 쌓으면 상세를 보러 온 사람이
   * 목록을 지나쳐 스크롤해야 한다. 돌아갈 길은 뒤로 버튼과 브레드크럼에 그대로 있다.
   */
  @media (max-width: 1080px) {
    .detail-split { grid-template-columns: minmax(0, 1fr); }
    .rail { display: none; }
  }

  .sec-label {
    font-size: 11.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
    color: var(--muted); margin: 0 0 10px;
  }

  /* ================= 통계 ================= */
  .stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; margin-bottom: 20px; }
  @media (max-width: 820px) { .stats { grid-template-columns: minmax(0, 1fr); } }
  .stat {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
    box-shadow: var(--shadow); padding: 20px; display: flex; gap: 16px; align-items: flex-start;
  }
  .stat-icon { width: 48px; height: 48px; flex: none; border-radius: var(--radius-lg); display: grid; place-items: center; }
  .stat-icon svg { width: 22px; height: 22px; stroke-width: 1.8; }
  .stat-icon.ok { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success-text); }
  .stat-icon.warn { background: color-mix(in srgb, var(--warning) 15%, transparent); color: var(--warning-text); }
  .stat-icon.crit { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger-text); }
  .stat-icon.info { background: color-mix(in srgb, var(--info) 15%, transparent); color: var(--info-text); }
  .stat-body { min-width: 0; }
  .stat-label { font-size: 13px; color: var(--muted); }
  .stat-value { font-size: 26px; font-weight: 700; color: var(--heading); line-height: 1.2; margin-top: 2px; font-variant-numeric: tabular-nums; }
  .stat-foot { font-size: 12.5px; color: var(--muted); margin-top: 2px; }

  .scope {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px 24px;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
    box-shadow: var(--shadow); padding: 14px 20px; margin-bottom: 20px; font-size: 13.5px;
  }
  .scope b { color: var(--heading); font-weight: 700; font-variant-numeric: tabular-nums; }
  .scope .div { width: 1px; height: 18px; background: var(--border); }

  /* ================= 표 ================= */
  .tbl-wrap { overflow-x: auto; }
  table { width: 100%; border-collapse: collapse; min-width: 560px; }
  th, td { text-align: left; padding: 13px 20px; }
  th { font-size: 12.5px; font-weight: 600; color: var(--muted); border-bottom: 1px solid var(--border); white-space: nowrap; }
  td { border-bottom: 1px solid var(--border); }
  tbody tr:last-child td { border-bottom: 0; }
  tbody tr:hover td { background: var(--surface-2); }
  td .name { font-weight: 600; color: var(--heading); white-space: nowrap; }
  td.mono { white-space: nowrap; font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: .92em; }
  td .name .s { display: block; font-family: var(--mono); font-size: 11.5px; color: var(--muted); font-weight: 400; }
  td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
  th.num { text-align: right; }

  /*
   * 정렬 가능한 머리글. 삼각형 자리를 **늘 비워 둔다** — 정렬 중일 때만 넣으면
   * 누르는 순간 머리글 폭이 바뀌어 표 전체가 옆으로 밀린다.
   */
  th.sortable { cursor: pointer; user-select: none; }
  th.sortable:hover, th.sorted { color: var(--heading); }
  .sort-caret {
    display: inline-block; width: 0; height: 0; margin-left: 6px; vertical-align: middle;
    border-left: 4px solid transparent; border-right: 4px solid transparent;
    border-top: 5px solid var(--border-input);
  }
  th.sortable:hover .sort-caret { border-top-color: var(--muted); }
  th.sorted.asc .sort-caret { border-top: 0; border-bottom: 5px solid var(--primary); }
  th.sorted.desc .sort-caret { border-top-color: var(--primary); }
  .linkish {
    background: none; border: 0; padding: 2px 0; font: inherit; font-weight: 600;
    color: var(--primary-text); text-align: left; white-space: nowrap; cursor: pointer;
  }
  .linkish:hover { text-decoration: underline; }

  /* ================= 배지 · 태그 ================= */
  .badge {
    display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600;
    padding: 3px 11px; border-radius: 999px; white-space: nowrap;
  }
  .badge .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
  .badge.ok { color: var(--success-text); background: color-mix(in srgb, var(--success) 14%, transparent); }
  .badge.warn { color: var(--warning-text); background: color-mix(in srgb, var(--warning) 15%, transparent); }
  .badge.crit { color: var(--danger-text); background: color-mix(in srgb, var(--danger) 14%, transparent); }
  .badge.neutral { color: var(--muted); background: var(--surface-2); border: 1px solid var(--border); }
  .badge.info { color: var(--info-text); background: color-mix(in srgb, var(--info) 14%, transparent); }
  /* 되돌리기 백업은 경고색이지만 의미가 다르므로 형태(점선)로 한 번 더 구분한다 */
  .badge.dashed { background: transparent; border: 1px dashed color-mix(in srgb, var(--warning) 60%, transparent); }
  .badge.dashed .dot { background: transparent; border: 1.5px solid currentColor; }
  /*
   * 중지 — 사람이 그렇게 두기로 한 상태다. 색을 빼고 점선으로 둔다: 빨강은
   * "고칠 것이 있다"는 뜻이고, 이 칩에는 고칠 것이 없다.
   */
  .badge.off { color: var(--muted); background: transparent; border: 1px dashed var(--border); }
  .badge.off .dot { background: transparent; border: 1.5px solid currentColor; }
  /* 중지된 줄은 흐리게. 숨기지는 않는다 — 표에 없는 것은 없는 것이어야 한다. */
  tbody tr.row-off td:not(:last-child) { opacity: .62; }

  .tag {
    display: inline-block; white-space: nowrap;
    font-family: var(--mono); font-size: 11.5px; font-weight: 500;
    color: var(--body); background: var(--surface-2); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 2px 8px;
  }
  .tag.on { color: var(--primary-text); background: var(--primary-soft); border-color: transparent; }
  .tag.off { color: var(--muted); background: transparent; border-style: dashed; }
  .tag.ok { color: var(--success-text); background: rgba(92, 193, 132, .14); border-color: transparent; }
  /* 조작 버튼은 한 줄에 나란히 — 좁은 칸에서 세로로 쌓이면 표가 들쭉날쭉해진다. */
  /* 섹션 제목 오른쪽에 그 섹션만 건드리는 버튼을 둔다. */
  .sec-head { display: flex; align-items: center; gap: 12px; }
  .sec-head .sec-label { margin: 0; }
  .sec-head .btn { margin-left: auto; }
  .sec-head .btn svg { width: 13px; height: 13px; }
  td.acts { white-space: nowrap; }
  /* .act 는 button 과 a 양쪽으로 쓰인다 — 링크도 버튼과 같게 보이고, 눌 수 없으면 그렇게 보여야 한다. */
  a.act { text-decoration: none; }
  .act:disabled { opacity: .45; cursor: not-allowed; }
  .act:disabled:hover { filter: none; }
  /* 경로는 길다 — 줄을 늘리지 말고 칸 안에서 잘라 보여주고 title 로 전체를 준다. */
  td.paths { max-width: 260px; }
  td.paths .s { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
  td.paths .s:first-child { color: var(--body); }
  td.acts .act + .act { margin-left: 6px; }
  .chips { display: inline-flex; gap: 6px; flex-wrap: wrap; align-items: center; }

  .chips-filter { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
  .chip {
    font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; padding: 7px 14px;
    border-radius: 999px; border: 1px solid var(--border-input); background: var(--surface);
    color: var(--body); display: inline-flex; align-items: center; gap: 7px;
  }
  .chip:hover { border-color: var(--muted); color: var(--heading); }
  .chip.on { background: var(--primary-soft); border-color: transparent; color: var(--primary-text); }
  .chip .n { font-weight: 500; opacity: .7; font-variant-numeric: tabular-nums; }
  .chip[aria-pressed="true"] { background: var(--primary-soft); border-color: transparent; color: var(--primary-text); }

  /* ================= 버튼 ================= */
  .btns { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  .btn {
    display: inline-flex; align-items: center; gap: 7px; font: inherit; font-size: 13.5px;
    font-weight: 600; padding: 8px 15px; border-radius: var(--radius-lg);
    border: 1px solid var(--border-input); background: var(--surface); color: var(--body);
    cursor: pointer; white-space: nowrap; transition: background .15s, border-color .15s, color .15s;
  }
  .btn:hover:not(:disabled) { background: var(--surface-2); color: var(--heading); }
  .btn svg { width: 16px; height: 16px; stroke-width: 1.9; }
  .btn.sm { padding: 5px 11px; font-size: 12.5px; }
  .btn.primary { background: var(--primary); border-color: var(--primary); color: var(--on-primary); }
  .btn.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--primary) 88%, #000); color: var(--on-primary); }
  .btn.soft { background: var(--primary-soft); border-color: transparent; color: var(--primary-text); }
  .btn.soft:hover:not(:disabled) { background: color-mix(in srgb, var(--primary) 20%, transparent); }
  .btn.danger { background: color-mix(in srgb, var(--danger) 12%, transparent); border-color: transparent; color: var(--danger-text); }
  .btn.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--danger) 20%, transparent); color: var(--danger-text); }
  .btn:disabled { opacity: .5; cursor: not-allowed; }

  .act {
    width: 32px; height: 32px; padding: 0; border: 0; border-radius: var(--radius);
    display: inline-grid; place-items: center; cursor: pointer;
    background: var(--primary-soft); color: var(--primary-text);
  }
  .act svg { width: 15px; height: 15px; stroke-width: 1.9; }
  .act.danger { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger-text); }
  .act:hover { filter: brightness(.95); }

  .btn:focus-visible, .act:focus-visible, .menu-item:focus-visible, .icon-btn:focus-visible,
  .linkish:focus-visible, .chip:focus-visible, input:focus-visible, textarea:focus-visible, .tab:focus-visible {
    outline: 2px solid var(--primary); outline-offset: 2px;
  }

  /* ================= 알림 ================= */
  .alert {
    display: flex; gap: 11px; padding: 13px 15px; border-radius: var(--radius-lg);
    font-size: 13px; line-height: 1.6; border: 1px solid transparent;
  }
  .alert b { font-weight: 700; }
  .alert code { font-family: var(--mono); font-size: 12px; }
  .alert svg { width: 17px; height: 17px; flex: none; margin-top: 2px; stroke-width: 1.9; }
  .alert.warn { color: var(--warning-text); background: color-mix(in srgb, var(--warning) 11%, transparent); border-color: color-mix(in srgb, var(--warning) 28%, transparent); }
  .alert.crit { color: var(--danger-text); background: color-mix(in srgb, var(--danger) 10%, transparent); border-color: color-mix(in srgb, var(--danger) 26%, transparent); }
  .alert.plain { color: var(--body); background: var(--surface-2); border-color: var(--border); }
  .alert + .alert, .alert + .card, .alert + * { margin-top: 10px; }

  /* ================= 정의 목록 · 폼 ================= */
  .kv { display: grid; grid-template-columns: 148px minmax(0, 1fr); gap: 9px 16px; margin: 0; font-size: 13.5px; }
  .kv dt { color: var(--muted); }
  .kv dd { margin: 0; font-family: var(--mono); font-size: 12.5px; color: var(--heading); overflow-wrap: anywhere; }

  .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
  .field label { font-size: 12.5px; font-weight: 600; color: var(--heading); display: flex; gap: 6px; align-items: baseline; }
  .req { color: var(--danger-text); font-size: 11px; font-weight: 600; }
  .opt { color: var(--muted); font-size: 11px; font-weight: 600; }
  input[type="text"], textarea, select {
    font-family: var(--mono); font-size: 13px; color: var(--heading); background: var(--surface);
    border: 1px solid var(--border-input); border-radius: var(--radius-lg); padding: 9px 12px; width: 100%;
  }
  textarea { min-height: 128px; resize: vertical; line-height: 1.7; }
  select { cursor: pointer; appearance: none;
    /* 화살표는 배경으로 그린다 — 외부 리소스를 쓰지 않는다. */
    background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
                      linear-gradient(135deg, var(--muted) 50%, transparent 50%);
    background-position: calc(100% - 17px) 50%, calc(100% - 12px) 50%;
    background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 32px; }
  input[type="text"]:focus, textarea:focus, select:focus { border-color: var(--primary); outline: none; }
  .hint { font-size: 12px; color: var(--muted); }
  .warn-text { color: var(--warning-text); }
  .check { display: flex; align-items: center; gap: 9px; font-size: 13.5px; color: var(--heading); }
  .check input { width: 16px; height: 16px; accent-color: var(--primary); }

  .switch { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--body); cursor: pointer; }
  .switch input { position: absolute; opacity: 0; width: 0; height: 0; }
  .switch .track { width: 36px; height: 20px; border-radius: 999px; background: var(--border-input); position: relative; transition: background .15s; flex: none; }
  .switch .track::after {
    content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
    border-radius: 50%; background: var(--surface); transition: transform .15s; box-shadow: 0 1px 2px rgba(0,0,0,.25);
  }
  .switch input:checked + .track { background: var(--primary); }
  .switch input:checked + .track::after { transform: translateX(16px); }
  .switch input:focus-visible + .track { outline: 2px solid var(--primary); outline-offset: 2px; }

  /* ---- 메뉴 그룹 (2단) ---- */
  .menu-sub { margin: 2px 0 6px 22px; padding-left: 11px; border-left: 1px solid var(--border); }
  .menu-sub .menu-item { font-size: 13.5px; padding: 7px 10px; }
  .menu-parent .caret { width: 16px; height: 16px; flex: none; margin-left: auto; transition: transform .18s; }
  .menu-group:not(.open) .menu-parent .caret { transform: rotate(-90deg); }
  .menu-group:not(.open) .menu-sub { display: none; }
  .menu-parent.has-active { color: var(--heading); font-weight: 600; }
  :root[data-menu="condensed"] .menu-sub { margin-left: 0; padding-left: 0; border-left: 0; }

  /* ================= 탭 ================= */
  /*
   * 탭 줄 오른쪽에 조작을 붙인다(모듈 상세의 제어 버튼). 탭을 옮겨 다녀도 같은
   * 자리에 있어야 해서 **본문이 아니라 틀**에 둔다 — 전에는 [제어] 탭 안에 있어서,
   * 로그를 보다 재시작하려면 탭을 옮겼다가 돌아와야 했다.
   *
   * 읽는 화면에 파괴적인 조작을 섞지 않는다는 규칙과 부딪히는 것처럼 보이지만,
   * 여기서는 본문에 섞지 않고 머리줄로 올려 둔 것이다. 대신 `삭제` 는 danger 색을
   * 유지하고, 무엇이 일어나는지는 옆의 ⓘ 가 말한다.
   */
  .tab-row { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
    border-bottom: 1px solid var(--border); margin-bottom: 20px; }
  .tab-row > .tabs { border-bottom: 0; margin-bottom: 0; flex: 1 1 auto; }
  .tab-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
    margin-left: auto; padding: 0 0 8px; }

  .tabs { display: flex; gap: 4px; flex-wrap: wrap; border-bottom: 1px solid var(--border); margin-bottom: 20px; }
  .tabs[hidden] { display: none; }   /* display:flex 가 UA 의 [hidden] 을 이긴다 */
  .tab {
    font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer; background: none;
    border: 0; border-bottom: 2px solid transparent; padding: 11px 15px; margin-bottom: -1px;
    color: var(--muted); display: inline-flex; align-items: center; gap: 8px;
    transition: color .15s, border-color .15s;
  }
  .tab:hover { color: var(--heading); }
  .tab.active { color: var(--primary-text); border-bottom-color: var(--primary); }
  .tabpane { display: none; }
  .tabpane.active { display: block; }
  .section { display: none; }
  .section.active { display: block; }

  .why { font-size: 12.5px; color: var(--muted); margin: 9px 0 0; }
  .tab .soon-dot {
    font-size: 11px; font-weight: 500; color: var(--muted);
    border: 1px dashed var(--border-input); border-radius: 999px; padding: 0 7px;
  }
  .tab:disabled { cursor: not-allowed; }
  .tab:disabled:hover { color: var(--muted); }

  /* 상세 화면의 되돌아가기 */
  .page-head .back { display: flex; align-items: center; gap: 10px; }
  .back-btn {
    width: 34px; height: 34px; flex: none; display: grid; place-items: center;
    border: 1px solid var(--border-input); border-radius: var(--radius-lg);
    background: var(--surface); color: var(--body); cursor: pointer;
    transition: background .15s, color .15s, border-color .15s;
  }
  .back-btn:hover { background: var(--surface-2); color: var(--heading); border-color: var(--muted); }
  .back-btn svg { width: 17px; height: 17px; stroke-width: 2; }
  .back-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

  /*
   * 대메뉴로 바로 들어오는 화면의 이전 버튼. 자리는 두고 누를 수 없음만 보인다 —
   * 아예 빼면 메뉴를 옮길 때마다 제목이 좌우로 움직인다.
   */
  .back-btn:disabled { opacity: .35; cursor: default; }
  .back-btn:disabled:hover {
    background: var(--surface); color: var(--body); border-color: var(--border-input);
  }

  /* 탭 안의 좁은 자리표시자 */
  .ph-inline {
    border: 1px dashed var(--border-input); border-radius: var(--radius-lg);
    padding: 30px 24px; text-align: center; background: var(--surface);
  }
  .ph-inline h4 { font-size: 15px; margin: 0 0 6px; color: var(--heading); }
  .ph-inline p { margin: 0 auto; max-width: 48ch; font-size: 13px; color: var(--muted); }

  /* ================= 모달 ================= */
  /* 네이티브 <dialog> 을 쓴다 — Esc 로 닫기, 포커스 가둠, 배경 비활성이 공짜로 따라온다. */
  dialog.modal {
    border: 0; padding: 0; border-radius: var(--radius-lg);
    background: var(--surface); color: var(--body);
    box-shadow: var(--shadow-lg);
    width: min(620px, calc(100vw - 32px)); max-height: calc(100vh - 64px);
  }
  dialog.modal::backdrop { background: rgba(20, 26, 33, .5); }
  dialog.modal .modal-body { padding: 4px 20px 20px; overflow-y: auto; max-height: calc(100vh - 230px); }
  /* 내용 보기 모달은 pre 가 스스로 스크롤하지 않고 모달 본문이 스크롤한다. */
  #view-content { max-height: none; }
  dialog.modal .modal-foot {
    display: flex; gap: 8px; justify-content: flex-end; align-items: center;
    padding: 14px 20px; border-top: 1px solid var(--border); background: var(--surface-2);
  }
  dialog.modal .modal-foot .spacer { flex: 1; }
  dialog.modal .modal-foot .hint { color: var(--danger-text); }
  dialog.modal textarea { min-height: 180px; }

  /* ================= 로그 ================= */
  /*
   * 로그 상자는 줄이 계속 덧붙는 유일한 화면이다. 높이를 고정해 두는 것이 핵심 —
   * 내용에 따라 늘어나면 새 줄이 올 때마다 카드가 들썩이고, 읽던 자리가 밀린다.
   */
  .lg-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
  .lg-bar .spacer { flex: 1; }
  .lg-bar .seg-btn { display: inline-flex; align-items: center; gap: 7px; }
  .lg-file { font-family: var(--mono); font-size: 11.5px; opacity: .85; }
  .lg-size { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
  .seg-btn.active .lg-size { color: inherit; opacity: .7; }

  .lg-check { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; color: var(--body); }
  .lg-q {
    width: 220px; padding: 5px 10px; font: inherit; font-size: 12.5px;
    border: 1px solid var(--border-input); border-radius: var(--radius);
    background: var(--surface); color: var(--heading);
  }

  .logbox {
    font-family: var(--mono); font-size: 12px; line-height: 1.55;
    background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-lg);
    padding: 10px 12px; margin: 0; height: 420px; overflow: auto;
    color: var(--heading); white-space: pre; tab-size: 4;
  }
  .logbox.tall { height: min(58vh, 560px); }
  /* 긴 줄은 접지 않고 가로로 흘린다 — 접으면 줄 번호 감각이 무너져 읽기 어렵다. */
  .lg-line { white-space: pre; }
  .lg-line.hot { color: var(--danger-text); }
  .lg-empty { color: var(--muted); padding: 16px 2px; font-family: var(--sans); }
  .lg-foot {
    display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
    margin: 8px 0 0; font-size: 12px; color: var(--muted);
  }
  .lg-foot b { color: var(--heading); font-weight: 600; }
  .lg-err { color: var(--danger-text); }
  dialog.modal.wide { width: min(1040px, calc(100vw - 32px)); }

  /* ================= 확인·알림 창 (SweetAlert2) ================= */
  /*
   * 기본 스킨은 이 콘솔의 색·글꼴과 남남이라, 토큰으로 다시 칠한다.
   * 버튼은 `buttonsStyling: false` 로 꺼 두고 `.btn` 을 그대로 입힌다 —
   * 대화 상자 안에서만 버튼이 다르게 생기면 그게 더 눈에 띈다.
   */
  .swal2-popup.swal-ours {
    background: var(--surface); color: var(--body);
    border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
    font-family: var(--sans); padding: 22px 22px 18px; width: min(520px, calc(100vw - 32px));
  }
  .swal2-popup.swal-ours .swal2-title { color: var(--heading); font-size: 17px; font-weight: 700; padding: 0 0 4px; }
  .swal2-popup.swal-ours .swal2-html-container {
    color: var(--body); font-size: 13.5px; line-height: 1.65; margin: 8px 0 0; text-align: left;
  }
  .swal2-popup.swal-ours .swal2-html-container p { margin: 0 0 8px; }
  .swal2-popup.swal-ours .swal2-html-container p:last-child { margin-bottom: 0; }
  .swal2-popup.swal-ours b { color: var(--heading); font-weight: 600; }
  .swal2-popup.swal-ours code {
    font-family: var(--mono); font-size: 12px; background: var(--surface-2);
    border: 1px solid var(--border); border-radius: var(--radius); padding: 1px 5px;
  }
  .swal2-popup.swal-ours .swal2-actions { gap: 8px; margin: 18px 0 0; }
  .swal2-popup.swal-ours .swal2-actions .btn { margin: 0; }
  .swal2-container.swal2-backdrop-show { background: rgba(20, 26, 33, .5); }

  /* 무엇이 걸려 있는지 세어 보게 한다 — 줄글로 적으면 몇 개인지 읽히지 않는다. */
  .swal-list {
    margin: 0 0 10px; padding: 9px 12px 9px 28px; list-style: disc;
    background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
    max-height: 190px; overflow-y: auto;
    font-family: var(--mono); font-size: 12px; color: var(--heading);
  }
  .swal-list li { margin: 2px 0; }
  .swal-list li.muted { font-family: var(--sans); color: var(--muted); list-style: none; margin-left: -14px; }
  .swal-why { color: var(--muted); font-size: 12.5px; }
  .swal-mono { font-family: var(--mono); font-size: 12px; color: var(--muted); word-break: break-all; }

  /* ================= 자리표시자 ================= */
  .placeholder {
    background: var(--surface); border: 1px dashed var(--border-input);
    border-radius: var(--radius-lg); padding: 36px 28px; text-align: center;
  }
  .placeholder .ph-icon {
    width: 56px; height: 56px; margin: 0 auto 16px; border-radius: 50%;
    display: grid; place-items: center; background: var(--surface-2); color: var(--muted);
  }
  .placeholder .ph-icon svg { width: 26px; height: 26px; stroke-width: 1.6; }
  .placeholder h3 { font-size: 17px; margin: 0 0 6px; }
  .placeholder > p { margin: 0 auto; max-width: 52ch; color: var(--muted); font-size: 13.5px; }
  .ph-plan { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; margin-top: 26px; text-align: left; }
  .ph-plan div { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 13px 15px; background: var(--surface-2); }
  .ph-plan b { display: block; color: var(--heading); font-size: 13px; margin-bottom: 3px; }
  .ph-plan span { font-size: 12.5px; color: var(--muted); }

  .empty { color: var(--muted); padding: 28px 20px; text-align: center; font-size: 13px; }

  /* ================= 결과 출력 · 이벤트 ================= */
  pre.out {
    font-family: var(--mono); font-size: 12px; line-height: 1.6; background: var(--surface-2);
    border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 12px 14px;
    margin: 0; overflow-x: auto; max-height: 260px; color: var(--heading);
  }
  .feed { max-height: 460px; overflow-y: auto; }
  .feed table { min-width: 560px; }
  #tpl-table { min-width: 420px; }
  .feed td { padding: 9px 20px; }
  .ev-name { font-family: var(--mono); font-weight: 600; }
  .ev-exit, .ev-error { color: var(--danger-text); }
  .ev-online, .ev-start { color: var(--success-text); }
  .ev-restart, .ev-stop { color: var(--warning-text); }

  .steps { margin: 0; padding-left: 20px; display: grid; gap: 9px;
    font-size: 13px; line-height: 1.65; color: var(--body); }
  .steps li::marker { color: var(--muted); font-weight: 700; }

  /* ---------- 툴팁 ---------- */
  /*
   * 늘 읽을 필요는 없지만 물어보면 답이 있어야 하는 문장을 담는다.
   * title 속성은 늦게 뜨고 모양을 정할 수 없어 직접 그린다. 키보드로도 열려야 하므로
   * :focus-visible 도 같이 본다 — 마우스에만 반응하면 쓸 수 없는 사람이 생긴다.
   */
  .tip {
    display: inline-grid; place-items: center; width: 16px; height: 16px; padding: 0;
    border: 0; background: none; color: var(--muted); cursor: help; position: relative;
    vertical-align: -3px; margin-left: 4px;
  }
  .tip svg { width: 14px; height: 14px; stroke-width: 1.8; }
  .tip:hover, .tip:focus-visible { color: var(--primary-text); outline: none; }
  .tip::after {
    content: attr(data-tip);
    /*
     * 아이콘 기준으로 **오른쪽으로 펼친다.** 가운데 정렬하면 왼쪽에 놓인 아이콘에서
     * 툴팁이 화면 밖으로 밀려 문장 앞부분이 잘린다 — 실제로 잘렸다. CSS 만으로는
     * 가장자리를 감지해 뒤집을 수 없으므로, 늘 안쪽으로 펼치는 쪽을 택한다.
     */
    position: absolute; bottom: calc(100% + 8px); left: -6px;
    width: max-content; max-width: 280px; white-space: normal; text-align: left;
    background: var(--heading); color: var(--surface); font-size: 12px; font-weight: 500;
    line-height: 1.55; border-radius: var(--radius-lg); padding: 8px 11px;
    box-shadow: var(--shadow-lg); opacity: 0; visibility: hidden; transition: opacity .12s;
    pointer-events: none; z-index: 20;
  }
  /*
   * 화면 오른쪽 끝에 놓이는 ⓘ 는 **왼쪽으로** 펼친다. 기본값(오른쪽)대로 두면
   * 280px 짜리 말풍선이 창 밖으로 나가 문장 뒷부분이 잘린다.
   */
  .tip.left::after { left: auto; right: -6px; }
  .tip:hover::after, .tip:focus-visible::after { opacity: 1; visibility: visible; }

  /* ---------- 배포 카드 ---------- */
  /* 파일이 여럿일 수 있어 카드로 둔다 — 표에는 내용이 들어갈 자리가 없다. */
  .dep-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
  .dep-card { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface);
    box-shadow: var(--shadow); display: flex; flex-direction: column; min-width: 0; }
  .dep-head { display: flex; align-items: flex-start; gap: 12px; padding: 13px 15px 10px; }
  .dep-file { font-family: var(--mono); font-size: 13.5px; font-weight: 700; color: var(--heading);
    overflow-wrap: anywhere; }
  .dep-sub { font-size: 12px; color: var(--muted); margin-top: 3px; }
  /* 표 칸과 정의목록 안에서 쓰는 버전 배지. .ver 는 flex 줄 끝으로 밀리는 용도라 따로 둔다. */
  /* 버전 칸: 코드 버전(배지) 위, 설정 버전(작은 글씨) 아래. */
  td.vers { line-height: 1.5; white-space: nowrap; }
  td.vers .s { font-size: 11px; color: var(--muted); }
  .vbadge { display: inline-block; font-family: var(--mono); font-size: 11px; font-weight: 700;
    color: var(--primary-text); background: var(--primary-soft); border-radius: 999px; padding: 1px 7px; }
  .ver { margin-left: auto; flex: none; font-family: var(--mono); font-size: 12px; font-weight: 700;
    color: var(--primary-text); background: var(--primary-soft); border-radius: 999px; padding: 3px 10px; }
  .dep-foot { display: flex; gap: 8px; flex-wrap: wrap; padding: 4px 15px 14px; margin-top: auto; }

  /* ---------- 에이전트 상세 · 모듈 탭 ---------- */
  .tab .n { margin-left: 5px; padding: 1px 7px; border-radius: 999px; font-size: 11.5px; font-weight: 600;
    color: var(--muted); background: var(--surface-2); font-variant-numeric: tabular-nums; }
  .tab.active .n { color: var(--primary-text); background: var(--primary-soft); }

  /* 그래프와 표는 같은 목록의 두 가지 그림이다. 고르는 곳은 헤더 한 군데뿐이다. */
  .seg { display: inline-flex; border: 1px solid var(--border-input); border-radius: var(--radius); overflow: hidden; }
  .seg-btn { border: 0; background: var(--surface); color: var(--body); font: inherit; font-size: 12.5px;
    font-weight: 600; padding: 6px 15px; cursor: pointer; transition: background .12s, color .12s; }
  .seg-btn + .seg-btn { border-left: 1px solid var(--border-input); }
  .seg-btn:hover { background: var(--surface-2); }
  .seg-btn.active { background: var(--primary-soft); color: var(--primary-text); }
  .seg-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

  /*
   * 모듈 목록의 에이전트 탭. 헤더 안이 아니라 통계 위에 혼자 서므로 아래 여백을 준다.
   * 에이전트가 늘면 가로로 넘치는데, 줄바꿈은 둥근 모서리가 가운데서 끊겨 망가진다 —
   * 가로로 밀어 보게 둔다. 100대까지는 이게 가장 덜 상한다.
   */
  #mod-tabs { margin-bottom: 16px; max-width: 100%; overflow-x: auto; overflow-y: hidden; }
  #mod-tabs[hidden] { display: none; }   /* inline-flex 가 UA 의 [hidden] 을 이긴다 */
  #mod-tabs .seg-btn { white-space: nowrap; }

  .legend-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
  .legend-row .spacer { flex: 1; }
  .legend-row .hint { font-size: 12.5px; color: var(--muted); }
  .legend { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: 12.5px; color: var(--body); }
  .legend span { display: inline-flex; align-items: center; gap: 6px; }
  .legend i { box-sizing: border-box; width: 9px; height: 9px; border-radius: 50%; background: var(--border-input); }
  .legend .div { width: 1px; height: 14px; background: var(--border); }
  .legend .ok i { background: var(--success); }
  .legend .warn i { background: var(--warning); }
  .legend .crit i { background: var(--danger); }
  /* 누락·미등록은 '없는 것'이라 점을 비우고 테두리만 남긴다. */
  .legend .missing i { background: transparent; border: 1.5px dashed var(--warning); }
  .legend .unreg i { background: transparent; border: 1.5px dashed var(--muted); }

  /*
   * 노드가 늘어나도 줄바꿈하지 않고 가로로 스크롤한다 — 줄이 바뀌면 연결선이
   * 끊어져서 "무엇이 무엇에 붙어 있나"라는 이 그림의 유일한 용건이 사라진다.
   */
  .graph { padding: 4px 20px 22px; overflow-x: auto; }
  .graph-inner { display: flex; flex-direction: column; align-items: center; min-width: min-content; }
  .g-stem { width: 0; height: 26px; border-left: 1px solid var(--border-input); }
  .g-children { display: flex; align-items: flex-start; gap: 14px; }
  .g-child { position: relative; padding-top: 26px; }
  .g-child::before { content: ''; position: absolute; top: 0; left: -7px; right: -7px;
    border-top: 1px solid var(--border-input); }
  .g-child::after { content: ''; position: absolute; top: 0; left: 50%; height: 26px;
    border-left: 1px solid var(--border-input); }
  .g-child:first-child::before { left: 50%; }
  .g-child:last-child::before { right: 50%; }
  .g-child:only-child::before { display: none; }

  /*
   * 폭은 이름이 정한다. 모듈 이름은 `pnp-log-dashboard-receiver1` 처럼 길고 띄어쓰기가
   * 없어서 접히지도 않는다 — 좁게 두면 글자가 카드 밖으로 그냥 넘쳐 나갔다.
   * 넓혀서 웬만한 이름은 담고, 그래도 넘치면 잘라 말줄임한다(전체는 툴팁).
   */
  .g-node { width: 292px; background: var(--surface); border: 1px solid var(--border);
    border-top: 2px solid var(--border-input); border-radius: var(--radius-lg); padding: 13px 14px; box-shadow: var(--shadow); }
  .g-node.ok { border-top-color: var(--success); }
  .g-node.warn { border-top-color: var(--warning); }
  .g-node.crit { border-top-color: var(--danger); }
  .g-node.missing { border-style: dashed; border-top-color: var(--warning); background: var(--surface-2); }
  .g-node.missing .g-ico { background: transparent; border: 1px dashed var(--border-input); color: var(--muted); }
  .g-node.missing .g-name .linkish { color: var(--body); }
  .g-node.agent { width: var(--g-card-w); background: rgba(92, 193, 132, .06); }
  .g-node.agent.crit { background: rgba(233, 103, 103, .06); }
  .g-head { display: flex; gap: 11px; align-items: flex-start; }
  /*
   * 글자 칸은 반드시 `min-width: 0` 이어야 한다. flex 항목의 기본 최소 폭은
   * "내용만큼"이라, 이게 없으면 긴 이름이 칸을 밀어내 말줄임이 아예 걸리지 않는다.
   */
  .g-text { min-width: 0; flex: 1; }
  /* 아이콘은 이름+부제 두 줄 높이에 맞춘다 — 작으면 머리가 비어 보인다. */
  .g-ico { flex: none; width: 38px; height: 38px; border-radius: var(--radius); display: grid; place-items: center;
    background: var(--surface-2); color: var(--body); }
  .g-ico svg { width: 19px; height: 19px; }
  /* 에이전트 노드는 종류 줄이 하나 더 있어 세 줄이다. */
  .g-node.agent .g-ico { width: 46px; height: 46px; }
  .g-node.agent .g-ico svg { width: 23px; height: 23px; }
  .g-node.ok .g-ico { background: rgba(92, 193, 132, .16); color: var(--success-text); }
  .g-node.warn .g-ico { background: rgba(240, 147, 78, .16); color: var(--warning-text); }
  .g-node.crit .g-ico { background: rgba(233, 103, 103, .16); color: var(--danger-text); }
  .g-kind { font-size: 11px; color: var(--muted); }
  /*
   * 환경은 카드 **오른쪽 위**의 고정 칸에 둔다. 이름 뒤에 붙여 두면 `.g-name` 이
   * 한 줄 말줄임이라 긴 호스트 이름이 라벨을 밀어낸다 — 잘려 나가는 쪽이 라벨이면
   * "이게 어느 환경인가"가 통째로 사라진다. 머리줄 오른쪽이면 이름 길이와 무관하다.
   */
  .g-env { flex: none; }
  .g-name { font-size: 14.5px; font-weight: 700; color: var(--heading); line-height: 1.3; }
  .g-name .linkish { font-size: inherit; font-weight: inherit; }
  .g-name .tag { margin-left: 6px; vertical-align: middle; }
  .g-sub { font-size: 12px; color: var(--body); margin-top: 2px; font-family: var(--mono); }
  /* 넘치면 자른다. 잘린 이름 전체는 title 로 붙어 있어 올려 두면 보인다. */
  .g-name, .g-sub, .g-name .linkish {
    display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .g-foot { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }

  /*
   * 플릿 연결도는 같은 부품을 **한 단 더** 쌓는다 — 콘솔 → 에이전트 → 모듈.
   * 가지 하나가 [노드 + 줄기 + 자식들]을 세로로 들고, 그 가지 안에서 노드가
   * 가운데 와야 한다. 가운데가 아니면 위에서 내려오는 `.g-child::after`(가지 폭의
   * 50% 지점에 그어진다)가 노드를 빗나가 선이 허공에서 끝난다.
   */
  .g-branch { display: flex; flex-direction: column; align-items: center; min-width: min-content; }

  /*
   * 모듈은 에이전트 **아래로 쌓는다**(`.g-children.col`).
   *
   *     [에이전트]
   *       │
   *       ├── [모듈1]
   *       ├── [모듈2]
   *       └── [모듈3]
   *
   * 가로로 펴면 폭이 모듈 수에 비례해 늘어난다 — 모듈 하나가 약 300px 라 열 개만
   * 넘어도 그림이 화면의 세 배가 되고(실측 11개 3,392px), 정작 읽고 싶은 "이 호스트에
   * 무엇이 붙어 있나"를 가로로 밀어 가며 세어야 했다. 세로로 쌓으면 폭은 노드 하나로
   * 고정되고 길이만 늘어난다 — 페이지는 어차피 세로로 미는 것이다.
   *
   * 줄기는 **왼쪽 한 줄**로 내리고 가지마다 짧은 턱을 붙인다. 노드를 세로로 이어
   * 붙이면(위 노드 바닥 → 아래 노드 머리) 모듈 사이에 연결이 있다는 말이 되는데
   * 그런 관계는 장부에 없다 — 없는 것을 선으로 그으면 거짓말이 된다.
   *
   * 줄기가 가로 단의 `.g-stem` 자리(26px)를 겸하므로 세로 단 앞에는 줄기 조각을
   * 따로 두지 않는다. 폭도 에이전트 노드(332px)보다 좁아(26+292=318px) 가지의
   * 폭을 늘리지 않는다 — 위에서 내려오는 `.g-child::after` 가 그대로 노드를 맞힌다.
   */
  .g-children.col {
    flex-direction: column; align-items: flex-start; gap: 0;
    /*
     * 폭을 에이전트 카드와 같게 잡는 이유: 가지 안에서 세로 단도 카드도 가운데
     * 정렬이므로, 폭이 같을 때만 둘의 왼쪽 선이 맞는다. 내용 폭에 맡기면(26+292=318)
     * 7px 씩 안쪽으로 밀려 줄기가 카드의 왼쪽 **테두리에** 붙어 버린다.
     * 왼쪽 여백 14px 은 카드 자신의 가로 여백과 같은 값이다 — 줄기가 카드의 내용이
     * 시작하는 선에서 내려오고, 모듈 노드의 오른쪽 끝(14+26+292=332)이 카드와 맞는다.
     */
    width: var(--g-card-w); padding: 26px 0 0 14px;
  }
  .g-children.col > .g-child { padding: 0 0 0 26px; margin-top: 12px; }
  .g-children.col > .g-child:first-child { margin-top: 0; }
  /* 턱은 아이콘 가운데 높이(2px 테두리 + 13px 여백 + 38px/2)에 맞춘다. */
  .g-children.col > .g-child::before { display: block; top: 34px; left: 0; right: auto; width: 26px; }
  /*
   * 줄기는 가지마다 자기 몫을 그린다. 한 번에 긋고 끝을 자르려면 **마지막 가지의
   * 높이**를 알아야 하는데 CSS 는 그걸 모른다(배지 줄 때문에 노드 높이가 제각각이다).
   * 그래서 각 가지가 [위 틈 + 자기 높이]를 긋고 마지막 가지만 자기 턱에서 멈춘다 —
   * 안 그러면 선이 마지막 노드 밑으로 허공에 늘어진다.
   */
  .g-children.col > .g-child::after { top: -12px; bottom: 0; left: 0; height: auto; }
  .g-children.col > .g-child:first-child::after { top: -26px; }
  .g-children.col > .g-child:last-child::after { bottom: auto; height: 46px; }
  .g-children.col > .g-child:first-child:last-child::after { height: 60px; }

  /*
   * 묻지 않았거나 닿지 않은 호스트의 모듈. **`.missing` 과 섞으면 안 된다** —
   * 저쪽은 "PM2 목록에 없다"는 관측이고 이쪽은 관측 자체가 없다는 뜻이다.
   * 주황(경고)을 쓰지 않는 것도 그래서다. 고칠 것이 있다는 말이 아니다.
   */
  .g-node.unknown { border-style: dashed; border-top-color: var(--border-input); background: var(--surface-2); }
  .g-node.unknown .g-ico { background: transparent; border: 1px dashed var(--border-input); color: var(--muted); }
  .g-node.unknown .g-name .linkish { color: var(--body); }
  .g-node.unknown .g-sub { color: var(--muted); }
  .g-node.agent.off { background: var(--surface-2); border-style: dashed; border-top-color: var(--border-input); }
  /* 뿌리는 콘솔 자신이다. 관측 대상이 아니라 보는 사람이 선 자리라 색을 따로 준다. */
  .g-node.root { width: var(--g-card-w); background: var(--primary-soft); border-top-color: var(--primary); }
  .g-node.root .g-ico { width: 46px; height: 46px; background: var(--surface); color: var(--primary-text); }
  .g-node.root .g-ico svg { width: 23px; height: 23px; }

  @media (max-width: 860px) {
    .app-menu { width: var(--menu-w-condensed); }
    .app-menu .logo-text, .app-menu .menu-item .label, .app-menu .menu-badge, .app-menu .menu-title { display: none; }
    .app-menu .menu-item { justify-content: center; padding: 11px 0; }
    .app-shell { padding-left: var(--menu-w-condensed); }
  }
  @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }

  /* ================= JSON 뷰어 (배포된 설정 보기) ================= */
  /*
   * 배포한 설정이 JSON 이면 **정렬해서 색을 입혀** 보여 준다.
   *
   * 처음에는 접을 수 있는 트리로 만들었는데 보기 불편했다 — 줄마다 삼각형이
   * 붙어 눈이 쉴 자리가 없고, 접힌 가지를 열어 가며 읽는 것은 "파일을 읽는"
   * 동작과 다르다. 설정 파일은 위에서 아래로 훑는 것이지 탐색하는 것이 아니다.
   * 그래서 그냥 들여쓰기된 원문으로 두고, 키와 값만 색으로 가른다.
   *
   * 색은 **기존 토큰을 그대로 쓴다.** JSON 전용 색을 새로 만들면 :root · 다크
   * 미디어쿼리 · [data-theme=dark] 세 곳에 같은 값을 또 적어야 하고, 한 곳을
   * 빠뜨리면 그 테마에서만 안 보이는 글자가 생긴다.
   */
  .jsonv-bar { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; }
  .jsonv-bar .spacer { flex: 1; }
  .jsonv-bar .hint { font-size: 12px; color: var(--muted); }

  pre.jsonv {
    font-family: var(--mono); font-size: 12.5px; line-height: 1.7;
    background: var(--surface-2); border: 1px solid var(--border);
    border-radius: var(--radius-lg); padding: 14px 16px;
    margin: 0; overflow: auto; max-height: min(64vh, 640px);
    color: var(--body);
    /* 긴 값(콤마로 끊은 한 줄 설정이 그렇다)은 줄바꿈하되 들여쓰기 열을 지킨다. */
    white-space: pre-wrap; overflow-wrap: anywhere;
    tab-size: 2;
  }

  /* 키와 값이 한눈에 갈라져야 한다 — 그게 이 뷰어의 용건이다. */
  .jsonv .k { color: var(--primary-text); font-weight: 600; }  /* 키 */
  .jsonv .s { color: var(--success-text); }                    /* 문자열 */
  .jsonv .n { color: var(--info-text); }                       /* 숫자 */
  .jsonv .b { color: var(--warning-text); font-weight: 600; }  /* true/false */
  .jsonv .z { color: var(--muted); font-style: italic; }       /* null */
  .jsonv .p { color: var(--muted); }                           /* 괄호·콤마·콜론 */

  /*
   * 파싱 실패. 이름이 .json 인데 못 읽히면 그 자체가 알아야 할 사실이므로
   * 조용히 원문으로 떨어지지 않고 이유를 보여 준다.
   */
  .jsonv-bad {
    font-size: 12px; color: var(--danger-text); background: var(--surface-2);
    border: 1px solid var(--border); border-left: 3px solid var(--danger);
    border-radius: var(--radius); padding: 8px 10px; margin-bottom: 8px;
  }

  /*
   * 필드 라벨 줄에 버튼을 같이 놓는다(배포 모달의 [JSON 정렬]). `.field label` 이
   * 이미 flex 라 그 안에 버튼을 넣을 수도 있지만, <label> 안의 버튼은 클릭이
   * 라벨의 포커스 이동과 겹쳐 눌러도 안 눌린 것처럼 보인다.
   */
  .field-head { display: flex; align-items: center; gap: 8px; }
  .field-head .spacer { flex: 1; }

  /* ==================================================================
     배포 (GitHub Actions)

     파이프라인은 표가 아니라 카드다. 한 줄에 담을 값이 이름·레포·워크플로·
     기본 브랜치뿐인데 각 줄의 주된 용건이 **누르는 것**(배포하기)이라,
     표로 만들면 버튼이 맨 오른쪽 좁은 칸으로 밀려 가장 중요한 것이 가장 작아진다.
     ================================================================== */
  .dp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }

  .dp-card {
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--surface); padding: 12px 14px;
    display: flex; flex-direction: column; gap: 6px; cursor: pointer;
    transition: border-color .12s, box-shadow .12s;
  }
  .dp-card:hover { border-color: var(--border-input); }
  .dp-card:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
  /* 카드 안의 버튼은 카드의 누름과 겹치지 않게 제 커서를 갖는다. */
  .dp-card .btn, .dp-card .icon-btn { cursor: pointer; }
  /* 실행 내역이 아래에 열려 있는 카드. 어느 것을 보고 있는지가 안 보이면 표가 떠 보인다. */
  .dp-card.is-open { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary) inset; }

  .dp-card-top { display: flex; align-items: center; gap: 6px; }
  .dp-card-top .spacer { flex: 1; }
  .dp-card-top b { font-size: 14px; }
  .dp-meta { font-size: 12px; color: var(--muted); }
  .dp-note { font-size: 12px; color: var(--body); margin-top: 2px; }
  .dp-card-foot { display: flex; gap: 8px; margin-top: 8px; }
  .dp-card-foot .btn { flex: 1; justify-content: center; }

  .icon-btn.sm { width: 26px; height: 26px; }
  .icon-btn.sm svg { width: 14px; height: 14px; }

  .empty-box { padding: 18px; text-align: center; color: var(--muted); }
  .empty-box p { margin: 0 0 6px; }

  /*
   * 실행 한 줄에 보낸 값을 그대로 늘어놓는다. GitHub 이 돌려주지 않는 유일한 정보라
   * 접어 두면 이 화면의 존재 이유가 한 번 더 클릭 뒤로 숨는다.
   */
  td.sent { max-width: 380px; }
  .kvchip {
    display: inline-flex; align-items: center; gap: 4px;
    font-size: 11.5px; font-family: var(--mono, ui-monospace, monospace);
    background: var(--surface-2); border: 1px solid var(--border);
    border-radius: 999px; padding: 1px 8px; margin: 2px 4px 2px 0;
  }
  .kvchip b { color: var(--muted); font-weight: 600; }
  td.right { text-align: right; }
  #dp-runs tbody tr { cursor: pointer; }

  .joblist { display: flex; flex-direction: column; gap: 6px; }
  .jobrow {
    display: flex; align-items: center; gap: 10px;
    border: 1px solid var(--border); border-radius: var(--radius); padding: 7px 10px;
  }
  .jobname { flex: 1; font-size: 13px; }

  /* 실행 내역의 브랜치 탭. 표 바로 위에 붙어 한 묶음으로 보이게 둔다. */
  .seg-wrap { padding: 10px 16px 0; }
  .seg-btn .n { margin-left: 6px; opacity: .6; font-variant-numeric: tabular-nums; }
  /* 탭 안의 라벨은 글자색만 쓴다. 칩 배경이 활성 탭 배경과 겹치면 색이 섞인다. */
  .seg-btn .lbl { background: transparent; border-color: transparent; padding: 0; }

  /*
   * 라벨 — 브랜치 · 환경 · 대상이 **같은 모양**을 쓴다.
   *
   * 세 칸이 나란히 있는데 모양이 다르면 눈이 칸마다 다시 적응해야 해서, 한 줄을
   * 훑는 일이 느려진다. 색은 **위험한 순서**다 — prod/main 빨강, stg 주황, dev 파랑.
   * 브랜치와 환경이 같은 축을 쓰는 것은 main 에서 돌린 배포가 곧 운영으로 가는 일이
   * 흔해서, 둘을 한 눈에 같은 뜻으로 읽게 되기 때문이다.
   */
  .lbl {
    display: inline-block; font-size: 11.5px; font-weight: 600; letter-spacing: .02em;
    border-radius: var(--radius-sm, 5px); padding: 2px 8px; white-space: nowrap;
    border: 1px solid transparent; line-height: 1.5;
  }
  .lbl.dev   { color: var(--info-text);    background: color-mix(in srgb, var(--info) 14%, transparent); }
  .lbl.stg   { color: var(--warning-text); background: color-mix(in srgb, var(--warning) 16%, transparent); }
  .lbl.prod  { color: var(--danger-text);  background: color-mix(in srgb, var(--danger) 14%, transparent); }
  .lbl.other { color: var(--muted);        background: var(--surface-2); border-color: var(--border); }
  /* 비웠다는 뜻. 점선으로 "적지 않았다"를 모양에 담는다 — 실선이면 고른 값처럼 보인다. */
  .lbl.all   { color: var(--muted); background: transparent; border-style: dashed; border-color: var(--border-input); }
  /* 서버 키. 등급이 없고 이름이 길어 여러 줄로 흐른다. */
  .lbl.key {
    color: var(--body); background: var(--surface-2); border-color: var(--border);
    font-family: var(--mono, ui-monospace, monospace); font-size: 11px; font-weight: 500;
    margin: 2px 4px 2px 0;
  }
  /* 누를 수 있는 라벨(배포 모달의 대상 고르기). */
  .lbl.pick { cursor: pointer; font: inherit; font-size: 11px; font-weight: 500; }
  .lbl.pick:hover { border-color: var(--muted); }
  .lbl.pick.on { color: var(--primary-text); background: var(--primary-soft); border-color: transparent; }
  .lbl.pick:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

  /* select 와 라벨을 한 줄에. */
  .row-mid { display: flex; align-items: center; gap: 10px; }
  .row-mid select { flex: 1; }

  /* 끈 옵션은 켠 것과 다르게 보여야 한다 — 같은 모양이면 둘을 거꾸로 읽는다. */
  .kvchip.off { opacity: .55; text-decoration: line-through; }
  /* 이 저장소가 배포하는 모듈. 남의 모듈 키와 섞여 있으므로 표시한다. */
  .kvchip.mine { color: var(--primary-text); border-color: color-mix(in srgb, var(--primary) 40%, transparent); }

  /* 설정 모달 */
  .kv-strip { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 12px; font-size: 12.5px; }
  .kv-strip .kv { color: var(--body); }
  .kv-strip .kv b { color: var(--muted); font-weight: 500; margin-right: 6px; }
  dialog.modal textarea.tall { min-height: min(46vh, 460px); font-size: 12.5px; line-height: 1.6; }
  .hint.ok { color: var(--success-text); }
  .hint.bad, .bad { color: var(--danger-text); }
  #cfg-summary .tbl-wrap { margin-top: 8px; }
  #cfg-summary table { font-size: 12.5px; }

  /* 대상 고르기 칸. .chips-filter 는 필터 줄용이라 아래 여백이 큰데, 필드 안에서는
     바로 밑의 설명과 떨어져 보인다. */
  #gd-targets { margin-bottom: 6px; gap: 6px; }
