/* style.css — Drawing Dashboard (formerly Label Dot v2).

   2026-07-24 full theme rebuild (user: "Lakukan full rebuild tema pada
   dashboard drawing" -- explicitly chose, when asked, to follow this
   project's own established root DESIGN.md ("Document Workspace -
   Airtable Inspired": white canvas, near-black ink actions, hairline
   borders, 12px card geometry) already implemented on the main
   dashboard.py page, over keeping this page's own prior ad-hoc dark
   "professional tool" palette).

   2026-09-06 Fase 1 redesign (design/DESIGN.md, "Light Clean Tool",
   Operate mode): the ad-hoc token ladder is replaced by the Fase-1 token
   set below — named surfaces (--surface/--surface-2/--canvas-bg), a real
   ONE-accent ramp (indigo, replacing the near-black-ink-as-accent of the
   2026-07-24 rebuild: a chromatic accent reads as "action/selection"
   against neutral chrome far better than yet another gray), semantic
   status tokens that MAP ONTO the canvas legend colors (--ok ≈ the
   auto-dot green, --err ≈ the empty-dot red, --warn ≈ the anchor-
   mengambang amber, --muted ≈ the excluded-number gray — see the canvas-
   marker token block further down), a 4pt spacing scale, a fixed-rem
   type scale (ratio ≈ 1.125, system sans, no fluid clamps), 2 radii,
   exactly 1 elevation, and z-index tiers. The old token NAMES remain as
   aliases pointing at the new values, so the ~200 existing references
   below (and every JS-built class) keep resolving. Canvas-overlay marker
   colors (dot/number/edge/route fills and strokes) are deliberately NOT
   re-themed: they are drawn on top of the rendered PDF page image, not
   this app's chrome, so their contrast is independent of the chrome
   palette; they are only ALIASED to the status tokens where the legend
   already gives them the same meaning. */

:root {
  /* ---- Fase 1 tokens (design/DESIGN.md §"Design tokens") ---- */
  /* Surfaces: white content, cool-gray panel/sidebar, slightly deeper
     drawing stage so the page reads as a stage under the drawing. */
  --surface: #ffffff;
  --surface-2: #f5f6f8;
  --canvas-bg: #e7eaf0;
  /* Ink: primary + secondary text (AA ≥4.5:1 on every surface above:
     measured 16.1 / 5.95 at worst). */
  --ink: #171c26;
  --ink-2: #4b5261;
  /* Hairline ladder. */
  --line: #dcdfe6;
  --line-soft: #eaebf0;
  --line-strong: #a9aeb9;
  /* ONE accent: primary action + current selection + status focus.
     --accent-bg is the quiet tinted-rest state for active controls
     (nav item, toggle, tab), --accent-softer the hover step inside it. */
  --accent: #2f5ae0;
  --accent-hover: #2548b8;
  --accent-active: #1d3a99;
  --accent-bg: #e9eefc;
  --accent-softer: #d8e1f9;
  --accent-text: #1f46c0;   /* accent as TEXT on light surfaces (5.6:1 AA) */
  /* Semantic status — mapped from the canvas legend's own marker colors
     (auto dot #3fbf5f, empty dot #ff5252, anchor mengambang #ff6d00,
     excluded number #777) with bg/border/text companions tuned to AA. */
  --ok: #1e7a34;
  --ok-bg: #e6f3e9;
  --ok-border: #79ab83;
  --ok-text: #14612a;
  --err: #cf2e2e;
  --err-bg: #fbe9e9;
  --err-border: #d89a9a;
  --err-text: #a82323;
  --warn: #bf5a00;
  --warn-bg: #fdf0e0;
  --warn-border: #d9a45f;
  --warn-text: #94470a;
  --muted: #5f6570;
  --muted-bg: #eef0f3;
  --muted-border: #c4c9d1;
  --muted-text: #4b5261;
  /* 4pt spacing scale. */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-6: 24px; --sp-8: 32px;
  /* Fixed-rem type scale, ratio ≈ 1.125 (dense data runs at fs-1/fs-0). */
  --fs-1: 0.6875rem;  /* 11px — table heads, badges, meta */
  --fs0: 0.75rem;     /* 12px — secondary/helper */
  --fs1: 0.8125rem;   /* 13px — body */
  --fs2: 0.9375rem;   /* 15px — emphasized body */
  --fs3: 1.0625rem;   /* 17px — card/dialog title */
  --fs4: 1.1875rem;   /* 19px — page title */
  /* Radius: small, consistent. */
  --radius: 6px; --radius-lg: 10px; --radius-full: 9999px;
  /* Exactly ONE elevation; hairlines carry the rest. */
  --elev-1: 0 1px 2px rgba(23, 28, 38, .06);
  /* z-index tiers (search results float over chrome, mode chip over
     canvas layers, modal above everything). */
  --z-chrome: 20;
  --z-overlay: 30;
  --z-modal: 100;
  /* State-only motion, 150–250ms per the craft floor. */
  --dur-fast: .15s;
  --dur: .2s;

  /* ---- pre-Fase-1 names kept as aliases (do not add new uses) ---- */
  --canvas: var(--surface); --surface-soft: var(--surface-2);
  --surface-strong: #e2e4e9;
  --bg-0: var(--surface); --bg-1: var(--surface-2);
  --bg-2: #ecedf1; --bg-3: var(--surface-strong);
  --hairline: var(--line); --border: var(--line);
  --border-soft: var(--line-soft); --border-strong: var(--line-strong);
  --ink-active: var(--accent-active);
  --body: #383e48; --faint: var(--ink-2);
  --text: var(--ink); --text-dim: var(--body); --text-faint: var(--faint);
  --accent-dim: var(--accent-bg);
  --active-bg: var(--accent-bg); --active-border: var(--accent);
  --active-text: var(--accent-text);
  /* --danger stays the destructive-action coral (a warm hue reserved for
     destruction so it never collides with the --err route-status red). */
  --danger: #aa2d00; --danger-bg: #fbeae4; --danger-border: #d9a084; --danger-text: #8f2400;
  --dotsize: 8px; --numsize: 10px;
  --edge-highlight: var(--elev-1);
  --focus-ring: 0 0 0 3px rgba(47, 90, 224, .22);
  --sidebar-w: 320px;
  --nav-w: 172px;
  /* Slim icon rail the left nav collapses to (2026-10) — a token so the
     width transition and the collapsed rules share one source. */
  --nav-rail-w: 54px;
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-feature-settings: "calt", "kern", "liga";
  background: var(--surface);
  color: var(--ink);
  font-size: var(--fs1);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
a { color: var(--accent-text); }
a:hover { color: var(--accent-hover); }
button, input, select { font-family: inherit; }
h2, h4, h5 { margin: 0 0 var(--sp-2); }

/* ---- browser surfaces, themed from the palette (craft floor) ---- */
::selection { background: rgba(47, 90, 224, .18); }
input, textarea { caret-color: var(--accent); }
/* Numeric data everywhere lines up: tables, sliders, pagination, status. */
.labeltable, .boxtable, .statusbar, .slider-value, .page-info, .pair-title,
#pairInfo, .num-cell, .num-th, .item-num, .cell-input, input[type="number"] {
  font-variant-numeric: tabular-nums;
}

/* Keyboard-focus ring (a visible 2px accent ring is its own elevation
   level) — :focus-visible only, so mouse clicks stay clean. */
button:focus-visible, a:focus-visible, .navbtn:focus-visible, .chk:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}

/* Thin, recessive scrollbars — chrome should never compete with content. */
* { scrollbar-width: thin; scrollbar-color: #c2c6cf transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: #c2c6cf; border-radius: 8px; border: 2px solid var(--surface); }
::-webkit-scrollbar-thumb:hover { background: var(--line-strong); }
::-webkit-scrollbar-track { background: transparent; }

/* ---------------- top bar (always visible, above the menu nav) --------- */

#topbar {
  position: sticky; top: 0; z-index: var(--z-chrome);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.topbar-row {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--line-soft);
}
.topbar-brand {
  font-weight: 600; font-size: var(--fs1); color: var(--ink);
  display: flex; align-items: center; gap: var(--sp-2);
  padding-right: var(--sp-3); border-right: 1px solid var(--line);
  white-space: nowrap;
}
.brand-title { color: var(--ink); font-weight: 650; letter-spacing: -0.01em; }
.topbar-file {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs0); color: var(--ink-2);
  min-width: 0; max-width: 100%; flex: 1 1 auto;
}
/* A real uploaded filename can be long (this project's own drawings often
   are, e.g. "VF8_NP_EEP35112200AA_HARNESS_BODY_...pdf") -- truncate with an
   ellipsis instead of forcing the whole top bar (and page) wider than the
   viewport. `min-width:0` is required for text-overflow to actually apply
   inside a flex item, not just a cosmetic addition. */
.current-file {
  color: var(--ink); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.topbar-row select, .topbar-row input {
  background: var(--surface); color: var(--ink); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 6.5px 10px; font-size: var(--fs0);
}
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: var(--focus-ring);
}
/* Unified command palette (Fase 2): no longer scoped to menu Drawing —
   the wrapper stays visible in every menu once a drawing is loaded.
   `[hidden]` needs an explicit rule here: the `display:flex` below is an
   author rule and would otherwise outrank the UA stylesheet's own
   `[hidden]{display:none}` on a specificity tie -- the exact trap
   `.btn[hidden]` already documents. While the palette is in use the
   inline box widens (.palette-open, state-only motion per the craft
   floor) so real labels ("SP-XX01", "RBCCXX") stay readable. */
.topbar-search { position: relative; display: flex; flex: 0 1 300px; min-width: 140px; transition: flex-basis var(--dur) ease; }
.topbar-search.palette-open { flex-basis: 440px; }
.topbar-search[hidden] { display: none; }
#searchInput { flex: 1 1 auto; min-width: 0; width: 100%; }
/* Results float over the page rather than reflowing the top bar, so
   opening them can never shift the canvas underneath. */
/* The palette overlay escapes its container and floats over chrome
   (--z-overlay), so opening it can never shift or be clipped by the
   canvas underneath. */
.search-results {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: var(--z-overlay);
  min-width: 320px;
  max-height: 50vh; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg); box-shadow: 0 8px 24px rgba(0,0,0,.14);
  padding: var(--sp-1);
}
.search-results[hidden] { display: none; }
.search-hit {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: 6px 8px; border: 1px solid transparent; border-radius: var(--radius);
  background: none; color: var(--ink); font: inherit; font-size: var(--fs0);
  text-align: left; cursor: pointer;
}
/* Accent = current selection, the same language as .navbtn.on — the
   keyboard-driven row is unmistakable next to mere hover. */
.search-hit:hover { background: var(--surface-2); }
.search-hit.active { background: var(--accent-bg); border-color: var(--accent-softer); }
.search-hit.active .hit-name { color: var(--accent-text); }
.search-hit .hit-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 550; }
.search-hit .hit-detail { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-2); font-size: var(--fs-1); }
.search-hit .hit-kind {
  flex: 0 0 auto; font-size: var(--fs-1); letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px;
}
/* Kind chips reuse the ONE status vocabulary (Fase 1 tokens): tabel ≈ ok
   green (it has circuit data), dot ≈ accent, splice ≈ warn amber (same
   family the canvas uses for review-worthy items), menu/wire/rute stay
   neutral so the chips never invent a second color language. */
.search-hit .hit-kind.tabel { color: var(--ok-text); border-color: var(--ok-border); background: var(--ok-bg); }
.search-hit .hit-kind.menu { color: var(--accent-text); border-color: var(--accent-softer); background: var(--accent-bg); }
.search-hit .hit-kind.splice { color: var(--warn-text); border-color: var(--warn-border); background: var(--warn-bg); }
.search-hit .hit-kind.sirkuit, .search-hit .hit-kind.clip, .search-hit .hit-kind.grom {
  color: var(--accent-text); border-color: var(--accent-softer); background: transparent;
}
.search-hit.active .hit-kind.menu { border-color: var(--accent); }
.search-note { padding: 6px 8px; font-size: var(--fs-1); color: var(--ink-2); }
.spacer { flex: 1; }

