/* =====================================================================
   Network Emulator - styles.css
   Theme follows Ohio U / ECT Izzy palette extracted from w:\tong\iz.
   Light UI overall; terminal/code surfaces commit to a dark palette.
   ===================================================================== */

:root {
  /* Brand */
  --green:        #00694E;
  --green-dark:   #024230;
  --white:        #FFFFFF;
  --putnam:       #756E65;
  --sycamore:     #E7ECC3;
  --marigold:     #AA8A00;
  --black:        #000000;

  /* Surfaces */
  --bg:           #F5F5F0;
  --bg-alt:       #FAFAF7;
  --bg-warmer:    #FFFBF0;
  --panel:        #FFFFFF;
  --panel-edge:   #E0E3CA;
  --hairline:     #C8CDB0;
  /* Semantic surface tokens used for panels, modal cards, palette
     items, etc. Defaulting to white in light mode; flipped to dark
     greys under body.dark below. Use these instead of var(--white)
     when the intent is "panel surface", not "white text". */
  --surface:      #FFFFFF;
  --surface-2:    #F8F8F2;
  --ink:          #000000;        /* primary body text */
  --ink-muted:    #756E65;        /* secondary text (= --putnam in light) */

  /* State */
  --warn-bg:      #FFF3CD;
  --warn-fg:      #5a4000;
  --error-bg:     #FCE6E6;
  --error-fg:     #C00000;

  /* Terminal palette */
  --term-bg:      #0D1117;
  --term-fg:      #C9D1D9;
  --term-ok:      #3FB950;
  --term-ok2:     #7EE787;
  --term-warn:    #D29922;
  --term-warn2:   #FFA657;
  --term-err:     #F85149;
  --term-err2:    #FF9E9E;
  --term-info:    #79C0FF;
  --term-dim:     #6E7681;
  --term-accent:  #E3B341;

  /* Type */
  --font-body:  'Segoe UI', Arial, sans-serif;
  --font-mono:  'Courier New', Consolas, monospace;

  /* Layout */
  --toolbar-h: 56px;
  --palette-w: 200px;
  --bottom-h:  240px;
  --resizer-w: 5px;
  --bottom-collapsed-h: 36px;
  --palette-collapsed-w: 28px;

  /* Easings */
  --t-fast: 0.15s ease;
  --t-slow: 0.35s ease;
}

/* ---- Dark mode (C5) -------------------------------------------------
   Toggled via body.dark from the hamburger Settings menu. Flips the
   palette / surface tokens; the brand greens stay (slightly lifted
   for legibility) and accent hues (marigold, error red) carry over
   so the visual identity reads the same. The terminal palette is
   already dark and stays put. */
body.dark {
  --green:        #4FC3A1;     /* lifted brand green for contrast on dark */
  --green-dark:   #2C8264;
  --putnam:       #9DA39B;
  --sycamore:     #2A352A;     /* dark-mode hover accent */
  --marigold:     #E3B341;     /* warmer for dark */

  --bg:           #161916;
  --bg-alt:       #1B1F1B;
  --bg-warmer:    #1F1B16;
  --panel:        #22272A;
  --panel-edge:   #2C3236;
  --hairline:     #3A4046;

  --surface:      #22272A;
  --surface-2:    #1B1F22;
  --ink:          #D8DAD2;
  --ink-muted:    #A4A89E;

  --warn-bg:      #3a2f12;
  --warn-fg:      #f0c674;
  --error-bg:     #3a1f1f;
  --error-fg:     #ff8181;
}

* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 14px;
  overflow: hidden;
}

button { font-family: inherit; font-size: inherit; }
input, textarea, select { font-family: inherit; font-size: inherit; }
.hidden { display: none !important; }
.kbd {
  display: inline-block; padding: 1px 6px;
  border: 1px solid var(--green); border-radius: 4px;
  font-family: var(--font-mono); font-size: 11px;
  background: var(--surface); color: var(--green-dark);
}

/* ----- Toolbar ----- */
#toolbar {
  height: var(--toolbar-h);
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 16px;
  background: var(--surface);
  border-bottom: 2px solid var(--green);
  position: relative;
  z-index: 10;
}
#toolbar .brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
/* Keep every toolbar button on one line. Without nowrap, a flex squeeze
   on the tools row lets a button's label and its chev land on different
   lines, which is uglier than just hiding the chev. The chev itself is
   handled by the @media-1100 rule below. */
#toolbar button { white-space: nowrap; }
#toolbar .brand-mark {
  display: inline-block; width: 24px; height: 24px;
  border-radius: 4px;
  object-fit: contain;
}
#toolbar .brand-text {
  color: var(--green-dark); font-weight: bold; font-size: 18px;
}
/* Default: full brand label visible, short form hidden. The narrow
   breakpoints below flip the visibility so the toolbar can keep
   showing a brand even when "ECT Network Emulator (ENE)" is too
   wide to fit alongside the dropdowns. */
#toolbar .brand-text-short { display: none; }
#toolbar .tools { display: flex; align-items: center; gap: 8px; }

/* ----- Buttons ----- */
.btn-primary, .btn-secondary, .btn-tertiary, .btn-icon {
  cursor: pointer; border-radius: 6px;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
  font-weight: bold;
}
.btn-primary {
  background: var(--green); color: var(--white);
  border: none; padding: 8px 18px;
}
.btn-primary:hover { background: var(--green-dark); }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }

.btn-secondary {
  background: var(--surface); color: var(--green);
  border: 2px solid var(--green); padding: 6px 14px;
}
.btn-secondary.toolbar-toggle.is-on {
  background: var(--green); color: var(--white);
  border-color: var(--green-dark);
}
.btn-secondary.toolbar-toggle.is-on:hover { background: var(--green-dark); }

/* Multi-line edit input for the Prompt modal (text-annotation editing). */
#prompt-textarea, #internet-list {
  width: 100%; box-sizing: border-box;
  min-height: 120px; padding: 8px;
  font: inherit; font-family: var(--font-mono);
  border: 1px solid var(--hairline); border-radius: 4px;
  background: var(--bg-alt); color: var(--ink);
  resize: vertical;
}
#prompt-textarea:focus, #internet-list:focus { outline: 2px solid var(--green); border-color: var(--green); }
.btn-secondary:hover { background: var(--sycamore); }

.btn-tertiary {
  background: transparent; color: var(--putnam);
  border: 2px solid var(--putnam); padding: 6px 14px;
  font-weight: normal;
}
.btn-tertiary:hover { background: var(--sycamore); color: var(--marigold); }

.btn-icon {
  background: transparent; color: var(--green-dark);
  border: none; padding: 6px 10px; font-size: 16px;
}
.btn-icon:hover { background: var(--sycamore); }

.chev { font-size: 11px; opacity: 0.8; margin-left: 6px; }

/* ----- Workspace area ----- */
#workspace {
  display: grid;
  grid-template-columns: var(--palette-w) var(--resizer-w) 1fr;
  grid-template-rows: 1fr var(--resizer-w) var(--bottom-h);
  height: calc(100vh - var(--toolbar-h));
  width: 100%;
  position: relative;
}
body.bottom-collapsed #workspace {
  grid-template-rows: 1fr 0 var(--bottom-collapsed-h);
}
body.palette-collapsed #workspace {
  grid-template-columns: var(--palette-collapsed-w) 0 1fr;
}
/* Full-screen mode: hide every piece of UI chrome (toolbar, palette
   column, console panel, resizers) and let the canvas fill the
   viewport. Independent of palette/bottom-collapsed state - those
   classes can coexist but fullscreen wins via display:none. */
body.fullscreen #toolbar,
body.fullscreen #palette,
body.fullscreen #palette-resizer,
body.fullscreen #bottom-resizer,
body.fullscreen #bottom-panel { display: none !important; }
body.fullscreen #workspace {
  height: 100vh;
  grid-template-rows: 1fr;
  grid-template-columns: 1fr;
}
/* #canvas-wrap is anchored at grid (column 3, row 1) for the normal
   palette + resizer + canvas layout. In fullscreen the grid collapses
   to one column, so we have to re-place the canvas at (1, 1) or it
   ends up sitting in an implicit phantom column. The SVG itself fills
   #canvas-wrap, so re-placing the wrap is enough. */
body.fullscreen #canvas-wrap {
  grid-column: 1;
  grid-row: 1;
}
/* Floating "Exit" pill: docked top-right while fullscreen is on. Click
   exits fullscreen, mirroring the F11 / Esc keybinds. Sits above
   everything (z-index 1000) so SVG content can't cover it. */
.fullscreen-exit-btn {
  position: fixed; top: 8px; right: 8px; z-index: 1000;
  height: 28px; padding: 0 12px;
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid #c0c5cc;
  border-radius: 14px;
  font-size: 12px; cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.fullscreen-exit-btn:hover { background: var(--surface); border-color: var(--green); color: var(--green); }
/* .hidden hides the pill in non-fullscreen mode. Body class also
   acts as a redundant safety net so a stale .hidden removal doesn't
   leak the pill onto the regular toolbar. */
body:not(.fullscreen) .fullscreen-exit-btn { display: none; }
/* Trashcan icon button on firewall rule rows. Tighter padding than
   .btn-tertiary so the icon sits compactly next to the Clone label
   button; hover paints the stroke red so the destructive intent
   reads at a glance. */
.fw-rule-del {
  padding: 4px 6px;
  line-height: 0;
  vertical-align: middle;
  color: #54595e;
}
.fw-rule-del:hover { color: #c5221f; border-color: #c5221f; }

/* ----- Compare Topologies (A2) ----- */
.diff-pill {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 8px;
  font-size: 10px;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  vertical-align: middle;
}
.diff-pill.diff-added    { background: #d4f4dd; color: #1f8a4c; }
.diff-pill.diff-removed  { background: #fddede; color: #c5221f; }
.diff-pill.diff-changed  { background: #fff4cc; color: #8a6d00; }
.diff-pill.diff-unchanged { background: #e3e6ea; color: #54595e; }

.compare-summary { margin: 8px 0 12px; padding: 8px 10px; background: #f5f7fa; border-left: 3px solid #888; border-radius: 4px; }
.compare-section { margin-top: 12px; }
.compare-section h3 { margin: 6px 0; font-size: 13px; color: #54595e; }
.compare-list { list-style: none; padding-left: 0; margin: 0; }
.compare-row {
  padding: 6px 0;
  border-bottom: 1px solid #eee;
}
.compare-row:last-child { border-bottom: none; }
.compare-row.compare-added    { background: linear-gradient(to right, rgba(31, 138, 76, 0.06), transparent); }
.compare-row.compare-removed  { background: linear-gradient(to right, rgba(197, 34, 31, 0.06), transparent); }
.compare-row.compare-changed  { background: linear-gradient(to right, rgba(138, 109, 0, 0.06), transparent); }
.compare-row-header {
  display: flex;
  align-items: center;
  gap: 4px;
  user-select: none;
}
.compare-collapsible .compare-row-header:hover { background: rgba(0, 0, 0, 0.03); }
.compare-chev {
  display: inline-block;
  width: 14px;
  font-size: 11px;
  color: #888;
  transition: color 100ms;
}
.compare-collapsible .compare-row-header:hover .compare-chev { color: #54595e; }
.compare-fieldlist {
  list-style: none;
  margin: 4px 0 0 24px;
  padding: 0;
  font-size: 12px;
  color: #54595e;
}
.compare-fieldlist.hidden { display: none; }
.compare-fieldlist li { padding: 2px 0; }
.compare-fieldlist code { background: #f0f2f5; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
.compare-arrow { color: #888; }
.compare-before { color: #c5221f; }
.compare-after  { color: #1f8a4c; }

/* Compare modal: user-resizable card. Scoped so other modals keep
   their fixed sizing. The body needs flex:1 + min-height:0 so it
   fills the resized card and lets #compare-results scroll. */
#compare-modal .modal-card {
  resize: both;
  overflow: hidden;
  min-width: 480px;
  min-height: 320px;
  max-height: none;
}
#compare-modal .modal-body {
  flex: 1;
  min-height: 0;
}

/* Router / Firewall modal: user-resizable so the firewall rules table
   (which has 12-13 columns after B22's Family selector) is readable
   without the right-most columns getting clipped. The default
   .modal-wide max width is min(820px, 96vw); users hit the resize grip
   in the bottom-right to drag it wider. The body fills the resized
   card so the rules table scrolls inside it. */
#router-modal .modal-card {
  resize: both;
  overflow: hidden;
  /* Open wide enough that the 12-13 column firewall rules table fits
     without horizontal scrolling on a typical screen; still resizable,
     and the body scrolls (x + y) when the card is narrower. */
  width: min(960px, 96vw);
  min-width: 640px;
  min-height: 420px;
  max-width: 98vw;
  max-height: 95vh;
}
#router-modal .modal-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
}
.compare-summary {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}
.compare-summary > .compare-summary-text { flex: 1; }
.compare-copy-btn { flex: 0 0 auto; }
.compare-copy-status { font-size: 12px; opacity: 0.75; margin-left: 6px; }

.banner-inline {
  margin: 8px 0;
  padding: 8px 10px;
  border-radius: 4px;
  background: #fdecea;
  border-left: 3px solid #c5221f;
  color: #54595e;
}
.banner-inline.error { background: #fdecea; border-color: #c5221f; }

/* Canvas overlay rings while Compare Topologies is open. Reuses the
   per-device shape elements (.dev-image-frame for icon devices,
   the cloud/router/firewall vector shapes) so the ring stays
   anchored to the visible silhouette regardless of port-drag
   adjustments. Selection's marigold stroke still wins when both
   classes are present. */
body.compare-active .dev.diff-mode.diff-status-changed .dev-image-frame,
body.compare-active .dev.diff-mode.diff-status-changed .dev-cloud-shape,
body.compare-active .dev.diff-mode.diff-status-changed .dev-router-shape,
body.compare-active .dev.diff-mode.diff-status-changed .dev-firewall-shape,
body.compare-active .dev.diff-mode.diff-status-changed .dev-shape {
  stroke: #f0883e;
  stroke-width: 4;
  stroke-dasharray: 6 4;
}
body.compare-active .dev.diff-mode.diff-status-removed .dev-image-frame,
body.compare-active .dev.diff-mode.diff-status-removed .dev-cloud-shape,
body.compare-active .dev.diff-mode.diff-status-removed .dev-router-shape,
body.compare-active .dev.diff-mode.diff-status-removed .dev-firewall-shape,
body.compare-active .dev.diff-mode.diff-status-removed .dev-shape {
  stroke: #c5221f;
  stroke-width: 4;
  stroke-dasharray: 2 3;
}
body.compare-active .dev.diff-mode.diff-status-removed { opacity: 0.55; }
.modal-actions {
  display: flex; gap: 8px; justify-content: flex-end; margin: 8px 0;
}

/* ----- Logs modal (banner history) ----- */
.logs-list { list-style: none; margin: 0; padding: 0; max-height: 60vh; overflow-y: auto; }
.logs-row {
  display: grid;
  grid-template-columns: auto auto 1fr;
  gap: 8px;
  align-items: baseline;
  padding: 6px 8px;
  border-bottom: 1px solid #eee;
  font-size: 12px;
}
.logs-row:last-child { border-bottom: none; }
.logs-row.logs-tone-warn  { background: rgba(240, 136, 62, 0.05); }
.logs-row.logs-tone-error { background: rgba(197, 34, 31, 0.06); }
.logs-ts { font-family: monospace; color: #6a7280; min-width: 80px; }
.logs-tone-pill {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 8px;
  font-size: 10px;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  background: #e3e6ea; color: #54595e;
}
.logs-tone-pill-warn  { background: #fff4cc; color: #8a6d00; }
.logs-tone-pill-error { background: #fddede; color: #c5221f; }
.logs-msg { color: #54595e; word-break: break-word; }
.logs-details { grid-column: 1 / -1; margin-left: 88px; }
.logs-details summary { cursor: pointer; color: #6a7280; font-size: 11px; }
.logs-details pre {
  margin: 4px 0 0; padding: 6px 8px;
  background: #f0f2f5; border-radius: 3px;
  font-family: monospace; font-size: 11px;
  white-space: pre-wrap; word-break: break-word;
}

/* ----- Palette ----- */
#palette {
  grid-column: 1;
  grid-row: 1 / span 3;
  background: var(--bg-alt);
  border-right: 1px solid var(--hairline);
  padding: 16px 12px;
  overflow-y: auto;
  overflow-x: hidden;
  min-width: 0;
  position: relative;
  /* Container query host so the h2 row + icons can shrink based on the
     palette pane's actual width (not viewport width). Lets a narrow
     compact-mode pane fit the word "Templates" without clipping. */
  container-type: inline-size;
}
#palette-toggle {
  width: 22px; height: 22px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 4px;
  color: var(--green-dark);
  cursor: pointer;
  /* Chevron glyph 2x its prior 12px size so the close-pane affordance
     is easy to spot next to the smaller compact-toggle icon. */
  font-size: 24px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  font-family: inherit;
}
#palette-toggle:hover { border-color: var(--green); color: var(--green); }
body.palette-collapsed #palette { padding: 12px 4px 0; }
body.palette-collapsed #palette .palette-item,
body.palette-collapsed #palette .palette-help,
body.palette-collapsed #palette .templates-text,
body.palette-collapsed #palette-compact-toggle { display: none; }
body.palette-collapsed #palette h2 {
  padding: 0; justify-content: center;
}
body.palette-collapsed #palette-resizer { display: none; }
#palette h2 {
  font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px;
  margin: 0 0 12px 4px; color: var(--putnam); font-weight: bold;
}
.palette-item {
  background: var(--surface);
  border: 2px solid var(--green);
  border-radius: 8px;
  padding: 10px;
  margin-bottom: 10px;
  cursor: grab;
  display: grid;
  grid-template-columns: 1fr 36px;
  grid-template-rows: auto auto;
  column-gap: 10px;
  align-items: center;
  transition: transform var(--t-fast), box-shadow var(--t-fast);
}
.palette-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 16px rgba(0,105,78,0.2);
}
.palette-item:active { cursor: grabbing; }

/* "RF Objects" drawer: a non-draggable parent tile (palette-drawer-tile)
   with a hidden popout panel that appears on hover/focus. Hovering or
   keyboard-focusing the parent reveals the popout to the right of the
   tile (toward the canvas, since the palette is docked on the left
   edge of the page), holding RFO and RFJ tiles which behave like
   normal palette items (drag to canvas to drop). The popout stays
   open while the pointer is over either the parent or itself thanks
   to `:hover` covering both elements through their shared
   `.palette-drawer` ancestor. */
.palette-drawer { position: relative; }
.palette-drawer-tile { cursor: default; }
.palette-drawer-tile:active { cursor: default; }
.palette-drawer-popout {
  /* Fixed (not absolute) so the popout escapes #palette's overflow:hidden
     clip and floats over the canvas. JS positions it next to the parent
     tile by setting `left` and `top` on .is-open, since fixed positioning
     can't otherwise follow a moving parent. */
  position: fixed;
  z-index: 50;
  min-width: 220px;
  padding: 10px;
  background: var(--surface);
  border: 2px solid var(--green);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
  opacity: 0;
  pointer-events: none;
  transform: translateX(-6px);
  transition: opacity var(--t-fast), transform var(--t-fast);
}
.palette-drawer.is-open .palette-drawer-popout {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(0);
}
.palette-drawer-popout .palette-item:last-child { margin-bottom: 0; }
/* In compact mode the popout still shows full-size tiles so the user
   can read what's in the drawer at a glance. */
body.palette-compact .palette-drawer-popout {
  min-width: 220px;
}
body.palette-compact .palette-drawer-popout .palette-item {
  padding: 10px;
}
body.palette-compact .palette-drawer-popout .palette-icon {
  width: 36px; height: 36px;
}
body.palette-compact .palette-drawer-popout .palette-label,
body.palette-compact .palette-drawer-popout .palette-sub {
  display: block;
}
.palette-icon {
  grid-column: 2;
  grid-row: 1 / span 2;
  width: 36px; height: 36px;
  border-radius: 6px;
  background: transparent;
  position: relative;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
.palette-label, .palette-sub { grid-column: 1; }
.palette-icon > img,
.palette-icon > svg {
  display: block;
  width: 100%; height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
}
.palette-label {
  font-weight: bold; color: var(--green-dark); font-size: 14px;
}
.palette-sub {
  font-size: 11px; color: var(--putnam);
}
.palette-help {
  margin-top: 12px; padding: 10px;
  background: var(--sycamore); color: var(--green-dark);
  border-left: 4px solid var(--green); border-radius: 4px;
  font-size: 12px; line-height: 1.5;
}
/* Compact mode: shrink palette items to just the icon, hide label /
   sub-label / help text. Activated by the small icon button at the
   top of the panel. The h2 stacks the action icons (compact + close)
   ABOVE the "Templates" word so the icons stay accessible at any
   pane width. Both the icons and the text scale via container
   queries so a very narrow pane still shows the heading without
   clipping. */
#palette h2 {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 4px;
  /* Scale font with the palette pane width: ~13% of pane width,
     clamped between 9px (compact) and 13px (full). */
  font-size: clamp(9px, 13cqi, 13px);
  margin: 0 0 12px 0;
}
#palette h2 .palette-h2-actions {
  display: flex; gap: 6px; align-items: center; justify-content: flex-end;
}
/* Scale the action buttons together with the h2 text so a narrow
   compact pane keeps everything proportional. Both buttons share these
   bounds via the cqi value below. */
#palette h2 .palette-h2-actions > button {
  width:  clamp(16px, 22cqi, 22px);
  height: clamp(16px, 22cqi, 22px);
}
#palette h2 .palette-h2-actions > button > svg {
  width:  clamp(11px, 16cqi, 16px);
  height: clamp(11px, 16cqi, 16px);
}
#palette h2 .palette-h2-actions > #palette-toggle {
  /* Chevron glyph scales with the pane too. */
  font-size: clamp(16px, 25cqi, 24px);
}
#palette h2 .templates-text {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  /* Use ellipsis (not clip) so a marginally-narrow pane reads as
     "Tem…" rather than chopping mid-word like "TEMPLA". */
  text-overflow: ellipsis;
  /* Anchor left so it doesn't centre awkwardly against the actions row. */
  text-align: left;
}
/* When the palette container shrinks below ~90 px there isn't room for a
   meaningful label - drop it entirely so the column shows just the icon
   stack and the action buttons stay legible. */
@container (max-inline-size: 90px) {
  #palette h2 .templates-text { display: none; }
}
#palette-compact-toggle {
  background: transparent;
  border: 1px solid var(--hairline);
  border-radius: 4px;
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  color: var(--green-dark);
  padding: 2px;
  margin-left: auto;
  font-family: inherit;
}
#palette-compact-toggle svg { width: 16px; height: 16px; display: block; }
#palette-compact-toggle:hover { border-color: var(--green); color: var(--green); }
#palette-compact-toggle.is-on {
  background: var(--green); color: var(--white); border-color: var(--green-dark);
}
body.palette-compact .palette-item {
  grid-template-columns: 1fr;
  padding: 6px;
  min-height: 0;
}
body.palette-compact .palette-item .palette-icon {
  grid-column: 1; grid-row: 1;
  margin: 0 auto;
}
body.palette-compact .palette-item .palette-label,
body.palette-compact .palette-item .palette-sub,
body.palette-compact .palette-help {
  display: none;
}

/* ----- Resizers ----- */
.resizer {
  background: var(--hairline);
  position: relative;
  z-index: 5;
  user-select: none;
  transition: background var(--t-fast);
}
.resizer:hover, .resizer.dragging { background: var(--green); }
.resizer-v { grid-column: 2; grid-row: 1 / span 3; cursor: col-resize; }
.resizer-h { grid-column: 3; grid-row: 2; cursor: row-resize; }
/* Widen the hit area without changing visual width. */
.resizer-v::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: -3px; right: -3px;
}
.resizer-h::before {
  content: ""; position: absolute; left: 0; right: 0; top: -3px; bottom: -3px;
}
body.bottom-collapsed #bottom-resizer { display: none; }
body.dragging-col, body.dragging-col * { cursor: col-resize !important; user-select: none; }
body.dragging-row, body.dragging-row * { cursor: row-resize !important; user-select: none; }

/* ----- Canvas ----- */
#canvas-wrap {
  grid-column: 3; grid-row: 1;
  background: var(--bg);
  position: relative;
  overflow: hidden;
  min-width: 0; min-height: 0;
}
/* Grid pattern stroke is set as an SVG attribute in index.html for
   light mode. Dark mode overrides via CSS so the gridlines don't
   blast the user's eyes on a dark canvas. */
body.dark #grid-pattern path { stroke: #2a302a; }
body.dark #grid-major   path { stroke: #353c34; }
#canvas {
  width: 100%; height: 100%; display: block;
  cursor: default;
  /* Without this, touch drags get hijacked as scroll/zoom on mobile and
     the marquee/pan/device drags never see the gesture. */
  touch-action: none;
  /* Prevent the cursor from accidentally highlighting device labels, port
     names, or any other text rendered on the canvas while panning, dragging,
     or wiring. The on-canvas SVG <text> nodes are visual labels, not
     selectable content. */
  user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
}
#canvas text { user-select: none; -webkit-user-select: none; }
/* Only show the grab cursor while actively panning the canvas, not as a
   resting default - the default cursor is just an arrow. Holding Ctrl/Meta
   "arms" pan mode and paints the open-hand cursor; an actual drag flips
   to the closed-hand grabbing cursor. */
