body {
  font-family: system-ui, sans-serif;
  background: #14181f;
  color: #e6e9ef;
  margin: 0;
  min-height: 100vh;
}

.center-viewport {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}

.card {
  background: #1e242e;
  border: 1px solid #2c333f;
  border-radius: 8px;
  padding: 2rem;
  width: 320px;
  max-height: 90vh;
  overflow-y: auto;
  box-sizing: border-box;
}

.card-wide {
  width: 560px;
  max-width: 90vw;
}

/* Reports' filter-row popup specifically - .card-wide's 560px leaves each of
   the 5 per-row controls (AND/OR, Column 1, Condition, Column 2, Constant)
   too cramped to read their own selected option. Its own class rather than
   widening .card-wide itself, which 6 other, much simpler single-column
   popups also share. */
.card-extra-wide {
  width: 820px;
  max-width: 95vw;
}

.form-row {
  display: flex;
  gap: 1rem;
}

.form-row > * {
  flex: 1;
  min-width: 0;
}

h1 {
  font-size: 1.25rem;
  margin: 0 0 1.5rem;
}

.tagline {
  color: #9aa4b2;
  font-size: 0.85rem;
  font-style: italic;
  margin: -1.25rem 0 1.5rem;
}

/* The CineSys logo file (assets/cinesys_logo.png) is a black wordmark meant
   for a light/white background (see the packing slip PDF letterhead, where
   it's used as-is) - inverted here since the login card is dark, same
   source file either way rather than keeping a second light/dark variant. */
.login-logo {
  display: block;
  max-width: 200px;
  width: 100%;
  height: auto;
  margin: 0 auto 1.25rem;
  filter: invert(1);
}

.login-center {
  text-align: center;
}

label {
  display: block;
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
  color: #9aa4b2;
}

input:not([type="checkbox"]),
select,
textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 0.5rem;
  margin-bottom: 1rem;
  border-radius: 4px;
  border: 1px solid #3a4250;
  background: #14181f;
  color: #e6e9ef;
  font-family: inherit;
  font-size: 1rem;
  resize: vertical;
}

/* Auto-filled-and-locked fields (e.g. a predefined Client/Vendor's address in
   the Ship/Return popup) - visually distinct from a normal editable field so
   it reads as "not yours to change here", not just a disabled-looking blank. */
input[readonly] {
  background: #1a1f28;
  color: #9aa4b2;
  cursor: default;
}

/* Checkboxes (row-select, select-all) are their own thing entirely - the
   text-input rule above was matching them too (plain `input` matches every
   type), stretching them to 100% width with a full 1rem bottom margin and a
   text-input border/background. That extra margin was the real cause of
   the "checkbox sits too high in the row" look reported on BOM - not a
   vertical-align issue, which is why fixing vertical-align alone had no
   effect. */
input[type="checkbox"] {
  width: auto;
  margin: 0;
}

button {
  width: 100%;
  padding: 0.6rem;
  border: none;
  border-radius: 4px;
  background: #3b82f6;
  color: white;
  font-weight: 600;
  cursor: pointer;
}

button:hover {
  background: #2f6fd6;
}

.error {
  color: #f87171;
  font-size: 0.85rem;
  margin-bottom: 1rem;
  min-height: 1em;
}

/* Non-blocking heads-up (e.g. "this item already has a prior shipment") -
   informational, not an error, so a distinct amber rather than red. */
.warning {
  color: #eab308;
  font-size: 0.85rem;
  background: rgba(234, 179, 8, 0.1);
  border: 1px solid rgba(234, 179, 8, 0.3);
  border-radius: 4px;
  padding: 0.5rem 0.75rem;
  margin-bottom: 1rem;
  white-space: pre-line;
}

.hidden {
  /* Must win regardless of specificity/source order against things like
     .panel-overlay's own `display`, since .hidden is meant to unconditionally
     override whatever else is set. */
  display: none !important;
}

/* Top navigation - sticky so it (and the toolbar below it, see .toolbar)
   stay visible while scrolling a tall table, per design feedback that these
   tables will commonly grow tall enough to need it. */
.topnav {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.75rem 1.5rem;
  background: #1e242e;
  border-bottom: 1px solid #2c333f;
  position: sticky;
  top: 0;
  z-index: 20;
}

.topnav a {
  color: #e6e9ef;
  text-decoration: none;
  font-weight: 600;
}

.topnav a:hover {
  color: #3b82f6;
}

.topnav .spacer {
  flex: 1;
}

.topnav button {
  width: auto;
  padding: 0.4rem 0.8rem;
}

/* Page content / tables */
.page-content {
  padding: 1.5rem;
  max-width: 960px;
  margin: 0 auto;
}