/* ---------------- Local-First Outbox Sync Indicator ------------------ */
.sync-indicator {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 8px; border-radius: var(--radius);
  font-size: var(--fs-1); font-weight: 500;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink-2);
  user-select: none; transition: all var(--dur) ease;
}
.sync-dot {
  width: 7px; height: 7px; border-radius: 50%; display: inline-block;
  background: currentColor; flex-shrink: 0;
}
.sync-indicator.sync-synced {
  color: var(--ok-text); background: var(--ok-bg); border-color: var(--ok-border);
}
.sync-indicator.sync-syncing {
  color: var(--accent-text); background: var(--accent-bg); border-color: var(--accent-softer);
}
.sync-indicator.sync-syncing .sync-dot {
  animation: sync-pulse 1.2s infinite ease-in-out;
}
@keyframes sync-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1.2); }
}
.sync-indicator.sync-pending,
.sync-indicator.sync-offline {
  color: var(--warn-text); background: var(--warn-bg); border-color: var(--warn-border);
}
.sync-flush-btn {
  padding: 1px 4px; margin-left: 2px; border-radius: 3px;
  line-height: 1; background: transparent; border: 1px solid currentColor;
  color: inherit; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
}
.sync-flush-btn:hover {
  background: var(--surface);
}
.sync-flush-btn[hidden] {
  display: none !important;
}

.statusbar {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: 6px var(--sp-4); border-top: 1px solid var(--line-soft);
  background: var(--surface-2);
  font-size: var(--fs-1); color: var(--ink-2); flex-wrap: wrap;
}
#status { flex: 1; min-width: 200px; }
#status.status-error { color: var(--err-text); }
#status.status-warn { color: var(--warn-text); }

/* ---------------- menu Drawing's own toolbar + legend ------------------ */

.drawing-toolbar {
  display: flex; align-items: center; gap: 6px;
  padding: 6px var(--sp-4); flex-wrap: wrap; row-gap: 6px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.tgroup { display: flex; align-items: center; gap: var(--sp-1); }
.tgroup-capsule {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 2px;
  gap: 2px;
}
.tgroup-capsule .btn,
.tgroup-capsule .icon-chk {
  border: 1px solid transparent;
  border-radius: calc(var(--radius) - 2px);
  background: transparent;
}
.tgroup-capsule .btn:hover:not(:disabled),
.tgroup-capsule .icon-chk:hover {
  background: var(--surface);
  border-color: var(--line-soft);
}
.tgroup-capsule .btn.on,
.tgroup-capsule .icon-chk:has(input:checked) {
  background: var(--active-bg);
  border-color: var(--active-border);
  color: var(--active-text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.tgroup-capsule .tool-delete-btn:not(:disabled):hover {
  background: #fee2e2;
  border-color: #fca5a5;
  color: #b91c1c;
}
.tgroup-inner-sep {
  width: 1px;
  height: 18px;
  background: var(--line);
  margin: 0 2px;
  flex: 0 0 auto;
}
.tsep { width: 1px; height: 22px; background: var(--line); margin: 0 4px; flex: 0 0 auto; opacity: 0.8; }
.quality-btn.on { background: var(--active-bg); border-color: var(--active-border); color: var(--active-text); }

/* 2026-07-21 follow-up -- every toolbar button/toggle in menu Drawing
   becomes an icon + tooltip instead of a text label (user: "sederhanakan
   menjadi simbol2"), reusing the SAME svg across all of them (no icon
   font, no CDN -- matches this project's own "no build step, self-
   contained static page" convention). Sizing/color lives here, once, so
   every icon (button or checkbox) looks consistent regardless of which
   element hosts it. */
.icon-btn.btn { padding: 6.5px 9px; min-width: 34px; justify-content: center; }
svg.icon { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto; }
.icon-chk { padding: 6.5px 8px; }
.icon-chk svg.icon { color: inherit; }
.icon-chk input { position: absolute; opacity: 0; width: 0; height: 0; } /* the label itself is the click target; the checkbox stays native/accessible but visually replaced by its icon */
.icon-chk:has(input:checked) { background: var(--active-bg); border: 1px solid var(--active-border); color: var(--active-text); }

/* Mode Edit / Mode Pratinjau (2026-07-21 follow-up, user: "tambahkan
   preview mode dan edit mode") -- same visual language as .quality-btn's
   own active-state pattern. */
.mode-btn.on { background: var(--active-bg); border-color: var(--active-border); color: var(--active-text); }

/* Side panel (editor/inspector) collapse (2026-07-24, user: "Tambahkan
   opsi collapse pada sidebar untuk memperluas" -- the drawing canvas read
   too narrow with the 320px editor panel always pinned open). Width
   collapses to 0 rather than just hiding content, so #viewport (flex:1)
   reclaims the space immediately; re-expands automatically the moment an
   editor is opened again (see app.js's `sidepanel:ensure-open` listener)
   so clicking a dot/edge/number/table box while collapsed never silently
   does nothing. */
#drawingSidePanel { transition: flex-basis .2s ease, width .2s ease; }
#drawingSidePanel.sidepanel-collapsed {
  width: 0; flex: 0 0 0; min-width: 0; border-left: none; overflow: hidden;
}

/* Editor-panel edge tab (2026-10, user: "petakan di kanan tengah
   posisinya") -- the side panel's collapse/expand handle pinned to the
   RIGHT-CENTER of the viewport edge: a slim vertical-edge tab, vertically
   centered on the drawing area, that stays visible while the panel is
   collapsed to 0 width so it can always be reopened. It is the panel's
   ONE handle since 2026-10 -- the toolbar's duplicate button (which also
   hid with the toolbar collapse) was removed per user decision.
   Token styling, .btn-like affordance, chevron flips toward the direction
   a click will move the panel. */
.edge-tab {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  z-index: var(--z-chrome);
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 72px; padding: 0;
  background: var(--surface); color: var(--ink-2);
  border: 1px solid var(--line); border-right: none;
  border-radius: var(--radius) 0 0 var(--radius);
  box-shadow: var(--elev-1);
  cursor: pointer;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.edge-tab:hover { background: var(--surface-2); color: var(--ink); border-color: var(--line-strong); }
.edge-tab .chevron { transition: transform var(--dur) ease; }
/* collapsed: point back outward (the panel re-opens to the left) */
.drawing-body:has(#drawingSidePanel.sidepanel-collapsed) .edge-tab .chevron { transform: rotate(180deg); }

/* Preview Mode (2026-07-21 follow-up): only zoom/fit, quality, and the 2
   table-visibility toggles stay -- every `.edit-only-toolbar` element
   (the 3 non-table view-toggles, the whole add-mode group, the whole
   compute group) disappears. Canvas edit interactions themselves are
   disabled in app.js/canvas.js (state.drawingMode checks), not by CSS. */
#sectionDrawing[data-mode="preview"] .edit-only-toolbar { display: none; }

/* The legend is grouped by the KIND of marker (dots / numbers / tables /
   routes & overlays) so a reviewer scans one small cluster at a time
   instead of one long flat row; every swatch references the same token
   the canvas layer paints with, so legend and canvas cannot drift.
   2026-10 (user: the permanent bar eats vertical space): the legend now
   lives in a helper popover -- absolutely positioned on #sectionDrawing
   (position:relative) so it escapes the toolbar's stacking context and
   overlays the canvas, top-right under the "?" button that opens it. */
#sectionDrawing { position: relative; }
.legend-panel {
  position: absolute; top: 46px; right: var(--sp-4); z-index: var(--z-overlay);
  display: flex; flex-direction: column; gap: var(--sp-3);
  width: min(360px, calc(100vw - 32px));
  max-height: min(70vh, 520px); overflow-y: auto;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); box-shadow: var(--elev-1);
}
.legend-panel[hidden] { display: none; }
.lg-panel-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  margin-bottom: calc(-1 * var(--sp-1));
}
.lg-panel-title {
  font-size: var(--fs-1); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-2);
}
.lg-panel-head .icon-btn { padding: 3px 6px; min-width: 0; }
.legend .lg-group { display: flex; align-items: center; gap: var(--sp-2) var(--sp-3); flex-wrap: wrap; }
.legend .lg-head {
  font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-2); margin-right: 2px;
}
.legend .lg { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; font-size: var(--fs-1); color: var(--body); }
/* The bbox-purpose note wraps (it's a sentence, not a swatch label) and
   reads quieter than the swatch entries (2026-11, bbox-confusion note). */
.legend .lg-note { white-space: normal; color: var(--ink-2); font-style: italic; max-width: 46rem; }
.legend .sw { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: 0 0 auto; }
.legend .sw.sq { border-radius: 2px; }
.legend .sw.ln { width: 14px; height: 0; border-top: 2px solid; border-radius: 0; }
.legend .sw.dash { border-top-style: dashed; }

/* ---------------- buttons / inputs ---------------- */
/* One button vocabulary: default (quiet hairline) / hover / focus-visible
   (accent ring) / active-pressed / disabled / loading (.busy, text stays
   readable — never a spinner blocking content) / .on (current selection,
   accent-tinted) / .primary (the ONE filled accent CTA per view) /
   .save (data commit) / .danger|clear (destructive) / .text (ghost). */

.btn {
  cursor: pointer; background: var(--surface); color: var(--ink); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 6.5px 12px; font-size: var(--fs0); font-weight: 500; line-height: 1.2;
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
/* `.btn`'s own `display: inline-flex` (an author-stylesheet rule) silently
   outranked the browser's UA-stylesheet `[hidden] { display: none }` rule
   at equal specificity (author origin always wins ties) -- found
   2026-07-20 while live-verifying the new table-box metadata editor's
   Reset button, which never actually hid despite `hidden` being toggled
   correctly in JS. Same bug also affected the pre-existing
   `#tableBoxResetBtn` (bbox reset) and `#dotDeleteBtn` (only meant to
   show for manually-added dots) -- both silently visible at all times.
   `.btn[hidden]` (class+attribute, higher specificity than `.btn` alone)
   fixes all three with one rule. */
.btn[hidden] { display: none; }
.btn:hover:not(:disabled) { background: var(--surface-2); border-color: var(--line-strong); }
.btn:active:not(:disabled) { background: var(--bg-3); border-color: var(--line-strong); }
.btn:disabled { opacity: .45; cursor: default; }
/* A disabled PRIMARY keeps a readable fill (its white label still clears
   AA) rather than fading to a washed-out tint. */
.btn.primary:disabled { opacity: 1; background: var(--line-strong); border-color: var(--line-strong); color: #ffffff; }
/* Loading: set .busy (or aria-busy) while the action runs — the button
   dims and drops its pointer, its label stays put so the user keeps
   context. No spinner is inserted and no content is blocked. */
.btn.busy, .btn[aria-busy="true"] { opacity: .6; cursor: progress; pointer-events: none; }
.btn.icon { padding: 6.5px 10px; min-width: 34px; justify-content: center; }
.btn.on { background: var(--active-bg); border-color: var(--active-border); color: var(--active-text); }
.btn.secondary { color: var(--body); }
/* The one filled CTA per view: solid accent fill, white text — the
   single strongest affordance. */
.btn.primary { background: var(--accent); border-color: var(--accent); color: #ffffff; font-weight: 600; }
.btn.primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn.primary:active:not(:disabled) { background: var(--accent-active); border-color: var(--accent-active); }
/* Save = commit — the ONE chrome use of green, kept because it signals a
   data-state commit (semantic), not decoration. */
.btn.save { background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); }
.btn.danger, .btn.clear { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger-text); }
.btn.text { background: transparent; border-color: transparent; color: var(--ink-2); box-shadow: none; }
.btn.text:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }

.chk {
  display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs0); color: var(--body);
  padding: 6.5px 9px; border-radius: var(--radius); cursor: pointer; user-select: none;
  border: 1px solid transparent;
}
.chk:hover { background: var(--surface-2); color: var(--ink); }
.chk input { accent-color: var(--accent); margin: 0; }

/* segmented group for mutually-exclusive toggles (2026-07-23 UX audit:
   the two table overlays silently unchecked each other as two free-floating
   checkboxes — visually one bordered "pick one" control now) */