#canvas.dragging { cursor: grabbing; }
#canvas.ctrl-held { cursor: grab; }
#canvas.ctrl-held.dragging { cursor: grabbing; }
#canvas.drop-target { outline: 3px dashed var(--green); outline-offset: -8px; }
.empty-hint {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
  color: var(--putnam); font-size: 16px; font-style: italic;
}
.link-pending {
  position: absolute; left: 12px; bottom: 12px;
  background: var(--green-dark); color: var(--white);
  padding: 8px 12px; border-radius: 6px;
  font-size: 12px;
  box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}

/* ----- SVG device styles ----- */
.dev-shape {
  fill: var(--white);
  stroke: var(--green);
  stroke-width: 2;
  cursor: pointer;
}
.dev.selected .dev-shape {
  stroke: var(--marigold); stroke-width: 3;
}
.dev-icon { pointer-events: none; }
.dev-icon-text {
  fill: var(--green-dark);
  font-family: var(--font-body);
  font-weight: bold;
  pointer-events: none;
}
.pending-line {
  stroke: var(--marigold);
  stroke-width: 2;
  stroke-dasharray: 5 4;
  fill: none;
  pointer-events: none;
}

/* Image-based devices (switch, client). The frame is the selection ring. */
.dev-image { cursor: pointer; }
.dev-image-frame {
  fill: none;
  stroke: transparent;
  stroke-width: 3;
  pointer-events: none;
}
.dev.selected .dev-image-frame { stroke: var(--marigold); }

/* "My Perspective" colored ring around an opted-in wifi client. The hue
   matches the dashed outlines drawn around the RFO shadow cones currently
   attenuating this client, so the user can pair shadows to clients at a
   glance. Selection (marigold) still wins if both classes are present. */