.toolbar {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

/* Simple (non-wide) pages: the whole page still scrolls normally, so the
   toolbar sticks below the nav as you scroll - `top` is set from JS (see
   app.js's layoutStickyRegions()) since the nav's actual height depends on
   font-size. Scoped to plain .page-content (not .page-content-wide) - wide
   pages use a different, better mechanism below (a bounded-height table
   area that scrolls internally, so nothing above it needs to be sticky at
   all). This scoping also matters for correctness, not just page layout:
   an earlier unscoped `.toolbar { position: sticky; background... }` rule
   was leaking into the Insert Line Item popup's Part Lookup/New Part tab
   buttons (also a .toolbar, reused for its flex-row layout), showing up as
   a stray dark box behind them. Scoping to .page-content descendants fixes
   that as a side effect. */
.page-content:not(.page-content-wide) .toolbar {
  position: sticky;
  z-index: 15;
  background: #14181f;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

.toolbar button {
  width: auto;
  padding: 0.5rem 1rem;
}

/* A <button> wrapped in a toolbar <a> (e.g. "Export CSV") isn't itself the
   flex item - the <a> is, and as an unstyled flex item it sized/aligned
   independently of its button child, throwing off both the height (stretched
   to the row's full height, with the button pinned to its top edge) and the
   width (auto-sizing was the anchor's box, not the button's, so a same-text
   button elsewhere in the same toolbar - e.g. "Import CSV" next to "Export
   CSV" - could end up a visibly different size even with identical CSS).
   `display: contents` removes the <a> from layout entirely - it keeps its
   click/href behavior but stops generating its own box, so the <button>
   inside becomes the toolbar's actual flex item, sized identically to every
   plain sibling <button> by construction rather than by tuning the anchor's
   own flex/alignment to approximate it. */
.toolbar a {
  display: contents;
}

/* Excel-AutoFilter-style "pulldown with checkboxes" widget - shared by the
   BOM page's global Header 1 filter and its per-column filters (same UI
   pattern the design doc describes for both). */
.filter-dropdown-wrap {
  position: relative;
  display: inline-block;
}

.filter-dropdown-panel {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 30;
  margin-top: 0.25rem;
  background: #1e242e;
  border: 1px solid #2c333f;
  border-radius: 6px;
  padding: 0.5rem;
  min-width: 220px;
  max-height: 320px;
  overflow-y: auto;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}

.filter-dropdown-panel .filter-dropdown-actions {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid #2c333f;
}

.filter-dropdown-panel .filter-dropdown-actions button {
  width: auto;
  padding: 0.2rem 0.5rem;
  font-size: 0.75rem;
}

/* Report template field picker (Reports page) - one row per reportable
   field, mode controls only shown once that field is included. */
#report-fields-list {
  max-height: 40vh;
  overflow-y: auto;
  border: 1px solid #2c333f;
  border-radius: 4px;
  padding: 0.5rem 0.75rem;
  margin-bottom: 1rem;
}

.report-field-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding: 0.3rem 0;
  border-bottom: 1px solid #2c333f;
}

.report-field-row:last-child {
  border-bottom: none;
}

.report-field-row label {
  margin-bottom: 0;
  white-space: nowrap;
}

.report-field-mode-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.report-field-mode-controls select {
  width: auto;
  margin-bottom: 0;
}