.seg-group { display: inline-flex; align-items: stretch; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.seg-group .chk { border-radius: 0; }
.seg-group .chk + .chk { border-left: 1px solid var(--line); }
.seg-group .icon-chk:has(input:checked) { border: none; border-radius: 0; }
.seg-group .icon-chk:has(input:checked) + .chk { border-left: 1px solid var(--line); }

input[type="text"], input[type="number"], input:not([type]) {
  background: var(--surface); color: var(--ink); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 7px 9px; width: 100%;
  transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
input::placeholder { color: var(--ink-2); opacity: .75; }

/* ---------------- status badges ---------------- */
/* One pill shape; the semantic hue comes from the same --ok/--err/
   --warn/--muted tokens the canvas legend swatches reference, so a
   status reads identically in the legend, in menu Rute's table, and in
   menu Topology's counts. */
.badge {
  display: inline-block; padding: 1px 8px; border-radius: var(--radius-full);
  border: 1px solid var(--muted-border); background: var(--muted-bg);
  color: var(--muted-text);
  font-size: var(--fs-1); font-weight: 500; line-height: 1.5; white-space: nowrap;
}
.badge.ok { background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); }
.badge.err { background: var(--err-bg); border-color: var(--err-border); color: var(--err-text); }
.badge.warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
.badge.muted { background: var(--muted-bg); border-color: var(--muted-border); color: var(--muted-text); }

/* ---------------- skeleton loader ---------------- */
/* Content-shaped placeholder used INSTEAD of a spinner while a region
   loads — the layout is already there, so nothing jumps when real
   content lands. */
@keyframes skel-shimmer { to { background-position: -200% 0; } }
.skel {
  display: block; border-radius: var(--radius);
  background: linear-gradient(90deg, var(--bg-2) 25%, var(--surface-2) 50%, var(--bg-2) 75%);
  background-size: 200% 100%;
  animation: skel-shimmer 1.4s ease infinite;
}
.skel.skel-line { height: 12px; margin: 6px 0; }
.skel.skel-row { height: 32px; margin: 4px 0; }
/* Canvas placeholder while the preview image downloads/decodes (Fase 4) —
   fills the drawing viewport with the same shimmer so the very first paint
   reads as "a drawing is coming" instead of a blank gray stage. Removed by
   canvas.loadImages() the moment real layers exist. */
.skel.skel-canvas { position: absolute; inset: 0; border-radius: 0; }
/* Skeleton rows hosted inside a data table's <tbody> — the shimmer block
   lives in each cell, so the table's own header/column structure stays put
   (a real column layout, not a single undifferentiated blob). */
.skel-row-host td { padding-top: 4px; padding-bottom: 4px; }

/* ---------------- banners ---------------- */

.banner {
  padding: var(--sp-2) var(--sp-4); font-size: var(--fs0); line-height: 1.4;
}
.banner-info { background: var(--accent-bg); color: var(--accent-text); }
.banner-warn { background: var(--warn-bg); color: var(--warn-text); border-top: 1px solid var(--warn-border); }
.banner-error { background: var(--err-bg); color: var(--err-text); border-top: 1px solid var(--err-border); }
.banner.small { padding: 6px 10px; border-radius: var(--radius); border-top: none; margin-bottom: var(--sp-2); }

/* ---------------- menu nav shell ---------------- */

#appBody { flex: 1; display: flex; min-height: 0; }

#appNav {
  width: var(--nav-w); flex: 0 0 var(--nav-w);
  background: var(--surface-2); border-right: 1px solid var(--line);
  display: flex; flex-direction: column; padding: var(--sp-3) var(--sp-2);
  overflow-y: auto; overflow-x: hidden;
  transition: width var(--dur) ease, flex-basis var(--dur) ease;
}
/* Rail collapse toggle: a full-width quiet row at the top of the nav; its
   chevron flips to point the way a click will move the rail. */
.nav-collapse-btn { align-self: stretch; margin-bottom: var(--sp-2); color: var(--ink-2); }
.nav-collapse-btn:hover:not(:disabled) { color: var(--ink); }
.nav-collapse-btn .chevron { transition: transform var(--dur) ease; }
/* Collapsed state (2026-10, user: the nav should collapse to give the
   canvas more room) -- slim icon rail: labels and group captions hidden,
   every menu still reachable via its icon (the button's title carries the
   name as the native tooltip), groups still separated by a hairline. */
#appNav.nav-collapsed { width: var(--nav-rail-w); flex-basis: var(--nav-rail-w); }
#appNav.nav-collapsed .nav-collapse-btn { align-self: center; }
#appNav.nav-collapsed .nav-collapse-btn .chevron { transform: rotate(180deg); }
#appNav.nav-collapsed .navgroup {
  align-items: center; margin-bottom: var(--sp-3); padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--line);
}
#appNav.nav-collapsed .navgroup:last-child { border-bottom: none; }
#appNav.nav-collapsed .navgroup-label,
#appNav.nav-collapsed .nav-label { display: none; }
#appNav.nav-collapsed .navbtn {
  justify-content: center; padding: 8px; width: 38px; min-height: 38px;
}
#appNav.nav-collapsed .navbtn.on { box-shadow: none; }
/* Fase 2 workflow grouping (Muat/Periksa/Ukur/Hasil): a quiet small-caps
   muted label heads each cluster — the established nav-group pattern, NOT
   a content kicker — with MORE space above it than below so it binds to
   the group it heads (same hierarchy rule .card-title documents). */
.navgroup { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--sp-3); }
.navgroup:last-child { margin-bottom: 0; }
.navgroup-label {
  font-size: var(--fs-1); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-2); padding: 0 12px; margin-bottom: 2px;
}
.navbtn {
  cursor: pointer; background: transparent; color: var(--body); border: 1px solid transparent;
  border-radius: var(--radius); padding: 8px 12px; font-size: var(--fs1); font-weight: 500; text-align: left;
  display: flex; align-items: center; gap: 9px;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.navbtn .icon { color: var(--ink-2); }
.navbtn:hover { background: var(--bg-2); color: var(--ink); }
.navbtn:hover .icon, .navbtn.on .icon { color: inherit; }
/* The active menu uses the ONE accent: tinted rest state + 2px accent
   rail, exactly the same "current selection" language the toolbar
   toggles, category tabs, and table rows use. */
.navbtn.on {
  background: var(--accent-bg); color: var(--accent-text); font-weight: 600;
  border-color: var(--accent-softer);
  box-shadow: inset 2px 0 0 var(--accent);
}

#appMain { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }

.app-section { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.app-section[hidden] { display: none; }

/* Berkas/Harness Option/Tabel/Sirkuit/Wire/Rute get a scrollable, padded
   page area -- menu Drawing manages its own full-bleed layout (canvas +
   toolbar) below. NOTE: every NEW padded menu section must be added here
   (learned the hard way with #sectionRute). Content is width-bounded so
   panels stop stretching into flat, empty rows on wide screens; tables
   inside keep full use of that bound. */
#sectionBerkas, #sectionHarness, #sectionTabel, #sectionSirkuit, #sectionWire, #sectionRute, #sectionIcwc, #sectionMaterial {
  overflow-y: auto; padding: var(--sp-6) var(--sp-6) var(--sp-8);
}
#sectionBerkas > *, #sectionHarness > *, #sectionTabel > *,
#sectionSirkuit > *, #sectionWire > *, #sectionRute > *,
#sectionIcwc > *, #sectionMaterial > * {
  max-width: 1440px;
}
/* Consistent page-header band across every menu: full-width hairline under
   the title, description constrained to a readable measure. */
.section-head {
  margin-bottom: var(--sp-4); padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--line-soft);
}
.section-head h2 { font-size: var(--fs4); font-weight: 650; letter-spacing: -0.015em; margin: 0 0 var(--sp-1); }
.section-desc { font-size: var(--fs0); color: var(--ink-2); margin: 0; line-height: 1.5; max-width: 760px; }
/* A "cara pakai" line inside a .section-desc: same size as the description
   but quieter, so the action guidance reads as a footnote to the
   what-it-does sentence, not a second paragraph. */
.section-desc .hint { color: var(--muted); font-size: var(--fs-1); }
.section-toolbar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: var(--sp-3);
}
.section-toolbar input[type="text"] { max-width: 320px; }
/* summary counts sit at the row's far right, consistently on every menu */
.section-toolbar .tabsummary { margin-left: auto; margin-bottom: 0; }

/* menu Tabel's category tabs (2026-08-19, C2) -- .btn.on already gives the
   active-tab look, this row just needs a tighter gap than the default
   .section-toolbar's 12px, since these are peers of one control, not
   unrelated toolbar items. */
.tabel-cat-row { gap: 6px; margin-bottom: var(--sp-2); }
.tabel-cat { font-size: var(--fs0); padding: 5px 10px; }

/* "Pendukung" category's two sub-sections (Belum Terikat / Sudah Terikat)
   live directly in the list container, not inside a .section-card, so
   .card-title's own styling (normally scoped to .section-card .card-title)
   is reused standalone here rather than duplicated with different values.
   Hierarchy rule everywhere a title appears: MORE space above it than
   below it, so the title binds to the group it heads. */
#tabelListContainer .card-title {
  font-size: var(--fs-1); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-2); margin: var(--sp-4) 0 var(--sp-2);
}
#tabelListContainer .card-title:first-child { margin-top: 0; }

/* Panel card: white surface on the page, hairline border, the ONE subtle
   elevation. Groups inside are tight; cards are separated generously. */
.section-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--sp-4); margin-bottom: var(--sp-4); max-width: 860px;
  box-shadow: var(--elev-1);
}
.section-card .card-title {
  font-size: var(--fs-1); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-2); margin: 0 0 var(--sp-3);
}
.section-card .field-hint { display: block; margin-top: var(--sp-3); }

.berkas-form {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; max-width: 720px;
}
.berkas-form select { flex: 1; min-width: 260px; }
.berkas-form .field-label { margin-top: 0; }
.field-hint { font-size: var(--fs0); color: var(--ink-2); line-height: 1.5; }

.table-pdf-preview {
  margin-top: var(--sp-3); max-width: 100%;
  display: flex; flex-direction: column; gap: var(--sp-2);
}
/* same author-vs-UA specificity trap as .btn[hidden] (documented there):
   the class's display:flex silently outranks the UA [hidden] rule */
.table-pdf-preview[hidden] { display: none; }
.table-pdf-preview img {
  max-width: 100%; border: 1px solid var(--border); border-radius: 4px;
  background: #fff;
}

/* menu Drawing: toolbar/legend (own rules above) + a full-bleed body row
   (canvas viewport + the existing inspector/pair/warnings side panel). */
.drawing-body { flex: 1; display: flex; min-height: 0; position: relative; }

/* active-mode chip pinned to the canvas (2026-07-23 UX audit) — text set
   via canvas.setModeChip() by app.js (add-modes) / panels.js (anchor-pick) */
.mode-chip {
  position: absolute; top: 10px; left: 10px; z-index: var(--z-chrome);
  background: var(--accent-bg); border: 1px solid var(--accent);
  color: var(--accent-text); padding: 5px 11px; border-radius: var(--radius);
  font-size: var(--fs0); font-weight: 500; pointer-events: none;
  max-width: min(520px, 70%);
}
/* The drawing stage: deliberately a step deeper than the chrome surfaces
   so the rendered PDF page reads as the hero on a recessed stage. */
#viewport {
  flex: 1; overflow: auto; cursor: grab; background: var(--canvas-bg);
  min-width: 0;
}
#viewport.adding, #viewport.edging, #viewport.numadding, #viewport.tableboxadding, #viewport.anchor-picking { cursor: crosshair; }
/* Space held: the next drag pans (Fase 3), so surface the grab hand even
   inside an add-mode that would otherwise show the crosshair. */
#viewport.pan-armed { cursor: grab; }
#canvasWrap { position: relative; transform-origin: 0 0; }
#canvasWrap img { display: block; width: 100%; height: auto; user-select: none; -webkit-user-drag: none; }

/* Live zoom readout in the zoom control cluster (Fase 3) — a quiet label,
   tabular numerals so the % doesn't jitter as the value changes width. */
.zoom-readout {
  align-self: center; padding: 0 4px 0 2px;
  font-size: var(--fs0); color: var(--ink-2);
  font-variant-numeric: tabular-nums; min-width: 4ch; text-align: right;
  user-select: none;
}

#drawingSidePanel {
  width: var(--sidebar-w); flex: 0 0 var(--sidebar-w);
  border-left: 1px solid var(--line);
  background: var(--surface-2);
  display: flex; flex-direction: column;
  overflow-y: auto;
}
.panel-section {
  padding: var(--sp-3) 14px;
  border-bottom: 1px solid var(--line);
}
.panel-section h3, .panel-section h4 {
  /* section label: small caps, positive tracking — taxonomy, not content */
  font-size: var(--fs-1); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-2); margin: 0 0 var(--sp-3);
}
/* "Hasil Inspeksi Rute" (result-only panel — the manual pair inspector was
   removed 2026-07-23, redundant with menu Rute; this shows the inspection
   started from menu Rute's "Lihat di Drawing"). */
.pair-title { font-size: var(--fs1); font-weight: 600; margin-bottom: 6px; font-variant-numeric: tabular-nums; }

/* ---------------- dots / numbers / edges ---------------- */

.dot {
  position: absolute; width: var(--dotsize); height: var(--dotsize); border-radius: 50%;
  transform: translate(-50%, -50%); cursor: pointer; box-sizing: border-box;
  border: 1px solid rgba(0,0,0,.6); opacity: .88; z-index: 2;
}
/* invisible enlarged hit target (2026-07-23 UX audit): at fit zoom a dot's
   visual size bottoms out at 4-7px — well under comfortable pointer-target
   guidance. The pseudo-element participates in the parent's hit-testing
   without changing the visual, same principle as the manual edges' wide
   invisible .edgehit stroke. */
