/* app-components.css - app-specific components shared by EVERY template
   (login page, menu manager, stat cards, DataTables tweaks, duct/network widgets,
   .hidden/.muted utilities...). Loaded after the template's own CSS. */
/* Small square icon-only action buttons (e.g. table row actions) - a
   plain single-color glyph rather than a labeled button, for compact
   rows with several actions. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border: none; border-radius: var(--radius);
  padding: 0; cursor: pointer; background: transparent; color: var(--color-muted);
  transition: background 0.15s ease, color 0.15s ease;
}
.icon-btn + .icon-btn { margin-left: 2px; }
.icon-btn svg { width: 15px; height: 15px; display: block; pointer-events: none; }
.icon-btn:hover { background: var(--color-surface-alt); color: var(--color-text); }
.icon-btn.icon-btn-danger:hover { background: #fde2e2; color: var(--color-danger); }

/* Icon-only buttons that (unlike .icon-btn above) always carry a solid
   color background, e.g. the Projects page's row actions - each action
   type gets its own color so several adjacent icon buttons stay easy to
   tell apart without any text label. */
.icon-btn-solid {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border: none; border-radius: var(--radius);
  padding: 0; cursor: pointer; color: #fff; flex-shrink: 0;
  box-shadow: 0 1px 2px rgba(16, 24, 32, 0.08);
  transition: filter 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease;
}
.icon-btn-solid + .icon-btn-solid { margin-left: 4px; }
.icon-btn-solid svg { width: 16px; height: 16px; display: block; pointer-events: none; }
/* Some of these (e.g. Projects page's "Open"/"Map" row actions) are <a>
   tags, not <button>s - without this explicit color, the generic
   `a:hover { color: var(--color-primary-dark) }` rule above (which wins
   simply because .icon-btn-solid:hover never declared `color` at all,
   regardless of its higher selector specificity) took over on hover and
   turned the currentColor-based SVG icon a dark/near-black color instead
   of staying its normal white. */
.icon-btn-solid:hover { filter: brightness(0.9); text-decoration: none; transform: translateY(-1px); box-shadow: 0 4px 10px rgba(16, 24, 32, 0.18); color: #fff; }
.icon-btn-solid:active { transform: translateY(0); }
.icon-btn-solid:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; }
.icon-btn-solid.icon-btn-primary { background: var(--color-primary); }
.icon-btn-solid.icon-btn-secondary { background: var(--color-muted); }
.icon-btn-solid.icon-btn-danger { background: var(--color-danger); }
.icon-btn-solid.icon-btn-map { background: var(--color-map-btn); }

/* "Layer categories" admin page (categories.php/categories.js) - a flat,
   already depth-first-ordered list rendered as an indented tree (each
   row's own inline margin-left handles the indentation, see
   categories.js's renderTree()). */
.cat-node {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 10px; border-radius: 6px;
}
.cat-node:hover { background: var(--color-surface-alt); }
.cat-node-name { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cat-node-actions { display: flex; gap: 4px; flex-shrink: 0; }
.cat-node-actions .icon-btn-solid { width: 26px; height: 26px; }
.cat-node-actions .icon-btn-solid svg { width: 13px; height: 13px; }
.catAddBtn { font-weight: 700; font-size: 15px; line-height: 1; }

/* Menu manager admin page (menu_manager.php/assets/js/pages/menu_manager.js) */
.menu-manager-columns { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.menu-manager-col { flex: 1 1 420px; min-width: 340px; }
.menu-node {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 10px; border-radius: 6px;
}
.menu-node:hover { background: var(--color-surface-alt); }
.menu-node-main { display: flex; align-items: center; gap: 8px; overflow: hidden; }
.menu-node-label { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-node-meta { font-size: 11px; color: var(--color-muted); white-space: nowrap; }
.menu-node-actions { display: flex; gap: 4px; flex-shrink: 0; }
.menu-node-actions .icon-btn-solid { width: 24px; height: 24px; }
.menu-node-actions .icon-btn-solid svg { width: 12px; height: 12px; }
.menu-node.menu-node-hidden { opacity: 0.6; }
.menu-node.menu-node-hidden .menu-node-label { font-style: italic; }
.menu-group-heading, .menu-submenu-heading {
  font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--color-muted); padding: 8px 10px 2px;
}
.menu-group-block, .menu-submenu-block { border: 1px dashed var(--color-border); border-radius: 8px; margin-bottom: 8px; padding-bottom: 4px; }
.menu-group-block + .menu-group-block { margin-top: 4px; }

/* Drag-and-drop reorder/move (menu_manager.js) - plain HTML5 DnD, no
   library. .menu-dropzone wraps every droppable bucket (a side's own
   top level, or one group/submenu's children); .menu-node is draggable. */
.menu-node-drag-handle { cursor: grab; color: var(--color-muted); flex-shrink: 0; display: flex; }
.menu-node-drag-handle svg { width: 12px; height: 16px; display: block; }
.menu-node.dragging { opacity: 0.4; }
.menu-node.drag-over-top { box-shadow: inset 0 2px 0 var(--color-primary); }
.menu-node.drag-over-bottom { box-shadow: inset 0 -2px 0 var(--color-primary); }
.menu-dropzone.drag-over-empty { background: var(--color-primary-soft); border-radius: 6px; }
.menu-dropzone { min-height: 6px; }
.menu-empty-hint { padding: 4px 10px; font-size: 12px; }


.mini-editor {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  overflow: hidden;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.mini-editor:focus-within {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-soft);
}
.mini-editor-toolbar {
  display: flex; align-items: center; gap: 2px; padding: 5px 6px;
  background: var(--color-surface-alt); border-bottom: 1px solid var(--color-border);
}
.mini-editor-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border: none; border-radius: 5px; padding: 0; cursor: pointer;
  background: transparent; color: var(--color-text); font-size: 13px;
}
.mini-editor-btn:hover { background: var(--color-border); }
.mini-editor-sep { width: 1px; height: 18px; background: var(--color-border); margin: 0 4px; }
.mini-editor-content {
  min-height: 70px; padding: 10px 12px; font-size: 14px; line-height: 1.5;
  color: var(--color-text);
}
.mini-editor-content:focus { outline: none; }
.mini-editor-content a { color: var(--color-primary); }

/* ---------------------------------------------------------------------
   Login page
   --------------------------------------------------------------------- */
.login-page {
  min-height: 100%;
  display: flex;
  /* No zoom of any kind on the sign-in screen: pan-y leaves out both
     pinch-zoom and double-tap-zoom, where `manipulation` kept pinch.
     Vertical panning stays, for a form taller than a landscape phone. */
  touch-action: pan-y;
}
/* 16px, not the 15px .form-control uses elsewhere. Mobile Safari zooms
   the page in automatically when a text field smaller than 16px takes
   focus, and then leaves it zoomed - which is the zoom people actually
   hit on this screen, not pinching. At 16px it does not fire at all.
   Applies to the forgot-password card too; it is the same card. */
.login-page .form-control {
  font-size: 16px;
}
.login-visual {
  flex: 0 0 70%;
  position: relative;
  overflow: hidden;
  background: linear-gradient(160deg, #1f6feb 0%, #123a73 100%);
}
.login-visual-slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1.2s ease;
}
.login-visual-slide.active { opacity: 1; }
/* The actual <img>/<video>/YouTube <iframe> - object-fit:cover replaces
   the old CSS background-image technique now that real media elements
   are used (needed for srcset/preload/loading=lazy on images, and
   because a background-image can't be a <video>/<iframe> at all). */
.login-visual-media {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  border: 0;
  transition: filter 0.6s ease, transform 0.6s ease;
}
.login-visual-media.blurred { filter: blur(6px); transform: scale(1.03); } /* scale hides the blurred edge */
/* YouTube embeds are an <iframe>, not a CSS background - the classic
   "fill the box, cropping like object-fit:cover" trick: oversize the
   iframe assuming a fixed 16:9 source and center it, rather than relying
   on object-fit (which iframes don't support). */
.login-visual-yt-wrap { position: absolute; inset: 0; overflow: hidden; }
.login-visual-yt-wrap.blurred { filter: blur(6px); transform: scale(1.03); }
.login-visual-yt-wrap .login-visual-media {
  position: absolute; top: 50%; left: 50%;
  width: 177.78vh; height: 56.25vw; /* 16:9 oversized both ways */
  min-width: 100%; min-height: 100%;
  transform: translate(-50%, -50%);
}
/* Per-slide loading spinner (reuses .preloader-spinner, see the
   fullscreen-preloader rule set below) - shown until that slide's own
   media fires onload/oncanplay (see render_login_visual() in
   includes/login_media.php), so a slow connection shows an in-place
   spinner instead of a blank gap where the picture will be. */
.login-visual-loader {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  opacity: 1; transition: opacity 0.3s ease;
  pointer-events: none;
}
.login-visual-loader.done { opacity: 0; }
.login-visual::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0) 40%);
  pointer-events: none;
}
.login-form-panel {
  flex: 0 0 30%;
  min-width: 320px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: var(--color-bg);
}
.login-card {
  width: 100%;
  max-width: 380px;
}
/* Brand logo above the title (app_brand_logo_url() - the App settings
   header logo, or the active UI theme's own). Capped in BOTH directions
   and object-fit:contain, so a wide wordmark and a tall square badge
   both land at a sensible size without distortion. */
