 
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap');
/* KOPPEL RMM — UI theme, matched to the support.koppel design system. */
:root {
  --bg:       #0A0A0A;
  --surf:     #111111;
  --surf2:    #161616;
  --surf3:    #1a1a1a;
  --border:   #1f1f1f;
  --border2:  #2a2a2a;
  --orange:      #E8650A;
  --orange-h:    #ff7a1a;
  --orange-d:    rgba(232,101,10,.14);
  --orange-dark: #8B3D06;
  --white:    #FFFFFF;
  --gray:     #777777;
  --gray-l:   #AAAAAA;
  --gray-d:   #444444;
  --green:    #10B981;
  --red:      #EF4444;
  --amber:    #F59E0B;
  --blue:     #3B82F6;
  --hover-1:  rgba(255,255,255,.04);
  --hover-2:  rgba(255,255,255,.07);
  --fd: 'Roboto', sans-serif;
  --fb: 'Roboto', sans-serif;
  --mono: ui-monospace, 'Cascadia Code', 'JetBrains Mono', Consolas, monospace;
  --radius: 6px;
  --shadow: 0 4px 24px rgba(0,0,0,.4);
  color-scheme: dark;
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg:       #F2F2EF;
  --surf:     #FFFFFF;
  --surf2:    #F8F8F6;
  --surf3:    #EFEFEC;
  --border:   #E4E4E0;
  --border2:  #D5D5CF;
  --white:    #1C1C19;
  --gray:     #85847E;
  --gray-l:   #55544E;
  --gray-d:   #CBCBC4;
  --hover-1:  rgba(20,20,15,.045);
  --hover-2:  rgba(20,20,15,.07);
  --shadow: 0 4px 24px rgba(0,0,0,.12);
}
*,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }
html { height:100%; height:100dvh; font-size:14px; }
body { background:var(--bg); color:var(--white); font-family:var(--fb); font-weight:400; line-height:1.6; height:100%; height:100dvh; overflow-x:hidden; -webkit-font-smoothing:antialiased; -webkit-tap-highlight-color:transparent; }
a { color:inherit; text-decoration:none; }
button { cursor:pointer; font-family:var(--fb); }
input,textarea,select { font-family:var(--fb); accent-color:var(--orange); }
.hidden { display:none !important; }
::-webkit-scrollbar { width:5px; height:5px; }
::-webkit-scrollbar-track { background:var(--surf); }
::-webkit-scrollbar-thumb { background:var(--border2); border-radius:3px; }
::-webkit-scrollbar-thumb:hover { background:var(--gray-d); }

/* ─── Buttons ─────────────────────────────────────────── */
.btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; padding:8px 16px; font-size:12px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; border:none; border-radius:var(--radius); transition:all .2s; white-space:nowrap; }
.btn-primary { background:var(--orange); color:var(--white); }
.btn-primary:hover { background:var(--orange-h); }
.btn-primary.danger { background:#DC2626; }
.btn-primary.danger:hover { background:#EF4444; }
.btn-secondary { background:var(--surf3); color:var(--gray-l); border:1px solid var(--border2); }
.btn-secondary:hover { color:var(--white); border-color:var(--gray-d); }
.btn:disabled { opacity:.45; cursor:not-allowed; }

/* ─── Forms ───────────────────────────────────────────── */
.form-group { margin-bottom:16px; }
.form-label { display:block; font-size:12px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--gray-l); margin-bottom:6px; }
.form-control { width:100%; background:var(--surf2); border:1px solid var(--border2); border-radius:var(--radius); padding:9px 12px; color:var(--white); font-size:13px; outline:none; transition:border-color .2s; }
.form-control::placeholder { color:var(--gray); }
.form-control:focus { border-color:var(--orange); }
.form-checkbox-row { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--white); cursor:pointer; user-select:none; }
.form-checkbox-row input[type="checkbox"], .form-checkbox-row input[type="radio"] { width:16px; height:16px; accent-color:var(--orange); cursor:pointer; }
/* Four-octet IP/subnet/gateway entry (Manage Local Machine script) - narrow fixed-width
   boxes instead of one full-width text field, dot separators between them. */
.octet-group { display:flex; align-items:center; gap:4px; }
.octet-group .octet { width:40px; flex:none; text-align:center; padding-left:2px; padding-right:2px; }
.octet-dot { color:var(--gray-l); font-weight:700; }
/* Reserves enough room for the largest script-run form (Create/Edit user: action select +
   one identity field + password + admin checkbox) so switching between Create and Edit -
   which shows a different subset of fields, not a different count - never visibly resizes
   the popup around it. */
#scriptRunFields { min-height:260px; }
select.form-control option, select.form-control optgroup { background:var(--surf); color:var(--white); }

/* Native <select> popups can't be fully themed — Chrome/Edge draw the hover/selected
   row using the OS's own highlight color (Windows blue), and no CSS can override that
   specific bar no matter what's set on <option>/<optgroup>. Anywhere that mismatch would
   actually show (site/client pickers with real option lists), the native select is kept
   in the DOM only as the value holder (visually hidden) and a fully custom, on-theme
   dropdown is rendered next to it instead — see enhanceSelect() in app.js.
   The panel is appended straight to <body> and position:fixed, exactly like
   .context-menu below — NOT nested/absolute inside the modal, which is what made it
   render "into the container" (clipped/confined by the modal's own box) instead of
   floating free on top of everything the way the right-click menu already does. */
.custom-select { position:relative; }
.custom-select-native { position:absolute; opacity:0; width:1px; height:1px; pointer-events:none; }
.custom-select-trigger { display:flex; align-items:center; justify-content:space-between; gap:8px; cursor:pointer; user-select:none; }
.custom-select-trigger .custom-select-chevron { color:var(--gray-l); font-size:20px; line-height:1; flex-shrink:0; }
.custom-select-panel { position:fixed; z-index:300; max-height:260px; overflow-y:auto; background:var(--surf2); border:1px solid var(--border2); border-radius:var(--radius); box-shadow:var(--shadow); padding:6px; display:flex; flex-direction:column; gap:1px; }
.custom-select-panel.hidden { display:none; }
.custom-select-group { padding:8px 10px 2px; font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--gray-l); }
.custom-select-option { padding:8px 10px; border-radius:6px; font-size:13px; color:var(--gray-l); cursor:pointer; user-select:none; }
.custom-select-option.grouped { padding-left:20px; }
.custom-select-option:hover { color:var(--white); background:var(--hover-2); }
.custom-select-option.active { background:var(--orange-d); color:var(--orange-h); }