.dot::before {
  content: ""; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%); border-radius: 50%;
  width: max(calc(var(--dotsize) + 14px), 24px); height: max(calc(var(--dotsize) + 14px), 24px);
}
#viewport.edging .dot,
#viewport.adding .dot,
.dot.picking,
.dot:hover {
  z-index: 10;
}
.dot.auto { background: var(--mk-dot-auto); }
.dot.overridden { background: var(--mk-dot-override); }
.dot.user { background: var(--mk-dot-user); cursor: move; } /* manual dots drag-to-move */
.dot.empty { background: var(--mk-dot-empty); }
.dot.hit { outline: 2px solid #ffb300; outline-offset: 1px; }
.dot.sel { outline: 2px solid #fff; z-index: 6; }
.dot.picking { outline: 2px solid #ffb300; z-index: 6; }
/* Manual edge naming this dot is inert (see wire_length_cache.save_unsnapped_manual_dots) */
.dot.unsnapped { outline: 2px dashed var(--mk-dot-empty); outline-offset: 1px; }
/* Snapped only via the wider fallback tier (_EXTENDED_DOT_SNAP_PT) -- counts
   toward wire length, but on a lower-confidence connection worth reviewing */
.dot.loosesnap { outline: 2px dashed #ffb300; outline-offset: 1px; }
.dot .tag {
  position: absolute; left: calc(var(--dotsize)); top: -8px; font-size: 10px; color: #ff0;
  background: rgba(0,0,0,.65); padding: 0 3px; border-radius: 3px; white-space: nowrap;
  pointer-events: none; display: none;
}
#canvasWrap.showtags .dot .tag { display: block; }

.num {
  position: absolute; transform: translate(-50%, -50%); font-size: var(--numsize);
  color: #111; background: #ffd54f; border: 1px solid #a80; border-radius: 3px;
  padding: 0 2px; cursor: pointer; opacity: .92; display: none; line-height: 1.25; z-index: 3;
}
#canvasWrap.shownums .num { display: block; }
.num.sel { outline: 2px solid #fff; }
.num.counted { background: var(--mk-num-counted); border-color: #067; }
/* counted but its own anchor doesn't lie on the measured route (>15pt) — review flag */
.num.counted.detached { border: 2px dashed var(--mk-detached); box-shadow: 0 0 0 2px #ff6d0055; }
.num.excluded { background: var(--mk-num-excluded); color: #eee; text-decoration: line-through; }
.num.included { background: var(--mk-num-included); }
/* printed offset from its own line (>15pt) but attached via its ink anchor
   and countable through it (the Tier-2 anchor fallback) — an informational
   marker, NOT an error. Was `.floating` painted error-red with a false
   "tak pernah terhitung" claim (2026-11 bug fix: such a number IS counted
   when its anchor lies on the route). */
.num.offset { border: 1.5px dashed var(--mk-detached); background: #fff7ec; }
.num.counted, .num.allroutes-counted { background: var(--mk-num-counted); border-color: #067; }
.num.included, .num.allroutes-included { background: var(--mk-num-included); border-color: #3a7a00; }
.num.moved { border-color: var(--mk-dot-user); }
.num.manual { border: 1.5px solid #7c3aed; cursor: move; } /* manual Jarak drag-to-move */
/* masked out while a redrawn standardized table covers this number's own
   printed position (2026-07-22 follow-up) -- higher specificity than the
   #canvasWrap.shownums .num rule above so it wins regardless of source order */
#canvasWrap.shownums .num.hidden-by-table { display: none; }

#canvasWrap img#routeOverlay {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  pointer-events: none; display: none;
}
#canvasWrap.showroute img#routeOverlay { display: block; }

#canvasWrap img#tableRedrawOverlay {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  pointer-events: none; display: none;
}
#canvasWrap.showtableredraw img#tableRedrawOverlay { display: block; }

/* Per-box opaque masks over the whole-page overlay's own stale ghosts
   (2026-07-22 follow-up) -- same visibility gate as the overlay/patch
   layers it sits between. */
#tableRedrawStaleMaskLayer {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  pointer-events: none; display: none;
}
#canvasWrap.showtableredraw #tableRedrawStaleMaskLayer { display: block; }

#tableBoxPatchLayer {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  pointer-events: none; display: none;
}
#canvasWrap.showtableredraw #tableBoxPatchLayer { display: block; }

#edgeSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
#edgeSvg .edgeline { stroke: #111; stroke-width: 2.2; fill: none; stroke-linejoin: round; }
#edgeSvg .edgeline.unsnapped { stroke: var(--mk-dot-empty); stroke-dasharray: 6 4; }
#edgeSvg .edgeline.sel { stroke: #2979ff; stroke-width: 3.6; }
/* Wider invisible stroke under the thin visible line -- 2.2px is too thin
   to reliably click, so this is the real hit target (see patchEdge's own
   docstring in canvas.js for why the old midpoint value-badge is gone). */
#edgeSvg .edgehit { stroke: transparent; stroke-width: 14; fill: none; pointer-events: stroke; cursor: pointer; }
#edgeSvg .routeline { fill: none; stroke: var(--mk-num-counted); stroke-width: 3.4; stroke-linecap: round; stroke-linejoin: round; opacity: .9; }

#leadSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 5; }
#leadSvg .numleader { stroke: #ff8c00; stroke-width: 1.5; fill: none; opacity: .9; vector-effect: non-scaling-stroke; display: none; }
#leadSvg .numleader.moved { stroke: var(--mk-dot-user); stroke-width: 2.5; }
#leadSvg .numanchor { stroke: #ff8c00; stroke-width: 7; stroke-linecap: round; fill: none; opacity: .95; vector-effect: non-scaling-stroke; display: none; }
#leadSvg .numanchor.moved { stroke: var(--mk-dot-user); stroke-width: 9; }
#canvasWrap.shownums #leadSvg .numleader, #canvasWrap.shownums #leadSvg .numanchor { display: block; }

/* Break-point ("titik rute buntu") markers for a disconnected pair --
   always visible while a disconnected pair result is active (no toggle:
   they exist only as part of an explicit Periksa/scan result and clear
   with it). Marker dots are zero-length round-capped paths with
   non-scaling stroke (the .numanchor trick), so they stay a constant
   on-screen size at any zoom. Red = an issue, distinct from every other
   layer color (cyan route, orange leaders, magenta overlay). */
#gapSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 6; }
/* One TAPExx's route polyline, drawn when menu Material's "Lihat di Drawing"
   is picked (2026-11) -- a hollow accent outline over the wrap route. */
#gapSvg .material-highlight { fill: none; stroke: var(--accent); stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; opacity: .85; vector-effect: non-scaling-stroke; }
/* Manual tape-route drawing (2026-11, user: "tarik manual garis rute setiap
   TAPE") -- the in-progress run paints dashed with one dot per placed point. */
#gapSvg .tape-draw-line { stroke: var(--accent, #2563eb); stroke-dasharray: 8 5; opacity: .95; }
#gapSvg .tape-draw-pt { fill: var(--accent, #2563eb); stroke: #fff; stroke-width: 1; vector-effect: non-scaling-stroke; }
.material-actions .material-draw { margin-left: .35rem; }
#materialDrawHint { font-style: italic; }
/* Floating finish/cancel bar on the Drawing canvas while a manual tape-route
   draw is armed (the Material toolbar's own buttons hide on section switch). */
.tape-draw-bar {
  position: fixed; left: 50%; bottom: 1.25rem; transform: translateX(-50%);
  z-index: 60; display: flex; align-items: center; gap: .5rem;
  background: var(--panel, #fff); border: 1px solid var(--line, #d6dbe3);
  border-radius: 8px; padding: .5rem .75rem;
  box-shadow: 0 6px 24px rgba(15, 23, 42, .18);
}
.tape-draw-bar-hint { font-size: .85rem; color: var(--muted, #5b6472); margin-right: .25rem; }

/* "Mode Pratinjau Rute" (2026-07-24): every dot<->table-box link found by
   a pure Label Sirkuit TEXT match (LabelDotService.dot_table_links(), NOT
   the wire-id-based pairing #edgeSvg .routeline/#routeOverlay show) drawn
   at once, so a reviewer sees every missing circuit correlation on the
   whole drawing without inspecting one pair at a time. A matched link
   reuses this app's existing "connected" cyan; an unmatched labeled dot
   or table is ringed in BLUE (deliberately, per the user's own explicit
   request — this app otherwise uses red for a measured/broken route,
   see #gapSvg above; "no counterpart at all" is a distinct condition
   from a route that was found but is physically disconnected). */
#dotTableLinkSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 4; display: none; }
#canvasWrap.showroutepreview #dotTableLinkSvg { display: block; }
#dotTableLinkSvg .rplink { stroke: var(--mk-num-counted); stroke-width: 2; stroke-dasharray: 5 4; fill: none; opacity: .85; vector-effect: non-scaling-stroke; }
#dotTableLinkSvg .rpunmatched { stroke: #2979ff; stroke-width: 11; stroke-linecap: round; fill: none; opacity: .92; vector-effect: non-scaling-stroke; }

/* "Pratinjau Semua Rute" (2026-07-24): every already-scanned wire route
   (menu Rute's route_scan()) drawn at once -- the whole-map counterpart to
   #edgeSvg .routeline/#gapSvg's single-pair highlight, reusing the exact
   same cyan/red color language so a route looks identical whether it's
   shown alone or as part of the whole map. terhubung = solid cyan (this
   app's existing "connected/measured" color); kena_cap = amber (a route
   WAS found but rejected by the runaway cap -- distinct from both a clean
   route and a fully broken one); a disconnected pair's break points reuse
   styling. 2026-08-12: the red break-point markers were removed. */
/* Material/TAPExx route overlay (2026-11): all detected tape routes at once,
   with a "TAPExx · material · tape type" label at each route's midpoint.
   Above the all-routes layer (z-index 5) so tape ink is never hidden by the
   wire-route preview; pointer-events: none so it never steals a canvas click. */
#materialSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 5; display: none; }
#materialSvg.showmaterial { display: block; }
#materialSvg .materialrouteline { stroke: #8e24aa; stroke-width: 3.2; stroke-linecap: round; stroke-linejoin: round; fill: none; opacity: .9; vector-effect: non-scaling-stroke; }
#materialSvg .materialrouteline.materialroute-manual { stroke-dasharray: 7 4; }
#materialSvg .materialroutelabel { fill: #6a1b9a; font-size: 9px; font-weight: 700; paint-order: stroke; stroke: #fff; stroke-width: 2.6px; stroke-linejoin: round; text-anchor: middle; dominant-baseline: middle; }
#allRoutesSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 4; display: none; }
#canvasWrap.showallroutes #allRoutesSvg { display: block; }
#allRoutesSvg .allroute-line { stroke: var(--mk-num-counted); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; fill: none; opacity: .85; vector-effect: non-scaling-stroke; }
#allRoutesSvg .allroute-line.kenacap { stroke: #ffb300; stroke-dasharray: 6 4; }
/* Anchor-mismatch flag aggregated across EVERY scanned pair (not just the
   one active pair -- see .num.counted.detached above for that), same
   visual language, kept as a separate class so the two mechanisms never
   fight over the same className on a re-render. */
.num.allroutes-detached { border: 2px dashed var(--mk-detached); box-shadow: 0 0 0 2px #ff6d0055; }
/* "Pratinjau Semua Rute" number coloring (2026-07-24, user: "tambahkan
   gambaran visual length/jarak yang terdeteksi pada rute dengan warna
   biru, sama dengan fitur cek rute a ke b akan terlihat dari drawing
   angka berwarna biru dan kuning") -- the SAME blue/green/grey visual
   language a single-pair Periksa (paintPairHighlight's .num.counted/
   .included/.excluded above) already uses, so a number reads identically
   whether its route is shown alone or as part of the whole-map overlay.
   Separate class names so the two overlays never fight for the same
   className when both are active at once (a Periksa result stays live
   while this toggle is on). Default yellow (.num above) = not on any
   resolved route. */
.num.allroutes-counted { background: var(--mk-num-counted); border-color: #067; }
.num.allroutes-included { background: var(--mk-num-included); border-color: #3a7a00; }
.num.allroutes-excluded { background: var(--mk-num-excluded); color: #eee; text-decoration: line-through; border-color: #444; }

/* interaction previews (2026-07-23 UX audit): the "Tambah Garis"
   rubber-band (dot A -> cursor) and the "Tambah Kotak Tabel" drag-to-size
   rectangle. Always visible while active — no toggle class gate — and
   never a click target. */
#drawPreviewSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 8; }
#drawPreviewSvg .edgepending { stroke: var(--accent); stroke-width: 2; stroke-dasharray: 6 4; fill: none; opacity: .9; vector-effect: non-scaling-stroke; }
#drawPreviewSvg .rectdraw { stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 5 3; fill: rgba(91,157,255,.08); vector-effect: non-scaling-stroke; }

/* menu Rute: scan-result table -- issue rows tinted red so broken routes
   read at a glance. Two tabs (Terputus | Terhubung) filter the result set;
   the active tab reuses the established .btn.on visual state. */
.rute-tabs { display: flex; gap: 8px; margin-bottom: 10px; }
.labeltable.rutetable td { vertical-align: top; }
.labeltable tr.rute-issue td { background: var(--err-bg); }
/* Route-variant badge (docs/variant_registry_design.md §3, additive
   read-time key): quiet geometry label next to the pair; broken_gap keeps
   the issue accent since it names the same fact the red row does. */
.route-variant-badge {
  display: inline-block; margin-left: 6px; padding: 0 6px;
  border-radius: var(--radius-full); border: 1px solid var(--muted-border);
  background: var(--muted-bg); color: var(--muted-text);
  font-size: 10px; font-weight: 500; white-space: nowrap;
  vertical-align: baseline;
}
.route-variant-badge[data-variant="broken_gap"] {
  background: var(--err-bg); border-color: var(--err-border);
  color: var(--err-text);
}

#vectorBoxSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; display: none; }
#canvasWrap.showvector #vectorBoxSvg { display: block; }
/* Box body/handles need real pointer events (click-to-select, drag-to-move/resize)
   -- unlike every other diagnostic overlay layer, which stays pointer-events:none. */
#vectorBoxSvg .vectorbox { pointer-events: auto; cursor: pointer; }
#vectorBoxSvg .vectorbox rect { fill: rgba(199,125,255,.08); stroke: var(--mk-dot-user); stroke-width: 2; vector-effect: non-scaling-stroke; cursor: move; }
/* The box's own circuit/table name, drawn just above its top edge (2026-07-22
   follow-up) -- text-anchor start/dominant-baseline text-after-edge so its
   baseline sits right against the box's top edge regardless of font size. */
#vectorBoxSvg .vectorbox-label { fill: #6a1b9a; font-weight: 700; pointer-events: none; dominant-baseline: text-after-edge; }
/* A box enrich_table_boxes() joined to a real ConnectorBlock (node_name/
   type/brand/PN/cavities) -- green, matching this page's existing
   "auto"/confirmed-data color, vs. plain purple for a box with no joined
   circuit data (a legend table, or one format-anchoring didn't recognize). */
#vectorBoxSvg .vectorbox.hasdata rect { stroke: #3fbf5f; fill: rgba(63,191,95,.08); }
#vectorBoxSvg .vectorbox.hasdata .vectorbox-label { fill: #1e7a34; }
/* A table no dot's Label Sirkuit resolves to (2026-09-01, user: "ubah
   frame tabel merah untuk yang belum mempunyai koneksi dengan dot label.
   Termasuk frame dalam drawing") -- red, and deliberately declared AFTER
   the .hasdata rule above so it wins the same-specificity tie: whether a
   table has joined circuit data and whether a dot points at it are two
   different questions, and the unanswered one is the one a reviewer is
   here to act on. Uses the canvas-marker --unbound red (see the token's
   own block near the aux-part overlay), not the chrome --danger token:
   these frames are drawn on the rendered PDF page, not on app chrome. The
   fill tint repeats the token's channels because CSS has no way to add
   alpha to a hex custom property without color-mix(), which nothing else
   in this file relies on yet. */
#vectorBoxSvg .vectorbox.nodotlink rect { stroke: var(--unbound); fill: rgba(224,49,49,.08); }
#vectorBoxSvg .vectorbox.nodotlink .vectorbox-label { fill: var(--unbound); }
#vectorBoxSvg .vectorbox.edited rect { stroke-dasharray: 6 3; }
#vectorBoxSvg .vectorbox.sel rect { stroke-width: 3; filter: drop-shadow(0 0 2px #fff); }

/* Handles live in their OWN layer, above dots (z-index up to 6)/numbers/
   edges -- a resize handle must always be grabbable even when a dot
   marker happens to sit on or near it (confirmed real on a dense
   239-dot drawing: a handle inside #vectorBoxSvg, z-index 0, was
   unreachable under an overlapping dot). */
#vectorBoxHandleSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 7; display: none; }
#canvasWrap.showvector #vectorBoxHandleSvg { display: block; }
#vectorBoxHandleSvg .vectorbox-handle {
  fill: #fff; stroke: #111; stroke-width: 1.5; vector-effect: non-scaling-stroke;
  pointer-events: auto;
}

/* Click-to-select targets for ALL tables in "pratinjau tabel standar" mode
   (2026-07-21 follow-up, user: "Jika pratinjau tabel standar centang, maka
   semua table bisa kita klik untuk resize") -- invisible by default (no
   fill/stroke, so the redrawn tables underneath stay perfectly clean),
   just a subtle dashed hover outline to signal clickability, plus a
   slightly stronger outline for whichever one is currently selected (its
   REAL resize outline/handles live in #tableBoxEditSvg, a HIGHER z-index
   below, which always wins a drag over this plain click layer). */
#tableRedrawClickSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; display: none; }
#canvasWrap.showtableredraw #tableRedrawClickSvg { display: block; }
#tableRedrawClickSvg .tableredraw-clicktarget {
  fill: transparent; stroke: none; cursor: pointer; pointer-events: auto;
}
#tableRedrawClickSvg .tableredraw-clicktarget:hover {
  stroke: #00acc1; stroke-width: 1.5; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke;
}
#tableRedrawClickSvg .tableredraw-clicktarget.sel {
  stroke: #00acc1; stroke-width: 1; stroke-dasharray: 2 4; vector-effect: non-scaling-stroke;
}

/* "Pratinjau tabel standar" mode's own single-box resize layer (2026-07-21
   follow-up, user: "apabila pratinjau tabel, tampilkan resize table
   seperti resize bbox") -- mutually exclusive with #vectorBoxSvg/
   #vectorBoxHandleSvg above (see app.js), so gated by .showtableredraw
   instead of .showvector. Positioned against the table's own rendered
   footprint, not its raw detection bbox -- teal, distinct from the
   purple/green "tabel terdeteksi" boxes, so the two modes never look
   visually interchangeable even in a screenshot. Sits ABOVE
   #tableRedrawClickSvg (z-index 7 vs 1) so a selected box's own draggable
   outline/handles always take priority over the plain click layer
   underneath. */
#tableBoxEditSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 7; display: none; }
#canvasWrap.showtableredraw #tableBoxEditSvg { display: block; }
#tableBoxEditSvg .tableboxedit-rect {
  fill: rgba(0,180,180,.06); stroke: #00acc1; stroke-width: 2; stroke-dasharray: 6 3;
  vector-effect: non-scaling-stroke; cursor: move; pointer-events: auto;
}
#tableBoxEditSvg .tableboxedit-handle {
  fill: #fff; stroke: #00acc1; stroke-width: 1.5; vector-effect: non-scaling-stroke;
  pointer-events: auto;
}

/* ---------------- inspector editor cards ---------------- */

.editor-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--sp-3); font-size: var(--fs0);
  box-shadow: var(--elev-1);
}
.editor-card h4 { display: flex; align-items: baseline; gap: var(--sp-2); }
.editor-id { font-weight: 400; color: var(--ink-2); font-size: var(--fs-1); }
.editor-meta { font-size: var(--fs-1); color: var(--ink-2); word-break: break-word; margin-bottom: var(--sp-2); line-height: 1.5; }

/* Supporting (aux) table editor rows (2026-11): one part per block, fields
   stacked like the main table-box editor. */
.aux-edit-row { padding: var(--sp-2) 0; border-top: 1px solid var(--line-soft); }
.aux-edit-row:first-child { border-top: none; padding-top: 0; }
.aux-edit-row .field-label { margin-top: var(--sp-2); }
.aux-edit-row .aux-ed { width: 100%; box-sizing: border-box; }
/* The POS input inside the side-panel supporting-table editor: full-width
   like the other stacked fields, overriding the narrower menu-Tabel column
   width (.tbox-pos-input's element-qualified rule). */
.aux-edit-row .aux-pos-slot input.tbox-pos-input { width: 100%; box-sizing: border-box; }
.aux-edit-row .aux-update { margin-top: var(--sp-2); }
.edited-mark { color: #6a1b9a; font-weight: 400; font-size: var(--fs-1); }

/* Supplier-catalogue verification badge (2026-07-31). `.muted` is the
   COMMON case, not a failure state -- only 38% of real, correct connector
   PNs exist in the catalogue at all -- so it stays deliberately quiet and
   borrows no warning colour. Only a genuine catalogued contradiction
   (`brand_mismatch`) gets `.warn`. Nothing here ever uses `--danger`. */
.vbadge {
  display: inline-block; margin-left: 8px; padding: 1px 7px;
  border-radius: var(--radius-full); border: 1px solid var(--line);
  font-size: 10.5px; font-weight: 500; white-space: nowrap;
  vertical-align: baseline; cursor: help;
}
.vbadge.ok { background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); }
.vbadge.warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
.vbadge.muted { background: var(--muted-bg); border-color: var(--muted-border); color: var(--muted-text); }
/* Empty states teach: they say what this region is for and what to do
   next, in quiet secondary text, never a bare blank. */
.editor-empty { font-size: var(--fs0); color: var(--ink-2); padding: var(--sp-1) 2px; line-height: 1.5; }

/* Aux parts already bound to a box's own POS (2026-08-19, C3: badge
   auto/manual). Clicking a row jumps to the editable "custom tabel
   pendukung" list in menu Tabel -> Pendukung (2026-08-28); a manual
   entry's own "Lepas" stays a separate, non-navigating action. */
.hwparts-list { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
.hwpart-row {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding: 5px 8px; border: 1px solid var(--line-soft); border-radius: var(--radius);
  background: var(--surface-2); font-size: var(--fs0); cursor: pointer;
}
.hwpart-row:hover { border-color: var(--line-strong); background: var(--bg-3); }
.hwpart-row .vbadge { margin-left: 0; cursor: default; flex-shrink: 0; }
.hwpart-kind { font-weight: 500; }
.hwpart-pn { color: var(--body); }
.hwpart-size { color: var(--ink-2); font-size: var(--fs-1); }
.hwpart-unbind { margin-left: auto; font-size: var(--fs-1); padding: 2px 6px; flex-shrink: 0; }
.field-label { font-size: var(--fs-1); color: var(--ink-2); margin-top: var(--sp-2); display: block; }
.row { display: flex; gap: 6px; margin-top: 8px; }
.row input { flex: 1; min-width: 0; }
.row button { flex: 1; }
.row.shape-row .shape-btn { flex: 1; font-size: 11px; padding: 7px 4px; text-align: center; }
.shape-btn.on { background: var(--active-bg); border-color: var(--active-border); color: var(--active-text); }

/* ------- wire-length tolerance sliders (menu Drawing, 2026-07-28) ------- */
/* Range inputs are the one control here the shared .btn/.chk vocabulary
   doesn't cover, so they get their own minimal styling in the same token
   language (accent thumb on a recessed track) rather than the browser
   default, which reads as unstyled next to everything else in this panel. */
/* Lives in menu Rute (a full-width app-section), so the sliders need their
   own readable measure — deliberately NOT in menu Drawing's side panel,
   which auto-collapses to 0 width whenever nothing is selected (the
   panel-follows-selection behavior) and would have made this unreachable. */
#measurebox { max-width: 520px; }
.slider-row { margin-top: 12px; }
.slider-row:first-of-type { margin-top: 4px; }
.slider-row label {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2);
  font-size: var(--fs-1); color: var(--body);
}
.slider-value {
  font-size: var(--fs0); font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.slider-row input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; margin: 8px 0 2px; padding: 0;
  background: transparent; cursor: pointer;
}
.slider-row input[type="range"]::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px; background: var(--bg-3); border: 1px solid var(--line);
}
.slider-row input[type="range"]::-moz-range-track {
  height: 4px; border-radius: 2px; background: var(--bg-3); border: 1px solid var(--line);
}
.slider-row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 15px; height: 15px; margin-top: -6px;
  border-radius: 50%; background: var(--accent); border: none;
}
.slider-row input[type="range"]::-moz-range-thumb {
  width: 15px; height: 15px;
  border-radius: 50%; background: var(--accent); border: none;
}
.slider-row input[type="range"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.slider-row .field-hint { margin: 2px 0 0; font-size: var(--fs-1); }
/* This card is not the narrow side panel the shared `.row button {flex:1}`
   rule was written for -- a single full-width reset button reads as a
   primary action it isn't. */
#measurebox .row { justify-content: flex-start; }
#measurebox .row button { flex: 0 0 auto; }
#measureSettingsNote { margin-top: var(--sp-2); }
#measureSettingsNote.changed { color: var(--accent-text); }

/* ---------------- confirmation modal ---------------- */
/* The app's ONE modal (2026-07-23, user: "buatkan modal card untuk
   confirmation") — used by menu Drawing's unified toolbar delete button.
   Esc / backdrop click / Batal all cancel. */
.modal-backdrop {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(23, 28, 38, .5);
  display: flex; align-items: center; justify-content: center;
}
.modal-backdrop[hidden] { display: none; } /* same author-vs-UA [hidden] trap as .btn[hidden] */
.modal-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg);
  box-shadow: var(--elev-1), 0 16px 48px rgba(0, 0, 0, .28);
  padding: var(--sp-4); width: min(420px, calc(100vw - 40px));
}
.modal-card h4 { margin: 0 0 var(--sp-2); font-size: var(--fs3); font-weight: 650; letter-spacing: -0.01em; }
.modal-card p { margin: 0; font-size: var(--fs0); color: var(--body); line-height: 1.55; word-break: break-word; }
.modal-actions { justify-content: flex-end; margin-top: var(--sp-4); }
.modal-actions .btn { flex: 0 0 auto; }

/* Toolbar delete button: grey while disabled (nothing deletable selected),
   danger-tinted once an actual deletable object is selected. */
#deleteObjectBtn:not(:disabled) {
  background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger-text);
}

/* ---------------- pair inspector ---------------- */

#pairbox .row { margin-top: 6px; }
#pairInfo {
  font-size: var(--fs0); margin-top: 10px; color: var(--body); white-space: pre-line; line-height: 1.55;
  font-variant-numeric: tabular-nums;
}

/* ---------------- menu Sirkuit/Wire lists ---------------- */

.tabsummary { font-size: var(--fs-1); color: var(--ink-2); margin-bottom: 6px; }

/* ---------------- pagination (menu Tabel/Sirkuit/Wire lists) ---------- */

.pagination {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  flex-wrap: wrap; margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--line-soft);
}
.pagination:empty { display: none; border-top: none; margin-top: 0; }
.page-info { font-size: var(--fs-1); color: var(--ink-2); font-variant-numeric: tabular-nums; }
.page-nav { display: flex; align-items: center; gap: var(--sp-2); }
.page-nav .btn { padding: 5px 10px; font-size: var(--fs0); }
.page-label { font-size: var(--fs0); color: var(--body); white-space: nowrap; }