.dev.dev-perspective .dev-image-frame {
  stroke: var(--perspective-color, #FF3B30);
}
.dev.dev-perspective.selected .dev-image-frame { stroke: var(--marigold); }

/* ----- Zones (resizable labeled rectangles, behind everything else) ----- */
/* Fill and stroke colors come from inline attributes set by Render so the
   per-zone pastel palette can take effect. CSS only owns the static parts. */
.zone-rect {
  stroke-width: 1.5;
  cursor: move;
}
.zone-label {
  font-weight: bold;
  pointer-events: none;
  user-select: none;
}
.zone.selected .zone-rect { stroke: var(--marigold) !important; stroke-width: 2; }
.zone-handle {
  fill: var(--white);
  stroke: var(--green-dark);
  stroke-width: 1.5;
  display: none;
}
.zone.selected .zone-handle { display: block; }
.zone-handle-nw { cursor: nwse-resize; }
.zone-handle-ne { cursor: nesw-resize; }
.zone-handle-sw { cursor: nesw-resize; }
.zone-handle-se { cursor: nwse-resize; }
.zone-handle-n,
.zone-handle-s  { cursor: ns-resize; }
.zone-handle-e,
.zone-handle-w  { cursor: ew-resize; }

/* ----- Free-floating text annotations ----- */
.text-anno { cursor: move; }
.text-label {
  fill: var(--green-dark);
  user-select: none;
  pointer-events: none;
}
.text-hit { fill: transparent; pointer-events: all; }
.text-anno.selected .text-hit {
  fill: rgba(170, 138, 0, 0.10);
  stroke: var(--marigold);
  stroke-width: 1;
  stroke-dasharray: 3 2;
}

/* While in pin-to-object mode, hint that any device or zone is the target. */
body.pin-mode,
body.pin-mode .dev,
body.pin-mode .zone,
body.pin-mode .zone-rect { cursor: crosshair !important; }

/* Marquee rectangle while dragging an empty-canvas selection lasso. */
.marquee {
  fill: rgba(0, 105, 78, 0.10);
  stroke: var(--green);
  stroke-width: 1;
  stroke-dasharray: 4 3;
  pointer-events: none;
  vector-effect: non-scaling-stroke;
}

/* C11: alignment guide line drawn while dragging a single device, when
   one of its edges or its center lines up with another device's. Pure
   visual feedback - it never moves anything. Marigold so it reads
   distinctly against the green selection chrome and the gray grid. */
.align-guide {
  stroke: var(--marigold, #e0a800);
  stroke-width: 1;
  stroke-dasharray: 5 4;
  pointer-events: none;
  vector-effect: non-scaling-stroke;
}

/* A5: guided walkthrough floating panel. Anchored to the bottom-centre
   of the viewport. Pointer-events all on the panel itself so the user
   can hit Next/Back/Close without losing focus to the canvas behind. */
.walkthrough-panel {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  max-width: min(560px, 92vw);
  background: var(--surface, #fff);
  color: var(--ink, #222);
  border: 1px solid var(--green, #00694E);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.20);
  padding: 12px 14px;
  z-index: 2000;
  font-family: var(--font-body, system-ui, sans-serif);
  font-size: 14px;
}
.walkthrough-panel .walkthrough-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 6px;
}
.walkthrough-panel .walkthrough-title {
  font-weight: 600;
  color: var(--green-dark, #024230);
}
.walkthrough-panel .walkthrough-body {
  white-space: pre-wrap;
  line-height: 1.4;
  margin: 4px 0 10px;
}
.walkthrough-panel .walkthrough-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
}
.walkthrough-panel .walkthrough-counter {
  color: var(--ink-muted, #667);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
}
/* Notes window: floating, draggable, resizable child window shown
   once when a demo / slate row with a `notes` CSV column is first
   loaded. Built on top of `.floating-console` so it inherits the
   border, shadow, drag, and resize behavior. The `.notes-window`
   variant overrides the title-bar color to amber so it's visually
   distinct from terminal (green) and sniffer (purple) windows. */
.floating-console.notes-window {
  min-width: 320px; min-height: 160px;
  background: var(--surface, #fff);
  color: var(--ink, #222);
  font-family: var(--font-body, system-ui, sans-serif);
  font-size: 14px;
}
.floating-console.notes-window .fc-title {
  background: #b85c00;
}
.floating-console.notes-window .fc-body {
  background: var(--surface, #fff);
  overflow: auto;
  padding: 12px 14px;
}
.floating-console.notes-window .notes-popup-body {
  white-space: pre-wrap;
  line-height: 1.4;
  color: var(--ink, #222);
}

/* A5 authoring: full-width JSON textarea inside the edit modal. */
.walkthrough-edit-json {
  width: 100%;
  min-height: 240px;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
  white-space: pre;
}
.hint.error {
  color: var(--error, #c62828);
}

/* C7: minimap / overview inset. Pinned bottom-right (drawn in screen
   px as a sibling of canvas-root). The group is click-through except
   the frame rect, so only the inset itself is interactive and the
   rest of the canvas behind it works normally. */
#layer-minimap { pointer-events: none; }
#layer-minimap .minimap-frame {
  fill: var(--surface, #ffffff);
  fill-opacity: 0.85;
  stroke: var(--hairline, #c9cdb4);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  pointer-events: all;
  cursor: pointer;
}
/* Fill + stroke are set inline from the same ZONE_PALETTE the main
   canvas uses, so don't hard-code them here. Slight transparency so
   device dots stay readable on top of the LAN square. */
#layer-minimap .minimap-zone {
  fill-opacity: 0.55;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
#layer-minimap .minimap-dev { fill: var(--green, #00694E); }
#layer-minimap .minimap-view {
  fill: rgba(170, 138, 0, 0.12);
  stroke: var(--marigold, #AA8A00);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

/* Cloud: white fill with dark outline, marigold when selected. */
.dev-cloud-shape {
  fill: var(--white);
  stroke: var(--black);
  stroke-width: 2;
  stroke-linejoin: round;
  cursor: pointer;
}
.dev.selected .dev-cloud-shape { stroke: var(--marigold); stroke-width: 3; }

/* Router: AWS-style orange circle with white arrows. */
.dev-router-shape {
  fill: #F58534;
  stroke: #B95D1F;
  stroke-width: 2;
  cursor: pointer;
}
.dev.selected .dev-router-shape { stroke: var(--marigold); stroke-width: 3; }
.dev-router-arrow {
  fill: var(--white);
  stroke: none;
  pointer-events: none;
}
/* Firewall: brick wall. Body uses the same orange family as the router so
   the two devices read as related, with darker mortar lines between bricks.
   Selection swaps the body stroke to marigold like everything else. */
.dev-firewall-shape {
  fill: #F58534;
  stroke: #B95D1F;
  stroke-width: 2;
  cursor: pointer;
}
.dev.selected .dev-firewall-shape { stroke: var(--marigold); stroke-width: 3; }
.dev-firewall-mortar {
  stroke: #5A2E0E;
  stroke-width: 2;
  fill: none;
  pointer-events: none;
}
/* Server rack body and slot bars. Body fill / stroke come from inline
   per-color attributes set by the renderer; here we only own selection
   highlight, the slot fill, and the cursor. */
.dev-server-shape { cursor: pointer; }
.dev.selected .dev-server-shape { stroke: var(--marigold) !important; stroke-width: 3; }
.dev-server-slot {
  fill: var(--white);
  pointer-events: none;
}
/* Red drop-shadow flash applied to a device when a packet is dropped at
   it. Triggered by Render.flashBlock(). The animation is scoped to the
   icon's shape elements so the surrounding text labels (hostname,
   sublabel, port labels, badges) stay legible and don't get the red
   halo bleeding into them. */
@keyframes dev-blocked-flash-anim {
  0%   { filter: drop-shadow(0 0 0   rgba(220, 38, 38, 0.0)); }
  20%  { filter: drop-shadow(0 0 14px rgba(220, 38, 38, 0.95)); }
  100% { filter: drop-shadow(0 0 0   rgba(220, 38, 38, 0.0)); }
}
.dev.dev-blocked-flash .dev-image,
.dev.dev-blocked-flash .dev-cloud-shape,
.dev.dev-blocked-flash .dev-router-shape,
.dev.dev-blocked-flash .dev-firewall-shape,
.dev.dev-blocked-flash .dev-server-shape {
  animation: dev-blocked-flash-anim 0.7s ease-out;
}
/* Same shape as dev-blocked-flash but blue, used when a router drops a
   packet because it has no matching route to the destination. */
@keyframes dev-noroute-flash-anim {
  0%   { filter: drop-shadow(0 0 0   rgba(56, 139, 253, 0.0)); }
  20%  { filter: drop-shadow(0 0 14px rgba(56, 139, 253, 0.95)); }
  100% { filter: drop-shadow(0 0 0   rgba(56, 139, 253, 0.0)); }
}
.dev.dev-noroute-flash .dev-image,
.dev.dev-noroute-flash .dev-cloud-shape,
.dev.dev-noroute-flash .dev-router-shape,
.dev.dev-noroute-flash .dev-firewall-shape,
.dev.dev-noroute-flash .dev-server-shape {
  animation: dev-noroute-flash-anim 0.7s ease-out;
}
/* C10: green glow that fires on the device the Ctrl+F search jumps
   to. Distinct color from the blocked (red) / no-route (blue) flashes
   so the three never get confused on a busy canvas. */
@keyframes dev-search-flash-anim {
  0%   { filter: drop-shadow(0 0 0   rgba(0, 105, 78, 0.0)); }
  20%  { filter: drop-shadow(0 0 14px rgba(0, 105, 78, 0.95)); }
  100% { filter: drop-shadow(0 0 0   rgba(0, 105, 78, 0.0)); }
}
.dev.dev-search-flash .dev-image,
.dev.dev-search-flash .dev-cloud-shape,
.dev.dev-search-flash .dev-router-shape,
.dev.dev-search-flash .dev-firewall-shape,
.dev.dev-search-flash .dev-server-shape {
  animation: dev-search-flash-anim 0.7s ease-out;
}
/* C10: floating hostname-search palette. Opens on Ctrl/Cmd+F, pinned
   top-centre of the viewport, narrow enough to leave the canvas
   visible behind it. Lives at z-index 2100 so it sits above the
   walkthrough panel and minimap. */
.search-palette {
  position: fixed;
  left: 50%;
  top: 88px;
  transform: translateX(-50%);
  width: min(480px, 92vw);
  background: var(--surface, #fff);
  color: var(--ink, #222);
  border: 1px solid var(--green, #00694E);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
  z-index: 2100;
  font-family: var(--font-body, system-ui, sans-serif);
  font-size: 14px;
  overflow: hidden;
}
.search-palette-input {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border: 0;
  border-bottom: 1px solid var(--hairline, #c9cdb4);
  background: transparent;
  color: inherit;
  font: inherit;
  outline: none;
}
.search-palette-list {
  list-style: none;
  margin: 0;
  padding: 4px 0;
  max-height: 320px;
  overflow-y: auto;
}
.search-palette-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 12px;
  cursor: pointer;
}
.search-palette-item.is-active,
.search-palette-item:hover {
  background: var(--surface-2, #f1f4ec);
}
.search-palette-host {
  font-weight: 500;
}
.search-palette-type {
  color: var(--ink-muted, #667);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.search-palette-empty {
  padding: 8px 12px;
  color: var(--ink-muted, #667);
  font-style: italic;
}
table.firewall-rules input,
table.firewall-rules select { width: 100%; }
/* Per-column minimum widths so the 12-13 column rule table never squashes
   its inputs into illegible slivers (the selects clipping "deny"/"any", the
   CIDR fields truncating). Each cell holds at its min; when the sum exceeds
   the card the modal body (overflow:auto) scrolls horizontally instead. */
table.firewall-rules { min-width: 880px; }
table.firewall-rules th { white-space: nowrap; }
table.firewall-rules select { min-width: 62px; }
table.firewall-rules .fw-rule-number { min-width: 44px; }
table.firewall-rules .fw-rule-net  { min-width: 116px; }
table.firewall-rules .fw-rule-port { min-width: 58px; }
table.firewall-rules .fw-rule-desc { min-width: 110px; }
/* Width override for the per-rule on/off checkbox - full-width looks weird
   on a single tickbox. */
table.firewall-rules input[type="checkbox"] { width: auto; }
/* B17: read-only ACL hit-count cell. Numeric, centered, muted so it
   reads as a meter rather than an editable field. */
table.firewall-rules .fw-rule-hits {
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted, #667);
  white-space: nowrap;
}
/* Disabled rule visual: grey + struck through so paused rules read as
   "still here, just not active". The On checkbox stays clickable. */
table.firewall-rules tr.rule-disabled td {
  opacity: 0.45;
  text-decoration: line-through;
}
table.firewall-rules tr.rule-disabled td:first-child {
  opacity: 1;
  text-decoration: none;
}
/* Drag-to-reorder handle on firewall rules. The cell sits in the far
   left column; the glyph is a small "≡" the user grabs to reposition
   the row inside the rule list. .fw-rule-dragging dims the source
   row; .fw-rule-drop-target adds a top border to the row the cursor
   is currently above so the user sees where the rule will land. */
table.firewall-rules .fw-rule-handle-cell {
  width: 18px; padding: 4px 2px; text-align: center;
  color: var(--putnam);
}
table.firewall-rules .fw-rule-handle {
  display: inline-block; line-height: 1;
  font-size: 14px; user-select: none;
  cursor: grab;
}
table.firewall-rules .fw-rule-handle:active { cursor: grabbing; }
table.firewall-rules tr.fw-rule-dragging { opacity: 0.4; }
table.firewall-rules tr.fw-rule-drop-target td { border-top: 2px solid var(--green); }
.dev-label {
  fill: var(--green-dark); font-family: var(--font-body);
  font-size: 14px; font-weight: bold; text-anchor: middle;
  cursor: move;
}
.dev-sublabel {
  fill: var(--putnam); font-family: var(--font-mono);
  font-size: 13px; text-anchor: middle;
  pointer-events: none;
}
/* When the name is positioned to overlap the icon, paint a white halo
   underneath the glyphs so the text stays legible against any color. */
.dev-label.label-overlay,
.dev-sublabel.label-overlay {
  paint-order: stroke;
  stroke: var(--white);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.dev-badge {
  fill: var(--marigold); stroke: var(--white); stroke-width: 1;
}
.dev-badge-nat      { fill: var(--marigold); }
.dev-badge-dhcp     { fill: var(--green); }
/* Distinct shade for "this iface acquired its IP via DHCP" so it reads
   differently from the green "serves DHCP" badge. Cool blue keeps it
   in the same visual family as the standard packet color. */
.dev-badge-dhcp-use { fill: #2A6BB1; }
/* Red variant: DHCP server enabled on one or more VLAN subinterfaces of a
   trunk port. Clickable - opens the router's DHCP tab. */
.dev-badge-dhcp-vlan { fill: #C0392B; }
.dev-badge-clickable { cursor: pointer; }
.dev-badge-clickable:hover { stroke: #FFE066; stroke-width: 1.5; }
.dev-badge-text {
  fill: var(--white); font-family: var(--font-body);
  font-size: 11px; font-weight: bold; text-anchor: middle;
  letter-spacing: 0.3px;
  pointer-events: none;
}

/* B11: address-conflict halo. Lights every flagged client/server
   with a yellow drop-shadow glow so the user can spot a misconfigured
   device at a glance. The hover tooltip carries the explanation -
   this is just the visual flag. */
.dev[data-addr-conflict="true"] {
  filter: drop-shadow(0 0 4px #f0c14b) drop-shadow(0 0 6px #f0c14b);
}

.port {
  fill: var(--white); stroke: var(--green); stroke-width: 1.5;
  cursor: crosshair;
}
.port.wired { fill: var(--green); }
.port:hover { stroke: var(--marigold); stroke-width: 2.5; }
.port.pending { stroke: var(--marigold); stroke-width: 2.5; fill: var(--marigold); }
/* B10: admin-down NICs paint the port circle red so the
   "wire is connected, traffic isn't flowing" state reads at
   a glance. Beats out .wired's green fill via specificity
   from the second class selector. */
.port.admin-down { fill: var(--error-fg, #c5221f); stroke: var(--error-fg, #c5221f); }
.port.admin-down.wired { fill: var(--error-fg, #c5221f); }

.link {
  stroke: var(--green-dark); stroke-width: 2.5;
  cursor: pointer;
  fill: none;
  pointer-events: none;
}

/* RF Wall (D3): pen-tool polyline annotation. Mid-thickness stroke
   in a desaturated brown/tan so it reads as a physical wall, not a
   fiber wire. Lives in layer-walls between zones and links so wires
   draw on top. The hit area is wider for easy clicking. Selection
   adds a subtle glow halo. */
.wall-line {
  stroke: #a08060;
  stroke-width: 4;
  stroke-linecap: round;
  fill: none;
  pointer-events: none;
}
.wall-hit {
  stroke: transparent;
  stroke-width: 14;
  fill: none;
  pointer-events: stroke;
  cursor: pointer;
}
.wall-hit:hover + .wall-line {
  stroke: #c5a376;
  stroke-width: 5;
}
.wall.selected .wall-line {
  stroke: #4DB6FF;
  filter: drop-shadow(0 0 4px rgba(77, 182, 255, 0.7));
}
.wall[data-material="concrete"] .wall-line { stroke: #6b6b6b; }
.wall[data-material="brick"]    .wall-line { stroke: #b85040; }
.wall[data-material="glass"]    .wall-line { stroke: #88b8d0; stroke-dasharray: 8 4; }
.wall[data-material="wood"]     .wall-line { stroke: #8b5a2b; }
.wall[data-material="drywall"]  .wall-line { stroke: #d4c0a0; }
/* Pen-tool draw mode: cursor flips to crosshair across the canvas;
   a small fixed badge sits top-right reminding the user to press
   Esc. The wall-draw-preview circle marks the most-recent point so
   the next click chains visibly. */
body.wall-draw-mode #canvas { cursor: crosshair; }
.wall-draw-preview {
  fill: #4DB6FF;
  stroke: #ffffff;
  stroke-width: 1.5;
  pointer-events: none;
}
.wall-draw-badge {
  position: fixed;
  top: 60px;
  right: 16px;
  z-index: 1000;
  background: #4DB6FF;
  color: #ffffff;
  padding: 6px 12px;
  border-radius: 6px;
  font-weight: bold;
  font-size: 13px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
  animation: wall-draw-pulse 2s ease-in-out infinite;
}
@keyframes wall-draw-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.04); }
}
/* Tool-mode palette item (RF Wall): looks like the others but
   click-to-arm rather than drag-to-drop, so we tweak the cursor. */
.palette-item.palette-tool-item { cursor: pointer; }
.palette-item.palette-tool-item:hover { background: rgba(77, 182, 255, 0.12); }

/* Transparent hit area for click/contextmenu. Wider than the visible
   stroke and pointer-events:stroke so the entire 14-px region is
   clickable - including the gaps in dashed wifi links. Sits as the
   previous sibling of the visible line so :hover can light up that
   sibling via the + combinator. */
.link-hit {
  stroke: transparent;
  stroke-width: 14;
  fill: none;
  pointer-events: stroke;
  cursor: pointer;
}
.link-hit:hover + .link { stroke: var(--marigold); stroke-width: 3.5; }
.link-hit:hover + .link.trunk { stroke: var(--marigold); stroke-width: 6; }
.link-hit:hover + .link.wifi { stroke: #58d68d; stroke-width: 3.5; }
.link-hit:hover + .link.wifi-unassoc { stroke: #E74C3C; stroke-width: 3.5; }
/* AP wired-uplink hidden mode: the visible line and its hit area are
   removed from the canvas. Packet animation still uses the wire path
   underneath, so traffic continues to flow. */
.link.link-hidden, .link-hit.link-hidden { display: none; }
/* STP-blocked link: a redundant switch-to-switch wire that the spanning
   tree has eliminated. Drawn dashed in muted gray so it's clearly
   present (still cabled) but visibly NOT carrying traffic. Stays under
   the trunk/wifi color rules below since this rule comes first - those
   colors win for active forwarding paths. */
.link.link-stp-blocked {
  stroke: #888 !important;
  stroke-width: 2.5 !important;
  stroke-dasharray: 8 5 !important;
  opacity: 0.55;
}
.link-hit.link-stp-blocked { cursor: help; }
.link-hit:hover + .link.link-stp-blocked { stroke: #aaa !important; }

/* Manually suspended link: dashed orange so it reads as "the user
   turned this off" instead of STP's "spanning tree turned this off".
   Functionally identical to a delete (rebuildIndex skips it) but the
   visible wire stays so the user can resume any time via right-click.
   Sits above the trunk/wifi rules so the suspend styling wins for any
   wire type. */
.link.link-suspended {
  stroke: #d97706 !important;
  stroke-width: 2.5 !important;
  stroke-dasharray: 4 4 !important;
  opacity: 0.6;
}
.link-hit.link-suspended { cursor: help; }
.link-hit:hover + .link.link-suspended { stroke: #f59e0b !important; }

/* Bandwidth display: small bps label at the midpoint of any link
   currently carrying traffic, only shown when the toolbar Bandwidth
   badge is on. Color tiers communicate load at a glance: trace=light
   gray, low=blue, mid=green, high=orange, peak=red. The off state is
   reserved for symmetry but unused (idle links drop the label). */
.link-bw-label { pointer-events: none; }
.link-bw-bg {
  fill: #ffffffd9;
  stroke: #00000022;
  stroke-width: 1;
}
.link-bw-text {
  font: 10px/1 system-ui, -apple-system, sans-serif;
  font-weight: 600;
  fill: #54595e;
}
.link-bw-label.bw-tier-trace .link-bw-text { fill: #6a7280; }
.link-bw-label.bw-tier-low   .link-bw-text { fill: #1f6feb; }
.link-bw-label.bw-tier-mid   .link-bw-text { fill: #1f9d55; }
.link-bw-label.bw-tier-high  .link-bw-text { fill: #c97206; }
.link-bw-label.bw-tier-peak  .link-bw-text { fill: #fff5f5; font-weight: 700; }
/* Peak (cap-binding) badge tints the whole pill red so it reads at a
   glance instead of just changing text color on a white background.
   Border darkens to match. */
.link-bw-label.bw-tier-peak .link-bw-bg {
  fill: #c5221f;
  stroke: #7a0c0a;
}
/* Drops/s sub-line: small bold yellow on the red pill, dim red on the
   normal pill (only ever non-empty on a rate-limited link, but the
   peak tier is by far the common case). */
.link-bw-drop {
  font: 9px/1 system-ui, -apple-system, sans-serif;
  font-weight: 700;
  fill: #c5221f;
  pointer-events: none;
}
.link-bw-label.bw-tier-peak .link-bw-drop { fill: #ffd60a; }

/* Sniff indicator: small "eye" badge at the midpoint of any link with an
   open sniffer window. Click toggles the sniffer off; right-click opens
   the regular link context menu. */
.link-sniff-marker { cursor: pointer; }
.link-sniff-marker .lsm-bg   { fill: #1f2933; stroke: #F2C744; stroke-width: 1.5; }
.link-sniff-marker .lsm-ring { fill: none;     stroke: #F2C744; stroke-width: 1.2; }
.link-sniff-marker .lsm-dot  { fill: #F2C744; }
.link-sniff-marker:hover .lsm-bg { fill: #2a3441; }
.link.active {
  stroke-dasharray: 8 6;
  animation: link-pulse 0.8s linear infinite;
}
/* Trunk links carry tagged VLAN traffic; paint them blue and twice as
   thick as access links so the topology reads at a glance. */
.link.trunk         { stroke: #1f6feb; stroke-width: 5; }
.link.trunk:hover   { stroke: #58a6ff; stroke-width: 6; }
.link.trunk.active  { stroke-width: 5; }
/* WiFi association links: green dashed once associated, red dashed before
   association (or after disassociate). Wifi styling wins over trunk blue
   so it's clear at a glance which wires are wireless. */
.link.wifi              { stroke: #2ECC71; stroke-width: 2.5; stroke-dasharray: 6 4; }
.link.wifi:hover        { stroke: #58d68d; stroke-width: 3.5; }
.link.wifi.trunk        { stroke: #2ECC71; stroke-dasharray: 6 4; }
.link.wifi-unassoc      { stroke: #C0392B; stroke-width: 2.5; stroke-dasharray: 4 4; }
.link.wifi-unassoc:hover{ stroke: #E74C3C; stroke-width: 3.5; }

/* RF Obstruction shadow cone: cast away from each AP through each RFO.
   Fill is a per-shadow linear gradient (set inline by buildRfoShadow-
   Polygon), opaque near the RFO and fading to transparent at the far
   end. Side edges are softened by a Gaussian-blur filter on the
   wrapping <g>. The whole layer is non-interactive so it never steals
   clicks from devices/links below. */
.rfo-shadow-wrap, polygon.rfo-shadow {
  pointer-events: none;
}
polygon.rfo-shadow { stroke: none; }
/* Per-client "My Perspective" cone outline: same cone polygon, drawn as
   a stroke-only sibling of the gray fill so it stays crisp (the rfo-
   shadow-wrap groups carry the Gaussian blur). */
polygon.rfo-perspective-outline { pointer-events: none; }

/* ---- RF Jammer ---- */
/* Translucent red emission region. The wrapping <g> pulses opacity so
   the user can see which devices are actively jamming without needing
   to look at the icon. Falloff comes from the radial gradient inside;
   the keyframes only animate group opacity. */
.rfj-jam-wrap, .rfj-jam {
  pointer-events: none;
}
.rfj-jam-wrap {
  animation: rfj-region-pulse 1.6s ease-in-out infinite;
}
@keyframes rfj-region-pulse {
  0%, 100% { opacity: 0.65; }
  50%      { opacity: 1.00; }
}
/* Pulsing red halo around an active jammer's device icon so the on-
   state is unmistakable even when the region is small or off-screen
   in the user's current pan. The halo uses drop-shadow so it tracks
   the icon's transparent edges instead of painting a square. */
g.dev.rfj-on image.dev-image {
  animation: rfj-icon-pulse 1.6s ease-in-out infinite;
}
@keyframes rfj-icon-pulse {
  0%, 100% { filter: drop-shadow(0 0 4px rgba(220, 38, 38, 0.7))
                     drop-shadow(0 0 8px rgba(220, 38, 38, 0.4)); }
  50%      { filter: drop-shadow(0 0 9px rgba(220, 38, 38, 1.0))
                     drop-shadow(0 0 18px rgba(220, 38, 38, 0.7)); }
}

/* WiFi "searching" state: the client/server has at least one remembered
   network but nothing wired on e0 - the scanner is hunting for an
   in-range matching AP. A pulsing blue halo around the icon makes the
   state visible at a glance, and the dashed outline reads as "I'm
   actively looking for something to connect to". */
g.dev[data-wifi-searching="true"] image.dev-image,
g.dev[data-wifi-searching="true"] rect.dev-server-shape {
  animation: wifi-searching-pulse 1.4s ease-in-out infinite;
}
@keyframes wifi-searching-pulse {
  0%, 100% { filter: drop-shadow(0 0 0 transparent); }
  50%      { filter: drop-shadow(0 0 7px #1f6feb) drop-shadow(0 0 14px rgba(31,111,235,0.45)); }
}
g.dev[data-wifi-searching="true"] .dev-image-frame {
  stroke: #1f6feb;
  stroke-dasharray: 4 3;
  fill: none;
  opacity: 0.7;
  animation: wifi-searching-spin 4s linear infinite;
}
@keyframes wifi-searching-spin {
  to { stroke-dashoffset: -28; }
}

/* AP transmit-power glow. The AP's <g> picks up data-tx-power=1..5 in
   renderDevice; the filter intensity below scales with the level so it
   reads as "this AP is broadcasting weakly / strongly" at a glance.
   Normal (3) gets no glow so the default state stays clean. */
g.dev[data-tx-power="1"] image.dev-image { filter: drop-shadow(0 0 2px #C0392B); opacity: 0.8; }
g.dev[data-tx-power="2"] image.dev-image { filter: drop-shadow(0 0 2px #E67E22); opacity: 0.9; }
g.dev[data-tx-power="3"] image.dev-image { /* normal: no decoration */ }
g.dev[data-tx-power="4"] image.dev-image { filter: drop-shadow(0 0 5px #2ECC71); }
g.dev[data-tx-power="5"] image.dev-image { filter: drop-shadow(0 0 9px #2ECC71) drop-shadow(0 0 18px rgba(46,204,113,0.5)); }

/* WiFi signal-strength bars next to a client's port label, lit up by the
   .sig-N modifier (0..4) based on RSSI distance from the AP. */
.port-signal rect              { fill: #c0c0c0; opacity: 0.45; }
.port-signal.sig-4 rect.b1,
.port-signal.sig-4 rect.b2,
.port-signal.sig-4 rect.b3,
.port-signal.sig-4 rect.b4     { fill: #2ECC71; opacity: 1; }
.port-signal.sig-3 rect.b1,
.port-signal.sig-3 rect.b2,
.port-signal.sig-3 rect.b3     { fill: #2ECC71; opacity: 1; }
.port-signal.sig-2 rect.b1,
.port-signal.sig-2 rect.b2     { fill: #F1C40F; opacity: 1; }
.port-signal.sig-1 rect.b1     { fill: #E67E22; opacity: 1; }
.port-signal.sig-0 rect        { fill: #C0392B; opacity: 1; }
/* W1: band number ("2"/"5") just left of the bars, naming the radio the
   client is on. Marigold (theme-adaptive, like the other accents) with a
   background-colored halo so it separates from the port label and bars
   and stays legible on either theme. */
.port-signal-band {
  font-family: var(--font-mono); font-size: 9px; font-weight: 700;
  fill: var(--marigold);
  paint-order: stroke; stroke: var(--bg); stroke-width: 2px;
  stroke-linejoin: round; pointer-events: none;
}
/* C14: Scenario Builder - full-width fields, Hotrack-style uppercase
   section headers, and a collapsed Administrative Tools drawer for the
   dev exports (output preview + Copy CSV row / Copy state code). */
#create-scenario-modal textarea { width: 100%; box-sizing: border-box; }
/* C16: Scenario Builder runs as a NON-modal floating window. The .modal
   container stops being a full-screen click-blocking overlay - it's
   click-through (pointer-events:none) so the canvas underneath stays
   editable - and the card floats top-right, draggable by its header.
   Sits below true modals (z-index 200) but above the canvas. */
.modal.cs-floating {
  display: block;
  pointer-events: none;
  z-index: 95;
}
.modal.cs-floating .modal-card {
  pointer-events: auto;
  position: fixed;
  top: 64px; right: 16px;
  width: min(440px, 92vw);
  max-height: calc(100vh - 96px);
}
/* The paste-solution box is an editable target, so keep the standard
   green field border rather than the muted display-only preview look. */
#create-scenario-paste-solution {
  width: 100%; box-sizing: border-box;
  font-family: var(--font-mono); font-size: 11px;
  white-space: pre; overflow: auto; resize: vertical; word-break: break-all;
}
#create-scenario-paste-info.cs-paste-bad { color: #c62828; }
#create-scenario-modal .cs-section {
  margin: 14px 0 4px; font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--green-dark);
}
#create-scenario-modal .cs-section em {
  text-transform: none; letter-spacing: 0; font-weight: 400; opacity: 0.75;
}
.cs-admin { margin: 12px 0 0; border-top: 1px solid rgba(128,128,128,0.3); padding-top: 6px; }
.cs-admin-summary {
  cursor: pointer; color: var(--green-dark); font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; padding: 4px 0;
  user-select: none; list-style: none;
}
.cs-admin-summary::-webkit-details-marker { display: none; }
.cs-admin-summary::before { content: "\25B8\00a0"; }   /* > collapsed */
.cs-admin[open] .cs-admin-summary::before { content: "\25BE\00a0"; }  /* v expanded */
.cs-subh { margin: 8px 0 4px; font-size: 11px; font-weight: 600; opacity: 0.85; }
.cs-admin-actions { margin-top: 6px; }
/* Read-only output preview - styled like Hotrack's: a muted, inset panel
   with a thin neutral border (NOT the green editable-field border), so it
   reads as display-only, not an input you can type into. */
.create-scenario-output {
  font-family: var(--font-mono); font-size: 11px;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--ink);
  white-space: pre; overflow: auto; resize: vertical; word-break: break-all;
}
.create-scenario-output:focus {
  outline: none;
  border-color: var(--hairline);
  background: var(--surface-2);
}
/* W1: the whole indicator is draggable like an interface label. */
.port-signal-draggable { cursor: move; }
.port-signal-draggable rect { pointer-events: auto; }
/* W1: active-radio badge in the white center-top of the AP icon -
   "2" / "5" / "2/5". Orange to read as part of the AP, bold for scale. */
.ap-radio-badge {
  font-family: var(--font-body); font-size: 13px; font-weight: 800;
  fill: #C2410C; letter-spacing: 0.3px; pointer-events: none;
  paint-order: stroke; stroke: #fff; stroke-width: 2px; stroke-linejoin: round;
}
/* W2: the AP's in-use channel(s), in blue, just below the band badge. */
.ap-channel-badge {
  font-family: var(--font-body); font-size: 10px; font-weight: 700;
  fill: #1d4ed8; letter-spacing: 0.3px; pointer-events: none;
  paint-order: stroke; stroke: #fff; stroke-width: 2px; stroke-linejoin: round;
}
@keyframes link-pulse {
  to { stroke-dashoffset: -28; }
}
/* Animated dot drawn on the overlay layer while a packet is traversing
   the path (one per ping / trace probe). Render.emitPacket adds & removes
   these. */
.packet-dot {
  pointer-events: none;
  filter: drop-shadow(0 0 4px rgba(121, 192, 255, 0.7));
  opacity: 0.95;
}
/* Packet ball wraps the dot + an optional VLAN label so both ride together
   along the wire path. The .tagged class is added by Render.emitPacket
   only on segments where the frame is actually carrying an 802.1Q tag,
   i.e. trunk-and-not-native legs. */
.packet-ball { pointer-events: none; }
.packet-ball.tagged .packet-dot {
  stroke: #F2C744; stroke-width: 2;
  filter: drop-shadow(0 0 5px rgba(242, 199, 68, 0.9));
}
.packet-ball .packet-vlan-label {
  fill: #F2C744; font-family: var(--font-mono);
  font-size: 9px; font-weight: bold;
  text-anchor: middle;
  paint-order: stroke;
  stroke: rgba(0,0,0,0.55); stroke-width: 2px;
  pointer-events: none;
}

.port-label {
  fill: var(--green-dark); font-family: var(--font-mono);
  font-size: 13px; pointer-events: none;
}
.port-label-static {
  fill: #051c14; font-family: var(--font-mono);
  font-size: 13px; font-weight: 700; pointer-events: none;
  paint-order: stroke; stroke: var(--white); stroke-width: 3.5px;
  stroke-linejoin: round;
  user-select: none;
}
/* The port-name labels are draggable for fine layout control. Re-enable
   pointer events and show a move cursor so the affordance is visible. */
.port-label-static.draggable-port-label {
  pointer-events: all;
  cursor: move;
}
.port-label-static.draggable-port-label:hover {
  fill: var(--marigold);
}

/* ----- Bottom panel ----- */
#bottom-panel {
  grid-column: 3; grid-row: 3;
  background: var(--bg-alt);
  display: flex; flex-direction: column;
  position: relative;
  min-height: 0; min-width: 0;
  overflow: hidden;
}
#bottom-toggle {
  position: absolute; top: 4px; right: 6px;
  width: 24px; height: 24px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 4px;
  color: var(--green-dark);
  cursor: pointer;
  z-index: 6;
  /* Chevron glyph 2x its prior 14px size so the toggle affordance is
     visible at a glance against the busy console-tab bar. */
  font-size: 28px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  font-family: inherit;
}
#bottom-toggle:hover { border-color: var(--green); color: var(--green); }
body.bottom-collapsed #terminal-bodies,
body.bottom-collapsed #bottom-empty { display: none; }
.tab-bar {
  display: flex; flex-wrap: wrap;
  background: var(--bg-alt);
  border-bottom: 1px solid var(--hairline);
  min-height: 32px;
  padding: 4px 36px 0 4px; /* leave room for #bottom-toggle */
}
.tab-bar .tab-item {
  display: flex; align-items: center; gap: 6px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-bottom: none;
  border-radius: 6px 6px 0 0;
  padding: 6px 10px;
  margin-right: 2px;
  cursor: pointer;
  font-size: 12px;
  color: var(--green-dark);
  user-select: none;
}
.tab-bar .tab-item.active {
  background: var(--term-bg); color: var(--term-fg);
  border-color: var(--green-dark);
}
.tab-bar .tab-item .close,
.tab-bar .tab-item .popout {
  cursor: pointer; opacity: 0.65;
  font-size: 11px; line-height: 1;
  user-select: none;
}
.tab-bar .tab-item .popout:hover { opacity: 1; color: var(--green); }
.tab-bar .tab-item .close:hover  { opacity: 1; color: var(--term-err); }
/* Edit DNS link in the docked tab. Inline-style chip so it matches
   the lightweight tab UI without the heavier btn-secondary look.
   Switches to a high-contrast palette when its tab is active so the
   chip stays legible against the dark terminal background. */
.tab-bar .tab-item .tab-edit-dns {
  cursor: pointer;
  font-size: 10px; line-height: 1; font-weight: bold;
  letter-spacing: 0.3px;
  padding: 2px 6px;
  border: 1px solid #2C8264;
  border-radius: 8px;
  color: #2C8264;
  background: transparent;
  user-select: none;
}
.tab-bar .tab-item .tab-edit-dns:hover {
  background: #2C8264; color: #fff;
}
.tab-bar .tab-item.active .tab-edit-dns {
  border-color: #4FD1A5;
  color: #4FD1A5;
}
.tab-bar .tab-item.active .tab-edit-dns:hover {
  background: #4FD1A5; color: #0D1117;
}
/* Implicit self-record row in the DNS modal: the DNS server's own
   hostname mapped to its own effective IP. Read-only, can't be
   deleted - it's auto-derived from the device, not stored in
   records[]. Tinted to read as "info / system" rather than a
   user-editable row. */
.dns-records-table .dns-self-record { background: rgba(44, 130, 100, 0.06); }
.dns-records-table .dns-self-record input[readonly] {
  background: #f5f7fa;
  color: #54595e;
  cursor: default;
}
.dns-records-table .dns-self-badge {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 8px;
  font-size: 10px;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  background: #2C8264;
  color: #fff;
}
.dns-section {
  border: 1px solid #d0d4d9;
  border-radius: 4px;
  margin-bottom: 10px;
  background: #fafbfc;
}
.dns-section[open] { background: var(--surface); }
.dns-section-summary {
  cursor: pointer;
  padding: 8px 12px;
  list-style: none;
  user-select: none;
  font-weight: 600;
  color: #2c8264;
  font-size: 13px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  gap: 6px;
}
.dns-section-summary::-webkit-details-marker { display: none; }
.dns-section-summary::before {
  content: "\25B8";
  display: inline-block;
  transition: transform 120ms ease;
  font-size: 10px;
  color: #6c757d;
}
.dns-section[open] > .dns-section-summary::before {
  transform: rotate(90deg);
}
.dns-section-body {
  padding: 8px 12px 12px;
  border-top: 1px solid #e7eaee;
}
.dns-section-title { display: inline; }
.dns-zone-row, .dns-fwd-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}
.dns-zone-multi-row .dns-zone-input { flex: 0 0 180px; }
.dns-zone-multi-row .dns-zone-soa-field { flex: 1; }
.dns-zone-label, .dns-fwd-label {
  flex: 0 0 90px;
  font-size: 12px;
  color: #54595e;
}
.dns-zone-input, .dns-fwd-input, .dns-zone-soa-field {
  flex: 1;
  padding: 4px 8px;
  border: 1px solid #d0d4d9;
  border-radius: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
}
.dns-soa-hint, .dns-fwd-hint, .dns-zone-empty {
  margin: 4px 0 0 0;
  font-size: 11px;
  color: #6c757d;
}
.dns-zone-add, .dns-record-add {
  margin: 8px 0 0 0;
}
.dns-zone-select {
  padding: 3px 6px;
  border: 1px solid var(--hairline);
  border-radius: 4px;
  font-size: 12px;
  background: var(--surface);
  color: var(--ink);
  min-width: 130px;
}
.dns-zone-pill {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 8px;
  font-size: 11px;
  background: #e8f1ed;
  color: #2c8264;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.tab-bar .tab-item.detached { display: none; }
.tab-bar .tab-item.sniffer-tab {
  /* Tinted to read as a sniffer tab next to terminal tabs in the same bar. */
  border-color: #4a3b8c;
  color: #4a3b8c;
}
.tab-bar .tab-item.sniffer-tab.active {
  background: var(--term-bg);
  color: #c0a8ff;
  border-color: #4a3b8c;
}
/* Notes tab - amber, matches the floating notes title bar so the
   docked + floating views read as the same window. */
.tab-bar .tab-item.notes-tab {
  border-color: #b85c00;
  color: #b85c00;
}
.tab-bar .tab-item.notes-tab.active {
  background: var(--surface, #fff);
  color: #b85c00;
  border-color: #b85c00;
}
/* When the notes body lives inside #terminal-bodies (docked mode),
   give it a white background + body font + padding so it reads
   correctly in the bottom panel (the panel's default is a terminal-
   black background which would clash with the notes text). */
#terminal-bodies .notes-body {
  background: var(--surface, #fff);
  color: var(--ink, #222);
  font-family: var(--font-body, system-ui, sans-serif);
  font-size: 14px;
  padding: 12px 14px;
  overflow: auto;
}

/* ----- Floating console window (detached terminal tab) ----- */
.floating-console {
  position: fixed; z-index: 90;
  background: var(--term-bg);
  border: 2px solid var(--green-dark);
  border-radius: 8px;
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5);
  display: flex; flex-direction: column;
  min-width: 360px; min-height: 180px;
  resize: both; overflow: hidden;
}
.floating-console.dragging { box-shadow: 0 18px 48px rgba(0, 0, 0, 0.6); }
.floating-console .fc-title {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px;
  background: var(--green-dark); color: var(--white);
  cursor: move;
  user-select: none;
  flex: 0 0 auto;
}
.floating-console .fc-name { font-weight: bold; flex: 1; font-size: 13px; }
.floating-console .fc-reattach {
  background: transparent; color: var(--white);
  border: 1px solid rgba(255,255,255,0.4); border-radius: 4px;
  font: inherit; font-size: 11px; padding: 2px 8px;
  cursor: pointer;
}
.floating-console .fc-reattach:hover { background: rgba(255,255,255,0.15); border-color: var(--white); }
.floating-console .fc-edit-dns {
  background: transparent; color: var(--white);
  border: 1px solid rgba(255,255,255,0.4); border-radius: 4px;
  font: inherit; font-size: 11px; padding: 2px 8px;
  cursor: pointer;
}
.floating-console .fc-edit-dns:hover { background: rgba(255,255,255,0.15); border-color: var(--white); }
.floating-console .fc-copy {
  background: transparent; color: var(--white);
  border: 1px solid rgba(255,255,255,0.4); border-radius: 4px;
  font: inherit; font-size: 13px; line-height: 1;
  padding: 2px 6px; cursor: pointer;
}
.floating-console .fc-copy:hover { background: rgba(255,255,255,0.15); border-color: var(--white); }
.floating-console .fc-close {
  background: transparent; color: var(--white);
  border: none; cursor: pointer;
  font-size: 18px; line-height: 1; padding: 0 4px;
  font-family: inherit;
}
.floating-console .fc-close:hover { background: rgba(255,255,255,0.15); }
.floating-console .fc-body {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  border-radius: 0 0 6px 6px;
}

/* ----- Session recorder (C9) ----- */
.floating-console.recorder-window {
  min-width: 380px; min-height: 0;
  resize: none;
}
.floating-console.recorder-window .fc-title {
  background: #6b3fa0;
}
.rec-body {
  padding: 10px 12px 12px 12px;
  display: flex; flex-direction: column;
  gap: 8px;
  color: var(--white, #fff);
  background: var(--term-bg);
  font-size: 13px;
}
.rec-status {
  font-weight: 600;
  padding: 4px 6px;
  background: rgba(255,255,255,0.05);
  border-radius: 4px;
}
.rec-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
.rec-btn {
  background: rgba(255,255,255,0.08);
  color: inherit;
  border: 1px solid rgba(255,255,255,0.25);
  border-radius: 4px;
  font: inherit;
  padding: 3px 10px;
  cursor: pointer;
}
.rec-btn:hover:not(:disabled) {
  background: rgba(255,255,255,0.18);
  border-color: rgba(255,255,255,0.5);
}
.rec-btn:disabled {
  opacity: 0.4;
  cursor: default;
}
.rec-speed {
  background: rgba(255,255,255,0.08);
  color: inherit;
  border: 1px solid rgba(255,255,255,0.25);
  border-radius: 4px;
  font: inherit;
  padding: 2px 4px;
  margin-left: auto;
}
.rec-speed:disabled { opacity: 0.4; }
.rec-scrub-row { flex-direction: column; align-items: stretch; gap: 2px; }
.rec-scrub { width: 100%; accent-color: #b18cf2; }
.rec-scrub-label {
  font-size: 11px;
  opacity: 0.7;
  font-family: ui-monospace, Consolas, monospace;
  min-height: 1.2em;
}

/* ----- Sniffer (link packet capture) ----- */
.floating-console.sniffer-window {
  min-width: 520px; min-height: 200px;
}
.floating-console.sniffer-window .fc-title {
  background: #4a3b8c; /* distinct from terminal title bar so the two windows are easy to tell apart */
}
.floating-console.sniffer-window .fc-body {
  overflow: hidden; background: var(--term-bg); padding: 0;
}
/* Body that can live in either a floating wrapper or docked into the
   bottom panel. Layout = toolbar on top, scrollable table below. */
.sniffer-body {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  background: var(--term-bg);
  min-height: 0;
}
.sniffer-toolbar {
  display: flex; align-items: center; gap: 6px;
  flex: 0 0 auto;
  padding: 4px 8px;
  background: rgba(255,255,255,0.04);
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.sniffer-toolbar .snf-filter-wrap {
  position: relative; flex: 1 1 auto; min-width: 100px;
  display: flex;
}
.sniffer-toolbar .snf-filter {
  flex: 1 1 auto; min-width: 0;
  background: rgba(0,0,0,0.25); color: var(--term-fg);
  border: 1px solid rgba(255,255,255,0.25); border-radius: 4px;
  padding: 2px 22px 2px 6px; font-family: var(--font-mono); font-size: 12px;
  outline: none;
}
.sniffer-toolbar .snf-filter:focus { border-color: rgba(255,255,255,0.6); }
.sniffer-toolbar .snf-filter.snf-filter-valid {
  /* Parsed as a Wireshark-style expression. */
  border-color: #3FB950;
  background: rgba(63,185,80,0.10);
}
.sniffer-toolbar .snf-filter.snf-filter-invalid {
  /* Did not parse - falling back to substring match. Hover for the error. */
  border-color: #EF4444;
  background: rgba(239,68,68,0.10);
}
.sniffer-toolbar .snf-filter-clear {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  display: none;
  width: 16px; height: 16px; padding: 0;
  border: none; background: transparent; color: var(--term-fg);
  font-size: 14px; line-height: 1; cursor: pointer;
  opacity: 0.55;
  border-radius: 50%;
}
.sniffer-toolbar .snf-filter-clear.visible { display: inline-flex; align-items: center; justify-content: center; }
.sniffer-toolbar .snf-filter-clear:hover { opacity: 1; background: rgba(255,255,255,0.15); }
.sniffer-toolbar button {
  background: transparent; color: var(--term-fg);
  border: 1px solid rgba(255,255,255,0.3); border-radius: 4px;
  font: inherit; font-size: 11px; padding: 2px 8px;
  cursor: pointer;
}
.sniffer-toolbar button:hover {
  background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.6);
}
.sniffer-toolbar .snf-pause.active {
  background: rgba(242,199,68,0.18); border-color: #F2C744; color: #F2C744;
}
.sniffer-toolbar .snf-follow.active {
  background: rgba(63,185,80,0.18); border-color: #3FB950; color: #3FB950;
}
.sniffer-toolbar .snf-menu {
  font-size: 14px; line-height: 1; padding: 0 8px; min-width: 28px;
}
.sniffer-scroll {
  flex: 1 1 auto; min-height: 0;
  overflow: auto;
}
.sniffer-table {
  width: 100%; border-collapse: collapse;
  font-family: var(--font-mono); font-size: 12px; color: var(--term-fg);
}
.sniffer-table thead th {
  position: sticky; top: 0; background: var(--green-dark); color: var(--white);
  padding: 4px 8px; text-align: left; font-weight: bold;
  border-bottom: 1px solid rgba(255,255,255,0.2);
  z-index: 1;
}
.sniffer-table tbody td {
  padding: 2px 8px;
  border-bottom: 1px solid rgba(255,255,255,0.06);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  user-select: text;
}
.sniffer-table .col-time  { width: 7em;  color: #8b949e; }
.sniffer-table .col-vlan  { width: 4em;  color: #c0a8ff; font-weight: bold; }
.sniffer-table .col-srcIp { width: 11em; }
.sniffer-table .col-dstIp { width: 11em; }
.sniffer-table .col-proto { width: 5em;  color: #79C0FF; }
.sniffer-table .col-port  { width: 5em;  color: #F2C744; }
.sniffer-table .col-dir   { width: 12em; color: #98c379; white-space: nowrap; }
.sniffer-table tbody td.col-info { white-space: normal; word-break: break-word; }
/* Wireshark-style row coloring. Tints are layered with low alpha so the
   dark terminal background still reads through. Hover uses an inset
   box-shadow overlay rather than a background change so it composes on
   top of the tint instead of replacing it. */
.sniffer-table tbody tr.ws-row-bad      { background: rgba(239,68,68,0.32);  color: #FFFF7A; }
.sniffer-table tbody tr.ws-row-icmp-err { background: rgba(239,68,68,0.32);  color: #FFD7D7; }
.sniffer-table tbody tr.ws-row-icmp     { background: #FCE0FF; }
.sniffer-table tbody tr.ws-row-icmp td  { color: #1a1a1a; }
.sniffer-table tbody tr.ws-row-arp      { background: rgba(255,252,156,0.14); }
.sniffer-table tbody tr.ws-row-dhcp     { background: rgba(255,235,59,0.12);  }
.sniffer-table tbody tr.ws-row-dns      { background: rgba(128,234,251,0.14); }
.sniffer-table tbody tr.ws-row-http     { background: rgba(144,238,144,0.16); }
.sniffer-table tbody tr.ws-row-tcp-handshake { background: rgba(160,160,160,0.18); }
.sniffer-table tbody tr.ws-row-tcp      { background: rgba(231,230,255,0.10); }
.sniffer-table tbody tr.ws-row-udp      { background: rgba(112,224,224,0.12); }
.sniffer-table tbody tr.ws-row-mgmt     { background: rgba(180,180,180,0.16); }
.sniffer-table tbody tr:hover { box-shadow: inset 0 0 0 9999px rgba(255,255,255,0.06); }
/* E3: NAT pre/post badge in the Info column. The "post" side
   (after a router has rewritten the source) is solid yellow so
   the rewrite is unmistakable on the wire; the "pre" side
   (before the router) is dimmer to read as "about to be
   translated." */
.sniffer-table .nat-badge {
  display: inline-block;
  padding: 0 6px;
  border-radius: 8px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10px;
  font-weight: bold;
  letter-spacing: 0.2px;
  margin-left: 4px;
  border: 1px solid currentColor;
}
.sniffer-table .nat-badge-post {
  background: rgba(242,199,68,0.22);
  color: #F2C744;
  border-color: rgba(242,199,68,0.7);
}
.sniffer-table .nat-badge-pre {
  background: rgba(121,192,255,0.16);
  color: #79C0FF;
  border-color: rgba(121,192,255,0.55);
}
.sniffer-connector {
  stroke: #F2C744; stroke-width: 1; stroke-dasharray: 4 4;
  opacity: 0.7; pointer-events: none; fill: none;
}

#terminal-bodies {
  flex: 1; position: relative;
  min-height: 0;
}
#bottom-empty {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--putnam); font-style: italic;
  pointer-events: none;
}

/* ----- Terminal ----- */
.terminal {
  position: absolute; inset: 0;
  background: var(--term-bg); color: var(--term-fg);
  font-family: var(--font-mono); font-size: 13px; line-height: 1.4;
  display: flex; flex-direction: column;
}
.terminal .screen {
  flex: 1; overflow-y: auto;
  padding: 8px 10px;
  white-space: pre-wrap;
  word-break: break-word;
}
.terminal .screen .ln { display: block; }
.terminal .screen .ln.ok    { color: var(--term-ok); }
.terminal .screen .ln.ok2   { color: var(--term-ok2); }
.terminal .screen .ln.err   { color: var(--term-err); }
.terminal .screen .ln.err2  { color: var(--term-err2); }
.terminal .screen .ln.warn  { color: var(--term-warn); }
.terminal .screen .ln.warn2 { color: var(--term-warn2); }
.terminal .screen .ln.info  { color: var(--term-info); }
.terminal .screen .ln.dim   { color: var(--term-dim); }
.terminal .screen .ln.accent{ color: var(--term-accent); }
.terminal .screen .ln a {
  color: var(--term-info);
  text-decoration: underline;
  cursor: pointer;
}
.terminal .screen .ln a:hover { color: var(--term-ok2); }

.terminal .input-row {
  display: flex; align-items: baseline;
  /* One extra space-width between the bash prompt's "$" and the
     active cursor so the typed command doesn't visually butt up
     against the prompt. The trailing space already in the prompt
     glyph + this gap feels like a real shell. */
  gap: 0.5ch;
  padding: 0 10px 6px 10px;
  background: var(--term-bg);
  border-top: none;
}
/* Hide the prompt + input while a long-running command (ping, trace,
   DHCP) is in flight. Restored when the command completes or is aborted. */
.terminal.busy .input-row { display: none; }
.terminal .prompt { font-weight: bold; flex: 0 0 auto; }
/* Bash-style colored prompt parts: user@host:~$ */
.terminal .p-user   { color: var(--term-ok2); font-weight: bold; }
.terminal .p-colon  { color: var(--term-fg); }
.terminal .p-path   { color: var(--term-info); font-weight: bold; }
.terminal .p-dollar { color: var(--term-fg); }
.terminal .screen .ln.prompt-line { color: var(--term-fg); }
.terminal .input {
  flex: 1 1 auto; min-width: 0;
  background: transparent !important;
  border: 0; outline: 0; box-shadow: none;
  color: var(--term-fg); font-family: inherit; font-size: inherit;
  padding: 0; margin: 0;
  caret-color: var(--term-fg);
  -webkit-appearance: none; appearance: none;
}
.terminal .input:focus { outline: 0; box-shadow: none; }
/* Defeat browser autofill repainting the input cream/yellow. */
.terminal .input:-webkit-autofill,
.terminal .input:-webkit-autofill:hover,
.terminal .input:-webkit-autofill:focus,
.terminal .input:-webkit-autofill:active {
  -webkit-box-shadow: 0 0 0 1000px var(--term-bg) inset !important;
  -webkit-text-fill-color: var(--term-fg) !important;
  caret-color: var(--term-fg) !important;
  transition: background-color 9999s ease-out 0s;
}

/* ----- Modals (general) ----- */
.modal {
  /* Above toolbar drawers (z-index 110) so an open modal's backdrop
     fully covers a stale drawer that didn't auto-close. Modals are
     the focused interaction layer. */
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center;
}
.modal-backdrop {
  position: absolute; inset: 0; background: rgba(2,66,48,0.45);
}
.modal-card {
  position: relative;
  background: var(--surface);
  border: 2px solid var(--green);
  border-radius: 10px;
  width: min(560px, 92vw);
  max-height: 88vh;
  overflow: hidden;
  display: flex; flex-direction: column;
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.modal-card.modal-wide  { width: min(820px, 96vw); }
.modal-card.modal-narrow { width: min(420px, 92vw); }
.modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 18px;
  background: var(--green-dark); color: var(--white);
  cursor: move;
  user-select: none;
}
/* Buttons inside the header (close X, etc.) keep their normal pointer so
   they don't look draggable. */
.modal-header button { cursor: pointer; }
.modal-header h2 { margin: 0; font-size: 16px; }
.modal-header .btn-icon { color: var(--white); }
.modal-header .btn-icon:hover { background: rgba(255,255,255,0.15); color: var(--white); }
.modal-body {
  padding: 18px;
  overflow-y: auto;
}
.modal-footer {
  padding: 12px 18px;
  border-top: 1px solid var(--hairline);
  display: flex; align-items: center; justify-content: flex-end;
  gap: 10px;
  background: var(--bg-alt);
}
.modal-footer .modal-error {
  flex: 1; color: var(--error-fg); font-size: 12px;
}

/* ----- Form fields ----- */
.field {
  display: block;
  margin-bottom: 12px;
}
.field > span {
  display: block;
  font-size: 12px; font-weight: bold; color: var(--green-dark);
  margin-bottom: 4px;
}
.field > span em { color: var(--putnam); font-style: normal; font-weight: normal; }
/* Always-visible help hint that sits between a field's label and its input.
   Quieter typography than the bold label so the eye still lands on the
   field name first. */
.field .field-hint {
  font-size: 11px; font-weight: normal; font-style: italic;
  color: var(--putnam);
  margin-bottom: 4px; line-height: 1.35;
}
input[type="text"], input[type="number"], select, textarea {
  width: 100%;
  background: var(--surface);
  border: 2px solid var(--green);
  border-radius: 4px;
  padding: 6px 8px;
  color: var(--ink);
}
input[type="text"]:focus, input[type="number"]:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--marigold);
  background: var(--bg-warmer);
}
input[type="checkbox"] { accent-color: var(--green); }
input[type="radio"]    { accent-color: var(--green); }
input[disabled], select[disabled] {
  background: var(--bg-alt); color: var(--putnam); opacity: 0.7;
}
.field-error {
  color: var(--error-fg);
  font-size: 11px;
  margin-top: 2px;
}
.field-warning {
  color: var(--warn-fg);
  font-size: 11px;
  margin-top: 2px;
}
.row { display: flex; gap: 10px; }
.row > .field { flex: 1; }
.muted { color: var(--putnam); }

/* ----- Router modal tabs ----- */
.modal-tabs {
  display: flex;
  background: var(--bg-alt);
  border-bottom: 1px solid var(--hairline);
}
.modal-tabs .tab {
  flex: 1;
  background: transparent; border: none;
  border-bottom: 3px solid transparent;
  padding: 10px 12px;
  cursor: pointer;
  font-weight: bold; color: var(--putnam);
}
.modal-tabs .tab[aria-selected="true"] {
  color: var(--green-dark);
  border-bottom-color: var(--green);
  background: var(--surface);
}
.tab-pane { padding: 16px 0; }

.iface-section {
  padding: 0;
  border: 1px solid var(--hairline);
  border-radius: 6px;
  margin-bottom: 14px;
  background: var(--bg-alt);
}
/* B2: DHCP relay rows inside the DHCP tab. Each row mirrors the
   pool-section padding so relay config visually nests under the
   pool list without an extra wrapper. */
.dhcp-relay-title { margin-top: 18px; margin-bottom: 4px; }
.dhcp-relay-hint { color: var(--ink-muted); font-size: 12px; margin: 0 0 10px; }
.dhcp-relay-row {
  display: flex; gap: 12px; align-items: flex-end;
  padding: 10px 14px;
  border: 1px solid var(--hairline);
  border-radius: 6px;
  margin-bottom: 8px;
  background: var(--bg-alt);
}
.dhcp-relay-row > .field { flex: 1; min-width: 120px; }
.dhcp-relay-row > .btn-link { white-space: nowrap; }
.iface-section > .iface-summary {
  display: flex; align-items: baseline; gap: 12px;
  padding: 10px 14px;
  list-style: none;
  cursor: pointer;
  user-select: none;
  font-size: 14px;
}
/* Hide the default triangle on Chrome/Safari while keeping accessibility. */
.iface-section > .iface-summary::-webkit-details-marker { display: none; }
.iface-section > .iface-summary::before {
  content: "▸";
  display: inline-block;
  width: 12px;
  color: var(--green-dark);
  transition: transform 0.15s;
}
.iface-section[open] > .iface-summary::before { transform: rotate(90deg); }
.iface-section .iface-name {
  font-weight: bold; color: var(--green-dark); font-family: var(--font-mono);
}
/* B51: read-only firewall rule<->zone equivalence preview. permit = green,
   deny = red; the matrix tints whole cells, the ACL list tints the action. */
.fw-equiv { margin-top: 12px; }
.fw-equiv-table { font-size: 11px; font-family: var(--font-mono); }
.fw-equiv-table th { font-size: 11px; }
tr.fw-equiv-permit td:first-child { color: #1f8a4c; font-weight: bold; }
tr.fw-equiv-deny td:first-child   { color: #c5221f; font-weight: bold; }
tr.fw-equiv-default td { opacity: 0.7; font-style: italic; }
.fw-equiv-matrix td.fw-equiv-permit { color: #1f8a4c; font-weight: bold; background: rgba(31,138,76,0.10); }
.fw-equiv-matrix td.fw-equiv-deny   { color: #c5221f; font-weight: bold; background: rgba(197,34,31,0.10); }
.fw-equiv-matrix td.fw-equiv-self   { color: #999; text-align: center; }
.iface-section .iface-status {
  font-family: var(--font-mono); font-size: 12px; color: var(--putnam);
}
/* On/off pill in the DHCP pool summary header. Always visible so the
   collapsed list tells the user which pools are actively serving. */
.iface-section .iface-state-badge {
  margin-left: auto;
  font-family: var(--font-mono); font-size: 10px; font-weight: bold;
  padding: 2px 8px; border-radius: 10px;
  letter-spacing: 0.5px;
}
.iface-section .iface-state-badge.on  { background: #D4EDDA; color: #2F6F45; }
.iface-section .iface-state-badge.off { background: #F2DEDE; color: #8B2E2E; }
.iface-section[open] > *:not(.iface-summary) {
  padding-left: 14px; padding-right: 14px;
}
.iface-section[open] > *:last-child:not(.iface-summary) { padding-bottom: 14px; }
.iface-section h3 {
  margin: 0 0 10px 0; font-size: 14px; color: var(--green-dark);
}
.iface-section .row { align-items: flex-end; }
.radio-row { display: flex; gap: 14px; margin-bottom: 8px; }
.radio-row label { font-weight: normal; }
.lease-info {
  background: var(--surface);
  border: 1px dashed var(--hairline);
  border-radius: 4px;
  padding: 8px;
  font-family: var(--font-mono); font-size: 12px;
  color: var(--green-dark);
}
.lease-info .lease-row {
  display: flex; gap: 12px;
  padding: 2px 0;
}
.lease-info .lease-key {
  flex: 0 0 110px;
  color: var(--putnam); text-transform: uppercase; letter-spacing: 0.3px;
  font-size: 11px;
}
.lease-info .lease-val { flex: 1 1 auto; word-break: break-all; }

.routes-table, .nat-table, .leases-table {
  width: 100%; border-collapse: collapse; margin-bottom: 10px;
}
.routes-table th, .nat-table th, .leases-table th,
.routes-table td, .nat-table td, .leases-table td {
  padding: 6px 8px; border-bottom: 1px solid var(--hairline);
  font-size: 12px;
}
.routes-table th, .nat-table th, .leases-table th {
  text-align: left; color: var(--green-dark); background: var(--bg-alt);
}
.routes-table input, .nat-table input, .nat-table select { padding: 4px 6px; font-size: 12px; }
.routes-table tr.route-readonly { background: var(--bg-alt); }
.routes-table tr.route-readonly .route-source { font-size: 11px; font-style: italic; }

/* Cloud modal: ISP picker + destinations editor */
.cloud-isp-row select.cloud-isp-select {
  padding: 4px 8px; font-size: 13px;
}
.cloud-isp-row .cloud-revert-btn { margin-left: 10px; font-size: 12px; }
.cloud-dest-table {
  width: 100%; border-collapse: collapse; margin: 6px 0 10px;
}
.cloud-dest-table th, .cloud-dest-table td {
  padding: 6px 8px; border-bottom: 1px solid var(--hairline);
  font-size: 12px; text-align: left;
}
.cloud-dest-table th {
  color: var(--green-dark); background: var(--bg-alt);
}
.cloud-dest-table input { padding: 3px 6px; font-size: 12px; width: 100%; box-sizing: border-box; }

/* Cloud sub-modal: traceroute hop editor */
.trace-hop-table {
  width: 100%; border-collapse: collapse; margin: 6px 0 10px;
}
.trace-hop-table th, .trace-hop-table td {
  padding: 6px 8px; border-bottom: 1px solid var(--hairline);
  font-size: 12px; text-align: left;
}
.trace-hop-table th {
  color: var(--green-dark); background: var(--bg-alt);
}
.trace-hop-table td:first-child { width: 2.5em; color: var(--putnam); }
.trace-hop-table input { padding: 3px 6px; font-size: 12px; width: 100%; box-sizing: border-box; }
.trace-hop-table input[type="number"] { max-width: 7em; }
.trace-edge-wrap { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }

/* Switch modal: per-port VLAN config table */
.switch-port-table {
  width: 100%; border-collapse: collapse; margin-top: 8px;
}
.switch-port-table th, .switch-port-table td {
  padding: 6px 8px; border-bottom: 1px solid var(--hairline);
  font-size: 12px; text-align: left;
}
.switch-port-table th {
  color: var(--green-dark); background: var(--bg-alt);
}
.switch-port-table td .vlan-mode { padding: 3px 6px; font-size: 12px; }
.switch-port-table td .vlan-num,
.switch-port-table td .vlan-allowed,
.switch-port-table td .vlan-desc {
  padding: 3px 6px; font-size: 12px;
  width: 100%; box-sizing: border-box;
}
.switch-port-table td .vlan-num { width: 5em; }
.switch-port-table td .vlan-allowed { width: 12em; }
.switch-port-table td .vlan-desc { min-width: 8em; }
.switch-port-table td input:disabled { background: #eee; color: #999; }

/* Router/firewall Interfaces tab: VLAN block + nested subinterface rows */
/* "Trunk" tag near a router/firewall port label - paired with the
   blue/thicker trunk-link styling so the user can see at a glance which
   ports are trunks. */
.port-trunk-badge {
  fill: #1f6feb; font-family: var(--font-mono);
  font-size: 9px; font-weight: bold; letter-spacing: 0.5px;
  pointer-events: none;
}

.iface-section .vlan-toggle-row {
  margin: 10px 14px 0; padding: 0;
}
.iface-section .vlan-toggle-label {
  display: inline-flex; align-items: center; gap: 6px;
  font-weight: bold; color: #4a3b8c; cursor: pointer;
}
.iface-section .vlan-block {
  margin: 6px 14px 10px; padding: 10px; border-radius: 6px;
  background: rgba(74,59,140,0.06);
  border: 1px solid rgba(74,59,140,0.18);
}
.iface-section .vlan-block-title {
  font-weight: bold; font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.5px; color: #4a3b8c; margin-bottom: 6px;
}
.iface-section .subiface-block {
  margin-top: 10px; padding-top: 10px;
  border-top: 1px dashed rgba(74,59,140,0.3);
}
.iface-section .subiface-block-title {
  font-size: 11px; font-weight: bold; color: #4a3b8c;
  text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px;
}
.iface-section .subiface-list { display: flex; flex-direction: column; gap: 8px; }
.iface-section .subiface-row {
  border: 1px solid var(--hairline); border-radius: 6px;
  background: var(--surface);
}
.iface-section .subiface-row > .subiface-head {
  display: flex; gap: 12px; align-items: baseline;
  padding: 8px 10px;
  list-style: none;
  cursor: pointer;
  user-select: none;
}
.iface-section .subiface-row > .subiface-head::-webkit-details-marker { display: none; }
.iface-section .subiface-row > .subiface-head::before {
  content: "▸";
  display: inline-block;
  width: 10px;
  color: var(--green-dark);
  transition: transform 0.15s;
}
.iface-section .subiface-row[open] > .subiface-head::before { transform: rotate(90deg); }
.iface-section .subiface-row[open] > *:not(.subiface-head) {
  padding-left: 10px; padding-right: 10px;
}
.iface-section .subiface-row[open] > *:last-child:not(.subiface-head) { padding-bottom: 10px; }
.iface-section .subiface-name {
  font-family: var(--font-mono); font-weight: bold; color: var(--green-dark);
}
.iface-section .subiface-status {
  color: var(--putnam); font-size: 11px;
}
.iface-section .subiface-remove {
  margin-top: 6px;
}

/* Simplified NAT pane: section heading + per-network rows. */
.field-section-header {
  font-weight: bold; color: var(--green-dark);
  font-size: 13px; margin: 12px 0 6px;
  text-transform: uppercase; letter-spacing: 0.5px;
}
.nat-internal-list { margin-bottom: 8px; }
.nat-internal-row {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 6px;
}
.nat-internal-row input {
  flex: 1 1 auto; padding: 6px 8px; font-family: var(--font-mono); font-size: 12px;
  border: 1px solid var(--hairline); border-radius: 4px;
}
.nat-internal-row input:focus { outline: 2px solid var(--green); border-color: var(--green); }
.row-actions { display: flex; gap: 6px; }

/* ----- Diagnostics panel inside router modal ----- */
.diagnostics {
  border-top: 1px solid var(--hairline);
  padding: 0 18px 8px 18px;
  background: var(--bg-alt);
}
.diagnostics summary {
  cursor: pointer; padding: 8px 0;
  color: var(--green-dark); font-weight: bold; font-size: 13px;
}
.diag-body {
  background: var(--term-bg); color: var(--term-fg);
  font-family: var(--font-mono); font-size: 12px; line-height: 1.4;
  border-radius: 4px; overflow: hidden;
  margin-bottom: 8px;
  max-height: 220px; display: flex; flex-direction: column;
}
.diag-screen {
  flex: 1; overflow-y: auto;
  padding: 6px 8px;
  white-space: pre-wrap;
  min-height: 80px; max-height: 160px;
}
.diag-screen .ln { display: block; }
.diag-screen .ln.ok    { color: var(--term-ok); }
.diag-screen .ln.err   { color: var(--term-err); }
.diag-screen .ln.warn  { color: var(--term-warn); }
.diag-screen .ln.info  { color: var(--term-info); }
.diag-screen .ln.dim   { color: var(--term-dim); }
.diag-input-row {
  display: flex; gap: 6px; padding: 4px 8px;
  border-top: 1px solid #1c2230; background: #0a0e15;
}
.diag-prompt { color: var(--term-ok2); font-weight: bold; }
.diag-input {
  flex: 1; background: transparent; border: none; outline: none;
  color: var(--term-fg); font-family: inherit; font-size: inherit;
}

/* ----- Workspace list ----- */
.workspace-list {
  list-style: none; margin: 12px 0 0; padding: 0;
}
.workspace-list li {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px;
  padding: 10px;
  border: 1px solid var(--hairline);
  border-radius: 6px;
  margin-bottom: 8px;
  background: var(--surface);
}
.workspace-list li.active { border-color: var(--green); background: var(--sycamore); }
.workspace-list .meta { font-size: 11px; color: var(--putnam); }
.workspace-list .name { font-weight: bold; color: var(--green-dark); }

/* ----- Three button modal ----- */
.three-button { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.three-button button { width: 100%; }

.confirm-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 14px; }

/* ----- Generate URL: import section ----- */
.gen-import {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--sycamore);
}
.gen-import textarea {
  font-family: var(--font-mono); font-size: 11px;
  resize: vertical;
}
.size-green  { color: #1b6; }
.size-yellow { color: var(--marigold); }
.size-red    { color: var(--error-fg); font-weight: bold; }
.hint {
  margin: 8px 0 0;
  font-size: 11px; color: var(--putnam);
  line-height: 1.5;
}
.gen-warning {
  margin: 8px 0 0;
  padding: 6px 10px;
  font-size: 12px; line-height: 1.5;
  color: var(--warn-fg);
  background: var(--warn-bg);
  border-left: 3px solid var(--marigold);
  border-radius: 3px;
}

/* ----- Advanced Mode display filter ----- */
/* ----- Read-only Tables view ----- */
/* Used by the Tables tab on router/switch modals and by the
   client/server right-click "Show tables..." modal. Compact,
   monospaced table; no input affordances. */
.tables-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  margin: 6px 0;
}
.tables-table th, .tables-table td {
  padding: 4px 8px;
  border-bottom: 1px solid var(--hairline);
  text-align: left;
}
.tables-table th {
  font-weight: bold;
  color: var(--green-dark);
  background: var(--sycamore);
}
.tables-table td {
  font-family: var(--font-mono);
}

/* ----- Welcome modal (first-run) ----- */
.welcome-card { max-width: 520px; }
.welcome-card ul { margin: 4px 0 12px 18px; padding: 0; line-height: 1.6; }
.welcome-card li { margin-bottom: 4px; }
.welcome-media {
  display: flex; justify-content: center;
  margin: 12px 0;
  padding: 12px;
  background: var(--sycamore);
  border-radius: 6px;
}
.welcome-media img {
  max-width: 100%; max-height: 220px;
  object-fit: contain;
}

/* ----- Save Assignment Set modal (internal class names keep "slate") ----- */
/* Scrollable list of every workspace project + every loaded assignment-set
   entry. Each row carries a checkbox, the project name, and a status
   chip (unchanged / changed / new / set-only). */
.save-slate-table {
  border: 1px solid var(--hairline);
  border-radius: 4px;
  max-height: 280px;
  overflow-y: auto;
  margin: 4px 0 12px;
  background: var(--surface);
}
.save-slate-row {
  display: flex; flex-direction: column; gap: 6px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--hairline);
  font-size: 13px;
}
.save-slate-row-top {
  display: flex; align-items: center; gap: 10px;
}
.save-slate-row:last-child { border-bottom: none; }
.save-slate-row label { display: flex; align-items: center; gap: 8px; flex: 1; cursor: pointer; }
.save-slate-row .save-slate-name { flex: 1; color: var(--green-dark); font-weight: bold; }
.save-slate-row .save-slate-meta { font-size: 11px; color: var(--putnam); }
.save-slate-solbtn { font-size: 11px; padding: 2px 8px; }
.save-slate-solbox { padding: 4px 0 4px 26px; }
.save-slate-solbox.hidden { display: none; }
.save-slate-solinput {
  width: 100%; box-sizing: border-box;
  font-family: monospace; font-size: 11px;
  padding: 4px 6px;
  border: 1px solid #c0c5cc; border-radius: 3px;
  resize: vertical;
}
.save-slate-solhint { font-size: 11px; margin-top: 2px; min-height: 1.2em; }
.save-slate-solhint-ok  { color: #1f8a4c; }
.save-slate-solhint-err { color: #c5221f; }
.save-slate-solpicker {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 6px;
  font-size: 11px;
}
.save-slate-solpicker > span { color: var(--putnam, #756E65); white-space: nowrap; }
.save-slate-solpicker-select {
  flex: 1; min-width: 0;
  font-size: 11px; padding: 3px 6px;
  border: 1px solid #c0c5cc; border-radius: 3px;
  background: var(--surface, white);
}
.save-slate-status {
  display: inline-block;
  padding: 2px 8px; border-radius: 9px;
  font-size: 10px; font-weight: bold;
  letter-spacing: 0.4px; text-transform: uppercase;
  background: #d0d4d8; color: #54595e;
}
.save-slate-status.changed  { background: #FFE4B5; color: #8A5A00; }
.save-slate-status.new      { background: #D4EDDA; color: #2F6F45; }
.save-slate-status.set-only { background: #C7E0F4; color: #2A5F8C; }

/* `.adv-only` is only a marker class. The actual hiding is applied via
   `.adv-hidden` by App.applyAdvancedFilter(), which walks the DOM and
   makes a hide-or-show decision per element so that:
     1) elements inside a modal-card with data-adv="on" stay visible
        regardless of the project flag,
     2) explicit per-element display values (e.g. .palette-item's grid)
        are not clobbered (CSS `display: revert` reverts to the UA
        default and would break those layouts). */
.adv-hidden { display: none !important; }

/* C13: `.v6-only` parallels `.adv-only` - it's a marker class that
   `App.applyAdvancedFilter()` walks and toggles `.v6-hidden` on/off.
   Elements stay visible when Layout.advancedMode is "adv6" OR the
   element lives inside a modal-card with data-adv-v6="on". Used
   for IPv6 surfaces (SLAAC, DHCPv6, PD, NDP, OSPFv3, link-local,
   `ip -6 *` CLI help, dual-stack columns) so faculty in "Off" or
   "Advanced" (v4-only) modes don't see them. */
.v6-hidden { display: none !important; }

/* The Advanced badge appears in the toolbar, on every modal title bar,
   and on every console tab / floating console title. Same shape so the
   toggle is recognisable everywhere. */
.adv-badge {
  display: inline-flex; align-items: center; justify-content: center;
  height: 18px; padding: 0 8px;
  border-radius: 9px;
  font-size: 10px; font-weight: bold;
  letter-spacing: 0.4px; text-transform: uppercase;
  border: 1px solid transparent;
  cursor: pointer;
  background: #d0d4d8; color: #54595e;
  transition: background var(--t-fast), color var(--t-fast);
  user-select: none;
}
/* Escape hatch: the "v4" / "v6" portions of the Advanced badge label
   stay lowercase even though the rest of the badge text is uppercased
   by text-transform above. The badge sets innerHTML wrapping the v
   parts in <span class="lc">. */
.adv-badge .lc { text-transform: none; }
.adv-badge.adv-on {
  background: #c5221f; color: #fff;
}

/* Check Solution toolbar button - shown only when the active project
   has a slate-supplied solution code attached. Orange to read as
   "graded / answer-key available" without colliding with the red
   destructive-action color. */
.check-solution-btn {
  background: #f0883e;
  border-color: #c97206;
  color: #fff;
}
.check-solution-btn:hover {
  background: #f49a5a;
  border-color: #c97206;
  color: #fff;
}
.adv-badge:disabled {
  cursor: not-allowed;
  opacity: 0.85;
}
.adv-badge-toolbar { margin-left: 4px; }
.adv-badge-modal   { margin-left: 8px; vertical-align: middle; }
.adv-badge-console { margin: 0 6px; }
/* W7: the Advanced + Answer toolbar badges adopt the Heatmap badge's
   larger pill geometry so every brand-area toolbar badge shares one size
   and shape. Each badge keeps its own fill color (Advanced gray/red,
   Answer blue). Scoped to the toolbar so the modal / console Advanced
   badges keep their compact size. */
.adv-badge.adv-badge-toolbar {
  height: auto;
  padding: 3px 9px;
  font-size: 11px;
  border-radius: 8px;
  letter-spacing: 0.3px;
}
/* A4 Answer Key badge: same pill shape as .adv-badge (the HTML carries
   both classes so off-state matches Advanced's gray pill). On-state is
   blue rather than red so the user can tell at a glance which toggle
   is which. Visible only when faculty is unlocked AND the active
   workspace carries an answer-key topology. */
.answer-key-badge.answer-key-on {
  background: #1565C0;
  color: #fff;
  border-color: #1565C0;
}
.answer-key-badge.answer-key-on:hover {
  background: #0D47A1;
  border-color: #0D47A1;
}
/* D2 Heatmap / RF-shadow badge in the toolbar brand area. Originally
   a binary on/off heatmap toggle; now a multi-state cycle that also
   gates the RF-shadow layer when RFO/RFJ devices are on the canvas.
   Visual states:
     badge-state-off       outline only - both off
     badge-state-heatmap   wifi-rainbow gradient - heatmap on
     badge-state-shadows   muted slate gradient - shadows on
     badge-state-both      rainbow + slate accent - both on
   The .adv-on backstop class is also applied for any non-off state
   so older selectors keep working. */
.heatmap-badge,
.heatmap-band-badge {
  font-size: 11px;
  font-weight: bold;
  padding: 3px 9px;
  border: 1px solid #4DB6FF;
  border-radius: 8px;
  color: #4DB6FF;
  background: transparent;
  cursor: pointer;
  user-select: none;
  letter-spacing: 0.3px;
  text-transform: uppercase;
}
.heatmap-badge.badge-state-heatmap {
  background: linear-gradient(90deg, #c5221f 0%, #f0883e 25%, #ffe066 50%, #50c878 75%, #4DB6FF 100%);
  color: #ffffff;
  border-color: rgba(255,255,255,0.55);
  box-shadow: 0 0 6px rgba(77,182,255,0.5);
}
.heatmap-badge.badge-state-shadows {
  background: linear-gradient(90deg, #1f2933 0%, #3b4a5a 60%, #62768a 100%);
  color: #ffffff;
  border-color: rgba(255,255,255,0.45);
  box-shadow: 0 0 6px rgba(31,41,51,0.55);
}
.heatmap-badge.badge-state-both {
  background:
    linear-gradient(90deg, rgba(31,41,51,0.45) 0%, rgba(31,41,51,0) 25%, rgba(31,41,51,0) 75%, rgba(31,41,51,0.45) 100%),
    linear-gradient(90deg, #c5221f 0%, #f0883e 25%, #ffe066 50%, #50c878 75%, #4DB6FF 100%);
  color: #ffffff;
  border-color: rgba(255,255,255,0.65);
  box-shadow: 0 0 7px rgba(77,182,255,0.55), 0 0 4px rgba(31,41,51,0.45);
}
.heatmap-badge:hover { background: rgba(77,182,255,0.10); }
.heatmap-badge.adv-on:hover { filter: brightness(1.07); }
.heatmap-badge.hidden,
.heatmap-band-badge.hidden { display: none; }

/* W7 per-band heatmap-filter badge. Same pill geometry as the heatmap
   badge (shared rule above); a solid fill colored by the active filter:
   2.4 GHz blue, 5 GHz yellow, Both green - echoing the heatmap palette.
   Dark text keeps contrast on all three bright fills. */
.heatmap-band-badge.band-both {
  background: #50c878; border-color: #50c878; color: #08240f;
  box-shadow: 0 0 6px rgba(80,200,120,0.5);
}
.heatmap-band-badge.band-24 {
  background: #4DB6FF; border-color: #4DB6FF; color: #06243a;
  box-shadow: 0 0 6px rgba(77,182,255,0.5);
}
.heatmap-band-badge.band-5 {
  background: #ffe066; border-color: #ffe066; color: #3a2f00;
  box-shadow: 0 0 6px rgba(255,224,102,0.5);
}
.heatmap-band-badge:hover { filter: brightness(1.07); }

/* Drives the Shadows half of the toolbar badge: when set on the SVG
   canvas root, the RFO shadow polygons + RFJ jamming regions both
   disappear (they share the same SVG group). The polygons stay in
   the DOM so re-showing is one class flip away - no rebuild needed. */
.rf-shadows-hidden #layer-rfo-shadows {
  display: none;
}
/* F1 simulation-speed pill group: 4-position selector for the
   global packet-animation speed plus a "Next" button that appears
   while step mode is active. Sits in the toolbar brand area. */
.sim-pill-group {
  display: inline-flex;
  align-items: center;
  margin-left: 8px;
  border: 1px solid var(--green);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
}
.sim-pill {
  background: transparent;
  border: none;
  border-right: 1px solid #d4dac2;
  padding: 3px 8px;
  font-size: 11px;
  font-weight: bold;
  color: var(--green-dark);
  cursor: pointer;
  user-select: none;
  letter-spacing: 0.2px;
}
.sim-pill:last-of-type { border-right: none; }
.sim-pill:hover { background: rgba(0, 105, 78, 0.08); }
.sim-pill.active {
  background: var(--green);
  color: #ffffff;
}
.sim-pill[data-sim-speed="step"].active {
  /* Step mode lights up amber to read as "paused, attention needed". */
  background: #f0883e;
  color: #ffffff;
}
.sim-next-btn {
  background: #f0883e;
  border: none;
  border-left: 1px solid rgba(255,255,255,0.4);
  padding: 3px 10px;
  font-size: 11px;
  font-weight: bold;
  color: #ffffff;
  cursor: pointer;
  user-select: none;
  letter-spacing: 0.2px;
}
.sim-next-btn:hover { filter: brightness(1.07); }
.sim-next-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  filter: none;
}
.sim-next-btn.hidden { display: none; }
/* The rasterized heatmap PNG image in the layer-heatmap SVG group.
   Doesn't intercept pointer events (so clicks land on devices /
   walls underneath it), and the layer itself reads as a
   background "weather map" overlay. */
#layer-heatmap, .heatmap-image { pointer-events: none; }
.heatmap-image { opacity: 0.85; }

/* ----- Dropdown drawers (workspace, settings) ----- */
.dropdown-drawer {
  /* Above the floating-console ceiling (which raises to 99 on
     focus) so any menu opened from the toolbar always overlays
     every detached terminal window on the canvas. Modals (z-index
     100) block the toolbar via their backdrop, so the drawer can
     freely sit above the float range without clashing with modals
     in normal use. */
  position: absolute; z-index: 110;
  background: var(--surface);
  border: 2px solid var(--green);
  border-radius: 8px;
  padding: 8px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.2);
  min-width: 280px;
  max-height: calc(100vh - var(--toolbar-h) - 24px);
  overflow-y: auto;
}
.drawer-item {
  display: block; width: 100%; text-align: left;
  background: transparent; border: none;
  padding: 8px 10px; border-radius: 4px;
  cursor: pointer; color: var(--green-dark); font-weight: bold;
}
/* Without this, the HTML `hidden` attribute doesn't actually hide a
   `.drawer-item` because the class selector's `display: block` beats
   the UA stylesheet's `[hidden] { display: none }` (same specificity,
   author CSS wins). Bites buttons like `ham-check-solution` and
   `ham-slate-clear` that gate themselves on `hidden = !has`. */
.drawer-item[hidden] { display: none; }
.drawer-item:hover { background: var(--sycamore); }
/* Demo rows show the project name on top and a short description below. */
.drawer-item.drawer-item-stacked {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  white-space: normal; line-height: 1.3;
}
.drawer-item-name { font-weight: bold; color: var(--green-dark); }
.drawer-item-desc {
  font-size: 11px; font-weight: normal; color: var(--putnam);
}
.drawer-item.danger:hover { background: var(--error-bg); color: var(--error-fg); }
/* Drawer entries that perform a destructive action (e.g. Clear Assignment Set)
   render their label in red so the action is visually distinct from
   benign menu items. Hover deepens the red background. */
.drawer-item.drawer-item-danger { color: var(--error-fg); }
.drawer-item.drawer-item-danger:hover { background: var(--error-bg); color: var(--error-fg); }
.drawer-item.drawer-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
/* Drawer item that expands an inline submenu (e.g. "Demo" in the hamburger
   menu). Layout matches drawer-toggle; the chev rotates 90 degrees when the
   submenu is open. */
.drawer-item.drawer-submenu-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.drawer-submenu-chev {
  display: inline-block;
  font-size: 11px; color: var(--putnam);
  transition: transform var(--t-fast);
}
.drawer-item.drawer-submenu-toggle.is-open .drawer-submenu-chev {
  transform: rotate(90deg);
}
/* Drawer popout: a parent drawer-item that expands a side submenu listing
   children. Used by the demo menus when there's more than one category.
   Hover opens the submenu on desktop; click toggles `.expanded` for
   keyboard / touch. The popout positions itself to the LEFT of the parent
   drawer (these drawers anchor to the right edge of the toolbar / hamburger
   button, so left-side popouts stay onscreen). */
.drawer-item.drawer-popout-parent {
  position: relative;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.drawer-popout-arrow {
  display: inline-block;
  font-size: 11px; color: var(--putnam);
  transition: transform var(--t-fast);
}
.drawer-item.drawer-popout-parent.expanded .drawer-popout-arrow {
  /* Submenu is visible to the left, so rotate the arrow to point that way. */
  transform: rotate(180deg);
}
.drawer-popout {
  display: none;
  /* position: fixed avoids being clipped by the parent dropdown-drawer's
     overflow-y: auto (which implicitly clips overflow-x too per the CSS
     spec). The actual top/right are written by JS from the parent's
     getBoundingClientRect() so the popout snaps next to the parent row. */
  position: fixed;
  background: var(--surface);
  border: 2px solid var(--green);
  border-radius: 8px;
  padding: 6px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.2);
  min-width: 220px;
  max-height: calc(100vh - var(--toolbar-h) - 24px);
  overflow-y: auto;
  /* Sit just above the parent drawer so a popout submenu renders
     in front of the rest of the menu, not behind it. Bumped in
     lockstep with .dropdown-drawer's raise to 95 above. */
  z-index: 96;
}
.drawer-item.drawer-popout-parent.expanded > .drawer-popout {
  display: block;
}
.drawer-popout .drawer-item {
  /* Children inside the popout look the same as top-level rows but a
     touch lighter, since the parent's hover already puts them in focus. */
  font-weight: normal;
}

/* Gold star next to the active workspace in the workspace drawer. */
.ws-active-star {
  color: #E0B400;
  font-size: 16px;
  text-shadow: 0 0 1px rgba(0, 0, 0, 0.4);
  margin-right: 2px;
  vertical-align: -1px;
}
/* Section heading inside a drawer (e.g. inside the hamburger menu). */
.dropdown-drawer .drawer-section {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--putnam); font-weight: bold;
  padding: 8px 10px 4px; margin-top: 4px;
  border-top: 1px solid var(--hairline);
}
.dropdown-drawer .drawer-section:first-child { border-top: none; margin-top: 0; }
/* Synthetic Traffic level slider - 4 stops (Off / Low / Med / High). */
.traffic-slider {
  padding: 6px 14px 10px;
  min-width: 200px;
}
.traffic-slider input[type="range"] {
  display: block; width: 100%; margin: 4px 0 6px;
  accent-color: var(--green);
  cursor: pointer;
}
.traffic-slider-ticks {
  display: flex; justify-content: space-between;
  font-size: 11px; color: var(--putnam); user-select: none;
}
.traffic-slider-ticks span {
  flex: 1 1 0; text-align: center;
}
.traffic-slider-ticks span:first-child { text-align: left; }
.traffic-slider-ticks span:last-child  { text-align: right; }
.traffic-slider-ticks span.is-active {
  color: var(--green); font-weight: 700;
}
/* ISP name sub-heading inside the Destinations drawer - lighter than the
   top-level section heading so the hierarchy reads at a glance. */
.dropdown-drawer .drawer-subsection {
  font-size: 12px; font-weight: 600;
  color: var(--ink); padding: 6px 10px 2px;
}
/* Top-of-drawer ISP banner in the Destinations dropdown - slightly larger
   than the body text and bolded so it reads as a title. */
.dropdown-drawer .drawer-isp-header {
  font-size: 14px; font-weight: 700;
  color: var(--green); padding: 8px 10px 6px;
}
/* Assignment-set roster header lives inside .drawer-isp-header (internal
   class names keep "slate-course-*"). The set name uses the inherited bold
   green; faculty + description sit below in smaller, secondary text so the
   header reads as a stack. */
.dropdown-drawer .slate-course-name {
  font-size: 14px; font-weight: 700; color: var(--green);
}
.dropdown-drawer .slate-course-faculty {
  font-size: 12px; font-weight: 600; color: var(--green-dark);
  margin-top: 2px;
}
.dropdown-drawer .slate-course-desc {
  font-size: 12px; font-weight: 400; color: var(--putnam);
  margin-top: 2px; line-height: 1.4;
}

/* Help drawer (Reachable destinations) - same chrome as the other dropdown
   drawers, just a couple of helper styles for the destination rows. */
.help-drawer { min-width: 320px; max-width: 420px; }
.help-drawer .help-note {
  font-size: 12px; color: var(--putnam);
  padding: 4px 10px 8px; line-height: 1.4;
}
.help-drawer .help-note.dim { color: var(--hairline); font-style: italic; }
.help-drawer .help-list { padding: 2px 10px 6px; }
.help-drawer .help-row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 3px 0;
  font-family: var(--font-mono); font-size: 12px;
}
.help-drawer .help-name { color: var(--green-dark); font-weight: bold; }
.help-drawer .help-ip   { color: var(--putnam); }
.help-drawer .help-svcs {
  margin-left: auto;
  color: var(--green); font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.3px;
}

/* About modal -- author list, subheads, and repo link */
.about-authors {
  list-style: none; padding: 0; margin: 4px 0 8px;
}
.about-authors li {
  font-weight: bold; color: var(--green-dark);
  padding: 2px 0;
}
.about-repo {
  margin: 8px 0 0; font-size: 13px;
}
.about-repo a {
  color: var(--green-dark); font-weight: bold;
}
.about-repo a:hover { color: var(--marigold); }
.about-copyright {
  margin: 16px 0 0; font-size: 11px; color: #6b7a72;
  border-top: 1px solid rgba(0,0,0,0.06); padding-top: 10px;
}
.modal-subhead {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--putnam);
  margin: 8px 0 4px;
  font-weight: bold;
}

/* NIC Up/Down - light-switch toggle in the router modal interfaces tab. */
.nic-toggle-row {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 0;
}
.nic-toggle-label {
  font-weight: bold; color: var(--green-dark);
}
.nic-switch {
  position: relative;
  width: 52px; height: 28px; padding: 0;
  border: 2px solid var(--green-dark); border-radius: 14px;
  background: var(--hairline);
  cursor: pointer;
  transition: background var(--t-fast);
  flex: 0 0 auto;
}
.nic-switch .nic-switch-thumb {
  position: absolute;
  top: 2px; left: 2px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(0,0,0,0.25);
  transition: transform var(--t-fast);
}
.nic-switch[aria-checked="true"] { background: var(--green); }
.nic-switch[aria-checked="true"] .nic-switch-thumb { transform: translateX(24px); }
.nic-state {
  font-weight: bold; min-width: 36px;
  color: var(--green-dark);
}
.drawer-toggle-state {
  font-weight: bold; font-size: 12px;
  padding: 2px 8px; border-radius: 999px;
  background: var(--hairline); color: var(--putnam);
  border: 1px solid var(--hairline);
}
.drawer-toggle.is-on .drawer-toggle-state {
  background: var(--green); color: var(--white);
  border-color: var(--green-dark);
}

/* ----- Banner area ----- */
.banner-area {
  position: fixed; left: 12px; right: 12px; bottom: 12px;
  z-index: 200;
  display: flex; flex-direction: column; gap: 8px; pointer-events: none;
}
.banner {
  pointer-events: auto;
  border: 2px solid var(--green);
  background: var(--sycamore);
  color: var(--green-dark);
  border-radius: 6px; padding: 10px 14px;
  display: flex; justify-content: space-between; align-items: center;
  gap: 12px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.banner.warn  { background: var(--warn-bg);  color: var(--warn-fg);  border-color: var(--marigold); }
.banner.error { background: var(--error-bg); color: var(--error-fg); border-color: var(--error-fg); }
.banner details { font-size: 11px; }
.banner details summary { cursor: pointer; }
.banner details pre {
  margin: 6px 0 0; padding: 6px;
  background: rgba(0,0,0,0.05); border-radius: 4px;
  font-family: var(--font-mono); font-size: 11px;
  white-space: pre-wrap;
}
.banner .close-btn {
  background: transparent; border: none; cursor: pointer;
  color: inherit; font-size: 16px;
  /* Always sit at the far right. order:999 forces last in flex
     order, margin-left:auto consumes leftover slack so the X hugs
     the edge. The action-button case below overrides the auto
     margin so the action+X group together on the right. */
  order: 999;
  margin-left: auto;
}
/* Action button inside a banner (e.g. "View Differences" on the
   Check Solution mismatch banner). Sits just left of the X with
   no gap of leftover space between them. */
.banner .banner-action {
  order: 998;
  margin-left: auto;
  margin-right: 6px;
}
/* When an action button is present, the close-btn drops its auto
   margin so the action's auto consumes ALL leftover space and the
   X sits right next to the action instead of sharing the slack. */
.banner .banner-action ~ .close-btn { margin-left: 0; }

/* ----- Context menu ----- */
.ctx-menu {
  position: fixed; z-index: 300;
  background: var(--surface);
  border: 2px solid var(--green);
  border-radius: 6px;
  padding: 4px;
  min-width: 140px;
  /* Never taller than the viewport: a long menu (e.g. a zone with every
     Arrange option) scrolls internally instead of clipping its bottom
     rows off the screen with no way to reach them. The position clamp in
     showContextMenu measures the post-cap height, so it still pins on
     screen. */
  max-height: calc(100vh - 12px);
  overflow-y: auto;
  box-shadow: 0 6px 20px rgba(0,0,0,0.25);
}
.ctx-menu button {
  display: block; width: 100%; text-align: left;
  background: transparent; border: none;
  padding: 6px 10px; border-radius: 4px;
  cursor: pointer; color: var(--green-dark); font-size: 13px;
}
.ctx-menu button:hover { background: var(--sycamore); }
.ctx-menu button.has-icon {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.ctx-menu button .ctx-icon {
  font-size: 14px; opacity: 0.75; line-height: 1;
}
.ctx-menu button:hover .ctx-icon { opacity: 1; }
.ctx-menu button.danger:hover { background: var(--error-bg); color: var(--error-fg); }
/* Submenu row: same row chrome as a regular item plus a right-aligned
   chevron so the user sees "this opens another menu". Click handler
   reopens the context menu at the row's right edge with the submenu's
   items. */
.ctx-menu button.ctx-item-submenu {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.ctx-menu button.ctx-item-submenu .ctx-item-chev {
  font-size: 12px; opacity: 0.65; line-height: 1;
}
.ctx-menu button.ctx-item-submenu:hover .ctx-item-chev { opacity: 1; }
/* Per-device Advanced override row: red when on, gray when off, so the
   current state reads at a glance. The label is the whole button text;
   no leading checkmark needed since color is the indicator. */
.ctx-menu .ctx-adv-on  { color: #c5221f; font-weight: bold; }
.ctx-menu .ctx-adv-off { color: #888; }
/* Submenu drill-down "Back" row: visually distinct from the rest of
   the items so the user can spot the navigation affordance. Slight
   indent + muted hairline beneath. */
.ctx-menu button.ctx-item-back {
  font-size: 12px; color: var(--ink-muted, #666); font-weight: 500;
  border-bottom: 1px solid var(--hairline);
  margin-bottom: 2px; padding-bottom: 6px;
}
.ctx-menu button.ctx-item-back:hover { color: var(--green-dark); }
/* Port picker for >8-port switches. Each row is a port name with a
   leading status dot - green = free, red = wired. Wired rows are
   dimmed (no-op on click) since we don't disconnect via the picker. */
.ctx-menu.port-picker {
  min-width: 0;
  max-height: 320px;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 2px;
}
.ctx-menu.port-picker button {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  padding: 4px 8px;
}
.port-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}
.port-dot-free  { background: #2EA043; box-shadow: 0 0 0 1px #1f7a32 inset; }
.port-dot-wired { background: #C5221F; box-shadow: 0 0 0 1px #8b1612 inset; }
.ctx-menu.port-picker button:has(.port-dot-wired) {
  opacity: 0.55;
  cursor: default;
}
.ctx-menu.port-picker button:has(.port-dot-wired):hover {
  background: transparent;
}
.port-pick-label { white-space: nowrap; }

.ctx-menu .ctx-swatch-row {
  padding: 6px 10px;
  display: flex; flex-direction: column; gap: 4px;
}
.ctx-menu .ctx-swatch-label {
  font-size: 13px; color: var(--green-dark);
}
/* Swatches wrap onto multiple rows (4 per row) like the Size grid, so an
   8-color palette no longer forces the menu to one very wide line. */
.ctx-menu .ctx-swatch-grid {
  display: grid; grid-template-columns: repeat(4, 18px); gap: 6px;
  align-items: center;
}
.ctx-menu button.ctx-swatch {
  width: 18px; height: 18px;
  padding: 0; margin: 0;
  border: 1px solid var(--hairline);
  border-radius: 50%;
  cursor: pointer;
  display: inline-block;
}
.ctx-menu button.ctx-swatch:hover { transform: scale(1.15); border-color: var(--green-dark); }
.ctx-menu button.ctx-swatch.selected { border: 2px solid var(--green-dark); box-shadow: 0 0 0 1px var(--white) inset; }

/* Chip picker: label on top, a wrapping row of small text pills below
   (used by Font). Mirrors the swatch / grid rows so the menu reads
   consistently and stays narrow. */
.ctx-menu .ctx-chip-row {
  padding: 6px 10px;
  display: flex; flex-direction: column; gap: 4px;
}
.ctx-menu .ctx-chip-label {
  font-size: 13px; color: var(--green-dark);
}
.ctx-menu .ctx-chips {
  display: flex; flex-wrap: wrap; gap: 4px;
}
.ctx-menu button.ctx-chip {
  display: inline-block; width: auto;
  padding: 3px 10px;
  border: 1px solid var(--hairline); border-radius: 4px;
  background: var(--surface); color: var(--green-dark);
  font-size: 12px; line-height: 1.3;
  cursor: pointer;
}
.ctx-menu button.ctx-chip:hover { background: var(--sycamore); border-color: var(--green); }
.ctx-menu button.ctx-chip.selected {
  background: var(--green); border-color: var(--green-dark); color: var(--white);
}

.ctx-menu .ctx-grid-row {
  padding: 6px 10px;
  display: flex; flex-direction: column; gap: 4px;
}
.ctx-menu .ctx-grid-label {
  font-size: 13px; color: var(--green-dark);
}
.ctx-menu .ctx-grid {
  display: grid; grid-template-columns: repeat(3, 22px); gap: 2px;
}
.ctx-menu .ctx-grid-cell {
  width: 22px; height: 22px; padding: 0;
  background: var(--surface); color: var(--green-dark);
  border: 1px solid var(--hairline); border-radius: 3px;
  font: inherit; font-size: 12px; line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.ctx-menu .ctx-grid-cell:hover { background: var(--sycamore); border-color: var(--green); }
.ctx-menu .ctx-grid-cell.selected {
  background: var(--green); border-color: var(--green-dark); color: var(--white);
}
.ctx-menu .ctx-grid-cell.disabled {
  background: var(--hairline); border-color: var(--hairline); cursor: default;
}

/* Slider row inside the context menu - same look as the Synthetic
   Traffic drawer (.traffic-slider) but tightened for the narrower
   context menu width. */
.ctx-menu .ctx-slider-row {
  padding: 6px 10px 8px;
  display: flex; flex-direction: column; gap: 4px;
  min-width: 200px;
}
.ctx-menu .ctx-slider-label {
  font-size: 13px; color: var(--green-dark);
}
.ctx-menu .ctx-slider.traffic-slider {
  /* Tighter padding inside the context menu - the parent
     .ctx-slider-row already provides the outer breathing room. */
  padding: 0; min-width: 0;
}
/* Equidistant tick labels for the context-menu slider variant. Each
   tick is absolutely positioned at its thumb stop (0%, 25%, 50%, 75%,
   100% for 5 stops) so labels line up with the thumb position. The
   default .traffic-slider-ticks flex distribution puts middle labels
   at 30/50/70% which doesn't match the actual thumb travel for an
   odd count of stops. */
.ctx-menu .traffic-slider-ticks.ctx-slider-ticks-aligned {
  position: relative;
  display: block;
  height: 16px;
}
.ctx-menu .traffic-slider-ticks.ctx-slider-ticks-aligned span {
  position: absolute;
  top: 0;
  flex: none;
  text-align: left;
  white-space: nowrap;
}

/* ----- DHCP / NAT badges ----- */
.dev-badge-nat      { fill: var(--marigold); }
.dev-badge-dhcp     { fill: var(--green); }
.dev-badge-dhcp-use { fill: #2A6BB1; }
.dev-badge-dhcp-vlan { fill: #C0392B; }
.dev-badge-clickable { cursor: pointer; }
.dev-badge-clickable:hover { stroke: #FFE066; stroke-width: 1.5; }
.dev-badge-circle { stroke: var(--white); stroke-width: 1; }

/* ----- Misc ----- */
small.muted { color: var(--putnam); }
.gen-output { margin-top: 14px; }
.gen-output textarea {
  font-family: var(--font-mono); font-size: 11px; resize: vertical;
  word-break: break-all;
}
.gen-meta {
  display: flex; justify-content: space-between; align-items: center;
  margin-top: 6px;
}
.gen-meta-actions { display: flex; gap: 8px; }

/* ----- Getting Started modal ----- */
#getting-started-modal .modal-body h3 {
  margin: 14px 0 6px;
  font-size: 14px;
  color: var(--green-dark);
}
#getting-started-modal .modal-body h3:first-of-type { margin-top: 8px; }
#getting-started-modal .modal-body ul {
  margin: 4px 0 8px 18px;
  padding: 0;
  line-height: 1.5;
}
#getting-started-modal .modal-body li { margin: 3px 0; }
#getting-started-modal .modal-body p { line-height: 1.5; margin: 6px 0; }
#getting-started-modal .modal-body code {
  font-family: var(--font-mono);
  font-size: 12px;
  background: var(--bg-alt);
  padding: 1px 5px;
  border-radius: 3px;
}

/* ----- Mobile / touch ------------------------------------------------- */

/* On touch pointers, bump tap-target sizes a bit and keep tabs scrollable
   horizontally rather than wrapping (more predictable for thumb scrolling). */
@media (pointer: coarse) {
  .tab-bar { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .tab-bar .tab-item { flex: 0 0 auto; padding: 8px 12px; font-size: 13px; }
  .tab-bar .tab-item .close,
  .tab-bar .tab-item .popout { font-size: 14px; padding: 0 4px; }
  .palette-item { padding: 12px; min-height: 56px; }
  .btn-primary, .btn-secondary { padding: 10px 14px; }
  .btn-icon { width: 32px; height: 32px; }
  /* Make the splitters easier to grab. The visual stays the same; only the
     hit area widens via the existing ::before overlay. */
  .resizer-v::before { left: -8px; right: -8px; }
  .resizer-h::before { top: -8px; bottom: -8px; }
  .floating-console .fc-close { font-size: 22px; padding: 0 8px; }
  .floating-console .fc-reattach { padding: 6px 12px; }
}

/* Narrow viewports: shrink the toolbar and default panel widths so the
   canvas isn't a sliver. The user can still resize via splitters or
   collapse panels with the chevron toggles. */
/* Default: long form of "Check Solution" visible, short hidden. The
   media query below swaps them. */
#check-solution-btn .label-short { display: none; }
/* Stage 1: Check Solution is the optional toolbar item - it appears
   only when the workspace carries a slate solution code, and its
   ~115px label is what tips the toolbar into "crowded" first. Drop
   it back to "Solution" before touching the brand or the chevs. */
@media (max-width: 1450px) {
  #toolbar #check-solution-btn .label-long { display: none; }
  #toolbar #check-solution-btn .label-short { display: inline; }
}
/* Stage 2: shorten the brand to "ENE" and hide the decorative
   dropdown chevs. Both free meaningful width without truncating any
   real button label. */
@media (max-width: 1280px) {
  #toolbar .brand-text-full { display: none; }
  #toolbar .brand-text-short { display: inline; }
  #toolbar .tools .chev { display: none; }
}
@media (max-width: 720px) {
  :root {
    --toolbar-h: 48px;
    --palette-w: 160px;
    --bottom-h: 200px;
  }
  #toolbar { padding: 0 8px; }
  #toolbar .tools { gap: 4px; }
  .btn-secondary, .btn-primary { padding: 6px 10px; font-size: 12px; }
  .palette-item { padding: 8px; }
  .palette-icon { width: 30px; height: 30px; }
  .palette-label { font-size: 13px; }
  .palette-sub { font-size: 10px; }
  .palette-help { font-size: 11px; padding: 8px; }
  #palette { padding: 12px 8px; }
}

@media (max-width: 480px) {
  :root {
    --palette-w: 140px;
    --bottom-h: 180px;
  }
  .modal-card { width: 96vw; }
}