.login-logo {
  display: block; margin: 0 auto 14px; max-width: 100%; max-height: 86px;
  width: auto; height: auto; object-fit: contain;
}
.login-card h1 {
  font-size: 22px;
  text-align: center;
  margin-bottom: 4px;
}
.login-card .subtitle {
  text-align: center;
  color: var(--color-muted);
  font-size: 13px;
  margin-bottom: 24px;
}
.login-card .btn { width: 100%; padding: 11px; font-size: 15px; }
.login-links {
  text-align: center;
  margin-top: 14px;
  font-size: 13px;
}
.login-links a { color: var(--color-primary); }

/* A phone: no picture (render_login_visual() also keeps it from
   downloading), just the form, and a screen that stays still - the page
   is exactly the viewport's height, so there is nothing to scroll or
   rubber-band. Only a form taller than the screen (a landscape phone,
   the keyboard up) scrolls, inside its own panel. */
@media (max-width: 800px) {
  html:has(> body.login-page), body.login-page {
    height: 100%;
    overflow: hidden;
    overscroll-behavior: none;
  }
  .login-page { flex-direction: column; height: 100vh; height: 100dvh; }
  .login-visual { display: none; }
  .login-form-panel {
    flex: 1 1 auto; min-width: 0;
    overflow-y: auto; overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
}

/* ---------------------------------------------------------------------
   Dropzone (bulk drag & drop upload)
   --------------------------------------------------------------------- */
.dropzone {
  border: 2px dashed var(--color-border);
  border-radius: var(--radius);
  padding: 36px 20px;
  text-align: center;
  color: var(--color-muted);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.dropzone:hover { border-color: var(--color-primary); }
.dropzone.dragover {
  border-color: var(--color-primary);
  background: var(--color-primary-soft);
  color: var(--color-primary);
}
.dropzone.disabled { pointer-events: none; opacity: 0.6; }
.dropzone-icon { font-size: 28px; margin-bottom: 8px; }

.image-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 14px;
}
.image-gallery-item {
  position: relative;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--color-surface-alt);
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.image-gallery-item:hover { box-shadow: var(--shadow); transform: translateY(-2px); }
.image-gallery-item img,
.image-gallery-item video {
  width: 100%;
  height: 120px;
  object-fit: cover;
  display: block;
  background: #000;
}
.image-gallery-meta {
  font-size: 12px;
  color: var(--color-muted);
  padding: 6px 8px;
}
.image-gallery-blur-toggle {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--color-muted);
  padding: 0 8px 8px;
  cursor: pointer;
}
.image-gallery-item .btnDeleteImage {
  position: absolute;
  top: 6px;
  right: 6px;
}

/* Small crown badge marking something visible/available to admins only
   (see includes/layout.php's admin_crown() helper). Plain single-color
   SVG, colored to match the existing "admin" badge's gold/amber tone. */