/* Running item numbers on every list menu (2026-07-23, user: "tambahkan
   nomor setiap item pada menu, untuk mempermudah user mengingat titik
   object daripada harus mengingat nama nama object") — positional within
   the current filter/sort, continuing across pages. */
.num-cell, .num-th { color: var(--ink-2); font-variant-numeric: tabular-nums; width: 40px; }
td.num-cell { text-align: right; padding-right: 8px; }
.item-num { color: var(--ink-2); font-variant-numeric: tabular-nums; margin-right: 2px; }

/* Dense data tables: quiet header band, hairline row separators, hover
   and accent-tinted current row — scannable at a glance. */
.labeltable { width: 100%; border-collapse: collapse; font-size: var(--fs0); font-variant-numeric: tabular-nums; }
.labeltable tr { cursor: pointer; }
.labeltable tbody tr:hover { background: var(--surface-2); }
.labeltable td { padding: 7px 10px; border-bottom: 1px solid var(--line-soft); }
.labeltable td.dim { color: var(--ink-2); font-size: 10.5px; text-align: right; }
.labeltable thead th {
  color: var(--ink-2); font-weight: 600; font-size: 10.5px;
  text-transform: uppercase; letter-spacing: .06em; text-align: left;
  padding: 7px 10px; border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}

/* menu ICWC (2026-10, tabbed tabular rework): ONE tab bar (the same
   .rute-tabs + .btn.on vocabulary menu Rute/Sirkuit use) picks the active
   workbook sheet, and that one sheet renders as a dense, evenly-aligned
   .labeltable — a leading "No" row counter, the workbook's own header,
   up to the render cap of real rows, then a "+N baris lagi" accounting
   note. The per-cell Edit affordance (a trailing action column) opens the
   row's source-mapped cells as .cell-inputs, saved back to the connector
   source data. */