/* ═══ LOGIN ═══════════════════════════════════════════════ */
.login-page { min-height:100vh; min-height:100dvh; background:var(--bg); display:flex; align-items:center; justify-content:center; padding:20px; }
.login-card { width:100%; max-width:420px; }
.login-logo { text-align:center; margin-bottom:36px; }
.login-logo img { max-width:70%; max-height:64px; object-fit:contain; margin:0 auto 14px; display:block; }
.login-logo .logo-sub2 { font-size:11px; letter-spacing:.2em; text-transform:uppercase; color:var(--gray); margin-top:4px; }
.login-box { background:var(--surf); border:1px solid var(--border); border-radius:var(--radius); padding:32px; }
.login-box h2 { font-family:var(--fd); font-size:22px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; margin-bottom:24px; }
.login-footer { text-align:center; margin-top:24px; font-size:12px; color:var(--gray); }
.login-footer .koppel-link { color:#FF6700; font-weight:600; text-decoration:none; }
.err { color:#f87171; font-size:13px; min-height:18px; margin-top:10px; text-align:center; }

/* ═══ APP SHELL ═══════════════════════════════════════════ */
#app { display:flex; height:100vh; height:100dvh; overflow:hidden; }

/* Sidebar */
.sidebar { position:relative; width:var(--sidebar-w,260px); min-width:180px; max-width:480px; background:var(--surf); border-right:1px solid var(--border); display:flex; flex-direction:column; height:100dvh; overflow:hidden; z-index:100; flex-shrink:0; }
.sidebar-logo { padding:14px 16px; border-bottom:1px solid var(--border); display:flex; align-items:center; min-height:64px; }
.sidebar-logo img { width:100%; height:auto; max-height:40px; object-fit:contain; object-position:left center; }
.sidebar-nav { flex:1; padding:8px 0; overflow-y:auto; }
/* Drag handle hugging the sidebar's right edge - widened hit target (8px) over a thin
   visible line (2px, centered) so it's easy to grab without feeling like a fat border. */
.sidebar-resize-handle { position:absolute; top:0; right:-4px; width:8px; height:100%; cursor:col-resize; z-index:101; touch-action:none; }
.sidebar-resize-handle::after { content:''; position:absolute; top:0; left:3px; width:2px; height:100%; background:transparent; }
.sidebar-resize-handle:hover::after, .sidebar-resize-handle.dragging::after { background:var(--orange); }
.nav-section-label { display:flex; align-items:center; justify-content:space-between; font-size:10px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--gray); padding:16px 16px 6px; cursor:pointer; user-select:none; }
.section-add { background:var(--surf3); border:1px solid var(--border2); color:var(--gray-l); width:24px; height:24px; border-radius:5px; font-size:17px; line-height:1; display:flex; align-items:center; justify-content:center; flex:none; }
.section-add:hover { color:var(--white); border-color:var(--orange); }
.section-label-actions { display:flex; align-items:center; gap:6px; }
.tree-search-row { display:flex; align-items:center; gap:6px; padding:0 16px 10px; }
.tree-search-row.hidden { display:none; }
.tree-search-row input { flex:1; min-width:0; background:var(--surf3); border:1px solid var(--border2); color:var(--white); font-size:13px; padding:7px 10px; border-radius:6px; outline:none; }
.tree-search-row input:focus { border-color:var(--orange); }
.tree-search-row input::placeholder { color:var(--gray); }
.tree-search-clear { background:none; border:none; color:var(--gray-l); font-size:16px; line-height:1; width:22px; height:22px; flex:none; border-radius:5px; display:flex; align-items:center; justify-content:center; }
.tree-search-clear:hover { color:var(--white); background:var(--hover-1); }
.nav-item { display:flex; align-items:center; gap:10px; padding:7px 16px; color:var(--gray-l); font-size:13px; font-weight:500; border-left:3px solid transparent; transition:all .15s; cursor:pointer; user-select:none; }
.nav-item:hover { color:var(--white); background:var(--hover-1); }
.nav-item.active { color:var(--white); border-left-color:var(--orange); background:var(--orange-d); }
.nav-item.nested { padding-left:38px; }
.nav-item .st { width:9px; height:9px; border-radius:50%; background:var(--gray-d); flex:none; }
.nav-item .st.online { background:var(--green); box-shadow:0 0 8px var(--green); }
.nav-item .st.offline { background:var(--red); }
.nav-item .st.alert { background:var(--orange); box-shadow:0 0 8px var(--orange); animation:pulse-alert 1.6s ease-in-out infinite; }
@keyframes pulse-alert { 0%,100%{opacity:1} 50%{opacity:.4} }
.nav-item .meta { min-width:0; }
.nav-item .name { font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.self-badge { display:inline-flex; align-items:center; margin-left:6px; font-size:9px; line-height:1; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--orange-h); background:var(--orange-d); border-radius:4px; padding:2px 5px; vertical-align:middle; }
.outdated-badge { display:inline-flex; align-items:center; margin-left:6px; font-size:9px; line-height:1; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--red); background:rgba(239,68,68,.15); border-radius:4px; padding:2px 5px; vertical-align:middle; }
.nav-item .os { font-size:11px; color:var(--gray); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Quick Support - unconfirmed row (2026-08-05): not clickable (onclick is null until
   confirmed, see buildQsItem), so the pointer cursor .nav-item sets globally would be
   misleading here. Was amber (this app's existing "needs attention" color) - Johan,
   2026-08-06: switch to the brand orange, matching the pairing code's own color in both
   the dashboard and the qs-pairing-window.ps1 popup, rather than the generic alert color. */
.nav-item.qs-pending { cursor:default; justify-content:space-between; border-left-color:var(--orange); }
.nav-item.qs-pending:hover { background:none; }
.nav-item.qs-pending .os b { color:var(--orange); }
.empty { color:var(--gray); font-size:13px; padding:10px 16px; }

/* Client / Site tree */
.tree-row { display:flex; align-items:center; gap:6px; padding:6px 16px; cursor:pointer; user-select:none; color:var(--gray-l); }
.tree-row:hover { color:var(--white); background:var(--hover-1); }
.tree-row.site-row { padding-left:26px; }
.tree-chevron { width:16px; flex:none; font-size:20px; line-height:1; display:inline-flex; align-items:center; color:var(--gray); transition:transform .15s; }
.tree-chevron.open { transform:rotate(90deg); }
.tree-name { flex:1; min-width:0; font-size:13px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.tree-row.site-row .tree-name { font-weight:500; font-size:12.5px; }
.tree-children.hidden { display:none; }
.tree-count { color:var(--gray); font-weight:400; margin-left:4px; }
/* Read-only chosen-path row (Add/Edit Backup Job modal) - same visual weight as
   .form-control but not an actual input, since paths only ever come from the Browse
   picker now, never typed. */
.bk-path-row { display:flex; align-items:center; gap:8px; background:var(--surf2); border:1px solid var(--border2); border-radius:var(--radius); padding:9px 12px; margin-bottom:6px; font-size:13px; color:var(--white); }
.bk-path-row span { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bk-path-row .btn-sm { width:28px; height:28px; padding:0; flex:none; display:flex; align-items:center; justify-content:center; }
.bk-path-row-nav { cursor:pointer; }
.bk-path-row-nav:hover { border-color:var(--orange); background:var(--orange-d); }
.bk-encrypt-warning {
  display:flex; gap:8px; align-items:flex-start; background:rgba(239,68,68,.08);
  border:1px solid rgba(239,68,68,.3); border-radius:var(--radius); padding:10px 12px;
  font-size:12px; line-height:1.5; color:#f87171; margin-top:4px;
}
/* Backup job wizard (Johan, 2026-08-07) - the Add/Edit modal used to be one long flat
   form; #bkWizardSteps is the numbered progress row above it, .bk-step the individual
   screens app.js's bkShowWizardStep() toggles one at a time via a .active class. */
.bk-wizard-steps { display:flex; align-items:flex-start; margin-bottom:22px; }
.bk-wizard-step { flex:1; display:flex; flex-direction:column; align-items:center; gap:6px; position:relative; }
.bk-wizard-step:not(:last-child)::after {
  content:''; position:absolute; top:13px; left:calc(50% + 20px); right:calc(-50% + 20px);
  height:2px; background:var(--border2);
}
.bk-wizard-step.done:not(:last-child)::after { background:var(--orange); }
.bk-wizard-num {
  width:26px; height:26px; border-radius:50%; display:flex; align-items:center; justify-content:center;
  font-size:12px; font-weight:700; line-height:1; background:var(--surf3); border:1px solid var(--border2); color:var(--gray-l);
  position:relative; z-index:1; transition:all .15s;
}
.bk-wizard-label { font-size:10px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; color:var(--gray); }
.bk-wizard-step.active .bk-wizard-num { background:var(--orange-d); border-color:var(--orange); color:var(--orange-h); }
.bk-wizard-step.active .bk-wizard-label { color:var(--white); }
.bk-wizard-step.done .bk-wizard-num { background:var(--orange); border-color:var(--orange); color:var(--white); }
.bk-step { display:none; }
.bk-step.active { display:block; }
/* Persistent close × (Johan, 2026-08-07) - the wizard's Cancel button only shows on
   step 1 (Back replaces it everywhere else), so without this there was no way out of
   steps 2-5 except clicking Back repeatedly. Generic enough to reuse on any other
   modal that gets a header row like this later. */
.modal-head-row { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:18px; }
.modal-head-row h3 { margin-bottom:0; }
.modal-close-x {
  background:none; border:none; padding:0; color:var(--gray-l); font-size:14px; line-height:1;
  width:28px; height:28px; flex:none; display:flex; align-items:center; justify-content:center;
  border-radius:var(--radius); cursor:pointer; transition:all .15s;
}
.modal-close-x:hover { color:var(--white); background:var(--surf3); }
.bk-review { background:var(--surf2); border:1px solid var(--border2); border-radius:var(--radius); padding:14px; margin-bottom:16px; font-size:13px; }
.bk-review-row { display:flex; gap:10px; padding:6px 0; border-bottom:1px solid var(--border2); }
.bk-review-row:last-child { border-bottom:none; }
.bk-review-row b { flex:none; width:110px; color:var(--gray-l); font-size:11px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; }
.bk-review-row span { flex:1; color:var(--white); white-space:pre-line; }
/* Native date input's own calendar-icon color follows color-scheme - dark keeps it
   legible against .form-control's dark background instead of the default near-black
   icon-on-dark browsers otherwise render. Border/focus theming already comes free from
   the shared .form-control rules (orange on focus, same as every other field here). */
input[type="date"].form-control { color-scheme:dark; }

/* Right-click context menu (clients/sites) */
.context-menu { position:fixed; z-index:300; background:var(--surf2); border:1px solid var(--border2); border-radius:var(--radius); box-shadow:var(--shadow); padding:6px; min-width:180px; display:flex; flex-direction:column; gap:1px; }
.context-menu button { background:none; border:none; color:var(--gray-l); text-align:left; padding:8px 10px; border-radius:6px; font-size:13px; }
.context-menu button:hover { color:var(--white); background:var(--hover-2); }
.context-menu button.danger { color:#f87171; }
.context-menu button.danger:hover { background:rgba(239,68,68,.12); }

/* Overview panel — no max-width, matching the landing dashboard's widget grid, which fills
   .content's full width edge-to-edge rather than sitting in a capped column. */
.card-box { background:var(--surf); border:1px solid var(--border); border-radius:var(--radius); padding:16px 18px; }
.ov-row { display:flex; justify-content:space-between; gap:10px; padding:7px 0; border-bottom:1px solid var(--border); font-size:13px; }
.ov-row:last-child { border-bottom:none; }
.ov-k { color:var(--gray); }
.ov-v { color:var(--white); font-weight:600; text-align:right; word-break:break-word; }
.ov-v-sub { color:var(--gray-l); font-weight:400; font-size:12px; }
.ov-v-line { margin-bottom:4px; }
.ov-v-line:last-child { margin-bottom:0; }
/* A status-dot + name row (hypervisor VM list) isn't a key/value pair like .ov-row - that
   class's justify-content:space-between pushed the dot to the far left and the name (via
   .ov-v's text-align:right) to the far right of the card instead of sitting next to it. */
.ov-row-list { display:flex; align-items:center; gap:8px; padding:7px 0; border-bottom:1px solid var(--border); font-size:13px; }
.ov-row-list:last-child { border-bottom:none; }
.ov-row-list .ov-v { text-align:left; font-weight:500; }
.ov-stats { display:flex; flex-direction:column; gap:10px; }
.stat-tile { display:flex; align-items:center; justify-content:space-between; }
.stat-label { font-size:11px; color:var(--gray); text-transform:uppercase; letter-spacing:.06em; }
.stat-val { font-size:18px; font-weight:900; font-family:var(--fd); }
.ov-alert { background:rgba(239,68,68,.12); border:1px solid rgba(239,68,68,.35); color:#f87171; border-radius:var(--radius); padding:8px 10px; font-size:12px; margin-top:4px; }
.ov-subhead { font-family:var(--fd); font-size:13px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--orange); margin-bottom:10px; }

/* Hardware inventory */
.btn-sm { padding:5px 12px; font-size:10px; }
.ov-volumes { display:flex; flex-direction:column; }
.hw-empty { color:var(--gray); font-size:12px; }
.disk-bar-row { margin-bottom:12px; }
.disk-bar-row:last-child { margin-bottom:0; }
.disk-bar-label { display:flex; justify-content:space-between; font-size:12px; color:var(--gray-l); margin-bottom:5px; }
.disk-bar-label b { color:var(--white); }
.disk-bar-track { background:var(--surf3); border-radius:4px; height:8px; overflow:hidden; }
.disk-bar-fill { background:var(--orange); height:100%; border-radius:4px; }
.disk-bar-fill.low { background:#f87171; }

/* Landing dashboard (Overview tab, shown when no endpoint is selected) */
.landing-panel { width:100%; }
.stat-widget-num { font-family:var(--fd); font-size:24px; font-weight:900; text-align:center; }
.patch.clickable { cursor:pointer; }
.patch.clickable:hover { border-color:var(--gray-d); }

/* Widget-card grid (landing dashboard) — Android-launcher-style drag/resize.
   Row height here must match ROW_H in app.js. Columns are square fixed-px cells set
   inline by app.js's updateGridCellVars() (however many fit the current width), not a
   fixed count here - this rule is just the pre-JS/mobile fallback. */
.widget-grid-toolbar { display:flex; justify-content:flex-end; margin-bottom:10px; }
.widget-grid {
  position:relative; display:grid;
  /* Must match ROW_H/GRID_GAP in app.js exactly - halved 2026-08-06 for finer resize increments. */
  grid-auto-rows:14px;
  gap:10px; margin-bottom:20px;
}
.widget-grid-dots {
  position:absolute; inset:0; z-index:0; pointer-events:none; border-radius:var(--radius);
  opacity:0; transition:opacity .15s ease;
  background-image:radial-gradient(circle, var(--orange) 1.5px, transparent 1.5px);
  background-size:var(--cell-w,100px) var(--cell-h,90px);
}
.widget-grid--editing .widget-grid-dots { opacity:1; }
.widget {
  position:relative; z-index:1; display:flex; flex-direction:column;
  transition:box-shadow .15s ease;
}
.widget-wide { max-width:none; }
.widget-grid--editing .widget-head { cursor:grab; touch-action:none; user-select:none; }
.widget.dragging { z-index:50; box-shadow:var(--shadow); cursor:grabbing; }
.widget.dragging .widget-head { cursor:grabbing; }
.widget.swap-preview { outline:2px dashed var(--orange); outline-offset:-2px; }
.widget-resize-handle {
  position:absolute; right:2px; bottom:2px; width:18px; height:18px; z-index:2;
  cursor:nwse-resize; touch-action:none;
  background:
    linear-gradient(135deg, transparent 0 40%, var(--gray-d) 40% 46%, transparent 46% 60%, var(--gray-d) 60% 66%, transparent 66%);
  opacity:.7; border-radius:0 0 var(--radius) 0;
}
.widget-resize-handle:hover { opacity:1; }
.widget-head { flex:none; display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; }
.widget-title { font-family:var(--fd); font-size:13px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--gray-l); }
.widget-count { font-family:var(--fd); font-size:13px; font-weight:900; color:var(--white); background:var(--surf3); border-radius:999px; padding:2px 10px; }
.widget-actions { display:flex; gap:8px; }
/* scrollbar-gutter:stable here too, same reason as .content: without it the gutter is
   only reserved once a card's content actually overflows, so its text visibly shifts
   left the moment that happens (and shifts back if it's resized taller again). */
.widget-body { flex:1; min-height:0; overflow:auto; scrollbar-gutter:stable; }
.widget-body-center { flex:1; min-height:0; display:flex; flex-direction:column; justify-content:center; }
/* Below the sidebar-drawer breakpoint, dragging/resizing a 6-col pixel grid isn't a
   realistic phone interaction — widgets stack full-width in layout order instead.
   Edit Layout is desktop/tablet-only in practice; the button still works but has
   nothing meaningful to drag against here. */
@media (max-width:768px) {
  .widget-grid { grid-template-columns:1fr; grid-auto-rows:auto; }
  .widget { grid-column:auto !important; grid-row:auto !important; }
  .widget-grid-dots, .widget-resize-handle { display:none; }
}

/* Donut chart (Endpoints widget) */
.donut-wrap { position:relative; width:120px; height:120px; margin:0 auto 16px; }
.donut { width:100%; height:100%; transform:rotate(-90deg); }
.donut-track { fill:none; stroke:var(--orange-dark); }
.donut-seg { fill:none; stroke:var(--orange); stroke-linecap:butt; transition:stroke-dashoffset .5s ease; }
.donut-center { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; }
.donut-num { font-family:var(--fd); font-size:22px; font-weight:900; }
.donut-sub { font-size:10px; text-transform:uppercase; letter-spacing:.06em; color:var(--gray); }
.donut-legend { display:flex; justify-content:center; gap:20px; font-size:12px; color:var(--gray-l); }
.donut-legend b { color:var(--white); }
.dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; }
.dot.online { background:var(--orange); }
.dot.offline { background:var(--orange-dark); }

/* Bar chart (Alerts by Type widget) */
.bar-chart { display:flex; justify-content:space-around; align-items:flex-end; height:140px; gap:12px; }
.bar-col { display:flex; flex-direction:column; align-items:center; height:100%; flex:1; cursor:pointer; }
.bar-track { flex:1; width:28px; display:flex; align-items:flex-end; background:var(--surf3); border-radius:4px; overflow:hidden; }
.bar-fill { width:100%; min-height:2px; border-radius:4px 4px 0 0; transition:height .5s ease; }
.bar-fill.tier-critical { background:var(--red); }
.bar-fill.tier-elevated { background:var(--orange); }
.bar-fill.tier-low { background:var(--blue); }
.bar-label { margin-top:8px; font-size:11px; color:var(--gray-l); text-align:center; }
.bar-label b { display:block; font-family:var(--fd); font-size:14px; color:var(--white); }

/* 24h alert history graph (landing dashboard) */
/* Real fixed-pixel padding, not just the SVG's internal viewBox-unit padding - the hover
   dot's glow is a fixed-px box-shadow, but the chart scales non-uniformly to fill the
   card (preserveAspectRatio="none"), so viewBox-unit padding alone translates to a
   different (and sometimes too-small) real pixel margin depending on card width. This
   guarantees actual pixel space between the chart and widget-body's overflow:auto edge
   regardless of how the card is sized. */
.alert-hist-wrap { position:relative; display:flex; flex-direction:column; height:100%; padding:14px 16px; }
.alert-hist-chart { flex:1; min-height:0; width:100%; display:block; }
.alert-hist-labels { display:flex; justify-content:space-between; font-size:11px; color:var(--gray); margin-top:2px; }
.alert-hist-hit { cursor:pointer; }
/* Real HTML element, not SVG - positioned in actual pixels at hover time (see
   wireAlertHistTooltip), so it stays a perfect circle regardless of the chart's
   non-uniform preserveAspectRatio="none" scaling. 7px beats the line's 2px stroke
   clearly without overwhelming it. */
.alert-hist-dot {
  position:absolute; width:7px; height:7px; margin:-3.5px 0 0 -3.5px;
  border-radius:50%; background:var(--orange-h);
  box-shadow: 0 0 6px 1px rgba(255,122,26,.9), 0 0 16px 4px rgba(255,122,26,.45);
  pointer-events:none; z-index:4;
}
/* position:fixed (viewport-relative), deliberately NOT absolute within .alert-hist-wrap -
   .widget-body is overflow:auto, and an absolutely-positioned descendant that overflows
   it still expands its scrollable content area even though it visually reads as
   "floating" - that grew a real scrollbar mid-hover for buckets with many alerting
   endpoints, which reflows the card and shifts the chart out from under the cursor,
   firing a spurious mouseleave (the tooltip then stays hidden until the next real mouse
   move, since a pure layout shift never generates one on its own). Fixed positioning
   escapes .widget-body's overflow entirely, so neither the clipping nor the reflow can
   happen. Safe against any ancestor creating a containing block for it (transform/filter/
   etc.) - none of .widget/.widget-body/.alert-hist-wrap set any of those.
   No max-height/scroll - grouped by client (see wireAlertHistTooltip) this is at most one
   row per client, so it just extends downward as far as it needs to; positionTooltip
   already flips it above the cursor if it wouldn't fit below. */
.alert-hist-tooltip, .bar-chart-tooltip {
  position:fixed; z-index:1000; pointer-events:none; max-width:320px;
  background:var(--surf3); border:1px solid var(--border2); border-radius:var(--radius);
  padding:8px 10px; font-size:11.5px; line-height:1.6; box-shadow:var(--shadow);
}
.alert-hist-tooltip-time { font-family:var(--fd); font-size:10px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--gray-l); margin-bottom:4px; }
/* No nowrap - client names vary a lot in length ("Specialised Outsourcing Services —
   3 alerting"); wrapping onto a second line beats overflowing past the box edge, which
   at a glance looked exactly like the tooltip clipping into the card again. */
.alert-hist-tooltip-row { color:var(--white); }
.alert-hist-tooltip-row b { color:var(--orange-h); font-weight:600; }
.alert-hist-tooltip-row.empty { color:var(--gray); font-style:italic; }

/* Deploy Agent panel */
.deploy-panel { max-width:520px; }
.deploy-title { font-family:var(--fd); font-size:20px; font-weight:900; letter-spacing:.02em; text-transform:uppercase; margin-bottom:8px; }
.deploy-sub { color:var(--gray); font-size:13px; margin-bottom:22px; line-height:1.6; }
.deploy-note { color:var(--gray); font-size:11px; margin-top:16px; line-height:1.7; }

/* Settings page (topbar cog) is now the same drag/resize/persist widget grid as the
   endpoint/hypervisor Overview tabs (createWidgetGrid, see settingsGrid in app.js) - no
   settings-panel/settings-section grid of its own anymore. tier-metric-note is kept -
   the warranty widget's "configured"/"not configured" status labels still use it. */
.tier-metric-note { text-transform:none; letter-spacing:0; font-weight:400; color:var(--gray); margin-left:6px; }
.cs-row { display:flex; gap:8px; margin-bottom:8px; align-items:center; }
.cs-row .form-control { margin-bottom:0; }
.cs-row .icon-btn { background:var(--surf3); border:1px solid var(--border2); color:var(--gray-l); border-radius:var(--radius); padding:7px 10px; font-size:12px; font-weight:600; flex:none; }
.cs-row .icon-btn:hover { color:var(--white); border-color:var(--gray-d); }
.cs-row .icon-btn.danger:hover { color:#f87171; border-color:#f87171; }

/* Scripts panel (endpoint tab, next to Patch Management) */
.scripts-head { display:flex; align-items:center; gap:14px; margin-bottom:14px; }
.spacer { flex:1; }
.script-list { display:flex; flex-direction:column; gap:10px; }
.script-card { background:var(--surf); border:1px solid var(--border); border-radius:var(--radius); padding:14px 16px; }
.script-card-head { display:flex; align-items:center; gap:10px; }
.script-card .s-name { font-weight:700; font-size:14px; }
.script-card .s-desc { color:var(--gray); font-size:12px; margin-top:2px; }
/* Scoped to .s-info specifically, not the bare .badge class shared by custom-select
   options/notify-prefs/etc elsewhere - now sits below the description text (moved into
   .s-info, after .s-desc, so it stacks under the text on any screen width instead of
   floating beside it in the header row) and needs its own top margin since it's no
   longer separated from .s-desc by .script-card-head's flex gap. */
.script-card .s-info .badge { margin-top:6px; display:inline-block; }
.script-card .s-actions { margin-left:auto; display:flex; gap:6px; flex:none; }
.script-card .icon-btn { background:var(--surf3); border:1px solid var(--border2); color:var(--gray-l); border-radius:var(--radius); padding:7px 10px; font-size:12px; font-weight:600; }
.script-card .icon-btn:hover { color:var(--white); border-color:var(--gray-d); }
.script-card .icon-btn.danger:hover { color:#f87171; border-color:#f87171; }
.script-output { margin-top:12px; background:#000; border:1px solid var(--border); border-radius:var(--radius); padding:10px 12px; font-family:var(--mono); font-size:12px; white-space:pre-wrap; max-height:220px; overflow:auto; color:#d6d6d6; }
.script-output .err { color:#f87171; }

/* Files panel (endpoint tab, next to Scripts) */
/* A real fixed-position element (see wireFilesDragDrop in app.js), not a CSS inset:0
   pseudo-element on #endpointContent - that element has scrollbar-gutter:stable, which
   reserves space between its padding edge and border edge for a scrollbar. Absolutely
   positioned children are contained within the PADDING edge, so inset:0 there left a
   scrollbar-gutter-width gap uncovered on the right. Measuring the real rect in JS and
   using position:fixed sidesteps that entirely. */
.files-drop-overlay { position:fixed; z-index:50; display:none;
  place-items:center; background:rgba(232,101,10,.10); border:2px dashed var(--orange);
  border-radius:var(--radius); color:var(--orange-h); font-size:15px; font-weight:600; pointer-events:none; }
.files-drop-overlay.show { display:grid; }
.files-head { display:flex; align-items:center; gap:10px; margin-bottom:14px; }
.files-head #fPath { flex:1; font-family:var(--mono); height:40px; }
.files-head #fGo, .files-head #fUp, .files-head #fVolume, .files-head #fRefresh { height:40px; }
.files-head #fGo { font-size:12px; letter-spacing:.04em; }
.files-head #fRefresh { font-size:18px; padding:0 12px; }
.files-head #backupBrowseUp, .files-head #backupBrowseVolume, .files-head #backupBrowsePath, .files-head #backupBrowsePathInput {
  height:32px; padding-top:0; padding-bottom:0; line-height:1;
}
.bk-browse-path-clickable { cursor:pointer; transition:all .15s; }
.bk-browse-path-clickable:hover { color:var(--white); border-color:var(--gray-d); }
.file-list { display:flex; flex-direction:column; gap:6px; }
.file-row { display:flex; align-items:center; gap:12px; background:var(--surf); border:1px solid var(--border); border-radius:var(--radius); padding:10px 14px; cursor:pointer; user-select:none; }
.file-row:hover { border-color:var(--gray-d); }
.file-row.selected { border-color:var(--orange); background:var(--orange-d); }
.file-row .f-icon { flex:none; font-size:15px; color:var(--gray-l); width:18px; text-align:center; }
/* File browser icons are emoji (fileIcon() in app.js) - the deliberate exception to the
   no-emoji rule, see the comment there. Every row gets one now (folder or page), so this
   only still matters for the restore-snapshot rows, which render an intentionally empty
   .f-icon and rely on it collapsing rather than leaving a blank gutter. */
.f-icon:empty { display:none; }
.file-row .f-name { flex:1; min-width:0; font-size:13px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.file-row .f-meta { flex:none; font-size:11px; color:var(--gray); margin-right:6px; }

/* Grid view (toggle in .files-head) - same rows, reflowed into square tiles.
   Fixed sizes throughout (icon, name block) rather than letting content decide -
   a short filename renders as 1 line vs. a long one wrapping to 2, and without a
   pinned height on .f-name that difference made aspect-ratio:1/1 on the tile
   itself unreliable (the row grew to fit whichever tile had the tallest content
   instead of staying uniformly square). */
.file-list.grid-view { display:grid; grid-template-columns:repeat(auto-fill,minmax(100px,1fr)); gap:4px; }
/* No card/box look here (unlike list view, where the bordered row makes sense) - a real
   icon grid (Explorer/Finder-style) has icons floating directly on the background, only
   picking up a highlight on hover/select. Johan, 2026-08-05: "take the folders out of the
   containers." Higher specificity than the plain .file-row rule (two classes vs one) wins
   without !important. */
/* min-width:0 is load-bearing, not decorative - a grid item's automatic minimum size is
   its unclamped min-content width (a long unbroken word/filename, e.g. "invoice_ninja_
   export_all_clients_2026.csv" with no space to break on) REGARDLESS of the f-name
   line-clamp below, so a single long name silently forced every 1fr column wider than
   the container and pushed the whole grid into horizontal scroll on mobile. Explicit
   min-width:0 zeroes that out so the columns actually respect their 1fr share and the
   line-clamp does the truncating instead. */
.file-list.grid-view .file-row { flex-direction:column; justify-content:center; align-items:center; text-align:center; gap:6px; padding:10px 6px; aspect-ratio:1/1; background:none; border:1px solid transparent; border-radius:var(--radius); min-width:0; }
.file-list.grid-view .file-row:hover { background:var(--surf2); border-color:transparent; }
.file-list.grid-view .file-row.selected { background:var(--orange-d); border-color:transparent; }
.file-list.grid-view .f-icon { flex:none; font-size:36px; width:auto; height:42px; line-height:42px; }
.file-list.grid-view .f-name {
  flex:none; width:100%; white-space:normal; word-break:break-word; font-size:12px; line-height:1.3;
  height:calc(1.3em * 2); display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden;
}
.file-list.grid-view .f-meta { display:none; }

/* Modal */
.modal-bg { position:fixed; inset:0; background:rgba(0,0,0,.6); display:grid; place-items:center; z-index:200; padding:20px; }
.modal { width:100%; max-width:520px; background:var(--surf); border:1px solid var(--border); border-radius:var(--radius); padding:26px; box-shadow:var(--shadow); max-height:90vh; overflow:auto; }
.modal h3 { font-family:var(--fd); font-size:18px; font-weight:700; letter-spacing:.03em; text-transform:uppercase; margin-bottom:18px; }
.modal .modal-actions { display:flex; gap:10px; margin-top:18px; }
.modal .modal-actions .btn { flex:1; }
.confirm-message { color:var(--gray-l); font-size:13.5px; line-height:1.6; white-space:pre-line; }
textarea.form-control.code { font-family:var(--mono); font-size:12.5px; min-height:180px; resize:vertical; }
.logo-preview-wrap { display:flex; justify-content:center; padding:20px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius); margin-bottom:16px; }
.logo-preview-wrap img { max-width:70%; max-height:70px; object-fit:contain; }
.avatar-preview-wrap { display:flex; justify-content:center; margin-bottom:16px; }
.avatar-preview { width:80px; height:80px; border-radius:50%; background:var(--orange-d); color:var(--orange); display:flex; align-items:center; justify-content:center; font-size:28px; font-weight:700; overflow:hidden; }
.avatar-preview img { width:100%; height:100%; object-fit:cover; }
.sidebar-footer { padding:12px; border-top:1px solid var(--border); }
.sidebar-user { display:flex; align-items:center; gap:10px; padding:8px; border-radius:var(--radius); cursor:pointer; transition:background .15s; }
.sidebar-user:hover { background:var(--hover-2); }
.sidebar-user .avatar { width:34px; height:34px; border-radius:50%; background:var(--orange-d); flex:none; display:flex; align-items:center; justify-content:center; font-weight:700; color:var(--orange); font-size:13px; overflow:hidden; }
.sidebar-user .avatar img { width:100%; height:100%; object-fit:cover; }
.sidebar-user .user-info { min-width:0; flex:1; }
.sidebar-user .user-name { font-size:13px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.sidebar-user .user-role { font-size:11px; color:var(--gray); text-transform:capitalize; }

/* Main */
.main { position:relative; flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
.topbar { height:64px; background:var(--surf); border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 20px; gap:14px; flex-shrink:0; position:relative; }
.topbar-toggle { background:none; border:none; color:var(--gray-l); font-size:20px; padding:6px; border-radius:var(--radius); display:none; }
.topbar-toggle:hover { color:var(--white); background:var(--hover-2); }
/* CSS-drawn hamburger for the mobile menu toggle (#ham) - was a trigram glyph, removed with
   the rest of the emoji/pictograph characters; three bars drawn instead so there's nothing
   font-dependent. */
#ham::before { content:""; display:block; width:18px; height:2px; background:currentColor; box-shadow:0 -5px currentColor, 0 5px currentColor; }
.topbar-title { font-family:var(--fd); font-size:18px; font-weight:900; letter-spacing:.02em; text-transform:uppercase; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:default; }
.topbar-right { margin-left:auto; display:flex; align-items:center; gap:14px; }
.conn { display:flex; align-items:center; gap:7px; height:32px; font-size:12px; color:var(--gray); text-transform:uppercase; letter-spacing:.06em; background:none; border:none; font-family:var(--fb); padding:0 8px; border-radius:var(--radius); cursor:pointer; transition:background .15s,color .15s; }
.conn:hover { background:var(--hover-2); color:var(--gray-l); }
.conn .live { width:8px; height:8px; border-radius:50%; background:var(--red); flex:none; }
.conn.on .live { background:var(--green); box-shadow:0 0 8px var(--green); }
.topbar-text-btn { display:flex; align-items:center; gap:7px; height:32px; font-size:12px; color:var(--gray); text-transform:uppercase; letter-spacing:.06em; background:none; border:none; font-family:var(--fb); padding:0 10px; border-radius:var(--radius); cursor:pointer; transition:background .15s,color .15s; flex:none; }
.topbar-text-btn:hover { background:var(--hover-2); color:var(--gray-l); }
/* Same dot .conn .live uses for "agent connection is live" - here it means "at least one
   conversation has a from-user reply you haven't opened yet" (see isConversationUnread). */
.unread-dot { width:8px; height:8px; border-radius:50%; background:var(--orange); flex:none; box-shadow:0 0 8px var(--orange); }
/* Optical centring for every glowing orb (Johan, 2026-09-16). align-items:center centres a
   dot on the label's LINE box, but these labels are all-caps 12px Roboto - no descenders -
   so the letters themselves sit ~0.5px above that centre and the orb reads as hanging low,
   which the glow only exaggerates. Nudge the orbs onto the cap-height centre instead. */
.conn .live, .unread-dot { position:relative; top:-0.5px; }
.topbar-text-btn.logout-btn { color:var(--orange-h); }
.topbar-text-btn.logout-btn:hover { color:var(--orange-h); background:var(--orange-d); }
/* .topbar-menu is display:contents on desktop - its buttons (Dark/Inbox/Settings/Sign Out)
   merge straight into .topbar-right's own row exactly as if this wrapper didn't exist. Only
   matters on mobile (see the 768px breakpoint below), where it becomes a real collapsible
   dropdown instead - same "some controls are always visible, the rest hide behind one
   button" pattern the Remote Desktop popup's own Tools button already uses. */
.topbar-menu { display:contents; }
.topbar-menu-btn { display:none; background:none; border:none; color:var(--gray-l); font-size:20px;
  line-height:1; padding:6px 10px; border-radius:var(--radius); cursor:pointer; }
.topbar-menu-btn:hover { color:var(--white); background:var(--hover-2); }

/* Tab bar */
.tabbar { display:flex; align-items:center; gap:2px; padding:0 20px; background:var(--surf); border-bottom:1px solid var(--border); flex-shrink:0; overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
.tabbar::-webkit-scrollbar { display:none; }
.tab { padding:14px 18px; font-size:12px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--gray-l); border:none; background:none; border-bottom:2px solid transparent; transition:all .15s; flex:0 0 auto; white-space:nowrap; }
.tab:hover { color:var(--white); }
.tab.active { color:var(--orange); border-bottom-color:var(--orange); }

/* Content + panels */
/* scrollbar-gutter:stable reserves the scrollbar's width whether or not content is
   actually tall enough to need it, so the page never reflows a few px narrower the
   moment it starts overflowing (which used to read as the cards' right padding no
   longer matching the left). The scrollbar itself is thin and transparent by default,
   only turning visible on hover/while scrolling - a "disappearing" scrollbar rather
   than either an always-visible one or no indicator at all. */
.content {
  /* No bottom padding - padding on a scrolling element is always part of its scrollable
     area, so a bottom pad here meant scrolling to the very end revealed nothing but
     that padding: a small, real, but pointless-looking scroll range even when all the
     actual content already fit. Top/side padding don't have this problem since they're
     never "the last thing" being scrolled into view. */
  flex:1; min-height:0; overflow:auto; padding:20px 24px 0;
  scrollbar-gutter:stable;
  scrollbar-width:thin; scrollbar-color:transparent transparent;
}
.content:hover { scrollbar-color:var(--gray-d) transparent; }
.panel.hidden { display:none; }
.placeholder { display:grid; place-items:center; height:100%; min-height:340px; color:var(--gray); text-align:center; }
/* Webcam tab - #wcImageWrap holds exactly one of the three children visible at a
   time (image / loading / error), none of it reusing .modal-actions (scoped to
   .modal .modal-actions, doesn't apply outside an actual modal) or the full-height
   .placeholder (sized for an entire empty PANEL, too heavy for a small inline
   status line nested inside an already-populated view). */
#wcImageWrap { margin:14px 0; min-height:200px; display:flex; align-items:center; justify-content:center;
  background:var(--surf2); border:1px solid var(--border2); border-radius:var(--radius); }
#wcImage { max-width:100%; max-height:60vh; border-radius:var(--radius); display:block; }
#wcImage.hidden { display:none; }
.wc-status { color:var(--gray); text-align:center; padding:24px; }
.wc-status.hidden { display:none; }

/* Same disappearing-scrollbar treatment for individual widget cards, which get their
   own internal scroll once resized shorter than their content. */
.widget-body { scrollbar-width:thin; scrollbar-color:transparent transparent; }
.widget-body:hover { scrollbar-color:var(--gray-d) transparent; }

.content::-webkit-scrollbar, .widget-body::-webkit-scrollbar { width:10px; }
.content::-webkit-scrollbar-track, .widget-body::-webkit-scrollbar-track { background:transparent; }
.content::-webkit-scrollbar-thumb, .widget-body::-webkit-scrollbar-thumb {
  background-color:transparent; background-clip:padding-box;
  border:3px solid transparent; border-radius:8px; transition:background-color .2s ease;
}
.content:hover::-webkit-scrollbar-thumb, .widget-body:hover::-webkit-scrollbar-thumb {
  background-color:var(--gray-d); background-clip:padding-box;
}
.content:active::-webkit-scrollbar-thumb, .widget-body:active::-webkit-scrollbar-thumb,
.content:focus-within::-webkit-scrollbar-thumb, .widget-body:focus-within::-webkit-scrollbar-thumb {
  background-color:var(--gray); background-clip:padding-box;
}

/* Remote desktop */
.rd-toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-bottom:14px; }
.chip { display:inline-flex; align-items:center; gap:6px; background:var(--surf3); border:1px solid var(--border2); color:var(--gray-l); border-radius:var(--radius); padding:8px 13px; font-size:12px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; transition:all .15s; }
.chip:hover { color:var(--white); border-color:var(--gray-d); }
.chip.toggle.on { border-color:var(--orange); color:var(--orange-h); background:var(--orange-d); }
.chip.info { text-transform:none; letter-spacing:0; color:var(--gray); cursor:default; }
.chip:disabled { opacity:.45; cursor:not-allowed; }
.screen-wrap { background:#000; border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; position:relative; }
#screen { width:100%; display:block; touch-action:none; }
.screen-hint { position:absolute; inset:0; display:grid; place-items:center; color:var(--gray); font-size:13px; pointer-events:none; }

/* Console */
.console { display:flex; flex-direction:column; height:100%; min-height:420px; }
.console-out { flex:1; background:#000; border:1px solid var(--border); border-radius:var(--radius); padding:14px; font-family:var(--mono); font-size:12.5px; line-height:1.5; white-space:pre-wrap; overflow:auto; color:#d6d6d6; }
.console-out .cmd { color:var(--orange-h); } .console-out .err { color:#f87171; }
.console-in { display:flex; gap:8px; margin-top:10px; }
.console-in .form-control { font-family:var(--mono); }
/* #cShell is now run through enhanceSelect (see app.js) for the same themed dropdown
   every other select on the page already uses - the plain native <select> it used to be
   stuck out against the rest of the UI (browser-default white popup, no dark styling).
   The real select becomes an invisible 1x1 node once enhanced (.custom-select-native),
   so the width constraint that used to target it directly now belongs on the visible
   wrapper/trigger instead. */
.console-in .custom-select { width:130px; flex:none; }
.console-in .custom-select-trigger { font-family:var(--mono); }

/* Patches */
.patch-head { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:14px; }
.patch-list { display:flex; flex-direction:column; gap:8px; }
.patch { display:flex; align-items:center; gap:12px; background:var(--surf); border:1px solid var(--border); border-radius:var(--radius); padding:12px 14px; }
/* Was an inline style="flex:1" - moved to a real class so the mobile override below
   (which needs to win over it) doesn't have to fight inline-style specificity. */
.bk-job-info { flex:1; min-width:0; }
.patch input { width:17px; height:17px; accent-color:var(--orange); flex:none; }
.patch .t { flex:1; min-width:0; }
.patch .t .title { font-size:13px; font-weight:600; }
.patch .t .kb { font-size:11px; color:var(--gray); }
.badge { font-size:10px; font-weight:700; padding:3px 8px; border-radius:4px; text-transform:uppercase; letter-spacing:.04em; flex:none; }
.badge.win { background:rgba(16,185,129,.15); color:var(--green); }
.badge.tp { background:rgba(245,166,35,.15); color:var(--amber); }
.badge.err { background:rgba(239,68,68,.15); color:var(--red); }
.badge.muted { background:var(--surf3); color:var(--gray); }
.badge.running { background:var(--orange-d); color:var(--orange-h); }
.chip.danger { color:#f87171; border-color:rgba(239,68,68,.35); }
.chip.danger:hover { background:rgba(239,68,68,.12); border-color:#f87171; }
.patch-search-row { display:flex; align-items:center; gap:8px; margin-bottom:14px; }
.patch-search-row #pSearch { flex:1; }
/* Patch toolbar on a phone: the row of actions (Scan Windows Updates / Installed Programs /
   Install selected / + Install Custom) crowded and wrapped awkwardly around the flex spacer.
   Stack them full-width instead - clear, thumb-sized, predictable as buttons show/hide. */
@media (max-width:768px) {
  .patch-head { flex-direction:column; align-items:stretch; gap:8px; }
  .patch-head .spacer { display:none; }
  .patch-head .chip, .patch-head .btn { width:100%; justify-content:center; }
  .patch-search-row { flex-wrap:wrap; }
  .patch-search-row #pSearch { flex:1 1 100%; }
  .patch-search-row #pRefreshBtn { width:100%; }
}
.patch-search-row #pSearch, .patch-search-row #pRefreshBtn { height:36px; }
.patch.selectable { cursor:pointer; user-select:none; }
.patch.selectable:hover { border-color:var(--gray-d); }
.patch.selected { border-color:var(--orange); background:var(--orange-d); }

/* Toast progress bar (Install Custom / bulk program actions / file upload). Fixed width,
   not the old min/max range - a shrink-wrapped toast resizes as its label text changes
   length (e.g. the upload toast's percent/speed suffix growing and shrinking), which
   also visibly changes the bar-track's own width every tick since it's a block child. */
.toast.progress { width:340px; max-width:340px; }
.toast .bar-track { width:100%; height:7px; border-radius:4px; background:var(--surf3); margin-top:8px;
  overflow:hidden; box-shadow:inset 0 1px 2px rgba(0,0,0,.25); }
.toast .bar-fill { height:100%; border-radius:4px; position:relative; overflow:hidden;
  background:linear-gradient(90deg, var(--orange-dark), var(--orange));
  transition:width .35s cubic-bezier(.22,.8,.32,1); }
/* Diagonal shine sweeping across the fill while a transfer is actively in progress -
   clipped by bar-fill's own overflow:hidden, so it only ever shows over the filled
   portion, not the empty track. Stops automatically once finish() adds .ok/.err (the
   transfer is done at that point, nothing left to communicate as "still working"). */
.toast .bar-fill::after {
  content:''; position:absolute; inset:0; width:50%;
  background:linear-gradient(90deg, transparent, rgba(255,255,255,.4), transparent);
  transform:translateX(-120%);
  animation:bar-shimmer 1.4s ease-in-out infinite;
}
.toast.ok .bar-fill { background:var(--green); }
.toast.err .bar-fill { background:var(--red); }
.toast.ok .bar-fill::after, .toast.err .bar-fill::after { animation:none; content:none; }
@keyframes bar-shimmer { 0% { transform:translateX(-120%); } 100% { transform:translateX(320%); } }
.toast.fade-out { opacity:0; transition:opacity .5s; }
.toast-progress-row { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.toast-progress-label { flex:1; min-width:0; }
/* Two separate lines, not one truncated string - a long filename eating the whole line
   was hiding the percent/speed that came after it in the same string entirely. The
   filename can safely truncate; the status line (short, fixed-ish length) never does,
   so percent/speed stays visible regardless of how long the filename is. */
.toast-progress-filename { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.toast-progress-meta { color:var(--gray-l); font-size:11px; margin-top:2px; }
.toast-progress-cancel { flex:none; cursor:pointer; background:none; border:1px solid var(--border2); color:var(--gray-l);
  font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; padding:3px 8px; border-radius:4px; }
.toast-progress-cancel:hover { color:var(--white); border-color:var(--gray-d); background:var(--hover-2); }

/* Toasts */
/* contain:layout tells the browser this fixed-position subtree can't affect anything
   outside it — without it, every toast mount/unmount was forcing a page-wide style
   recalc, which very briefly drops and reapplies :hover on whatever the mouse happened
   to be over elsewhere (any :hover with a transition then visibly "flashes" through it). */
.toasts { position:fixed; bottom:18px; right:18px; display:flex; flex-direction:column; gap:8px; z-index:400; contain:layout style; }
.toast { background:var(--surf2); border:1px solid var(--border2); border-left:3px solid var(--orange); border-radius:var(--radius); padding:11px 14px; font-size:13px; box-shadow:var(--shadow); max-width:320px; text-align:left; }
.toast.err { border-left-color:var(--red); } .toast.ok { border-left-color:var(--green); }
.toast.elevated { border-left-color:var(--amber); } .toast.critical { border-left-color:var(--red); }
/* On a phone, center the notifications horizontally (bottom-center) instead of pinning them
   to the bottom-right corner, and keep them within the screen width. */
@media (max-width:768px) {
  .toasts { left:50%; right:auto; transform:translateX(-50%); align-items:center; max-width:calc(100vw - 24px); }
  .toast { max-width:calc(100vw - 24px); text-align:center; }
  .toast.progress { width:auto; max-width:calc(100vw - 24px); }
}

.notify-prefs-list { display:flex; flex-direction:column; gap:2px; max-height:50vh; overflow-y:auto; }
.notify-pref-row { display:flex; align-items:center; gap:10px; padding:8px 4px; border-bottom:1px solid var(--border); cursor:pointer; }
.notify-pref-row:last-child { border-bottom:none; }
.notify-pref-row input[type=checkbox] { width:16px; height:16px; accent-color:var(--orange); flex:none; }
.notify-pref-label { flex:1; font-size:13px; color:var(--gray-l); }
.notify-pref-row .custom-select { width:120px; flex:none; }
/* Rows with inline threshold/duration inputs (highCpu, cpuTempHigh, etc.) run noticeably
   longer than a plain label - the modal itself is widened (see #notifyPrefsModalBg .modal
   inline style) so these stay on one line on desktop; only the row-var variant forces
   nowrap, so plain single-line labels keep wrapping normally on narrow screens like before. */
.notify-pref-row-var .notify-pref-label { white-space:nowrap; }
.notify-pref-num { width:30px; height:30px; background:var(--surf2); border:1px solid var(--border2); border-radius:6px;
  padding:0; color:var(--white); font-size:13px; text-align:center; flex:none; -moz-appearance:textfield; }
.notify-pref-num:focus { border-color:var(--orange); outline:none; }
/* Native spin-button arrows (type=number) eat space on the right of the box - at this
   box's 30px width that squeezes the text left of center regardless of text-align, so
   they're removed entirely (box is small enough that scroll-wheel/arrow-key editing is
   plenty, the arrows aren't pulling their weight here). */
.notify-pref-num::-webkit-outer-spin-button, .notify-pref-num::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
/* A browser window narrower than the modal's own max-width still has to shrink it to fit
   (modal-bg only ever grants `100% - padding`) - past that point, force a wrap instead of
   letting the row overflow the modal's own edge and get clipped/scrollable. */
@media (max-width:900px) {
  .notify-pref-row-var { flex-wrap:wrap; }
  .notify-pref-row-var .notify-pref-label { flex-basis:100%; white-space:normal; }
}

/* Chat with the end user - native popup on their own desktop, this side is a plain
   modal (reachable standalone from the endpoint list, not nested inside remote.js's
   pop-out). Bubbles reuse the same orange-for-admin/neutral-for-them convention
   .chip.toggle.on and file-row.selected already use elsewhere for "this is the active
   one" - here it's "this is your side of the conversation" instead. */
.chat-modal { width:480px; max-width:92vw; }
.chat-modal-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.chat-modal-titles { min-width:0; }
/* Who you're actually talking to (their logged-in Windows full name, best-effort - falls
   back to username, then the endpoint's own label if nobody's logged in at all) comes
   first and reads like a name, not a shouty all-caps label. The real PC hostname (not the
   friendly dashboard label, which "MESSAGE MEDIA SERVER" used to show here) sits smaller
   and muted below it, since that's reference info, not the headline. */
.chat-user-name { font-family:var(--fd); font-size:15px; font-weight:700; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; }
.chat-host-name { font-size:11px; color:var(--gray); letter-spacing:.02em; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; }
.chat-log { height:320px; overflow-y:auto; background:var(--surf2); border:1px solid var(--border2);
  border-radius:var(--radius); padding:10px; margin:14px 0; display:flex; flex-direction:column; gap:8px; }
.chat-empty { color:var(--gray); font-size:12px; text-align:center; margin:auto; }
.chat-msg { max-width:80%; padding:8px 12px; border-radius:var(--radius); font-size:13px; line-height:1.4;
  overflow-wrap:break-word; }
/* Softer than the solid brand orange (was jarring as a big filled bubble, fine as a small
   accent) - alpha over the surface instead of a flat fill, text color follows --white
   (which itself flips light/dark per theme) so it stays readable either way. */
.chat-msg.from-admin { align-self:flex-end; background:rgba(232,101,10,.55); color:var(--white); }
.chat-msg.from-user { align-self:flex-start; background:var(--surf3); color:var(--white); }
/* Underlined either way so a link reads as clickable regardless of bubble color -
   from-admin's own background is already the orange accent, so a same-color link
   text there would be low-contrast; inherits the bubble's white instead. from-user's
   neutral background has no such clash, so the accent color works directly. */
.chat-msg a { text-decoration:underline; }
.chat-msg.from-admin a { color:inherit; }
.chat-msg.from-user a { color:var(--orange); }
.chat-msg.system { align-self:center; color:var(--gray); font-size:11px; font-style:italic; }
/* Classic three-dot "typing" bubble (WhatsApp/Messenger/iMessage-style) instead of literal
   "Vincent is typing…" text - reuses .chat-msg.from-user's bubble (same side/color a real
   reply from him would use), just replaces the text content with three staggered-bounce
   dots. inline-flex (not flex) + a small dedicated padding, not .chat-msg's own 8px 12px -
   Johan, 2026-08-05: "still too big and wide" after a first pass - a block-level flex
   container sizes to its AVAILABLE width, not what its content needs; inline-flex shrinks
   to fit just the 3 small dots, and they need far less breathing room than a line of text. */
.chat-typing { display:inline-flex; align-items:center; gap:3px; padding:6px 10px; }
.chat-typing span { width:5px; height:5px; border-radius:50%; background:var(--gray-l);
  animation:chat-typing-bounce 1.2s infinite ease-in-out; }
.chat-typing span:nth-child(2) { animation-delay:.15s; }
.chat-typing span:nth-child(3) { animation-delay:.3s; }
@keyframes chat-typing-bounce {
  0%, 60%, 100% { transform:translateY(0); opacity:.5; }
  30% { transform:translateY(-4px); opacity:1; }
}
/* Inbox transcript only (renderMessageRow) - the live chat modal's own bubbles
   (appendChatMessage) don't build this nested span, so they're unaffected. */
.chat-msg-time { display:block; font-size:10px; margin-top:3px; opacity:.65; }
.chat-input-row { display:flex; gap:8px; margin-top:14px; margin-bottom:14px; position:relative; }
.chat-input-row .form-control { flex:1; }

#broadcastInput { margin-top:10px; }
.broadcast-editor { min-height:80px; max-height:220px; overflow-y:auto; line-height:1.25; }
.broadcast-editor:empty:before { content:attr(data-placeholder); color:var(--gray); }
.broadcast-editor b, .broadcast-editor strong { font-weight:600; }
/* No default underline - only an EXPLICIT underline from Word's own inline style
   should show one (that still wins here: inline style beats this class rule on
   specificity). Word's own "remove underline" edit doesn't reliably leave an
   explicit text-decoration:none behind (it can just fall back to inheriting from a
   stylesheet class we already strip), so defaulting to none is the only way both
   "kept the underline" and "removed the underline" round-trip correctly. */
.broadcast-editor a { color:var(--orange); text-decoration:none; }
.broadcast-editor p, .broadcast-editor div { margin:0 0 3px 0; }
.broadcast-editor p:last-child, .broadcast-editor div:last-child { margin-bottom:0; }
.broadcast-hint { font-size:11px; color:var(--gray); margin:6px 0 0; }

/* Logs - Chat/Commands/Patches/Alerts, one container with a type switcher. Chat used to
   be a two-pane conversation list + thread (the old "Inbox", its own .inbox-* CSS); as of
   2026-08-05 all four tabs share this same flat, read-only-list look for visual
   consistency - Johan wanted Chat to match the others rather than stand out. Replying to
   a real endpoint jumps into its own live chat modal instead (.chat-log/.chat-msg below,
   unchanged); Vincent gets his own standing quick-reply bar (.chat-input-row, reused as-is
   from that same modal) pinned above the list since he isn't a real endpoint. */
/* Same underline-tab look as the endpoint tabbar (.tabbar/.tab) rather than its own
   separate chip-button style - Johan, 2026-08-05, wanted Logs to match. Not literally the
   same element (.tabbar lives outside .content and stays hidden for this whole view, see
   showLogsView) - just the same visual language, adapted to sit inside .content's own
   padding (no background/side-padding of its own, a plain border-bottom instead). */
.logs-type-tabs { display:flex; align-items:center; gap:2px; margin:0 0 14px;
  border-bottom:1px solid var(--border); flex-shrink:0; overflow-x:auto; }
.log-type-tab { position:relative; display:inline-flex; align-items:center; padding:14px 18px; font-size:12px; font-weight:600; letter-spacing:.06em;
  text-transform:uppercase; color:var(--gray-l); border:none; background:none;
  border-bottom:2px solid transparent; transition:all .15s; flex:0 0 auto;
  white-space:nowrap; cursor:pointer; }
.log-type-tab:hover { color:var(--white); }
.log-type-tab.on { color:var(--orange); border-bottom-color:var(--orange); }
/* "New data since you last looked" orb on a log category tab - inline right before the
   label, matching the topbar "Logs" unread dot (.unread-dot). See markLogNew in app.js. */
.log-type-tab.has-new::before { content:""; width:8px; height:8px; border-radius:50%; flex:none;
  background:var(--orange); box-shadow:0 0 8px var(--orange); margin-right:7px;
  position:relative; top:-0.5px; }
.log-type-tab.on.has-new::before { display:none; }
/* Space-used indicator, left of Clear Log (Johan, 2026-08-06) - updated per-category on
   every switchLogType(), same orange as the active tab rather than inventing a new accent. */
.log-size-indicator { font-size:11px; font-weight:600; letter-spacing:.04em; color:var(--orange); flex:none; margin-right:4px; }
.log-type-panel.hidden { display:none; }

/* Per-category search box (Johan, 2026-08-06) - filters the already-fetched rows
   client-side (see loadChatLog/etc. in app.js), no extra request per keystroke. Same
   margin rhythm as .chat-input-row above it so Chat's search box sits flush under
   Vincent's reply bar rather than looking like a second, disconnected row. */
.log-search-row { margin-bottom:14px; }
.log-search-row .log-search-input { width:100%; }
/* Flat read-only timelines - no two-pane scroll needed, they just flow in .content's own
   normal page scroll. */
.log-flat-list { display:flex; flex-direction:column; }
.log-row { padding:12px 14px; border-bottom:1px solid var(--border2); font-size:12.5px; }
.log-row-top { display:flex; justify-content:space-between; align-items:baseline; gap:8px; flex-wrap:wrap; }
.log-row-host { font-weight:700; }
.log-row-time { font-size:11px; color:var(--gray); flex-shrink:0; margin-left:auto; }
.log-row-detail { color:var(--gray-l); margin-top:4px; overflow-wrap:anywhere; }
.log-row-detail code { background:var(--surf2); padding:1px 5px; border-radius:4px; font-size:11.5px; }

.chat-emoji-btn { background:none; border:1px solid var(--border2); border-radius:var(--radius);
  width:38px; flex-shrink:0; cursor:pointer; display:flex; align-items:center; justify-content:center; }
.chat-emoji-btn:hover { border-color:var(--orange); }
.chat-emoji-btn img { width:18px; height:18px; }
/* Pops out over the chat log instead of pushing the input row down when toggled -
   bottom:100% anchors it directly above .chat-input-row (its positioned parent),
   independent of whatever height things above it end up being. */
.chat-emoji-picker { position:absolute; left:0; bottom:100%; margin-bottom:8px; z-index:20;
  display:grid; grid-template-columns:repeat(8,1fr); gap:2px; background:var(--surf2);
  border:1px solid var(--border2); border-radius:var(--radius); padding:6px;
  width:280px; max-width:80vw; max-height:180px; overflow-y:auto; box-shadow:var(--shadow); }
.chat-emoji-picker button { background:none; border:none; line-height:1; cursor:pointer;
  padding:4px; border-radius:6px; display:flex; align-items:center; justify-content:center; }
.chat-emoji-picker button img { width:20px; height:20px; }
.chat-emoji-picker button:hover { background:var(--surf3); }

/* Chat is always a pinned corner panel, never a centered blocking modal - Johan wants to
   keep working in the rest of the dashboard while chatting, always, not as an opt-in.
   #chatModalBg still covers the full viewport but is click-through (pointer-events:none)
   everywhere except over the panel itself, which re-enables events. */
#chatModalBg { background:transparent; display:block; padding:0; pointer-events:none; }
#chatModalBg .chat-modal { position:fixed; right:18px; bottom:18px; width:460px; max-width:92vw;
  max-height:min(560px,80vh); pointer-events:auto; }

/* Mobile */
.sidebar-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:90; }
@media (max-width:768px) {
  .topbar-toggle { display:inline-flex; align-items:center; justify-content:center; }
  .sidebar { position:fixed; top:0; left:0; height:100dvh; width:min(260px,85vw); transform:translateX(-100%); transition:transform .25s ease; box-shadow:0 0 50px rgba(0,0,0,.6); }
  .sidebar.open { transform:translateX(0); }
  .sidebar-backdrop.show { display:block; }
  .sidebar-resize-handle { display:none; } /* fixed-width overlay drawer here, resizing doesn't apply */
  .content { padding:16px; }
  .conn span.txt { display:none; }
  /* Touch target sizing — bumped up from the mouse-cursor-sized defaults, gated to
     mobile only so desktop density stays as-is. */
  .btn { min-height:44px; }
  .chip { min-height:40px; }
  .topbar-text-btn { min-height:40px; }
  .context-menu button { padding:12px 10px; }
  /* Dark/Inbox/Settings/Sign Out collapse behind the ⋮ button - with the connection dot
     already down to just a dot (see .conn span.txt above) and the sidebar hamburger also
     competing for space, five separate buttons plus the endpoint title genuinely didn't
     fit on a phone-width topbar (confirmed live: wrapped to a second row that clipped/
     overlapped the tab bar below it). Only conn (the actual live/reconnecting status,
     glanceable at all times) stays outside the collapse - everything else is a settings
     action you reach for occasionally, not something worth permanent screen space for. */
  .topbar-menu-btn { display:inline-flex; align-items:center; justify-content:center; }
  .topbar-menu { display:none; }
  .topbar-menu.open {
    display:flex; flex-direction:column; align-items:stretch; gap:4px;
    position:absolute; top:100%; right:20px; margin-top:8px; z-index:95;
    background:var(--surf2); border:1px solid var(--border2); border-radius:var(--radius);
    padding:8px; box-shadow:0 8px 24px rgba(0,0,0,.4); min-width:180px;
  }
  .topbar-menu.open .topbar-text-btn { justify-content:flex-start; }
  /* #logsView specifically (not .content generally) - a phone screen is narrow enough
     that the desktop card's own padding reads as wasted space rather than visual polish
     across all four flat-list tabs; drop it here for something closer to edge-to-edge.
     Higher specificity than the plain .content rule (16px) so this wins without !important. */
  #logsView { padding:8px; }
  /* Scripts tab: .script-card-head is a single row on desktop (.s-info - name, desc,
     shell badge, all stacked inside it - then the Run/Edit/Delete buttons, pinned right
     via margin-left:auto) - on a phone-width card a long description wraps across many
     lines, and without this override .s-info would only take its own content width
     (flex default), leaving it narrower than the card and the actions row awkwardly
     placed beside it instead of below (confirmed live via a real screenshot before the
     badge lived inside .s-info). align-items:flex-start (not the default stretch) keeps
     the actions row at its own content width instead of stretching edge-to-edge - only
     .s-info is forced full-width since it's the one thing that should wrap at the card's
     full width. margin-left:0 cancels the row-layout's right-alignment trick, which
     would otherwise flip the button row to the right edge under flex-start. */
  .script-card-head { flex-direction:column; align-items:flex-start; gap:10px; }
  .script-card .s-info { width:100%; }
  .script-card .s-actions { margin-left:0; }
  /* Review step's fixed 110px label column ate too much of an already-narrow modal,
     same "stack label above value" treatment as .notify-pref-row-var above. */
  .bk-review-row { flex-wrap:wrap; }
  .bk-review-row b { flex-basis:100%; width:auto; }
  /* File Browser grid view (Johan, 2026-08-09) - auto-fill/minmax(100px,1fr) settles
     on 3 columns at typical phone widths; a fixed 4-column grid with a tighter gap
     packs more icons on screen at once, which is what was actually asked for here. */
  .file-list.grid-view { grid-template-columns:repeat(4,1fr); gap:3px; }
  /* File Browser's own toolbar (Johan, 2026-08-09) - Up/drive/path/Go/Refresh/spacer/
     List/Grid/Upload is 9 items in one un-wrapped flex row, nowhere near enough width
     on a phone. Let it wrap, and force the path input onto its own full-width line
     (flex-basis:100% always starts a new line) since it's the one field that actually
     needs room to be usable for typing/reading a real path - everything before it
     (Up/drive) settles onto line 1, everything after (Go/Refresh/view toggles/Upload)
     wraps onto its own line(s) below since there's no space left after a 100%-wide
     path field. */
  .files-head { flex-wrap:wrap; }
  .files-head #fPath { flex-basis:100%; }
}

/* Phone-in-portrait — narrower than the drawer-nav breakpoint above; console input
   row and modal padding get cramped well before 768px does. */
@media (max-width:480px) {
  .console-in { flex-wrap:wrap; }
  .console-in #cShell { flex:1 1 100%; }
  .console-in #cInput { flex:1 1 auto; }
  .modal-bg { padding:12px; }
  .modal { padding:18px; }
}