.admin-crown {
  display: inline-flex;
  vertical-align: middle;
  cursor: default;
  color: #c9891a;
}
.admin-crown svg { width: 18px; height: 18px; display: block; }
html[data-theme="dark"] .admin-crown { color: #f5b862; }
h2 .admin-crown, h3 .admin-crown { margin-left: 8px; }
h2 .admin-crown svg, h3 .admin-crown svg { width: 22px; height: 22px; }
th .admin-crown { margin-left: 4px; }
th .admin-crown svg { width: 15px; height: 15px; }

/* Team-manager/plain-user icons shown next to a name in the header
   topbar alongside .admin-crown above (see user_role_icon_html()). */
.role-icon { display: inline-flex; vertical-align: middle; cursor: default; }
.role-icon svg { width: 16px; height: 16px; display: block; }
.role-icon-manager { color: #c9891a; }
html[data-theme="dark"] .role-icon-manager { color: #f5b862; }
.role-icon-user { color: var(--color-muted); }

/* Host -> tenant tree (multi-tenant servers - includes/hosting/tenancy.php,
   only ever one level deep) - used by both App servers (see
   treeOrderTeamRows() in app_servers.js) and Server health
   (assets/js/pages/server_health.js) - keeps a host's row immediately
   above its own tenant row(s); this is just the visual connector/indent
   on top of that ordering. */
.server-tree-child-cell { padding-left: 26px; }
.server-tree-connector { color: var(--color-muted); font-family: ui-monospace, "SFMono-Regular", Consolas, monospace; margin-right: 2px; }
.server-tree-tenant-count { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 8px; font-size: 11px; font-weight: 600; background: var(--color-primary-soft); color: var(--color-primary); vertical-align: middle; }

/* Admin-only content used to get a yellow background highlight here
   (paired with the admin_crown() badge) - removed per request, the crown
   badge alone now marks admin-only titles/columns. Class kept (still
   applied in several PHP/JS files) but intentionally left with no visual
   effect, so none of those call sites need touching. */
.admin-only-bg { background: transparent !important; }
.admin-only-bg .card, .admin-only-bg.card, .admin-only-bg.stat-card { background: var(--color-surface) !important; }
td.admin-only-bg, th.admin-only-bg { background: transparent !important; }

/* Drag-and-drop reorderable table rows (e.g. Project statuses) - a
   dedicated narrow leftmost column holding just a grip icon, the row
   itself is draggable="true" (see the relevant page's .js). */
th.drag-handle-col { width: 32px; padding-left: 16px !important; padding-right: 0 !important; }
td.drag-handle-cell {
  width: 32px; padding-left: 16px !important; padding-right: 0 !important;
  color: var(--color-muted); cursor: grab;
}
td.drag-handle-cell svg { width: 16px; height: 16px; display: block; }
tr.draggable-row.dragging { opacity: 0.4; }
tr.draggable-row:active { cursor: grabbing; }

/* DataTables auto-detects "numeric"/"date"-looking columns (e.g. Layers/
   Features/Members/Projects counts, Created dates) and right-aligns them
   via its own dt-type-numeric/dt-type-date classes - overridden back to
   left here, GLOBALLY, for every DataTable in the app (not just one
   table id), so only a column that explicitly opts into .text-right (the
   Actions column, in every table) is ever right-aligned. The vendor
   stylesheet's own matching selector (table.dataTable th.dt-type-numeric
   etc.) has equal specificity to a plain `table.data-table th.dt-type-
   numeric` rule, and which one wins would then depend on load order -
   app.css loads BEFORE the vendor CSS on most pages but AFTER it on
   map.php, so a plain equal-specificity override would win on some pages
   and lose on others. The `html[data-theme]` prefix (present on every
   page) adds one extra class-level selector, making ours strictly more
   specific than the vendor's regardless of load order - same technique
   already used below for the dark-mode paging-button fix. */
html[data-theme] table.data-table th.dt-type-numeric, html[data-theme] table.data-table td.dt-type-numeric,
html[data-theme] table.data-table th.dt-type-date, html[data-theme] table.data-table td.dt-type-date {
  text-align: left;
}
html[data-theme] table.data-table th.text-right, html[data-theme] table.data-table td.text-right {
  text-align: right;
}

/* Project page: Versions / Static layers side by side (50/50) instead of
   stacked, so the page fits without unnecessary vertical scrolling.
   Stacks back to full-width columns on narrow screens.
   NOTE: basis/max-width use calc(50% - 8px) (half the 16px gap), not a
   bare 50% - with flex-shrink:0, two bare-50% children plus a 16px gap
   overflow their flex container by 16px (gap isn't shrunk away when
   shrink is 0), which pushed the right-hand card 16px further right than
   intended - i.e. too close to / overlapping the space before the right
   sidebar. calc(50% - 8px) makes both cards + the gap add up to exactly
   100%, so the right card's edge lines up correctly again. */
.project-columns { display: flex; gap: 16px; align-items: stretch; margin-bottom: 16px; }
.project-columns .card { margin-bottom: 0; }
.project-col-50 { flex: 0 0 calc(50% - 8px); max-width: calc(50% - 8px); min-width: 0; }
@media (max-width: 900px) {
  .project-columns { flex-direction: column; }
  .project-col-50 { flex-basis: auto; max-width: 100%; width: 100%; }
}

/* DataTables integration - blend the plugin's default markup with app styling. */
.dt-container { width: 100%; font-size: 14px; color: var(--color-text); }
.dt-container .dt-layout-row:first-child { padding-bottom: 12px; }
.dt-container .dt-search input,
.dt-container .dt-length select {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 6px 10px;
  font-size: 14px;
  color: var(--color-text);
  background: var(--color-surface);
}
.dt-container .dt-search input:focus,
.dt-container .dt-length select:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-soft);
}
.dt-container .dt-info, .dt-container .dt-paging { color: var(--color-muted); font-size: 13px; }
.dt-container .dt-paging .dt-paging-button {
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 4px 10px;
  margin-left: 4px;
  cursor: pointer;
  color: var(--color-text);
}
.dt-container .dt-paging .dt-paging-button.current {
  background: var(--color-primary);
  color: #fff;
}
.dt-container .dt-paging .dt-paging-button.disabled { color: var(--color-muted); opacity: 0.5; cursor: default; }
.dt-container .dt-paging .dt-paging-button:hover:not(.disabled):not(.current) { background: var(--color-surface-alt); }
/* The vendor stylesheet (dataTables.dataTables.min.css) hardcodes
   !important colors on paging buttons (color:inherit, and - critically -
   color:rgba(0,0,0,0.5) on .disabled) that ignore our theme variables,
   and it's loaded AFTER app.css, so with EQUAL specificity it would win
   regardless. In light mode that accidentally still looks fine (inherit
   resolves to our own text color), but in dark mode it's badly wrong:
   disabled Previous/Next buttons render near-black text on a dark
   background (invisible), and .current/hover text can also fail to pick
   up our palette. Fix: re-declare every paging button state with a
   `html[data-theme]` prefix (present on every page, either "light" or
   "dark") to make our selectors STRICTLY more specific than the vendor's
   `div.dt-container ...` ones, so ours always wins on specificity alone
   - not just by source order (which vendor's, loaded later, would win by
   default at equal specificity). */
html[data-theme] .dt-container .dt-paging .dt-paging-button,
html[data-theme] .dt-container .dt-paging .dt-paging-button:hover:not(.disabled):not(.current) {
  color: var(--color-text) !important;
}
html[data-theme] .dt-container .dt-paging .dt-paging-button.disabled,
html[data-theme] .dt-container .dt-paging .dt-paging-button.disabled:hover,
html[data-theme] .dt-container .dt-paging .dt-paging-button.disabled:active {
  color: var(--color-muted) !important;
  opacity: 0.5;
  background: transparent !important;
}
html[data-theme] .dt-container .dt-paging .dt-paging-button.current,
html[data-theme] .dt-container .dt-paging .dt-paging-button.current:hover {
  color: #fff !important;
  background: var(--color-primary) !important;
  border-color: var(--color-primary) !important;
}
html[data-theme] .dt-container .dt-paging .dt-paging-button:hover:not(.disabled):not(.current) {
  background: var(--color-surface-alt) !important;
  border-color: var(--color-border) !important;
}
table.dataTable thead th, table.dataTable thead td {
  border-bottom: 1px solid var(--color-border);
}
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control::before {
  background-color: var(--color-primary);
  top: 50%;
  transform: translateY(-50%);
}
table.dataTable > tbody > tr.child ul.dtr-details { width: 100%; }
table.dataTable > tbody > tr.child span.dtr-title { color: var(--color-muted); font-weight: 600; }


/* Layer/template "characterization" badge (civil/network/topography) -
   see App.characterizationBadgeHtml() in app.js, used by the map viewer's
   own layer list, the Attribute Table page's layer list, and the admin
   Team layers catalog table. Deliberately tiny/compact (used inline next
   to a layer's name in a narrow sidebar row, not just in a wider table
   cell) - smaller font/padding than the general-purpose .badge above. */
.char-badge {
  display: inline-block; padding: 1px 6px; border-radius: 999px;
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .02em;
  vertical-align: middle; line-height: 1.5;
}
.char-badge-civil { background: #fde6cf; color: #93540c; }
.char-badge-network { background: #e0ecff; color: #1c4f9c; }
.char-badge-topography { background: #e3f5e6; color: #226b3a; }
html[data-theme="dark"] .char-badge-civil { background: rgba(253, 176, 88, 0.2); color: #f5b862; }
html[data-theme="dark"] .char-badge-network { background: rgba(90, 150, 255, 0.2); color: #8fb8ff; }
html[data-theme="dark"] .char-badge-topography { background: rgba(70, 200, 120, 0.18); color: #7fe6a0; }

/* Project status badge. The admin-defined colour (project_statuses.color)
   comes in as the --sb-* custom properties from status_badge_style()
   (includes/settings.php) / App.statusBadgeStyle() (app.js), which emit
   both themes' values; this picks one. A soft tint plus a same-hue label
   rather than a solid fill - see that function for why. The fallbacks
   keep a badge rendered without those properties legible. */
.status-badge {
  background: var(--sb-bg, rgba(107, 114, 128, .14));
  color: var(--sb-fg, #374151);
  border: 1px solid var(--sb-bd, rgba(107, 114, 128, .38));
  font-weight: 700;
}
html[data-theme="dark"] .status-badge {
  background: var(--sb-bg-dark, rgba(160, 170, 185, .22));
  color: var(--sb-fg-dark, #cbd5e1);
  border-color: var(--sb-bd-dark, rgba(160, 170, 185, .45));
}

/* ---------------------------------------------------------------------
   Dashboard stat cards
   --------------------------------------------------------------------- */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin-bottom: 20px;
}
.stat-card {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 78px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 16px 18px;
  transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease, border-color 0.2s ease;
}
.stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
.stat-icon {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-primary-soft);
  color: var(--color-primary);
}
.stat-icon svg { width: 22px; height: 22px; }
.stat-icon.stat-icon-teams { background: rgba(143, 107, 255, 0.14); color: #8f6bff; }
html[data-theme="dark"] .stat-icon.stat-icon-teams { background: rgba(143, 107, 255, 0.2); color: #b09dff; }
.stat-icon.stat-icon-users { background: rgba(253, 176, 88, 0.16); color: #c9891a; }
html[data-theme="dark"] .stat-icon.stat-icon-users { background: rgba(253, 176, 88, 0.2); color: #f5b862; }
.stat-icon.stat-icon-success { background: rgba(35, 165, 102, 0.14); color: #1d7a45; }
html[data-theme="dark"] .stat-icon.stat-icon-success { background: rgba(62, 207, 142, 0.18); color: #7fe6b8; }
.stat-icon.stat-icon-layers { background: rgba(8, 145, 178, 0.14); color: #0891b2; }
html[data-theme="dark"] .stat-icon.stat-icon-layers { background: rgba(34, 211, 238, 0.18); color: #67e8f9; }
.stat-icon.stat-icon-objects { background: rgba(219, 39, 119, 0.12); color: #db2777; }
html[data-theme="dark"] .stat-icon.stat-icon-objects { background: rgba(244, 114, 182, 0.18); color: #f9a8d4; }
.stat-info { min-width: 0; }
.stat-card .stat-value { font-size: 26px; font-weight: 800; line-height: 1.15; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stat-card .stat-label { color: var(--color-muted); font-size: 13px; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Paired 50/50 dashboard cards (Recent projects/Projects by status, All
   projects by status/Recent activity) should always match height, even
   when one side's content is much shorter - .project-columns already
   stretches (see above) and a flex-column card body here lets a trailing
   link ("View all projects", "Manage statuses") pin to the bottom via
   margin-top:auto instead of leaving an awkward empty gap underneath a
   short widget. */
.project-columns .card { display: flex; flex-direction: column; }
.card-footer-link { margin-top: auto; padding-top: 14px; margin-bottom: 0; }

/* Status breakdown widget - a small horizontal-bar "distribution" list
   (colored dot + name + count, with a proportional track/fill bar
   underneath) rather than a loose wrap of badges, so it reads as a real
   mini chart and fills the card's height sensibly regardless of how many
   statuses there are. */
.status-list { display: flex; flex-direction: column; gap: 12px; }
.status-list-row { }
.status-list-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; font-size: 13px; }
.status-list-name { display: flex; align-items: center; gap: 7px; font-weight: 600; min-width: 0; }
.status-list-name span.label-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-list-dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.status-list-count { color: var(--color-muted); font-weight: 700; flex: 0 0 auto; }
.status-list-track { height: 8px; border-radius: 999px; background: var(--color-surface-alt); overflow: hidden; }
.status-list-fill { height: 100%; border-radius: 999px; transition: width 0.3s ease; }

/* Generic flex-wrap-with-gap row, reused for the project detail page's
   Sub-projects quick-link buttons. */
.status-breakdown { display: flex; flex-wrap: wrap; gap: 8px; }

/* Admin "Activity log" page - hand-rolled 24-bar hour-of-day chart (no
   charting library in this app) - see assets/js/pages/activity.js. */
.hour-chart-inner { display: flex; align-items: flex-end; gap: 3px; height: 140px; }
.hour-bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; min-width: 0; }
.hour-bar-value { font-size: 10px; color: var(--color-muted); margin-bottom: 2px; height: 12px; }
.hour-bar { width: 100%; background: var(--color-primary); border-radius: 3px 3px 0 0; min-height: 1px; transition: height 0.3s ease, background 0.15s ease; }
.hour-bar-col:hover .hour-bar { background: var(--color-primary-dark); }
.hour-bar-label { font-size: 10px; color: var(--color-muted); margin-top: 4px; }

/* ---------------------------------------------------------------------
   Toggle switch (used for view/edit permission grid - no lock icons)
   --------------------------------------------------------------------- */
.toggle-group { display: inline-flex; border: 1px solid var(--color-border); border-radius: 999px; overflow: hidden; }
.toggle-group button {
  border: none;
  background: var(--color-surface);
  padding: 5px 12px;
  font-size: 12px;
  cursor: pointer;
  color: var(--color-muted);
}
.toggle-group button.active { background: var(--color-primary); color: #fff; }
.toggle-group button + button { border-left: 1px solid var(--color-border); }

.switch { position: relative; display: inline-block; width: 42px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .slider {
  position: absolute; cursor: pointer; inset: 0;
  background-color: #c7cdd6; transition: 0.2s; border-radius: 999px;
}
html[data-theme="dark"] .switch .slider { background-color: #394254; }
.switch .slider::before {
  position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px;
  background-color: white; transition: 0.2s; border-radius: 50%;
}
.switch input:checked + .slider { background-color: var(--color-success); }
.switch input:checked + .slider::before { transform: translateX(18px); }

.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
.toolbar .search { max-width: 260px; }

/* "Previously uploaded logos" gallery on the App settings page (see
   renderLogoHistory() in app_settings.js). A chequerboard sits behind
   each tile so a logo with transparency - or a white one - is still
   clearly a picture rather than an empty box. */
.logo-history { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.logo-history-item {
  position: relative; width: 104px; height: 56px; padding: 6px; cursor: pointer;
  border: 1px solid var(--color-border); border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center;
  background-color: var(--color-surface);
  background-image:
    linear-gradient(45deg, rgba(128, 128, 128, 0.16) 25%, transparent 25%, transparent 75%, rgba(128, 128, 128, 0.16) 75%),
    linear-gradient(45deg, rgba(128, 128, 128, 0.16) 25%, transparent 25%, transparent 75%, rgba(128, 128, 128, 0.16) 75%);
  background-size: 12px 12px; background-position: 0 0, 6px 6px;
}
.logo-history-item:hover { border-color: var(--color-primary); }
.logo-history-item img { max-width: 100%; max-height: 100%; object-fit: contain; }
.logo-history-item.is-current { border-color: var(--color-success, #1e9e62); border-width: 2px; cursor: default; }
.logo-history-badge {
  position: absolute; bottom: -1px; left: -1px; right: -1px; text-align: center;
  font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  background: var(--color-success, #1e9e62); color: #fff; border-radius: 0 0 var(--radius) var(--radius);
}
.logo-history-del {
  position: absolute; top: -7px; right: -7px; width: 18px; height: 18px; line-height: 1;
  border-radius: 50%; border: 1px solid var(--color-border); background: var(--color-surface);
  color: var(--color-danger, #c0392b); font-size: 13px; font-weight: 700; cursor: pointer; padding: 0;
}
.logo-history-del:hover { background: var(--color-danger, #c0392b); color: #fff; }

/* Duct types page (duct_types.php/duct_types.js) - subduct color list
   swatch (table cell) and the per-position color-picker rows in the
   "Manage subduct colors" modal. */
.duct-color-swatch {
  display: inline-block; width: 20px; height: 20px; border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.45); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
  vertical-align: middle;
}
html[data-theme="dark"] .duct-color-swatch {
  border-color: rgba(255, 255, 255, 0.55); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.duct-subduct-row {
  display: flex; align-items: center; gap: 10px; padding: 6px 0;
  border-bottom: 1px solid var(--color-border);
}
.duct-subduct-row:last-child { border-bottom: none; }
/* Two columns in the "Subduct colors" modal (see openColorsModal()) -
   a type with 24 subducts is unusable as one long column. Multi-column
   rather than grid on purpose: it fills the FIRST column top to bottom
   before starting the second (#1-#12, then #13-#24), where a grid lays
   items out across each row instead (#1, #2 / #3, #4 ...). It also
   balances the two columns by itself, whatever the subduct count is.
   Collapses to one column when there is no room for two. */
.duct-subduct-grid { column-count: 2; column-gap: 28px; }
.duct-subduct-grid .duct-subduct-row { break-inside: avoid; -webkit-column-break-inside: avoid; }
@media (max-width: 720px) { .duct-subduct-grid { column-count: 1; } }
.duct-subduct-position { width: 32px; flex-shrink: 0; font-weight: 600; }
.duct-subduct-row input[type="color"] { width: 36px; height: 28px; padding: 2px; flex-shrink: 0; }
.duct-subduct-row label { display: flex; align-items: center; gap: 4px; font-size: 13px; flex-shrink: 0; }
.duct-subduct-error { flex: 1; text-align: right; }
/* Colour name beside a swatch ("white", "red/white" - see colorName()
   in duct_types.js), so a colour can be read as well as seen. */
.ds-color-name, .duct-color-name {
  font-size: 12px; color: var(--color-muted); white-space: nowrap; text-transform: capitalize;
}
.ds-color-name { min-width: 84px; }
.duct-color-name { margin-left: 6px; vertical-align: middle; }

/* Reusable color-picker field (duct outer/feeder colors + subduct colors
   - see colorFieldHtml()/openColorPopover-style handlers in duct_types.js)
   - a small swatch button that opens a floating popover with a "Basic
   colors" tab (fixed palette) and a "Custom" tab (native color input +
   typed hex code). */
.color-field { display: inline-block; vertical-align: middle; flex-shrink: 0; }
/* A neutral-grey border is invisible against a white (or near-white)
   colour on a white panel - the swatch then reads as an empty box, and a
   white half of a stripe reads as nothing at all. Every swatch gets a
   real contrasting edge, plus an inner hairline so a very DARK colour
   keeps a visible boundary too. */
.color-swatch-btn {
  width: 32px; height: 28px; border-radius: 6px;
  border: 1px solid rgba(0, 0, 0, 0.45);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
  cursor: pointer; padding: 0; vertical-align: middle;
}
html[data-theme="dark"] .color-swatch-btn {
  border-color: rgba(255, 255, 255, 0.55);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.color-swatch-btn:disabled { cursor: not-allowed; opacity: .55; }
.color-popover {
  position: fixed; z-index: 2100; width: 220px; padding: 10px;
  background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg, 0 8px 24px rgba(0, 0, 0, .2));
}
.color-popover-tabs { display: flex; gap: 4px; margin-bottom: 8px; }
.color-tab-btn {
  flex: 1; padding: 4px 6px; font-size: 12px; border: 1px solid var(--color-border); border-radius: 6px;
  background: var(--color-surface-alt); color: var(--color-text); cursor: pointer;
}
.color-tab-btn.active { background: var(--color-primary); border-color: var(--color-primary); color: #fff; }
.color-popover-body.hidden { display: none; }
.color-basic-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
.color-basic-swatch {
  width: 22px; height: 22px; border-radius: 5px; padding: 0; cursor: pointer;
  border: 1px solid rgba(0, 0, 0, 0.45); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}
html[data-theme="dark"] .color-basic-swatch {
  border-color: rgba(255, 255, 255, 0.55); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.color-basic-swatch.active { outline: 2px solid var(--color-primary); outline-offset: 1px; }
.color-popover-custom-row { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
.colorPopoverNative { width: 36px; height: 30px; padding: 2px; border: 1px solid var(--color-border); border-radius: 6px; cursor: pointer; flex-shrink: 0; }
.colorPopoverHex { flex: 1; min-width: 0; }
.colorPopoverHex.is-invalid { border-color: #c0392b; }


/* Network object Connections modal (map_viewer.js's openNetworkConnectionsModal())
   - a node-diagram layout: the object itself as a box in the middle,
   its input ports down the left, output ports down the right, and every
   nearby Duct end (see the "Duct-first" snap-priority feature) listed
   below as its own draggable row - a port row and a duct row can be
   dragged onto EACH OTHER (either direction) to connect them, as an
   alternative to the existing picker/select + button flow (kept as a
   fallback for anyone who prefers/needs it, or just to disconnect). */
.net-conn-diagram { display: flex; align-items: stretch; gap: 18px; margin-bottom: 16px; }
.net-conn-col { flex: 1; min-width: 0; }
.net-conn-col h4 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: var(--color-muted); }
.net-conn-col-box { flex: 0 0 150px; display: flex; align-items: center; justify-content: center; }
.net-conn-box {
  width: 100%; border: 2px solid var(--color-primary); border-radius: var(--radius); background: var(--color-primary-soft);
  padding: 16px 10px; text-align: center; font-weight: 600; font-size: 13px; word-break: break-word;
}
.net-conn-port-row {
  display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 6px 8px; margin-bottom: 6px;
  border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 12px; background: var(--color-surface);
}
.net-conn-port-row[draggable="true"] { cursor: grab; }
.net-conn-port-row.drag-over { background: var(--color-primary-soft); border-color: var(--color-primary); }
.net-conn-port-row .duct-subduct-position { font-weight: 600; }
.net-conn-nearby-ducts { border-top: 1px solid var(--color-border); padding-top: 12px; }
.net-conn-nearby-ducts h4 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: var(--color-muted); }
.net-duct-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid var(--color-border); border-radius: var(--radius); margin-bottom: 6px; flex-wrap: wrap; }
.net-duct-row[draggable="true"] { cursor: grab; }
.net-duct-row.drag-over { background: var(--color-primary-soft); border-color: var(--color-primary); }
.net-duct-row-label { flex: 1; min-width: 140px; font-size: 13px; }
.net-duct-family-checks { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 4px; }
.net-duct-family-check { font-weight: 400; font-size: 13px; display: flex; align-items: center; gap: 4px; }
.net-port-duct-conn { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
/* Connection state, spelled out (see networkPortRowHtml() in
   map_viewer.js) - a wired port used to look exactly like an empty one,
   both being muted grey text. */
.net-port-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.net-port-state {
  font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  padding: 1px 7px; border-radius: 999px; white-space: nowrap;
}
.net-port-state-on { background: var(--color-success, #1e9e62); color: #fff; }
.net-port-state-off { background: var(--color-surface-alt, #eef1f6); color: var(--color-muted, #6b7785); }
.net-conn-port-row.is-connected { border-color: var(--color-success, #1e9e62); }
.net-duct-row.is-connected { opacity: .75; }
.net-port-target { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12.5px; margin-top: 5px; }
.net-port-target .btn { margin-left: auto; }
.net-whole-duct-tag {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  background: var(--color-primary-soft, #e7f0fe); color: var(--color-primary, #1f6feb);
  padding: 1px 6px; border-radius: 999px;
}
.netDuctSubductSwatch .duct-color-swatch { vertical-align: middle; }


/* "Overlaps" sidebar row (map.php/map_viewer.js) - a fixed, always-
   present, non-deletable/non-editable entry, like Notes - tinted when it
   currently has at least one self-crossing duct to flag. */
#overlapsRow { cursor: default; }
#overlapsRow.overlaps-has-items .layer-name { color: var(--color-danger); font-weight: 600; }

/* Recycle bin row (map.php) - same fixed, non-deletable/non-editable
   single-row pattern as Overlaps above, tinted when it currently has at
   least one deleted object within the selected day-window. */
#recycleBinRow { cursor: default; }
#recycleBinRow.recycle-bin-has-items .layer-name { color: var(--color-danger); font-weight: 600; }

/* Duct route-choice modal (openDuctRouteModal() in map_viewer.js). */
.duct-route-item {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 10px; border: 1px solid var(--color-border); border-radius: 6px; margin-bottom: 6px; cursor: pointer;
}
.duct-route-item:hover { background: var(--color-surface-alt); }
.duct-route-item.active { border-color: var(--color-primary, #1f6feb); }
.duct-route-item-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.duct-route-highlight-btn.active,
.duct-route-item.active .duct-route-highlight-btn { color: var(--color-primary, #1f6feb); }


.muted { color: var(--color-muted); font-size: 13px; }
.hidden { display: none !important; }

/* ---------------------------------------------------------------------
   Theme color settings (admin)
   --------------------------------------------------------------------- */
.theme-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
}
.theme-grid-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  border-bottom: 1px solid var(--color-border);
}
.theme-grid-row label { flex: 1 1 auto; font-size: 14px; }
.theme-grid-row input[type="color"] {
  width: 42px;
  height: 30px;
  padding: 2px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  cursor: pointer;
}
.theme-grid-hex { font-family: monospace; font-size: 12px; width: 70px; }

/* ---------------------------------------------------------------------
   Project log console (assets/js/pages/project_log.js) - a near-fullscreen
   modal holding a dense, live-appending list of everything that has
   happened in one project.
   --------------------------------------------------------------------- */
/* .modal-90 is general-purpose: any modal that wants the screen rather
   than a dialog-sized box. */
.app-modal.modal-90 {
  width: 90vw;
  max-width: 90vw;
  height: 90vh;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  padding: 0;
}
.plog { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.plog-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 18px 10px; border-bottom: 1px solid var(--color-border);
}
.plog-head h3 { margin: 0; font-size: 16px; }
.plog-project { color: var(--color-muted); font-weight: 600; margin-left: 6px; }
.plog-live { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--color-muted); white-space: nowrap; }
.plog-filters {
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
  padding: 10px 18px; border-bottom: 1px solid var(--color-border); background: var(--color-surface-alt);
}
.plog-filters .form-control { width: auto; min-width: 130px; padding: 5px 8px; font-size: 12px; height: auto; }
.plog-filters #plogQ { flex: 1 1 220px; min-width: 180px; }
.plog-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 0 4px; }
.plog-empty { padding: 24px 18px; color: var(--color-muted); font-size: 13px; }
/* Monospace, tight rows, no wrapping - it is a console, not a report. */
.plog-table {
  width: 100%; border-collapse: collapse;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px; line-height: 1.45;
}
.plog-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--color-surface); border-bottom: 1px solid var(--color-border);
  text-align: left; padding: 5px 8px; font-size: 10px; text-transform: uppercase;
  letter-spacing: .04em; color: var(--color-muted); font-weight: 700;
}
.plog-table td { padding: 2px 8px; border-bottom: 1px solid var(--color-border); white-space: nowrap; }
.plog-table tbody tr:hover td { background: var(--color-surface-alt); }
.plog-time { color: var(--color-muted); }
.plog-user { font-weight: 700; }
.plog-action { font-weight: 700; }
.plog-a-add { color: #1d7a45; }
.plog-a-del { color: #c0392b; }
.plog-a-upd { color: #9a6700; }
.plog-a-other { color: var(--color-text); }
html[data-theme="dark"] .plog-a-add { color: #7fe6b8; }
html[data-theme="dark"] .plog-a-del { color: #ff9b91; }
html[data-theme="dark"] .plog-a-upd { color: #f5c26b; }
.plog-id { color: var(--color-muted); margin-left: 3px; }
/* The detail column is the one that may be long - it gets whatever width
   is left and is clipped, with the full JSON on the row's tooltip. */
.plog-detail { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 1px; width: 100%; }
.plog-ip { color: var(--color-muted); }
/* A line that arrived while the console was open fades its highlight out,
   so new activity is visible without the list jumping. */
.plog-new td { animation: plog-flash 1.6s ease-out; }
@keyframes plog-flash {
  from { background: rgba(31, 111, 235, 0.22); }
  to { background: transparent; }
}
.plog-foot {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 18px; border-top: 1px solid var(--color-border);
}
.plog-foot .plog-count { margin-right: auto; font-size: 12px; }

@media (max-width: 800px) {
  .app-modal.modal-90 { width: 96vw; max-width: 96vw; height: 92vh; max-height: 92vh; }
  .plog-ip, .plog-table thead th:last-child { display: none; }
}

/* ---------------------------------------------------------------------
   Installable app (assets/js/pwa.js)
   --------------------------------------------------------------------- */
/* A bar at the bottom of a phone screen rather than a dialog: it is an
   offer, not a question, and must never be in the way of signing in. */
.pwa-hint {
  position: fixed; left: 12px; right: 12px; bottom: 12px; z-index: 4000;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 14px; border-radius: 10px;
  background: var(--color-surface); color: var(--color-text);
  border: 1px solid var(--color-border);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
}
.pwa-hint-body { display: flex; flex-direction: column; gap: 2px; flex: 1 1 200px; min-width: 0; }
.pwa-hint-body strong { font-size: 13px; }
.pwa-hint-body span { font-size: 12px; color: var(--color-muted); }
.pwa-hint-actions { display: flex; gap: 8px; flex-shrink: 0; }

/* Two-step sign-in (assets/js/pages/totp_setup.js, index.php) */
.totp-prompt { font-size: 13px; color: var(--color-muted); margin: 0 0 12px; }
.totp-on { font-size: 13px; margin: 0 0 6px; }
.totp-low { color: #c0392b; font-weight: 700; }
html[data-theme="dark"] .totp-low { color: #ff9b91; }
.totp-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.totp-steps { font-size: 13px; line-height: 1.6; padding-left: 20px; margin: 0 0 14px; }
.totp-open-app { display: inline-block; margin-bottom: 14px; text-decoration: none; }
.totp-secret { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Monospace and roomy: this gets typed by hand off a screen. */
.totp-secret code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 15px; letter-spacing: .06em; padding: 6px 10px; border-radius: 5px;
  background: var(--color-surface-alt); border: 1px solid var(--color-border);
}
.totp-codes {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 6px; margin: 12px 0 4px;
}
.totp-codes code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px; text-align: center; padding: 6px 4px; border-radius: 5px;
  background: var(--color-surface-alt); border: 1px solid var(--color-border);
}

/* The captcha sits between the password and the Sign in button; centred
   so it does not look bolted to one edge of a narrow card. */
.login-captcha { display: flex; justify-content: center; margin: 0 0 14px; }
.login-captcha .g-recaptcha { transform-origin: center; }
/* Google's widget has a fixed 304px width, which overflows the sign-in
   card on a small phone - scaled down rather than clipped. */
@media (max-width: 360px) {
  .login-captcha .g-recaptcha { transform: scale(0.86); }
}

/* The sign-in form's device opt-in - only shown to the installed app. */
.login-remember { margin-bottom: 14px; }
.login-remember-label { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.login-remember-hint { font-size: 11px; color: var(--color-muted); margin-top: 4px; line-height: 1.45; }

/* Projects list: the recycle-bin toggle and the badge on a deleted row.
   A deleted project still works completely - see migration 121 - so the
   row is dimmed rather than struck through or greyed out entirely. */
.toolbar-spacer { flex: 1 1 auto; }
.show-deleted-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--color-muted); margin: 0; white-space: nowrap;
}
.deleted-badge { font-size: 10px; text-transform: uppercase; letter-spacing: .03em; }

/* ---------------------------------------------------------------------
   Element sets (assets/js/pages/element_sets.js)
   --------------------------------------------------------------------- */
.card.es-card {
  margin-bottom: 12px; padding: 14px 16px; background: var(--color-surface-alt); box-shadow: none;
}
.card.es-card:last-child { margin-bottom: 0; }
.es-card-actions { display: flex; align-items: center; gap: 6px; }
.es-scope {
  font-size: 11px; font-weight: 600; color: var(--color-muted);
  padding: 1px 7px; border-radius: 999px; background: var(--color-surface-alt);
  margin-left: 6px; vertical-align: middle;
}
/* The set's contents as chips: one set is a handful of types, and a chip
   row reads faster than a table of two columns. */
.es-items { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.es-item {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 6px 3px 4px; border-radius: 999px; font-size: 12px;
  background: var(--color-surface-alt); border: 1px solid var(--color-border);
}
.es-item-kind {
  font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .03em;
  padding: 1px 5px; border-radius: 999px;
}
.es-item-duct { background: rgba(31, 111, 235, 0.16); color: #1f6feb; }
.es-item-network_element { background: rgba(47, 158, 68, 0.16); color: #2f7d32; }
html[data-theme="dark"] .es-item-duct { background: rgba(110, 168, 254, 0.2); color: #8fb8ff; }
html[data-theme="dark"] .es-item-network_element { background: rgba(62, 207, 142, 0.18); color: #7fe6b8; }
.es-item-remove {
  border: none; background: none; cursor: pointer; padding: 0 2px;
  line-height: 1; font-size: 14px; color: var(--color-muted);
}
.es-item-remove:hover { color: #c0392b; }
.es-starter {
  display: inline-flex; align-items: center; gap: 6px; font-size: 12px; margin: 0 0 10px;
}
.es-starter-badge {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  padding: 1px 7px; border-radius: 999px; margin-left: 6px; vertical-align: middle;
  background: rgba(47, 158, 68, 0.16); color: #2f7d32;
}
html[data-theme="dark"] .es-starter-badge { background: rgba(62, 207, 142, 0.18); color: #7fe6b8; }

/* ---------------------------------------------------------------------
   A catalog type's "Columns" (assets/js/type_fields_editor.js)
   --------------------------------------------------------------------- */
.tf-rows { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.tf-row { display: grid; grid-template-columns: minmax(0, 1.3fr) 110px minmax(0, 1.4fr) auto auto; gap: 6px; align-items: center; }
.tf-row .tf-default, .tf-row .tf-tags { grid-column: 3; }
.tf-required { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; margin: 0; white-space: nowrap; }
@media (max-width: 560px) {
  .tf-row { grid-template-columns: minmax(0, 1fr) 100px auto; }
  .tf-row .tf-default, .tf-row .tf-tags { grid-column: 1 / 3; }
}

/* ---------------------------------------------------------------------
   Admin sidebar search + collapse-all (assets/js/nav_tools.js)
   --------------------------------------------------------------------- */
.nav-tools {
  display: flex; align-items: center; gap: 6px;
  padding: 8px 10px; flex-shrink: 0;
}
.nav-tools-search { position: relative; flex: 1 1 auto; min-width: 0; }
.nav-tools-input {
  width: 100%; font: inherit; font-size: 12px;
  padding: 5px 24px 5px 8px; border-radius: 5px;
  border: 1px solid var(--color-border);
  background: var(--color-surface); color: var(--color-text);
}
.nav-tools-input:focus { outline: none; border-color: var(--color-primary); }
/* Sits inside the field; the native search-clear cross is inconsistent
   across browsers and invisible in dark mode. */
.nav-tools-input::-webkit-search-cancel-button { display: none; }
.nav-tools-clear {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  border: none; background: none; cursor: pointer;
  font-size: 15px; line-height: 1; padding: 0 5px; color: var(--color-muted);
}
.nav-tools-clear:hover { color: var(--color-text); }
.nav-tools-toggle {
  font: inherit; font-size: 11px; font-weight: 600; white-space: nowrap;
  padding: 5px 8px; border-radius: 5px; cursor: pointer;
  border: 1px solid var(--color-border);
  background: var(--color-surface); color: var(--color-muted);
}
.nav-tools-toggle:hover { background: var(--color-surface-alt); color: var(--color-text); }
/* Filtered out by the search - display:none rather than visibility, so
   nothing leaves a gap where a hidden item used to be. */
.nav-filtered-out { display: none !important; }

/* ---------------------------------------------------------------------
   Per-project duct outer colours (assets/js/pages/duct_colors.js)
   --------------------------------------------------------------------- */
.duct-color-swatch {
  display: inline-block; border-radius: 50%; border: 1px solid var(--color-border);
  vertical-align: middle; flex-shrink: 0;
}
.duct-colors-list { margin-bottom: 12px; }
.duct-color-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid var(--color-border);
}
.duct-color-row:last-child { border-bottom: none; }
.duct-color-pick { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; margin: 0; font-size: 13px; }
.duct-color-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.duct-color-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; flex-shrink: 0; }
.duct-color-own {
  padding: 1px 6px; border-radius: 999px; font-weight: 700;
  background: rgba(31, 111, 235, 0.14); color: #1f6feb;
}
html[data-theme="dark"] .duct-color-own { background: rgba(110, 168, 254, 0.2); color: #8fb8ff; }
/* The count that explains why a colour cannot be unticked. */
.duct-color-used { color: var(--color-muted); font-weight: 600; }
.duct-color-add {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding-top: 12px; border-top: 1px solid var(--color-border);
}
.duct-color-add input[type="color"] {
  width: 34px; height: 30px; padding: 2px; border: 1px solid var(--color-border);
  border-radius: 4px; background: var(--color-surface); cursor: pointer;
}
.duct-color-add .form-control { flex: 1 1 140px; min-width: 120px; }
.duct-color-striped { display: flex; align-items: center; gap: 5px; font-size: 12px; white-space: nowrap; margin: 0; }

/* Header brand texts as reorderable chips (assets/js/pages/brand_text.js).
   The value on the wire is still one comma-separated string; this is only
   a better way to edit it than a line of commas. */
.brand-tags {
  border: 1px solid var(--color-border); border-radius: var(--radius);
  background: var(--color-surface); padding: 6px;
}
.brand-tags-list { display: flex; flex-wrap: wrap; gap: 6px; }
.brand-tags-list:not(:empty) { margin-bottom: 6px; }
.brand-tag {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 6px 3px 8px; border-radius: 999px;
  background: var(--color-surface-alt); border: 1px solid var(--color-border);
  font-size: 12px; font-weight: 600; cursor: grab; user-select: none;
}
.brand-tag.dragging { opacity: .45; }
.brand-tag-drop { box-shadow: inset 2px 0 0 var(--color-primary); }
.brand-tag-remove {
  border: none; background: none; cursor: pointer; padding: 0 2px; line-height: 1;
  font-size: 14px; color: var(--color-muted);
}
.brand-tag-remove:hover { color: #c0392b; }
.brand-tags-input { border: none; background: none; padding: 4px 4px; height: auto; }
.brand-tags-input:focus { outline: none; box-shadow: none; }

/* A menu item only a team manager (or admin) can open - menu_items
   .required_role = 'manager'. See menu_role_badge_html(). */
.nav-role-badge {
  display: inline-block; margin-left: 6px; padding: 0 4px;
  border-radius: 3px; font-size: 9px; font-weight: 800; letter-spacing: .04em;
  background: rgba(31, 111, 235, 0.16); color: #1f6feb; vertical-align: middle;
}
html[data-theme="dark"] .nav-role-badge { background: rgba(110, 168, 254, 0.2); color: #8fb8ff; }
/* The crown variant - an icon rather than letters, matching admin_crown()
   everywhere else in the app. */
.nav-role-badge-admin { background: rgba(201, 137, 26, 0.16); color: #a8730f; padding: 1px 4px; }
.nav-role-badge-admin svg { width: 10px; height: 10px; display: block; }
html[data-theme="dark"] .nav-role-badge-admin { background: rgba(240, 180, 70, 0.2); color: #f0b446; }

/* "This project's own types" - a real button rather than one more quiet
   link, since it is the way into per-project customisation and was being
   missed next to the plain "Catalog" link. */
.pp-own-types { white-space: nowrap; text-decoration: none; }
.pp-own-types:hover { text-decoration: none; }

/* Permissions grid: access someone has by being an administrator or a
   team manager, which no per-project grant is involved in. */
.perm-by-role { font-size: 13px; font-weight: 700; }
.perm-by-role .muted { font-weight: 600; }

/* Remembered devices list (sessions.php) */
.device-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--color-border); }
.device-row:last-child { border-bottom: none; }
.device-main { flex: 1 1 auto; min-width: 0; }
.device-name { font-weight: 700; font-size: 13px; }
.device-meta { font-size: 12px; color: var(--color-muted); }
.device-current { font-size: 11px; font-weight: 700; color: #1d7a45; }
html[data-theme="dark"] .device-current { color: #7fe6b8; }

/* Network catalog tabs (includes/network_catalog.php) - Duct types,
   Network element types and Packages read as tabs of one page. Same
   look as the project page's own tabs (.pp-tabs in project.php). */
.netcat-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--color-border); margin: 0 0 16px; overflow-x: auto; }
.netcat-tab {
  padding: 9px 14px; font-size: 14px; font-weight: 600; color: var(--color-muted); text-decoration: none;
  border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; border-radius: 8px 8px 0 0;
}
.netcat-tab:hover { color: var(--color-text); background: var(--color-surface-alt); text-decoration: none; }
.netcat-tab.is-active { color: var(--color-primary); border-bottom-color: var(--color-primary); }

/* Starter package editor (element_sets.php "Packages" tab,
   assets/js/pages/starter_package.js) - each catalog type is a tile you
   click, carrying its own colour, name and shape. The real checkbox
   stays in the DOM and focusable (just not painted), so this is still an
   ordinary checkbox group to the keyboard and to a screen reader. */
.starter-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  margin: 0 0 18px;
}
.starter-stats { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; }
.starter-stat { display: flex; align-items: baseline; gap: 6px; }
.starter-stat-num { font-size: 26px; font-weight: 800; line-height: 1; color: var(--color-primary); }
.starter-stat-label { font-size: 12px; font-weight: 600; color: var(--color-muted); }
.starter-stat-note { font-size: 12px; color: var(--color-muted); }
.starter-filter { max-width: 240px; }

.starter-section + .starter-section { margin-top: 22px; }
.starter-section-head {
  display: flex; align-items: baseline; gap: 10px; padding-bottom: 6px; margin-bottom: 12px;
  border-bottom: 1px solid var(--color-border);
}
.starter-section-head h4 { font-size: 14px; margin: 0; }
.starter-section-count { font-size: 12px; color: var(--color-muted); }
.starter-empty, .starter-nomatch { font-size: 13px; margin: 0; }

.starter-group + .starter-group { margin-top: 14px; }
.starter-group-head { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }
.starter-group-name {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--color-muted);
}
.starter-group-count {
  font-size: 11px; font-weight: 700; color: var(--color-muted); background: var(--color-surface-alt);
  border: 1px solid var(--color-border); border-radius: 999px; padding: 1px 7px;
}
/* Sits next to its group's count, not flung to the far edge of the
   card, and carries no button chrome - it is a quiet toggle, not an
   action competing with Save. */
.starter-group-head .starter-all {
  border: 0; background: transparent; padding: 2px 8px; border-radius: 6px; cursor: pointer;
  font-size: 11px; font-weight: 600; color: var(--color-primary); font-family: inherit;
}
.starter-group-head .starter-all:hover { background: var(--color-primary-soft); }
.starter-group-head .starter-all:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }

.starter-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 8px; }
.starter-tile {
  position: relative; display: flex; align-items: center; gap: 10px; margin: 0; padding: 9px 11px;
  border: 1px solid var(--color-border); border-radius: 10px; background: var(--color-surface);
  cursor: pointer; transition: border-color .15s, background-color .15s, box-shadow .15s;
}
.starter-tile:hover { border-color: var(--color-primary-dark); background: var(--color-surface-alt); }
.starter-tile:focus-within { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.starter-tile.is-selected {
  border-color: var(--color-primary); background: var(--color-primary-soft);
  box-shadow: inset 0 0 0 1px var(--color-primary);
}
/* Focusable, not visible - see this block's own comment above. */
.starter-tile .starter-check { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; }
.starter-tile-bar { width: 4px; align-self: stretch; min-height: 26px; border-radius: 3px; flex-shrink: 0; }
.starter-tile-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.starter-tile-name { font-size: 13px; font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; }
.starter-tile-meta { font-size: 11px; color: var(--color-muted); }
.starter-tick {
  margin-left: auto; flex-shrink: 0; width: 18px; height: 18px; border-radius: 50%;
  border: 1px solid var(--color-border); background: var(--color-surface); color: transparent;
  display: flex; align-items: center; justify-content: center;
}
.starter-tick svg { width: 11px; height: 11px; }
.starter-tile.is-selected .starter-tick { background: var(--color-primary); border-color: var(--color-primary); color: #fff; }
/* Read-only (a team manager looking at what an admin set) - the tiles
   still show what is in the package, they just stop inviting a click. */
.starter-tile.is-readonly { cursor: default; }
.starter-tile.is-readonly:hover { border-color: var(--color-border); background: var(--color-surface); }
.starter-tile.is-readonly.is-selected:hover { border-color: var(--color-primary); background: var(--color-primary-soft); }

/* Floats just above the bottom of the viewport while there is something
   to save. Inset rather than bled to the card's edges, since the card's
   padding changes with the density setting. */
.starter-bar-save {
  position: sticky; bottom: 12px; z-index: 2; display: flex; align-items: center; gap: 10px;
  margin-top: 18px; padding: 10px 14px; background: var(--color-surface);
  border: 1px solid var(--color-primary); border-radius: 12px; box-shadow: 0 6px 20px rgba(0, 0, 0, .14);
}
.starter-bar-text { font-size: 13px; font-weight: 600; margin-right: auto; }

/* The rotating brand text is sized by app.js (fitBrandTextSize()) - a
   theme's `transition: all` on it (Mary's vendor .iq-sidebar-logo a span)
   would animate each fit and make the next measurement read a half-way
   size, leaving the name cut off. */
.header-brand-text { transition: none !important; }