.icwc-tabs { margin-bottom: var(--sp-2); }
.icwc-sheet-meta {
  display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: var(--sp-2);
}
.icwc-sheet-desc { color: var(--ink-2); font-size: var(--fs0); line-height: 1.5; }
.icwc-preview-scroll { max-height: 560px; }
.icwc-preview { table-layout: auto; width: max-content; min-width: 100%; }
.icwc-preview thead th { position: static; }
.icwc-preview td, .icwc-preview thead th { padding: 4px 8px; text-align: left; }
.icwc-preview td { white-space: nowrap; }
.icwc-preview .num-td { color: var(--ink-2); font-variant-numeric: tabular-nums; }
.icwc-act-th, .icwc-act-td { width: 1%; white-space: nowrap; }
.icwc-edit-btn { padding: 1px 6px; font-size: var(--fs-1); }
.icwc-cell-input { min-width: 90px; }
tr.icwc-editing td { background: var(--accent-softer); }
/* A source-mapped ICWC cell advertises that it's editable (2026-10, user:
   "editable ICWC not implemented" — it was, but hidden behind a tiny Edit
   button): accent-tinted, pointer cursor, pencil-style affordance on the
   cell itself. Clicking it opens the row editor. */
.icwc-editable-cell { cursor: pointer; background: var(--accent-softer); color: var(--accent-text, var(--accent)); font-weight: 500; }
.icwc-editable-cell:hover { background: var(--accent-bg, var(--accent-softer)); outline: 1px solid var(--accent); outline-offset: -1px; }
.icwc-preview-more { color: var(--muted); font-size: var(--fs-1); padding: 6px 8px; }

.wiretable { cursor: default; }
.wiretable thead th {
  position: sticky; top: 0; background: var(--surface-2); color: var(--ink-2);
  font-weight: 600; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em;
  text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line);
}
.wiretable tbody tr:hover { background: var(--surface-2); }
.wiretable td { white-space: nowrap; }

/* Teaching empty state: what this region is for + what to do next. */
.empty-note { font-size: var(--fs0); color: var(--ink-2); margin: var(--sp-1) 0 var(--sp-3); line-height: 1.5; }
/* menu ICWC (2026-09-19) toggles `hidden` on .empty-note at runtime --
   plain block element, but the same author-vs-UA `[hidden]` specificity
   trap .btn[hidden]/.topbar-search[hidden] document, so keep an explicit
   rule rather than relying on the UA stylesheet. */
.empty-note[hidden] { display: none; }
.review-list { list-style: none; margin: 0 0 var(--sp-3); padding: 0; font-size: var(--fs-1); }
.review-list li { padding: 4px 0; border-bottom: 1px solid var(--line-soft); color: var(--body); }

/* ---------------- table box editor (menu Drawing) + menu Tabel list ---- */

#tableBoxDataFields input { font-size: var(--fs-1); margin-bottom: 2px; }

.table-scroll { max-height: 220px; overflow: auto; margin-top: 6px; border: 1px solid var(--line-soft); border-radius: var(--radius); }
.section-table-scroll { max-height: none; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
/* Bounded, internally-scrolling list area for the paginated menus
   (2026-07-23, user: "tambahkan scroll biar user tidak scroll halaman ke
   bawah untuk next halaman") -- caps the list to what fits the viewport
   (minus topbar/statusbar/section head/toolbar/pagination chrome) so the
   Sebelumnya/Berikutnya controls stay visible without scrolling the page;
   the list itself scrolls inside. The min-height floor keeps a small
   window from collapsing the list to nothing. */
.list-viewport { max-height: calc(100vh - 340px); min-height: 240px; overflow-y: auto; }
.boxtable { width: 100%; border-collapse: collapse; font-size: var(--fs-1); font-variant-numeric: tabular-nums; }
.boxtable th, .boxtable td { padding: 3px 6px; border-bottom: 1px solid var(--line-soft); text-align: left; white-space: nowrap; }
.boxtable th { position: sticky; top: 0; background: var(--surface-2); color: var(--ink-2); font-weight: 600; letter-spacing: .04em; }

/* Editable cavity table + Harness Option matrix (2026-07-23) */
.cell-input {
  width: 100%; min-width: 52px; box-sizing: border-box;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 4px;
  padding: 2px 5px; font-size: var(--fs-1); font-family: inherit;
  font-variant-numeric: tabular-nums;
}
.cell-input:focus { border-color: var(--accent); outline: none; box-shadow: var(--focus-ring); }
.cavtable td { padding: 2px 3px; }
.cavtable .corr-cell { color: var(--body); font-size: 10px; }
.cavity-editor-actions { margin-top: var(--sp-2); }
.editable-hint { color: var(--ink-2); font-weight: 400; text-transform: none; letter-spacing: 0; }
.cav-del-row, .harness-del-row, .harness-del-opt { color: var(--danger); padding: 0 6px; }
/* trash icons (replaced the old "×" glyph, too similar to the x mark toggle) */
.cav-del-row svg, .harness-del-row svg, .harness-del-opt svg { display: block; }

/* One meta row above the Cav table: LABEL SIRKUIT | TYPE | SUPPLIER NAME |
   SUPPLIER PN [Update] (2026-07-23). Wraps on narrow panels. */
.meta-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 8px; margin-bottom: 6px;
}
.meta-row .meta-field .field-label { margin-top: 0; }
.meta-row input { width: 100%; box-sizing: border-box; }
/* Format/variant override picker (2026-10): a .meta-field like the metadata
   inputs above it, but its control is a <select> (also .cell-input) and it
   carries a quiet .field-hint explaining the detected-vs-overridden state. */
.variant-field select { width: 100%; box-sizing: border-box; }
.variant-field .field-hint { margin: 4px 0 0; }
.pn-update { display: flex; gap: 4px; align-items: stretch; }
.pn-update input { flex: 1 1 auto; min-width: 0; }
.pn-update .btn { flex: 0 0 auto; padding: 4px 8px; font-size: 11px; }

/* Blanking-plug suggestion under an empty cavity's Plug cell (2026-08-06).
   Sits BELOW the cell's own input rather than replacing it, so the column
   stays hand-editable and the suggestion reads as an offer, not a value.
   Uses --warn (an unanswered question, needs a decision), not --ok/--danger
   — neither approving nor declining is the "correct" outcome. */
.plug-cell { min-width: 96px; }
.plug-suggest {
  display: flex; flex-wrap: wrap; align-items: center; gap: 3px;
  margin-top: 3px; font-size: 10px;
}
.plug-suggest-pn {
  color: var(--warn-text); background: var(--warn-bg);
  border: 1px solid var(--warn-border); border-radius: 4px;
  padding: 0 4px; font-variant-numeric: tabular-nums;
}
.plug-suggest .btn.text { padding: 0 4px; font-size: 10px; }
.plug-suggest .plug-approve { color: var(--ok-text); }
.plug-suggest .plug-decline { color: var(--danger-text); }
.plug-suggest.declined .plug-suggest-pn {
  color: var(--ink-2); background: transparent; border-color: var(--line-soft);
}

/* Type cell dropdown (energy_level_lookup catalog + "Lainnya…" custom) */
select.cell-input {
  appearance: auto; min-width: 76px;
  background: var(--surface); color: var(--ink);
}
/* Harness Option matrix (2026-10, "option tidak pas"): fixed layout so the
   No/PN/Name/option columns keep sane widths instead of Harness Name
   ballooning and squeezing every option column into a sliver on the right.
   PN+Name are capped; each option column is compact & centered (its cell is
   only an x/- toggle), and the matrix scrolls horizontally if many options
   are added rather than crushing the identity columns. */
/* Harness Option matrix (2026-10, "tidak simetris"): auto layout with the
   identity columns pinned to their natural content width (PN/Name read as
   narrow text fields) and every option column sharing the remaining width
   evenly — so the grid is symmetric instead of narrow identity + one huge
   option sprawl. */