.report-field-status-fixed {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.filter-dropdown-panel label {
  display: block;
  font-weight: normal;
  font-size: 0.85rem;
  padding: 0.15rem 0;
  white-space: nowrap;
}

/* Per-column filter buttons live inside a sticky <th> (see "thead th" rule
   below), which already establishes a positioning context for the panel's
   position:absolute - and resets the th's own uppercase/small-caps styling
   so the panel's checkbox labels read normally. */
.column-filter-btn {
  width: auto;
  padding: 0 0.25rem;
  margin-left: 0.2rem;
  font-size: 0.7rem;
  background: transparent;
  border: none;
  color: #9aa4b2;
  cursor: pointer;
}

.column-filter-btn:hover {
  color: #e6e9ef;
}

.column-filter-btn.filter-active {
  color: #3b82f6;
}

th .filter-dropdown-panel {
  text-transform: none;
  font-weight: normal;
}

/* createSearchableSelect (app.js) - reuses .filter-dropdown-wrap/-panel for
   the positioning/chrome, adds its own option-row styling. .ship-to-wrap
   overrides .filter-dropdown-wrap's inline-block (sized for a small header
   filter button) to go full-width in a stacked form. */
.ship-to-wrap {
  display: block;
}
.ship-to-wrap .filter-dropdown-panel {
  width: 100%;
  box-sizing: border-box;
}
.searchable-select-option {
  padding: 0.4rem 0.5rem;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  white-space: nowrap;
}
.searchable-select-option:hover {
  background: #2c333f;
}

/* Click-to-sort column headers (app.js's enableColumnSort) - a small
   always-present ::after reserves the arrow's width so a header's text
   doesn't shift sideways the moment it becomes the active sort column. */
.sortable-column {
  cursor: pointer;
}
.sortable-column::after {
  content: '';
  display: inline-block;
  width: 0.8em;
}
.sortable-column.sort-asc::after {
  content: '▲';
}
.sortable-column.sort-desc::after {
  content: '▼';
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}

th,
td {
  text-align: left;
  vertical-align: middle;
  padding: 0.25rem 0.6rem;
  border-bottom: 1px solid #2c333f;
}

th {
  color: #9aa4b2;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* Applied to a <div> wrapping the buttons INSIDE a <td>, never to the <td>
   itself - display:flex on a table cell overrides its table-cell display
   type, which breaks border-collapse rendering for that cell (most visible
   when it's empty, since it then has nothing to give it normal sizing). */
.row-actions-inner {
  display: flex;
  gap: 0.4rem;
}

.row-actions-inner button {
  width: auto;
  padding: 0.3rem 0.6rem;
  font-size: 0.8rem;
}

/* Add to PO popup's per-line Order Qty table - a long selection shouldn't
   grow the popup past the viewport, so it scrolls internally instead. */
.add-to-po-lines {
  max-height: 40vh;
  overflow-y: auto;
  margin: 0.5rem 0;
  border: 1px solid #2c333f;
  border-radius: 4px;
}

.add-to-po-lines input[type='number'] {
  width: 6rem;
}

.btn-secondary {
  background: #3a4250;
}

.btn-secondary:hover {
  background: #4a5262;
}

.btn-danger {
  background: #dc2626;
}

.btn-danger:hover {
  background: #b91c1c;
}

.badge {
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
}

.badge-active {
  background: #14532d;
  color: #86efac;
}

.badge-inactive {
  background: #450a0a;
  color: #fca5a5;
}

/* Wide pages: bounded to exactly fill the viewport below the sticky nav
   (height set from JS - see app.js's layoutStickyRegions()) and laid out as
   a flex column, so only .grid-scroll (the table area) scrolls internally.
   This is what makes the toolbar/description/table-header-row genuinely
   static while scrolling the table (rather than each independently sticky
   and drifting into place at slightly different moments), and it's also
   what keeps the horizontal scrollbar always visible instead of only
   appearing once you've scrolled all the way to the bottom of a tall table -
   it now lives at the bottom of a fixed-height box, not the bottom of the
   (potentially very tall) table itself. */
.page-content-wide {
  max-width: none;
  padding: 1.5rem;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Everything above the table (toolbar rows, description text, the storage
   meter on Logs, etc.) keeps its natural size - only .grid-scroll is meant
   to grow/shrink/scroll. */
.page-content-wide > *:not(.grid-scroll):not(.tab-panel-fill) {
  flex-shrink: 0;
}

/* A tabbed wide page (Logs, Reports) nests its .grid-scroll one level
   deeper than usual - inside whichever tab panel is currently visible - so
   .grid-scroll's own flex:1/min-height:0 has nothing to size against
   unless that panel is ALSO a bounded flex column filling the space
   .page-content-wide would otherwise have given .grid-scroll directly.
   Without this, the panel (a plain block div, caught by the flex-shrink:0
   rule above) just grows to fit its full unbounded table and the whole
   PAGE scrolls instead - the table's own scrollbar never appears, and
   anything below it (pagination) gets pushed off past the fold. The other,
   inactive tab panel is display:none via .hidden (!important - see that
   rule) so it never competes for space regardless of this class. */
.tab-panel-fill {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
.tab-panel-fill > *:not(.grid-scroll) {
  flex-shrink: 0;
}

.inline-form {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.inline-form input,
.inline-form select {
  margin-bottom: 0;
  width: 220px;
}

.inline-form button {
  width: auto;
  padding: 0.5rem 1rem;
}

/* The table area within a wide page - scrolls both directions internally
   (see .page-content-wide above) and fills whatever space is left after
   the toolbar/description. min-height:0 is required for a flex child to
   actually be allowed to shrink/scroll instead of just growing to fit its
   content and overflowing its flex container. */
.grid-scroll {
  overflow: auto;
  border: 1px solid #2c333f;
  border-radius: 6px;
  flex: 1;
  min-height: 0;
}

.pagination-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0;
}
.pagination-row button {
  width: auto;
}

/* Sticks to the top of .grid-scroll's own internal scroll (its nearest
   scrolling ancestor) - stays visible as rows scroll past underneath.
   Needs an opaque background since rows now scroll directly beneath it. */
thead th {
  position: sticky;
  top: 0;
  background: #14181f;
  z-index: 5;
}

.permission-grid {
  border-collapse: collapse;
  white-space: nowrap;
}

.permission-grid th,
.permission-grid td {
  border: 1px solid #2c333f;
  padding: 0.4rem 0.6rem;
  font-size: 0.85rem;
}

.permission-grid thead th {
  background: #1e242e;
  vertical-align: top;
}

.permission-grid .role-name-input {
  width: 140px;
  margin-bottom: 0.4rem;
}

.permission-grid td {
  text-align: center;
}

/* Toggle switch (Roles page permission grid) */
.switch {
  position: relative;
  display: inline-block;
  width: 36px;
  height: 20px;
  vertical-align: middle;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.switch .slider {
  position: absolute;
  cursor: pointer;
  inset: 0;
  background-color: #3a4250;
  transition: 0.15s;
  border-radius: 20px;
}

.switch .slider::before {
  position: absolute;
  content: "";
  height: 14px;
  width: 14px;
  left: 3px;
  bottom: 3px;
  background-color: #e6e9ef;
  transition: 0.15s;
  border-radius: 50%;
}

.switch input:checked + .slider {
  background-color: #3b82f6;
}

.switch input:checked + .slider::before {
  transform: translateX(16px);
}

.switch input:disabled + .slider {
  opacity: 0.5;
  cursor: not-allowed;
}

/* A .switch (above) paired with its own text label, for standalone toggles
   outside a table (e.g. BOM's "Show Removals" filter) - the whole label is
   clickable, not just the switch itself. */
.switch-toggle-label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  font-size: 0.85rem;
  color: #9aa4b2;
  margin-bottom: 0;
}

.permission-grid .sticky-col {
  position: sticky;
  left: 0;
  background: #1e242e;
  text-align: left;
}

.group-row th {
  background: #2c333f;
  color: #9aa4b2;
  text-align: left;
  text-transform: uppercase;
  font-size: 0.75rem;
  letter-spacing: 0.03em;
}

.btn-small {
  width: auto;
  padding: 0.2rem 0.5rem;
  font-size: 0.75rem;
  margin-top: 0.3rem;
}

/* Storage meter (Logs page). Status colors are the dataviz skill's fixed,
   dark-surface-validated palette - never repurposed for anything else. */
.meter-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.meter-track {
  position: relative;
  width: 220px;
  height: 10px;
  border-radius: 999px;
  overflow: hidden;
}

.meter-fill {
  height: 100%;
  border-radius: 999px;
}

.meter-label {
  font-size: 0.85rem;
  color: #9aa4b2;
  white-space: nowrap;
}

.meter-label .meter-status-text {
  font-weight: 600;
}

.meter-good .meter-fill { background: #0ca30c; }
.meter-good .meter-track { background: rgba(12, 163, 12, 0.18); }
.meter-good .meter-status-text { color: #0ca30c; }

.meter-warning .meter-fill { background: #fab219; }
.meter-warning .meter-track { background: rgba(250, 178, 25, 0.18); }
.meter-warning .meter-status-text { color: #fab219; }

.meter-serious .meter-fill { background: #ec835a; }
.meter-serious .meter-track { background: rgba(236, 131, 90, 0.18); }
.meter-serious .meter-status-text { color: #ec835a; }

.meter-critical .meter-fill { background: #d03b3b; }
.meter-critical .meter-track { background: rgba(208, 59, 59, 0.18); }
.meter-critical .meter-status-text { color: #d03b3b; }

/* BOM header/divider rows. H1/H2/H3 are ordered depth, not unordered
   categories, so this is the dataviz skill's "ordinal tiers" case: one hue
   (blue), stepped by lightness - steps 400/500/600 of its sequential blue
   ramp, staying at/above its documented dark-surface floor for an ordinal
   ramp (no darker than step 600, which still clears 2:1 contrast). */
.bom-header-row td {
  font-weight: 600;
  color: #ffffff;
}
.bom-h1 td { background: #3987e5; }
.bom-h2 td { background: #256abf; }
.bom-h3 td { background: #184f95; }

/* A negative Qty is always a removal/zero-out (e.g. Dupe Rows' auto-negated
   clones) - tinting the row makes that jump out at a glance in a long BOM
   instead of requiring a per-line Qty check. Same red/alpha as
   .meter-critical for one consistent "danger" color across the app, at a
   background rather than the selection box-shadow's layer so it still
   composites correctly if the row also gets selected (box-shadow paints on
   top, same reasoning as .row-selected above). */
.bom-row-negative-qty td { background: rgba(208, 59, 59, 0.18); }

/* Insert H1/H2/H3 buttons match their row colors (next step down the same
   ramp on hover, keeping the progression consistent). */
.btn-h1 { background: #3987e5; }
.btn-h1:hover { background: #2a78d6; }
.btn-h2 { background: #256abf; }
.btn-h2:hover { background: #1c5cab; }
.btn-h3 { background: #184f95; }
.btn-h3:hover { background: #104281; }

/* Click-anywhere-on-row selection (no checkboxes) - shared by any table that
   opts in via the .selectable-rows class (Parts Database, Project Pricing,
   BOM). An inset box-shadow rather than `background`, since a plain
   background would just lose to a colored row's own background (e.g. BOM's
   .bom-h1/h2/h3 header rows) in the cascade instead of visibly tinting it.
   box-shadow paints as its own layer on top, so this reads as "selected"
   correctly whether the row underneath is plain or already colored. Amber,
   deliberately not blue, so on BOM it never reads as "another header level"
   next to the H1/H2/H3 ramp. */
.selectable-rows tbody tr {
  cursor: pointer;
}
.selectable-rows tbody tr.row-selected td {
  box-shadow: inset 0 0 0 999px rgba(250, 204, 21, 0.22);
}

.bom-table {
  white-space: nowrap;
}

/* Equipment Inventory has grown to 34+ columns (see the "Equipment
   Inventory rework" plan) - same nowrap-to-force-natural-width treatment as
   .bom-table above, so a long value (a Hostname, say) widens its column
   instead of wrapping to stay artificially narrow. */
.inventory-table {
  white-space: nowrap;
}

/* Logistics View toggle - PO/Received Date/Configured/Shipped/Installed/
   Cabled are hidden by default (no JS needed for the default state - the
   bare .logistics-col rule alone covers it) and only shown once
   public/equipmentInventory.js adds .show-logistics-view to the table,
   either from the toggle itself or a restored prior state. */
.inventory-table .logistics-col {
  display: none;
}
.inventory-table.show-logistics-view .logistics-col {
  display: table-cell;
}

/* Inline cell editing (double-click a data-field cell instead of opening
   Update Selected - see public/equipmentInventory.js) - a subtle hover cue
   marking which cells are actually editable this way, and a tight override
   of the global input/select/textarea rule above (padding: 0.5rem;
   margin-bottom: 1rem would blow the row height out completely) so the
   editor sits flush inside the cell it replaces. */
.inventory-table td[data-field] {
  cursor: text;
}
.inventory-table td[data-field]:hover {
  box-shadow: inset 0 0 0 999px rgba(255, 255, 255, 0.05);
}
.cell-editor {
  width: 100%;
  box-sizing: border-box;
  padding: 0.15rem 0.3rem;
  margin-bottom: 0;
  font-family: inherit;
  font-size: inherit;
}

/* Sub-component rows (Phase 3 - Add Sub-component) - the "↳" prefix on
   Line ID (added in the template) already signals the parent/child
   relationship on its own; this just dims the row slightly so a long list
   of devices-with-sub-components stays easy to scan for the top-level rows
   at a glance. */
.inventory-subrow td {
  color: #9aa4b2;
}

/* Conn Type's color dot (Column Manager-defined, arbitrary per-project
   color) - a small swatch next to the name rather than coloring the whole
   cell, since a full-cell background would need to compute a readable
   foreground color against whatever arbitrary color someone picks. */
.conn-type-swatch {
  display: inline-block;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 999px;
  margin-right: 0.4rem;
  vertical-align: middle;
}

/* Configured/Shipped/Installed/Cabled/Asset Tag - short Yes/No/N/A values
   that would otherwise size to their own tiny content width even with
   nowrap above; explicitly widened since they're now a prominent block up
   front (right after Received Date) rather than left cramped-looking. */
.inventory-table .col-wide {
  min-width: 110px;
}

/* Live totals row (public/bom.js's recomputeBomTotals) - sticks to the
   BOTTOM of .grid-scroll's own internal scroll, mirroring thead th's
   sticky-to-top rule above, so it stays visible while scrolling through a
   long BOM instead of just being the last thing in the table. Scoped to
   .bom-table specifically rather than a bare `tfoot td` rule, since the
   Quote/CO summary tfoot below already has its own different (display:
   block-based) sticky approach that a global rule would collide with. */
/* BOM Export's Custom column builder (public/bomExport.js) - one row per
   picked column, a growing <select> plus fixed-width reorder/remove
   buttons. Deliberately its own rule rather than reusing .form-row (that's
   built for equal-flex label+input pairs, not a select alongside several
   small icon buttons). */
.bom-export-custom-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.bom-export-custom-row select {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}
.bom-export-custom-row button {
  width: auto;
  padding: 0.3rem 0.6rem;
}

/* Column Manager's Equipment Conn Types builder - same shape as
   .bom-export-custom-row above (a growing text input plus fixed-width
   reorder/remove buttons), swapping the select for a name input + a native
   color picker. */
.conn-type-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.conn-type-row .conn-type-name {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}
.conn-type-row .conn-type-color {
  width: 2.5rem;
  padding: 0.15rem;
  margin-bottom: 0;
}
.conn-type-row button {
  width: auto;
  padding: 0.3rem 0.6rem;
}

/* A Submitted PO's Edit form locks Vendor and Description (cost-only edit -
   see public/pos.js's openForm). The browser default leaves a disabled field
   looking exactly like an editable one on this dark theme, so make the lock
   visible rather than leaving it to be discovered by typing. */
#po-form input:disabled,
#po-form select:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* The POs page's totals row (public/pos.js's recomputePoTotals) shares this
   exact treatment - same .grid-scroll layout, same need to stay in view. */
.bom-table tfoot td,
.po-table tfoot td {
  position: sticky;
  bottom: 0;
  background: #14181f;
  z-index: 5;
  font-weight: 600;
  border-top: 2px solid #2c333f;
}

/* Model can run long enough to blow the whole table out to an unusable
   width (see .bom-table's own white-space: nowrap above) - capped and
   ellipsized instead, with the full text available via the cell's title
   attribute (native browser tooltip on hover). */
.bom-model-cell {
  max-width: 210px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Description gets the wider treatment: instead of truncating to one
   ellipsized line, it wraps within a fixed width so the full text stays
   on-screen (the <td> itself still keeps a title attribute too, for a
   quick copy/hover read without needing to widen the row).
   Both `width` and `max-width` set directly on the <td> turned out to be
   non-authoritative here - table-layout: auto gives a wrap-enabled column
   a tiny "minimum content width" (its longest single unbreakable word)
   versus a nowrap column's (its full text width), and with this table
   already needing far more total width than the viewport (triggering the
   intended horizontal scroll), the layout algorithm squeezes the one
   column that CAN shrink that far down toward its minimum, overriding
   whatever width/max-width was declared on the cell itself (confirmed via
   getComputedStyle - it was resolving width to ~256px regardless of which
   of the two was declared as 500px).
   Fix: force the width via a block-level child instead of the <td>/<th>
   itself - a table cell must grow to fit a forced-width child's layout
   box, which reliably wins where styling the cell directly didn't. */
.bom-description-cell {
  width: 500px;
  white-space: normal;
  overflow-wrap: break-word;
}

/* Bottom Quote/CO summary table - a separate, smaller block below the main
   BOM table's own internal scroll area (see .grid-scroll and
   .page-content-wide > *:not(.grid-scroll) keeping natural size). Its own
   row-count cap lives on #summary-tbody below, not here. */
.bom-summary {
  margin-top: 1rem;
}

.bom-summary h2 {
  font-size: 1rem;
  margin: 0 0 0.5rem;
}

/* Collapsible header row - the Hide/Show button sits inline with the title
   rather than in a toolbar of its own, so collapsing the panel leaves just
   this one compact line behind (see views/bom.ejs). */
.bom-summary-header {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.bom-summary-header h2 {
  margin: 0;
}

#bom-summary-toggle {
  width: auto;
  padding: 0.15rem 0.6rem;
  font-size: 0.75rem;
}

.bom-summary tfoot td {
  font-weight: 600;
  border-top: 2px solid #2c333f;
  border-bottom: none;
}

/* Caps visible rows so the summary table can't grow upward into the main
   BOM table - thead/tfoot always stay put, only the row list scrolls.
   Classic "display:block on thead/tbody/tfoot, display:table on tr" trick;
   since that splits column-width calculation per section, the nth-child
   widths below keep all three in sync. #summary-tbody's own max-height is
   set at runtime in bom.js (measured from actual rendered row heights, same
   philosophy as app.js's layoutStickyRegions - not hardcoded, since actual
   row height depends on font-size and whether Description wraps).
   IMPORTANT: because each <tr> now lays itself out independently via
   display:table, colspan stops meaning "take N columns' width" - it just
   picks whichever nth-child rule matches that cell's position in ITS OWN
   row, ignoring the cells it's supposedly spanning. The tfoot row in
   bom.ejs deliberately has the same 10 plain <td>s as every other row (no
   colspan) so nth-child alignment stays correct - don't reintroduce colspan
   here without rethinking this whole layout approach. */
.bom-summary-table {
  display: block;
  width: 100%;
}

.bom-summary-table thead,
.bom-summary-table tbody,
.bom-summary-table tfoot {
  display: block;
  width: 100%;
}

.bom-summary-table tr {
  display: table;
  width: 100%;
  table-layout: fixed;
}

#summary-tbody {
  overflow-y: auto;
}

.bom-summary-table td:nth-child(3) {
  overflow-wrap: break-word;
}

.bom-summary-table th:nth-child(1),
.bom-summary-table td:nth-child(1) {
  width: 4%;
}
.bom-summary-table th:nth-child(2),
.bom-summary-table td:nth-child(2) {
  width: 11%;
}
.bom-summary-table th:nth-child(3),
.bom-summary-table td:nth-child(3) {
  width: 18%;
}
.bom-summary-table th:nth-child(4),
.bom-summary-table td:nth-child(4) {
  width: 7%;
}
.bom-summary-table th:nth-child(5),
.bom-summary-table td:nth-child(5) {
  width: 10%;
}
.bom-summary-table th:nth-child(6),
.bom-summary-table td:nth-child(6) {
  width: 10%;
}
.bom-summary-table th:nth-child(7),
.bom-summary-table td:nth-child(7) {
  width: 10%;
}
.bom-summary-table th:nth-child(8),
.bom-summary-table td:nth-child(8) {
  width: 10%;
}
.bom-summary-table th:nth-child(9),
.bom-summary-table td:nth-child(9) {
  width: 7%;
}
.bom-summary-table th:nth-child(10),
.bom-summary-table td:nth-child(10) {
  width: 7%;
}
.bom-summary-table th:nth-child(11),
.bom-summary-table td:nth-child(11) {
  width: 6%;
}

/* Modal-style popup panels. Explicit z-index needed now that the nav/
   toolbar are sticky with their own z-index (20/15) - without one, a
   popup's default z-index:auto can lose to those in paint order. */
.panel-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

/* Drawing-change confirmation (BOM: Insert Line Item / Update Selected) -
   see bom.js's confirmDrawingChange. Narrower and centered, unlike the
   left-aligned form cards it's layered on top of (z-index below .panel-
   overlay's own base but this one always opens ON TOP of an already-open
   form overlay, so it needs to win the stacking order explicitly). */
.drawing-change-card {
  width: 360px;
  text-align: center;
}
#drawing-change-overlay {
  z-index: 110;
}
.drawing-change-icon {
  font-size: 2.5rem;
  line-height: 1;
  margin-bottom: 0.75rem;
}
.drawing-change-message {
  margin: 0 0 1.5rem;
  line-height: 1.5;
}
.drawing-change-buttons {
  display: flex;
  justify-content: center;
  gap: 1rem;
}
.drawing-change-buttons button {
  width: auto;
  min-width: 5rem;
}

/* Stale-data banner (public/app.js's initStaleDataCheck) - a small, fixed,
   dismissible toast, not a modal - it should never block interaction with
   the page underneath it. Pinned just below the sticky nav (its `top` is set
   in JS, by layoutStickyRegions - same "measure the nav's real rendered
   height" approach the toolbar sticky-offset already uses, since it depends
   on font-size/theme rather than being a safe constant). Amber, not the
   site's usual blue, is deliberate - this is the one alert-style element in
   the app, and it needs to read as "notice this" at a glance against an
   otherwise all-blue/dark palette, not blend in like a normal action button
   would. z-index above .panel-overlay (100/110) so it's still visible/
   clickable even with a form popup open. */
.stale-data-banner {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.85rem;
  background: #f59e0b;
  border: 1px solid #b45309;
  border-radius: 8px;
  padding: 0.6rem 0.7rem 0.6rem 1rem;
  box-shadow: 0 4px 20px rgba(245, 158, 11, 0.35);
  z-index: 150;
  font-size: 0.9rem;
  color: #1e1508;
  font-weight: 600;
}
.stale-data-banner .stale-data-refresh {
  width: auto;
  margin-bottom: 0;
  padding: 0.3rem 0.75rem;
  background: #1e1508;
  color: #ffd782;
}
.stale-data-banner .stale-data-refresh:hover {
  background: #33270f;
}
.stale-data-banner .stale-data-dismiss {
  width: auto;
  margin-bottom: 0;
  background: transparent;
  border: none;
  color: #1e1508;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0 0.2rem;
  cursor: pointer;
  opacity: 0.65;
}
.stale-data-banner .stale-data-dismiss:hover {
  opacity: 1;
}

/* PO "View Items" preview - Header 1/2/3 divider rows, same blue ramp as
   the BOM Export PDF's own header rows (views/bomExportPdf.ejs) so a
   Header row reads the same whether it's on screen or on paper. */
.po-items-header-row {
  font-weight: bold;
  color: #fff;
}
.po-items-header_1 { background: #3987e5; }
.po-items-header_2 { background: #256abf; }
.po-items-header_3 { background: #184f95; }

/* Reports' filter-row builder (views/reports.ejs) - same "+ Add Row" flex
   shape as .bom-export-custom-row, just with more per-row fields (AND/OR,
   Column 1, Condition, Column 2, Constant). */
.report-filter-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.report-filter-row select,
.report-filter-row input {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}
.report-filter-row .report-filter-connector {
  flex: 0 0 5.5rem;
}
.report-filter-row button {
  width: auto;
  padding: 0.3rem 0.6rem;
  flex: 0 0 auto;
}
/* Status's 4 tri-state (Any/Yes/No) subfields swap in for Condition/Column
   2/Constant on a row whose Column 1 is Status (public/reports.js's
   updateRowRhsVisibility) - takes up the same stretch of row real estate
   those would otherwise fill. Tri-state rather than a plain checkbox, so
   "must NOT have this status" (No) is distinguishable from "don't care"
   (Any, the default) - a checkbox can only ever express the former. */
.report-filter-status-checkboxes {
  flex: 3;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
  min-width: 0;
}
.report-filter-status-option {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-weight: normal;
  font-size: 0.85rem;
  white-space: nowrap;
}
.report-filter-status-option select {
  flex: 0 0 auto;
  width: auto;
  min-width: 4.5rem;
  margin-bottom: 0;
}

/* The About/home page's page-by-page user guide (views/home.ejs) - the only
   page in the app with real prose (headings, paragraphs, lists) rather than
   a table+toolbar, so it's the only one that needs base element styling for
   any of this. Scoped to .guide rather than bare h2/h3/ul/a, so it can't
   leak into every other page's own layout. */
.guide-header {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
}
.guide-version {
  color: #9aa4b2;
  font-size: 0.85rem;
  background: #1e242e;
  border: 1px solid #2c333f;
  border-radius: 4px;
  padding: 0.15rem 0.5rem;
}
.guide-toc {
  background: #1e242e;
  border: 1px solid #2c333f;
  border-radius: 8px;
  padding: 1rem 1.5rem;
  margin: 1.5rem 0 2rem;
  columns: 2;
  column-gap: 2rem;
}
.guide-toc li {
  break-inside: avoid;
}
.guide h2 {
  font-size: 1.1rem;
  margin: 2rem 0 0.25rem;
  padding-top: 1.5rem;
  border-top: 1px solid #2c333f;
}
.guide section:first-of-type h2 {
  border-top: none;
  padding-top: 0;
}
.guide h3 {
  font-size: 0.95rem;
  margin: 1rem 0 0.4rem;
  color: #9aa4b2;
}
.guide p {
  line-height: 1.5;
  margin: 0 0 0.75rem;
}
.guide ul {
  margin: 0 0 0.75rem;
  padding-left: 1.25rem;
  line-height: 1.5;
}
.guide li {
  margin-bottom: 0.35rem;
}
.guide a {
  color: #6fa8f5;
}
.guide code {
  background: #1e242e;
  border: 1px solid #2c333f;
  border-radius: 3px;
  padding: 0.05rem 0.3rem;
  font-size: 0.9em;
}

/* Changelog - a stack of native <details> elements, one per version, so
   collapse/expand needs no JS. Newest entry starts open (see home.ejs); the
   rest start collapsed. */
.guide-changelog {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-bottom: 2rem;
}
.changelog-entry {
  background: #1e242e;
  border: 1px solid #2c333f;
  border-radius: 8px;
  padding: 0.75rem 1rem;
}
.changelog-entry summary {
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  font-weight: 600;
  outline: none;
}
.changelog-entry[open] summary {
  margin-bottom: 0.5rem;
}
.changelog-version {
  color: #e6e9ef;
}
.changelog-date {
  color: #9aa4b2;
  font-size: 0.85rem;
  font-weight: normal;
}
.changelog-entry ul {
  margin: 0;
}

/* "Equipment by Location" tab - one <details> per Location, same
   collapsible shape as .changelog-entry above (native <details>/<summary>,
   no JS needed for expand/collapse). */
.rack-elevation {
  background: #1e242e;
  border: 1px solid #2c333f;
  border-radius: 8px;
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
}
.rack-elevation summary {
  cursor: pointer;
  font-weight: 700;
  text-align: center;
  outline: none;
  padding: 0.4rem;
  border: 2px solid #2c333f;
  border-radius: 4px;
}
.rack-elevation[open] summary {
  margin-bottom: 0.75rem;
}
/* The rack table itself - RU# columns on both edges (narrow, centered),
   borders on every cell so an empty RU slot still reads as a real,
   present-but-unoccupied row rather than a gap in the layout. */
.rack-table {
  width: 100%;
  border-collapse: collapse;
}
.rack-table td, .rack-table th {
  border: 1px solid #2c333f;
  padding: 0.3rem 0.6rem;
}
.rack-table th {
  color: #9aa4b2;
  font-weight: 600;
  text-align: left;
  font-size: 0.8rem;
}
.rack-ru-col {
  width: 3rem;
  text-align: center;
  color: #9aa4b2;
  background: #171b22;
}
/* Equipment with a Location but no (usable) RU - listed above its rack,
   same 3 info columns, no RU edge columns since there's no slot to label. */
.rack-unracked-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 0.75rem;
}
.rack-unracked-table td, .rack-unracked-table th {
  border: 1px solid #2c333f;
  padding: 0.3rem 0.6rem;
}
.rack-unracked-table td {
  color: #9aa4b2;
}
.rack-unracked-table th {
  color: #9aa4b2;
  font-weight: 600;
  text-align: left;
  font-size: 0.8rem;
}
/* "Equipment by Switch" tab - one row per populated Port (never a blank
   slot, unlike .rack-table above), so it's really just .rack-unracked-table
   again with a single narrow Port column (reusing .rack-ru-col) on the
   left instead of no edge column at all. */
.switch-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 0.75rem;
}
.switch-table td, .switch-table th {
  border: 1px solid #2c333f;
  padding: 0.3rem 0.6rem;
}
.switch-table th {
  color: #9aa4b2;
  font-weight: 600;
  text-align: left;
  font-size: 0.8rem;
}

/* Row status: red if Configured/Installed/Cabled has any explicit No, green
   otherwise (Yes and N/A both read as "fine"). Left as default background
   for an empty/unoccupied rack slot - nothing to judge there. The RU edge
   columns keep their own dark background regardless, so the RU# stays
   readable either way. */
.rack-status-good td:not(.rack-ru-col) {
  background: rgba(46, 160, 67, 0.18);
}
.rack-status-bad td:not(.rack-ru-col) {
  background: rgba(220, 60, 60, 0.22);
}

/* Status icons (BOM grid + Project Pricing) - hand-drawn SVG/text instead
   of emoji characters, which render very differently across OSes (each
   platform draws its own artwork for the same codepoint - e.g. the "on an
   approved Quote/CO" 💲 came out dark grey on macOS, bright green on
   Windows). These look identical everywhere since nothing but this
   stylesheet controls their color. Same green used elsewhere for "good"
   status (.rack-status-good above); the cart is reused open/grey (on a PO,
   not yet submitted/ordered) vs filled/green (submitted/ordered) instead of
   needing a separate "receipt" icon shape. */
.status-icon {
  display: inline-block;
  vertical-align: middle;
  width: 14px;
  height: 14px;
  margin-right: 2px;
}
.status-icon-dollar {
  width: auto;
  height: auto;
  color: #2ea043;
  font-weight: 700;
  font-size: 0.85rem;
  font-family: Arial, Helvetica, sans-serif;
}
.status-icon-check {
  width: auto;
  height: auto;
  font-size: 0.8rem;
}
.status-icon-cart-open {
  fill: none;
  stroke: #9aa4b2;
}
.status-icon-cart-filled {
  fill: rgba(46, 160, 67, 0.35);
  stroke: #2ea043;
}
.status-icon-lock {
  fill: none;
  stroke: #9aa4b2;
}

/* PDU Config's outlet builder (views/equipmentInventory.ejs's
   #pdu-template-outlets-list) - same "+ Add Row" flex shape as Reports'
   own .report-filter-row above, just two plain inputs instead of several
   selects. */
.pdu-outlet-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.pdu-outlet-row input {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}
.pdu-outlet-row button {
  width: auto;
  padding: 0.3rem 0.6rem;
  flex: 0 0 auto;
}

/* PDU tables bracketing a Location's rack elevation on Equipment by
   Location (see partials/pduSideTable.ejs and partials/pduTransposedTable.ejs) -
   .rack-pdu-row lays out Left PDU(s) / the rack-table's own column / Right
   PDU(s) side by side; .rack-pdu-center stacks Top PDU(s), the rack-table,
   then Bottom PDU(s) vertically. .rack-pdu-side stacks multiple same-side
   PDUs (see the confirmed "stack in creation order" behavior). Both align
   to the top so a short PDU table doesn't stretch to match a tall rack. */
.rack-pdu-row {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
}
.rack-pdu-center {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  flex: 1;
  min-width: 0;
}
.rack-pdu-side {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  flex: 0 0 auto;
}
/* Deliberately smaller/tighter than .rack-table/.switch-table - these sit
   alongside the main rack elevation, not as its own primary content, and
   were reading as oversized/overpowering the page at the same size (real
   user feedback). font-size cascades down to every cell AND the outlet
   <select> below, which is the main fix - the global select rule
   (styles.css, near the top) applies a full 0.5rem/1rem-font padding
   that reads huge inside a table cell this size otherwise. */
.pdu-table {
  border-collapse: collapse;
  margin-bottom: 0;
  font-size: 0.72rem;
}
.pdu-table caption {
  caption-side: top;
  text-align: left;
  font-weight: 700;
  font-size: 0.72rem;
  padding-bottom: 0.15rem;
}
.pdu-table td, .pdu-table th {
  border: 1px solid #2c333f;
  padding: 0.1rem 0.35rem;
}
.pdu-table th {
  color: #9aa4b2;
  font-weight: 600;
  text-align: left;
}
/* Transposed (Top/Bottom) orientation - the field-label column on the left
   is the same role .rack-ru-col plays on the rack table itself (a narrow,
   shaded, always-present edge column), so it gets the same treatment. */
.pdu-table-transposed th {
  width: 4.5rem;
  background: #171b22;
  white-space: nowrap;
}
.pdu-table .pdu-outlet-select {
  margin-bottom: 0;
  min-width: 5.5rem;
  padding: 0.1rem 0.25rem;
  font-size: inherit;
}