.harnesstable { table-layout: auto; width: 100%; }
.harnesstable .num-th, .harnesstable .num-cell { width: 44px; }
.harnesstable .pn-th, .harnesstable td.harness-pn-cell { width: 200px; }
.harnesstable .name-th, .harnesstable td.harness-name-cell { width: 280px; }
.harnesstable .rowdel-th, .harnesstable td.harness-rowdel-cell { width: 52px; }
.harnesstable .harness-opt-th { text-align: center; white-space: nowrap; }
.harnesstable .cell-input { min-width: 0; width: 100%; box-sizing: border-box; }
.harnesstable .harness-mark-cell { text-align: center; padding-left: 6px; padding-right: 6px; }
/* Compact x/- toggle centered in the option column — not a full-width
   button (2026-10, "malah jadi 1 button gede"). */
.harness-mark { width: 44px; min-width: 44px; justify-content: center; font-variant-numeric: tabular-nums; }
.harness-mark.on { background: var(--active-bg); border-color: var(--active-border); color: var(--active-text); }
#harnessNewOptionInput {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 6px 10px; font-size: var(--fs0); max-width: 200px;
}
#harnessNewOptionInput:focus { border-color: var(--accent); outline: none; box-shadow: var(--focus-ring); }

/* Menu Tabel's browsable list -- full section width, so a grid makes
   better use of the space than the old narrow-sidebar single column. */
.tbox-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--sp-2); margin-bottom: 10px; }
.tbox-row {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; cursor: pointer;
  background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 8px 10px; font-size: var(--fs0);
  box-shadow: var(--elev-1);
  transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.tbox-row:hover { background: var(--surface-2); border-color: var(--line-strong); }
.tbox-row.hasdata { border-left: 3px solid var(--ok); }
/* Same "belum punya koneksi dot" flag as the canvas frame, on the list
   row -- after .hasdata so the red rail wins on a box that has both.
   Uses --err (unbound = a route/status fact), not the destructive
   --danger. */
.tbox-row.nodotlink { border-color: var(--err-border); border-left: 3px solid var(--err); background: var(--err-bg); }
.tbox-row.nodotlink:hover { border-color: var(--err); }
/* The "diubah user" purple keeps its meaning (it matches the manual/
   edited marker vocabulary) but uses the DARK variant this file already
   carries for `.vectorbox-label`: the marker purple #c77dff is tuned to
   be a stroke on a rendered PDF page and measures only 2.69:1 as 11px
   text on this page's own white chrome, well under WCAG AA's 4.5:1.
   #6a1b9a is 9.39:1 on white and 8.05:1 on the red row tint, and adds
   no new colour to the file (2026-09-01 audit). */
.tbox-row.edited .tbox-meta { color: #6a1b9a; }
.tbox-row.active { border-color: var(--accent); background: var(--accent-bg); }
.tbox-title { font-weight: 500; }
.tbox-meta { font-size: 10.5px; color: var(--ink-2); }

/* ---------------- menu Tabel/Sirkuit: list + inline detail panel ------
   A row click shows its data HERE, in the same menu, instead of jumping
   to menu Drawing (2026-07-21 follow-up -- user: "sebelum setiap klik
   direct ke drawing... tambah button untuk lihat di drawing"). Editing
   the rectangle (drag-resize) still needs the canvas, so that -- and every
   other existing editor action -- stays reachable via the "Lihat di
   Drawing" button, which reuses the unchanged menu-Drawing editor as-is. */
.section-split { display: flex; align-items: flex-start; gap: 16px; min-width: 0; }
/* "Pendukung" (2026-08-27) reverted to the same list+detail split every
   other category uses -- its own wide 9-column table now scrolls inside
   .section-split-detail's .table-scroll exactly like a cavity editor
   table does for any other kind of table, rather than needing a
   dedicated full-width carve-out. */
.section-split-list { flex: 1 1 380px; min-width: 0; }
.section-split-detail {
  flex: 1 1 420px; min-width: 0; align-self: stretch;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--sp-4); font-size: var(--fs0);
  box-shadow: var(--elev-1);
  /* a long detail (big cavity table) scrolls INSIDE, same bound as the
     .list-viewport next to it — the page itself stays put */
  max-height: calc(100vh - 280px); overflow-y: auto;
}
.labeltable tr.active { background: var(--accent-bg); }

.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
.detail-head h4 { margin: 0; font-size: var(--fs3); word-break: break-word; }
.detail-fields { margin: 10px 0; }
.detail-row { display: flex; gap: var(--sp-2); padding: 4px 0; border-bottom: 1px solid var(--line-soft); font-size: var(--fs0); }
.detail-label { flex: 0 0 150px; color: var(--ink-2); }
.detail-value { flex: 1; min-width: 0; word-break: break-word; }
.detail-value .btn.text { padding: 2px 0; font-size: var(--fs0); }

/* menu Sirkuit's "Tabel Terkait" -- direct dot<->table links, distinct
   from the wire-to-OTHER-circuit correlation tables nearby. */
.chip-list { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 10px; }
.chip-list .btn { background: var(--surface-2); border-color: var(--line); }

@media (max-width: 860px) {
  .section-split { flex-direction: column; }
  .detail-label { flex: 0 0 120px; }
}

/* ---------------- responsive ---------------- */
/* Narrow viewport (tablet/phone, or a small split-screen window): the left
   nav becomes a horizontal, scrollable strip instead of a fixed-width
   sidebar, and menu Drawing's canvas+side-panel stack vertically instead
   of sitting side by side -- everything else (section content, tables,
   editor cards) is already fluid width, so no further change is needed. */
@media (max-width: 860px) {
  #appBody { flex-direction: column; }
  #appNav, #appNav.nav-collapsed {
    width: auto; flex: 0 0 auto;
    flex-direction: row; overflow-x: auto; overflow-y: hidden;
    border-right: none; border-bottom: 1px solid var(--border);
    padding: 6px 10px;
  }
  .nav-collapse-btn { display: none; } /* the rail state is desktop-only; the narrow layout already goes horizontal */
  .navbtn { white-space: nowrap; }
  #appNav.nav-collapsed .nav-label,
  #appNav.nav-collapsed .navgroup-label { display: block; }
  #appNav.nav-collapsed .navbtn { width: auto; min-height: 0; padding: 8px 12px; }
  #appNav.nav-collapsed .navgroup { margin-bottom: 0; padding-bottom: 0; border-bottom: none; flex-direction: row; }
  .drawing-body { flex-direction: column; }
  #viewport { min-height: 45vh; }
  #drawingSidePanel { width: auto; flex: 0 0 auto; border-left: none; border-top: 1px solid var(--border); max-height: 50vh; }
  .topbar-row { row-gap: 8px; }
  .topbar-search { flex: 1 1 100%; order: 5; }
  .berkas-form select { min-width: 0; flex: 1 1 100%; }
}

/* A printed position the drawing records but that never bound to a dot
   (see label_dot_service_core._dotless_clips) -- flagged rather than hidden,
   since its data does still reach the ICWC. */
.warn-text { color: var(--warn); }

/* A Cav filled by the splice L/R suggestion, not read from the drawing.
   Marked because the suggestion is right only ~2 times in 3 (measured), so
   saving it must be a deliberate act rather than something that slips
   through with an unrelated edit. Cleared on the next render, which reads
   whatever was actually stored. */
.cell-input.cav-suggested {
  border-color: var(--warn);
  background: var(--warn-bg);
}

/* POS input on an aux-part row (2026-08-24) -- narrow, since a POS code is
   short; sits beside the row's own buttons rather than on its own line. */
/* Width is the ONLY thing specific to this control: it sits inline in a
   table row, so the shared `input[type="text"]` rule's width:100% is the
   one value that is wrong for it -- surface, border, radius and padding
   all deliberately fall through so it matches every other input.
   The selector is qualified with the element ON PURPOSE: that shared rule
   is (0,1,1) and a bare class is (0,1,0), so an unqualified
   `.tbox-pos-input` LOSES to it and the width silently never applies
   (measured -- the control rendered full-width). Matching its specificity
   and coming later in the file is what makes this win. */
input.tbox-pos-input { width: 7.5rem; }

/* Editable columns on the "Pendukung" table (2026-08-27, user: "Pada tabel
   pendukung, tambahkan editable column"). The cells carry values read off
   the page -- a misread PN, a qty the parser never found -- and what is
   typed here reaches the real ICWC Items sheet, so they are ordinary
   inputs, not a read-only display that happens to look editable.
   Same element-qualified selector as .tbox-pos-input above and for the
   same measured reason: the shared `input[type="text"]` rule is (0,1,1),
   so a bare class loses to it and the width never applies. */
input.aux-cell { width: 9rem; min-width: 4rem; }
/* Per-column widths sized to what each column actually holds, so the whole
   row -- including its buttons -- fits without a horizontal scroll at a
   normal window width. Measured on a real drawing: at one uniform width
   the row needed ~250px more than the viewport gave it. The element+
   attribute selector is (0,2,1) and beats the (0,1,1) rule above. */
input.aux-cell[data-field="qty"] { width: 3.5rem; }
input.aux-cell[data-field="kind"],
input.aux-cell[data-field="size"] { width: 6rem; }
input.aux-cell[data-field="description"] { width: 12rem; }
input.aux-cell[data-field="part_number"],
input.aux-cell[data-field="supplier_pn"],
input.aux-cell[data-field="supplier_name"] { width: 8rem; }
/* A column a human actually corrected, so a reviewer can tell at a glance
   which values are the parser's reading and which are a decision. Green =
   "committed data", the same meaning .btn.save and the manual-binding
   .vbadge.ok already carry on this page -- never the accent, which is
   reserved for interactive chrome. */
input.aux-cell.edited { border-color: var(--ok-border); background: var(--ok-bg); }
/* Buttons and the bind select sit inline on their own row; the shared
   .boxtable rule already prevents wrapping, so these only need spacing. */
.auxtable td.aux-actions { display: flex; gap: 4px; }

/* "Tabel Pendukung" overlay (2026-08-26) — the printed Part Number | Qty |
   Supplier part Number | ... sub-tables, boxed at their own rect with a line
   to the POS their parts reach on the Items sheet. Amber, distinct from the
   table-box purple/green and from the cyan route layers: this is a
   different KIND of object, not another table box.

   These three are canvas-marker colours: they are drawn ON TOP of the
   rendered PDF page, not on this app's chrome, which is why they sit
   outside the DESIGN.md chrome palette on purpose. They are TOKENS rather
   than literals so the legend swatches in index.html can reference the
   very same values instead of repeating them -- a legend that drifts from
   the markers it explains is worse than no legend (2026-08-27). */
:root {
  --aux-auto: #f08c00;   /* POS chosen by the proximity window */
  --aux-manual: #2f9e44; /* POS confirmed by a human -- same "committed
                            data" green as .btn.save / .vbadge.ok */
  /* Reaches nothing it should be attached to; needs review. Shared by
     the aux-part overlay (no POS) and the table frame (no dot with a
     matching Label Sirkuit, 2026-09-01) -- one "unbound" red across
     both, rather than a second red for the same condition. */
  --unbound: #e03131;
  --aux-unbound: var(--unbound); /* reaches no POS at all */
}

/* Canvas markers, continued. Same rule as --aux-auto/--unbound above:
   drawn ON TOP of the rendered PDF page, so deliberately outside the
   DESIGN.md chrome palette -- and tokens rather than literals so the
   legend swatches in index.html reference the very values they explain
   instead of repeating them (2026-09-02; the four already here proved
   the pattern, these finish the job). */
:root {
  /* The SEMANTIC markers alias the Fase-1 status tokens: an auto
     (resolved) dot IS the "ok/terhubung" green, an empty (unlabeled) dot
     IS the "err/needs action" red, a detached anchor IS the "warn/review"
     amber, an excluded number IS the "muted" gray — one vocabulary
     across canvas, legend, and panels. The exact hues stay the marker
     colors the legend always showed; only their NAMES are now shared. */
  --mk-dot-auto: #3fbf5f; /* dot resolved automatically ≈ --ok */
  --mk-dot-override: #4da3ff; /* label changed by a user */
  --mk-dot-user: #c77dff; /* dot added by a user; also the table box with no circuit data */
  --mk-dot-empty: #ff5252; /* dot with no label ≈ --err */
  --mk-num: #ffd54f; /* printed dimension number */
  --mk-num-counted: #00e5ff; /* number counted for the active pair; also the measured route */
  --mk-num-included: #7CFC00; /* number forced into the total */
  --mk-num-excluded: #777777; /* number excluded from the total ≈ --muted */
  --mk-detached: #ff6d00; /* anchor sits off its route -- needs review ≈ --warn */
  --mk-edge-manual: #111111; /* line drawn by a user */
  --mk-route: #ff00c8; /* detected route overlay (magenta: appears nowhere else on a drawing) */
}
#auxPartSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 4; }
/* The supporting (aux) tables are printed ON the drawing itself, so their
   click overlay stays available even when "tabel terdeteksi" is OFF (2026-11,
   user: "kotak pendukung unclickable" — the layer was display:none unless
   the vector-box toggle was on, so the visible heatshrink/Type-Code boxes
   could not be clicked). With the toggle off the frame/link fade to a
   hairline (the printed table already shows through) but the rect keeps
   pointer-events:auto, so clicking it still opens menu Tabel → Pendukung;
   turning the toggle on restores the full frame + link + label. */
#auxPartSvg .auxbox { fill: none; stroke: var(--aux-auto); stroke-width: 2; vector-effect: non-scaling-stroke; pointer-events: all; cursor: pointer; }
/* Supporting-table click target (2026-11, user: hover shows no pointer
   cursor): the printed heatshrink/Type-Code box is always visible on the
   PDF, so its rect must STAY clickable even when the vector overlay is off.
   fill:transparent (not fill:none) keeps the whole rect a hit area — with
   fill:none only the (hidden) stroke would be hoverable, so the cursor
   never turned into a pointer and the box felt unclickable. The frame/link
   stay visually hidden until hover; pointer-events:all makes even the
   transparent interior receive hover/click. */
#canvasWrap:not(.showvector) #auxPartSvg .auxbox,
#canvasWrap:not(.showvector) #auxPartSvg .auxbox.manual,
#canvasWrap:not(.showvector) #auxPartSvg .auxbox.unbound { stroke: transparent; stroke-dasharray: none; fill: transparent; pointer-events: all; cursor: pointer; }
#canvasWrap:not(.showvector) #auxPartSvg .auxbox:hover { stroke: var(--aux-auto); fill: rgba(199,125,255,.10); cursor: pointer; }
#canvasWrap:not(.showvector) #auxPartSvg .auxlink,
#canvasWrap:not(.showvector) #auxPartSvg .auxlabel { display: none; }
/* A manual binding is a human decision and reads solid; the automatic
   proximity-window one is a guess and reads dashed. */
#auxPartSvg .auxbox.manual { stroke: var(--aux-manual); }
#auxPartSvg .auxbox.unbound { stroke: var(--aux-unbound); stroke-dasharray: 6 4; }
#auxPartSvg .auxlink { stroke: var(--aux-auto); stroke-width: 2; stroke-dasharray: 5 4; fill: none; opacity: .85; vector-effect: non-scaling-stroke; }
#auxPartSvg .auxlink.manual { stroke: var(--aux-manual); stroke-dasharray: none; }
#auxPartSvg .auxlabel { fill: var(--aux-auto); font-size: 10px; font-weight: 600; }

/* ---- dotless CLIP/GROM labels (click-to-edit SPN, 2026-11) ---- */
/* The whole <g> is the hit area; pointer-events:none on the layer + all on
   the group mirrors #auxPartSvg so the map under it stays pannable. The tag
   reads as a subtle hardware-chip (a small ring + the label text) so it's
   discoverable but doesn't fight the printed text it sits on. */
#dotlessSvg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 4; }
#dotlessSvg .dotlesshw { cursor: pointer; }
/* A hollow frame around the printed label (the label text itself is on the
   drawing underneath, so the box must NOT fill over it -- fill:transparent
   keeps the interior a live hit area while letting the printed text show
   through, like a table box). The frame is the clickable area. */
#dotlessSvg .dotlesshw .hit { fill: transparent; stroke: var(--warn); stroke-width: 1.6; vector-effect: non-scaling-stroke; pointer-events: all; }
#dotlessSvg .dotlesshw:hover .hit { fill: rgba(191,90,0,.12); stroke: var(--warn-text); }
#dotlessSvg .dotlesshw.edited .hit { stroke: var(--ok); }
#dotlessSvg .dotlesshw.edited:hover .hit { fill: rgba(30,122,52,.12); }

/* ---------------- tooltip (styled pop-up, replaces bare native title) ---- */
/* Standardized rich tooltip popover across the whole application toolbar.
   Keyboard users get the same hint on focus; native title attributes are
   dropped to prevent jarring OS-level tooltip clashes. */
.has-tt { position: relative; }
.tt {
  position: absolute; top: calc(100% + 8px); left: 50%;
  transform: translateX(-50%) translateY(-3px); z-index: var(--z-overlay);
  min-width: 140px; max-width: 260px; width: max-content;
  background: rgba(18, 22, 31, 0.96); color: #f8fafc;
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius); padding: 7px 10px;
  font-size: var(--fs-1); font-weight: 400; line-height: 1.4;
  white-space: normal; text-align: left;
  box-shadow: 0 10px 25px -4px rgba(0, 0, 0, 0.45), 0 4px 10px -2px rgba(0, 0, 0, 0.25);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease;
}

/* Upward-pointing arrow caret */
.tt::before {
  content: ''; position: absolute; bottom: 100%; left: 50%;
  transform: translateX(-50%);
  border-width: 5px; border-style: solid;
  border-color: transparent transparent rgba(18, 22, 31, 0.96) transparent;
}
.tt::after {
  content: ''; position: absolute; bottom: 100%; left: 50%;
  transform: translateX(-50%);
  border-width: 6px; border-style: solid;
  border-color: transparent transparent rgba(255, 255, 255, 0.12) transparent;
  z-index: -1;
}

.tt-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 3px;
}
.tt-title {
  display: inline-block; font-weight: 650; font-size: 11.5px;
  color: #ffffff; letter-spacing: -0.01em; line-height: 1.25;
}
.tt-kbd {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 10px; font-weight: 600; color: rgba(255, 255, 255, 0.9);
  background: rgba(255, 255, 255, 0.16);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 3px; padding: 0 4px; min-width: 16px; height: 16px;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.3);
}
.tt-desc {
  display: block; font-size: 11px; color: rgba(255, 255, 255, 0.75);
  line-height: 1.35;
}

.has-tt:hover > .tt,
.has-tt:focus-visible > .tt,
.has-tt:focus-within > .tt {
  opacity: 1; visibility: visible;
  transform: translateX(-50%) translateY(0);
}

/* Tooltips near right edge (e.g. Help button) align to right edge */
.tt-align-right .tt {
  left: auto; right: 0; transform: translateY(-3px);
}
.tt-align-right .tt::before,
.tt-align-right .tt::after {
  left: auto; right: 10px; transform: none;
}
.tt-align-right:hover > .tt,
.tt-align-right:focus-visible > .tt,
.tt-align-right:focus-within > .tt {
  transform: translateY(0);
}

/* Tooltips near left edge align to left edge */
.tt-align-left .tt {
  left: 0; transform: translateY(-3px);
}
.tt-align-left .tt::before,
.tt-align-left .tt::after {
  left: 10px; transform: none;
}
.tt-align-left:hover > .tt,
.tt-align-left:focus-visible > .tt,
.tt-align-left:focus-within > .tt {
  transform: translateY(0);
}

/* menu Material (2026-11) -- the TAPExx table.

   Nine columns, so the shared fixed widths that suit a 4-column panel
   (.num-cell's 40px, a cramped select) squeeze the category dropdowns
   (2026-11, user: "terlalu sempit"). The numeric columns now size to their
   content, each category dropdown gets a readable minimum, and the table is
   allowed to exceed the panel width and scroll rather than compress -- the
   same way the other list panels behave. Rows are NOT clickable (only their
   two action buttons are), so they opt out of .labeltable's pointer cursor. */
/* menu Material (2026-11) -- the TAPExx table.
   Nine/Ten columns: straight, symmetrical, aligned columns with uniform control heights. */
.materialtable { width: 100%; min-width: 65rem; table-layout: fixed; border-collapse: collapse; }
.materialtable thead th {
  padding: 8px 6px; font-size: var(--fs0); font-weight: 700;
  color: var(--muted); border-bottom: 2px solid var(--line);
  vertical-align: middle; text-align: left;
}
.materialtable td { padding: 5px 6px; vertical-align: middle; border-bottom: 1px solid var(--line-soft); }
.materialtable tr, .materialtable tbody tr { cursor: default; }

/* Column alignment & exact straight widths */
.materialtable th:nth-child(1), .materialtable td:nth-child(1) { width: 3rem; text-align: center; } /* No */
.materialtable th:nth-child(2), .materialtable td:nth-child(2) { width: 5.5rem; text-align: left; } /* TAPE */
.materialtable th:nth-child(3), .materialtable td:nth-child(3) { width: 7.5rem; } /* Material */
.materialtable th:nth-child(4), .materialtable td:nth-child(4) { width: 9.5rem; } /* Tape Type */
.materialtable th:nth-child(5), .materialtable td:nth-child(5) { width: 8rem; } /* Temperature */
.materialtable th:nth-child(6), .materialtable td:nth-child(6) { width: 7.5rem; } /* Color */
.materialtable th:nth-child(7), .materialtable td:nth-child(7) { width: 5.5rem; } /* Width */
.materialtable th:nth-child(8), .materialtable td:nth-child(8) { min-width: 11rem; } /* SPN */
.materialtable th:nth-child(9), .materialtable td:nth-child(9) { width: 5rem; text-align: right; } /* Units */
.materialtable th:nth-child(10), .materialtable td:nth-child(10) { width: 8rem; text-align: center; } /* Aksi */

.materialtable .cell-input {
  width: 100%; height: 28px; line-height: 28px; padding: 2px 6px;
  font-size: var(--fs0); border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); color: var(--body); box-sizing: border-box;
}
.materialtable .material-units { font-variant-numeric: tabular-nums; font-weight: 600; padding-right: 8px; }
.materialtable .material-actions { white-space: nowrap; text-align: center; }
.materialtable .material-actions .btn { padding: 3px 8px; font-size: 11.5px; margin: 0 2px; }

/* --- Kartu atribut TAPE manual + editor tabel SPN (2026-11 rebuild) --- */
.tape-attr-card { max-width: 540px; width: 92vw; }
.tape-attr-route { font-weight: 500; color: var(--muted); font-size: var(--fs0); }
.tape-attr-grid {
  display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--sp-2) var(--sp-3);
  margin: var(--sp-3) 0;
}
.tape-attr-grid label {
  display: flex; flex-direction: column; gap: var(--sp-1);
  font-size: var(--fs0); font-weight: 600; color: var(--body);
}
.tape-attr-grid label.col-2 { grid-column: span 2; }
.tape-attr-grid label.col-3 { grid-column: span 3; }
.tape-attr-grid select {
  height: 32px; padding: 4px 8px; font-size: var(--fs0);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); color: var(--body); box-sizing: border-box; width: 100%;
}
.tape-attr-spn { margin: var(--sp-2) 0 0; font-size: var(--fs0); color: var(--body); }
.tape-attr-spn strong { letter-spacing: 0.01em; }

/* --- Kartu atribut TAPE manual: daftar length + kandidat cloud + drag ---
   (2026-11 cloud rebuild) */
.tape-attr-backdrop { align-items: flex-start; }
.tape-attr-card { max-width: 560px; width: 92vw; cursor: default; }
.tape-attr-card.dragging { opacity: 0.92; box-shadow: 0 12px 32px rgba(0,0,0,.28); }
.tape-attr-draghandle {
  cursor: grab; user-select: none; margin: calc(-1 * var(--sp-2)) calc(-1 * var(--sp-2)) var(--sp-2);
  padding: var(--sp-2); border-radius: var(--radius);
}
.tape-attr-draghandle:active { cursor: grabbing; }
.tape-attr-section { margin: var(--sp-3) 0; }
.tape-attr-section-head { font-size: var(--fs0); font-weight: 650; color: var(--body); margin-bottom: var(--sp-2); }
.tape-attr-segments {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  max-height: 120px; overflow: auto; padding: var(--sp-2);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel);
}
.tape-attr-segments label {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: 2px var(--sp-2); border: 1px solid var(--line); border-radius: 999px;
  font-size: var(--fs0); background: var(--bg); cursor: pointer; white-space: nowrap;
}
.tape-attr-segments input { margin: 0; }
.tape-attr-total { margin: var(--sp-2) 0 0; font-size: var(--fs0); color: var(--body); }
.tape-attr-candidates {
  max-height: 180px; overflow: auto;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel);
}
.tape-attr-candidates table { width: 100%; border-collapse: collapse; }
.tape-attr-candidates td, .tape-attr-candidates th {
  padding: var(--sp-1) var(--sp-2); font-size: var(--fs0); text-align: left;
  border-bottom: 1px solid var(--line);
}
.tape-attr-candidates tr[data-pick] { cursor: pointer; }
.tape-attr-candidates tr[data-pick]:hover { background: var(--bg); }
.tape-attr-candidates tr.picked { background: rgba(37,99,235,.14); }
.tape-attr-candidates .spn-cand-desc { color: var(--muted); }
