:root {
  color-scheme: light;
  --ke-brand-50: #f0f7ff;
  --ke-brand-100: #e0effe;
  --ke-brand-200: #b9dffe;
  --ke-brand-300: #7cc6fd;
  --ke-brand-400: #36aafa;
  --ke-brand-500: #0c8eeb;
  --ke-brand-600: #0070c9;
  --ke-brand-700: #0059a3;
  --ke-brand-800: #054b86;
  --ke-brand-900: #0a3f6f;
  --ke-canvas: #f0f4ff;
  --ke-surface: rgba(255, 255, 255, 0.72);
  --ke-surface-hover: rgba(255, 255, 255, 0.88);
  --ke-surface-secondary: rgba(255, 255, 255, 0.48);
  --ke-dropdown-bg: rgba(255, 255, 255, 0.97);
  --ke-dropdown-border: rgba(88, 102, 135, 0.2);
  --ke-muted-surface: rgba(0, 0, 0, 0.04);
  --ke-border: rgba(255, 255, 255, 0.72);
  /* Structural shell dividers (topbar underline, brand/sidebar rail edges).
     Kept separate from --ke-border, which is the white glass-highlight edge
     frosted cards use against the light canvas — that reads as no line at all
     on the near-white topbar. This one is a true grey in light mode and
     matches the dark-mode border weight in dark. */
  --ke-chrome-border: rgba(88, 102, 135, 0.24);
  --ke-text: #1a1a2e;
  --ke-text-muted: #555570;
  --ke-text-subtle: #8888a0;
  --ke-track: rgba(0, 0, 0, 0.06);
  --ke-shadow: 0 18px 60px rgba(20, 35, 80, 0.12);
  --ke-shadow-sm: 0 8px 28px rgba(20, 35, 80, 0.08);
  --ke-tooltip-bg: #1a1a2e;
  --ke-tooltip-fg: #ffffff;
  /* Stage 40 — squared-corner design decision: every chrome radius token
     flattens to 0 platform-wide. Exceptions: true circles (avatars, dots,
     the tools FAB, radio inputs, rounded-circle) and the form-switch pill. */
  --ke-radius-card: 0;
  --ke-radius-control: 0;
  --ke-sidebar-width: 13rem;
  --ke-sidebar-collapsed-width: 5.5rem;
  --ke-topbar-height: 4rem;

  /* Stacking scale — every chrome-level z-index in this file references one of
     these tokens; never restate the literals. The values interleave with
     Bootstrap's own layers (sticky 1020, fixed 1030, offcanvas backdrop 1040,
     offcanvas 1045, modal backdrop 1050, modal 1055, toast 1090), which is why
     the numbers are irregular. Component-local stacking (z-index 1/2 inside a
     card or table) stays literal — only viewport-level chrome belongs here. */
  --ke-z-fixed: 1030; /* viewport-pinned floats: rail tip/search, tools FAB */
  --ke-z-topbar: 1045; /* topbar — under the sidebar */
  --ke-z-sidebar: 1060; /* app sidebar — above centred modals (1055) */
  --ke-z-modal-backdrop-lift: 1062; /* backdrop lifted above the sidebar while the preview modal is open */
  --ke-z-modal-fullscreen: 1065; /* fullscreen modals lifted above the sidebar */
  --ke-z-dropdown: 1080; /* dropdowns clear modals and the sidebar */
  --ke-z-overlay: 1085; /* fixed helper panels above dropdowns */
  --ke-z-toast: 1090; /* matches Bootstrap $zindex-toast */
  --ke-z-skip-link: 1100; /* a11y skip link — topmost */
  --bs-body-font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  --bs-body-bg: var(--ke-canvas);
  --bs-body-color: var(--ke-text);
  --bs-primary: var(--ke-brand-600);
  --bs-primary-rgb: 0, 112, 201;
  --bs-link-color: var(--ke-brand-700);
  --bs-link-hover-color: var(--ke-brand-800);
  --bs-border-color: rgba(88, 102, 135, 0.24);
  --bs-border-radius: var(--ke-radius-control);
  --bs-border-radius-sm: 0;
  --bs-border-radius-lg: var(--ke-radius-card);
  --bs-border-radius-xl: 0;
  --bs-border-radius-xxl: 0;
  --bs-border-radius-pill: 0;
  --bs-focus-ring-color: rgba(12, 142, 235, 0.25);
  --bs-secondary-color: var(--ke-text-muted);
  --bs-tertiary-color: var(--ke-text-subtle);
  /* Drawing takeoff overlay colours — prototype hues darkened for >=4.5:1
     contrast on the light canvas; the prototype's original (brighter) values
     are restored in the dark theme block below. */
  --ke-takeoff-measure: hsl(187, 92%, 28%);
  --ke-takeoff-weld: hsl(30, 95%, 34%);
  --ke-takeoff-fixture: hsl(25, 95%, 36%);
  --ke-takeoff-note: hsl(45, 92%, 27%);
  --ke-takeoff-calibrate: hsl(142, 76%, 28%);
  --ke-takeoff-selection: hsl(258, 70%, 50%);
  --ke-takeoff-temp: hsl(217, 91%, 45%);

  /* Delivery-status traffic-light tokens — kept in step with the delivery
     doughnut palette (apps/planning/views/dashboard_panel_views.py) so the
     legend swatches match their chart segments exactly. Theme-independent: the
     chart uses the same values in light and dark, and these vivid mid-tones
     read on both canvases. */
  --ke-status-on-time: #10b981;
  --ke-status-running-late: #f59e0b;
  --ke-status-overdue: #ef4444;

  /* Project timeline (Gantt) geometry — the sticky label column, how much
     canvas one day of programme takes, and the height of one plotted row. */
  --gantt-label-w: 15rem;
  --gantt-day-w: 3px;
  --gantt-row-h: 2.25rem;
}

[data-bs-theme="dark"] {
  color-scheme: dark;
  --ke-canvas: #0c0c14;
  --ke-surface: rgba(255, 255, 255, 0.065);
  --ke-surface-hover: rgba(255, 255, 255, 0.095);
  --ke-surface-secondary: rgba(255, 255, 255, 0.04);
  --ke-dropdown-bg: #1a1a23;
  --ke-dropdown-border: rgba(255, 255, 255, 0.14);
  --ke-sidebar-bg: rgba(10, 13, 27, 0.96);
  --ke-muted-surface: rgba(255, 255, 255, 0.055);
  --ke-border: rgba(255, 255, 255, 0.105);
  --ke-chrome-border: rgba(255, 255, 255, 0.105);
  --ke-text: #f0f0f8;
  --ke-text-muted: #a0a0b8;
  --ke-text-subtle: #72728a;
  --ke-track: rgba(255, 255, 255, 0.08);
  --ke-shadow: 0 18px 60px rgba(0, 0, 0, 0.36);
  --ke-shadow-sm: 0 8px 28px rgba(0, 0, 0, 0.25);
  --ke-tooltip-bg: #f0f0f8;
  --ke-tooltip-fg: #0c0c14;
  --bs-body-bg: var(--ke-canvas);
  --bs-body-color: var(--ke-text);
  --bs-link-color: var(--ke-brand-300);
  --bs-link-hover-color: var(--ke-brand-200);
  --bs-border-color: rgba(255, 255, 255, 0.11);
  --bs-secondary-color: var(--ke-text-muted);
  --bs-tertiary-color: var(--ke-text-subtle);
  /* Drawing takeoff overlay colours — prototype palette (plan Appendix A). */
  --ke-takeoff-measure: hsl(187, 92%, 50%);
  --ke-takeoff-weld: hsl(30, 95%, 55%);
  --ke-takeoff-fixture: #f97316;
  --ke-takeoff-note: #facc15;
  --ke-takeoff-calibrate: hsl(142, 72%, 45%);
  --ke-takeoff-selection: hsl(258, 90%, 66%);
  --ke-takeoff-temp: hsl(217, 91%, 60%);
}

html {
  min-height: 100%;
  background: var(--ke-canvas);
  scroll-behavior: smooth;
}

body {
  min-height: 100vh;
  background: var(--ke-canvas);
  color: var(--ke-text);
  font-size: 0.875rem;
}

.page-body p,
.modal p {
  margin-bottom: 0;
}

/* Right-align the close control in every modal header. This Bootstrap build's
   `.modal-header` has no `justify-content: space-between`, so it relies on
   Bootstrap's native `.btn-close` (which carries `margin-left: auto`). Custom
   close buttons used by some modals lack that margin and would otherwise sit
   next to the title — this restores the top-right corner placement globally. */
.modal-header > .btn-close,
.modal-header > [data-bs-dismiss="modal"] {
  margin-left: auto;
}

[x-cloak] {
  display: none !important;
}

img,
svg {
  vertical-align: middle;
}

.canvas-bg {
  background:
    radial-gradient(circle at 20% 0%, #dbeafe 0%, transparent 42%),
    radial-gradient(circle at 80% 20%, #e0e7ff 0%, transparent 38%),
    radial-gradient(circle at 50% 82%, #d1fae5 0%, transparent 42%),
    var(--ke-canvas);
}

[data-bs-theme="dark"] .canvas-bg {
  background:
    radial-gradient(circle at 30% 20%, rgba(59, 130, 246, 0.16) 0%, transparent 48%),
    radial-gradient(circle at 70% 80%, rgba(139, 92, 246, 0.12) 0%, transparent 42%),
    radial-gradient(circle at 50% 50%, rgba(6, 182, 212, 0.08) 0%, transparent 42%),
    var(--ke-canvas);
}

.glass-surface,
.glass-card,
.modal-content,
.dropdown-menu,
.offcanvas {
  background: var(--ke-surface);
  border-color: var(--ke-border);
  box-shadow: var(--ke-shadow-sm);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

.glass-card {
  border: 1px solid var(--ke-border);
  border-radius: var(--ke-radius-card);
  box-shadow: var(--ke-shadow);
}

.dropdown-menu {
  --bs-dropdown-bg: var(--ke-dropdown-bg);
  --bs-dropdown-border-color: var(--ke-dropdown-border);
  --bs-dropdown-color: var(--ke-text);
  --bs-dropdown-link-color: var(--ke-text);
  --bs-dropdown-link-hover-bg: var(--ke-muted-surface);
  --bs-dropdown-link-hover-color: var(--ke-text);
  --bs-dropdown-link-active-bg: var(--ke-brand-600);
  --bs-dropdown-link-active-color: #fff;
  --bs-dropdown-divider-bg: var(--ke-dropdown-border);
  z-index: var(--ke-z-dropdown);
  background: var(--ke-dropdown-bg);
  border: 1px solid var(--ke-dropdown-border);
  box-shadow: var(--ke-shadow);
}

.dropdown-item {
  color: var(--ke-text);
}

.dropdown-item:hover,
.dropdown-item:focus-visible {
  background-color: var(--ke-muted-surface);
  color: var(--ke-text);
}

/* Modal panels must stay readable even where backdrop-filter does not render
   (e.g. GPU acceleration disabled on some Windows / remote-desktop setups) —
   base them on the opaque surface token, not the translucent glass token,
   so page content never bleeds through. */
.modal-content {
  background: var(--ke-dropdown-bg);
}

.modal-backdrop.show {
  opacity: 0.6;
}

/* Fallback for browsers/contexts without backdrop-filter support: make the
   frosted surfaces solid so nothing behind them shows through. */
@supports not ((backdrop-filter: blur(20px)) or (-webkit-backdrop-filter: blur(20px))) {
  .glass-surface,
  .glass-card,
  .modal-content,
  .dropdown-menu,
  .offcanvas,
  .btn-glass {
    background: var(--ke-dropdown-bg);
  }
}

/* Invite Team Members modal — deliberately oversized. The body is a two-step
   builder: a multi-column grid of role cards on top and the staged-invitation
   list below it. A much taller, wider frame lets most of the ~27 role cards and
   a useful stretch of the staged list show at once without scrolling. */
.modal-invite {
  --bs-modal-width: min(1280px, 95vw);
}
.modal-invite .modal-content {
  min-height: min(80vh, 760px);
}

/* Document preview modal — sized to sit inside the main content area, clear of
   the fixed sidebar and topbar, rather than filling the whole viewport (which
   tucked the header behind the higher-z-index chrome). */
.modal-preview {
  max-width: none;
  width: auto;
  margin: 4.5rem 1rem 1rem;
}
@media (min-width: 992px) {
  /* Clear the 5.5rem fixed desktop sidebar rail (app-main-wrap margin) + a gutter. */
  .modal-preview {
    margin-left: 6.5rem;
  }

  html.sidebar-expanded .modal-preview {
    margin-left: calc(var(--ke-sidebar-width) + 1rem);
  }
}
/* The preview modal sits beside the sidebar rather than over it, so the
   sidebar (--ke-z-sidebar, above Bootstrap's backdrop at 1050) would be the
   one piece of chrome left undimmed. While this modal is open, lift its
   backdrop above the sidebar and the modal above that backdrop. */
#document-preview-modal {
  z-index: var(--ke-z-modal-fullscreen);
}
body:has(#document-preview-modal.show) .modal-backdrop {
  z-index: var(--ke-z-modal-backdrop-lift);
}

.modal-preview .modal-content {
  height: calc(100vh - 5.5rem);
}
.modal-preview .modal-body {
  overflow: auto;
}
.doc-preview-stage {
  min-height: 0;
}
.doc-preview-frame {
  height: 100%;
}
.doc-preview-image {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.glass-muted {
  background: var(--ke-muted-surface);
}

.bg-ke-track {
  background-color: var(--ke-track) !important;
}

.bg-primary.text-primary {
  background-color: rgba(var(--bs-primary-rgb), 0.14) !important;
  color: var(--bs-link-color) !important;
}

.bg-danger.text-danger {
  background-color: rgba(var(--bs-danger-rgb), 0.14) !important;
  color: var(--bs-danger-text-emphasis) !important;
}

.border-top.border:not(.border-bottom):not(.border-start):not(.border-end),
.border-bottom.border:not(.border-top):not(.border-start):not(.border-end),
.border-start.border:not(.border-top):not(.border-bottom):not(.border-end):not(.border-4),
.border-end.border:not(.border-top):not(.border-bottom):not(.border-start):not(.border-4) {
  border-right-width: 0 !important;
  border-bottom-width: 0 !important;
  border-left-width: 0 !important;
}

.border-bottom.border:not(.border-top):not(.border-start):not(.border-end) {
  border-top-width: 0 !important;
  border-bottom-width: var(--bs-border-width) !important;
}

.border-start.border:not(.border-top):not(.border-bottom):not(.border-end):not(.border-4) {
  border-top-width: 0 !important;
  border-bottom-width: 0 !important;
  border-left-width: var(--bs-border-width) !important;
}

.border-end.border:not(.border-top):not(.border-bottom):not(.border-start):not(.border-4) {
  border-top-width: 0 !important;
  border-right-width: var(--bs-border-width) !important;
  border-bottom-width: 0 !important;
  border-left-width: 0 !important;
}

.btn-primary {
  --bs-btn-bg: var(--ke-brand-600);
  --bs-btn-border-color: var(--ke-brand-600);
  --bs-btn-hover-bg: var(--ke-brand-700);
  --bs-btn-hover-border-color: var(--ke-brand-700);
  --bs-btn-active-bg: var(--ke-brand-800);
  --bs-btn-active-border-color: var(--ke-brand-800);
}

.btn-outline-primary {
  --bs-btn-color: var(--ke-brand-700);
  --bs-btn-border-color: rgba(0, 112, 201, 0.45);
  --bs-btn-hover-bg: var(--ke-brand-600);
  --bs-btn-hover-border-color: var(--ke-brand-600);
}

[data-bs-theme="dark"] .btn-outline-primary {
  --bs-btn-color: var(--ke-brand-300);
  --bs-btn-border-color: rgba(124, 198, 253, 0.45);
}

.btn-glass {
  --bs-btn-color: var(--ke-text);
  --bs-btn-bg: var(--ke-surface);
  --bs-btn-border-color: var(--ke-border);
  --bs-btn-hover-color: var(--ke-text);
  --bs-btn-hover-bg: var(--ke-surface-hover);
  --bs-btn-hover-border-color: var(--ke-border);
  box-shadow: var(--ke-shadow-sm);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

.btn-topbar-icon {
  --bs-btn-color: var(--ke-text-muted);
  --bs-btn-bg: transparent;
  --bs-btn-border-color: transparent;
  --bs-btn-hover-color: var(--ke-text);
  --bs-btn-hover-bg: var(--ke-muted-surface);
  --bs-btn-hover-border-color: transparent;
  --bs-btn-active-color: var(--ke-text);
  --bs-btn-active-bg: var(--ke-muted-surface);
  --bs-btn-active-border-color: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border-radius: 0;
}

.btn-topbar-user {
  width: auto;
  padding-inline: 0.5rem;
}

/* A phone has to fit the burger, breadcrumb, search, notifications, theme
   toggle and user menu on one row — at full size the right group overlaps
   the breadcrumbs. The grid slims below sm (and topbar.html hides the
   AI-assistant stub there) so every remaining control keeps its slot. */
@media (max-width: 575.98px) {
  .btn-topbar-icon {
    width: 2rem;
    height: 2rem;
  }
}

/* --- Global search (topbar) ------------------------------------------ */
.global-search {
  width: min(22rem, 30vw);
}

.global-search-field {
  position: relative;
}

.global-search-icon {
  position: absolute;
  left: 0.625rem;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  color: var(--ke-text-muted);
  pointer-events: none;
}

.global-search-input {
  padding-left: 2.25rem;
}

/* The search results page's own query box: the topbar's .global-search is
   hidden below md, so the page carries a form at every width — full-width on
   a phone, capped on anything larger. */
.search-page-form {
  max-width: 28rem;
}

/* The typeahead panel is deliberately not a .dropdown-menu (that sets
   display:none, which would defeat Alpine's x-show), so the dropdown-item
   variables its rows use have to be re-declared here. */
.global-search-panel {
  --bs-dropdown-link-color: var(--ke-text);
  --bs-dropdown-link-hover-bg: var(--ke-muted-surface);
  --bs-dropdown-link-hover-color: var(--ke-text);
  position: absolute;
  top: calc(100% + 0.25rem);
  right: 0;
  width: min(28rem, 92vw);
  max-height: 26rem;
  overflow-y: auto;
  z-index: var(--ke-z-dropdown);
  /* .glass-card alone paints the translucent --ke-surface; only .dropdown-menu
     picks up the solid --ke-dropdown-bg, and this panel deliberately isn't one
     (see above). Nested inside .app-topbar's own backdrop-filter the frosting
     never resolved either, so page content read straight through the results.
     Take the solid dropdown surface and drop the filter that wasn't working. */
  background: var(--ke-dropdown-bg);
  border-color: var(--ke-dropdown-border);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* The panel opens on focus but is filled by htmx a moment later, and the
   fragment is deliberately empty below the minimum query length — an empty
   panel must paint nothing rather than an empty card. */
.global-search-panel:empty {
  display: none;
}

/* [class*="cad-"] buttons are exempt from this reset: its :not() chains carry
   ~11 classes of specificity, which no feature-level `.foo.active` rule can
   beat, so the CAD editor's button states (active tool, osnap chips, toggles)
   were silently overridden. static/cad/css/cad.css owns all cad-* button
   chrome instead. [class*="silas-map-"] is exempt for the same reason: the
   map markers are <button>s whose badge chrome (background, ring, white
   glyph) the Interactive maps section owns — the reset was flattening them
   to transparent, invisible against a basemap. */
button:not(.btn):not(.btn-close):not(.page-link):not(.dropdown-item):not(.nav-link):not(.accordion-button):not(.navbar-toggler):not([class*="cad-"]):not([class*="silas-map-"]) {
  color: inherit;
  font: inherit;
  line-height: inherit;
  cursor: pointer;
}

button:not(.btn):not(.btn-close):not(.page-link):not(.dropdown-item):not(.nav-link):not(.accordion-button):not(.navbar-toggler):not([class*="bg-"]):not(.glass-surface):not(.glass-card):not(.glass-muted):not([class*="cad-"]):not([class*="silas-map-"]) {
  background-color: transparent;
}

button:not(.btn):not(.btn-close):not(.page-link):not(.dropdown-item):not(.nav-link):not(.accordion-button):not(.navbar-toggler):not([class*="border"]):not([class*="cad-"]):not([class*="silas-map-"]) {
  border: 0;
}

button:not(.btn):not(.btn-close):not(.page-link):not(.dropdown-item):not(.nav-link):not(.accordion-button):not(.navbar-toggler):not([class*="p-"]):not([class*="px-"]):not([class*="py-"]):not([class*="cad-"]):not([class*="silas-map-"]) {
  padding: 0;
}

button:not(.btn):disabled {
  cursor: not-allowed;
  opacity: 0.58;
}

.btn-icon-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

.btn-icon-ghost:hover,
.btn-icon-ghost:focus-visible {
  background: var(--ke-muted-surface);
  color: var(--ke-text);
}

[data-bs-theme="dark"] [data-theme-visible-when="light"],
[data-bs-theme="light"] [data-theme-visible-when="dark"] {
  display: none !important;
}

/* Silas brand lockup — theme-aware two-file swap (Stage 40, D2). The class
   names follow the artwork's ink colour, not the theme it appears in: the
   dark-ink lockup (SilasLogo_dark.png) shows on the light theme, and the
   light-ink lockup (SilasLogo_light.png) shows on the dark theme. */
.brand-logo-light {
  display: none;
}

[data-bs-theme="dark"] .brand-logo-light {
  display: inline-block;
}

[data-bs-theme="dark"] .brand-logo-dark {
  display: none;
}

.form-control,
.form-select {
  background-color: color-mix(in srgb, var(--ke-surface) 82%, transparent);
  border-color: rgba(88, 102, 135, 0.24);
  color: var(--ke-text);
}

.page-body input:not([type]),
.page-body input[type="date"],
.page-body input[type="datetime-local"],
.page-body input[type="email"],
.page-body input[type="month"],
.page-body input[type="number"],
.page-body input[type="password"],
.page-body input[type="search"],
.page-body input[type="tel"],
.page-body input[type="text"],
.page-body input[type="time"],
.page-body input[type="url"],
.page-body input[type="week"],
.page-body select,
.page-body textarea,
.modal input:not([type]),
.modal input[type="date"],
.modal input[type="datetime-local"],
.modal input[type="email"],
.modal input[type="month"],
.modal input[type="number"],
.modal input[type="password"],
.modal input[type="search"],
.modal input[type="tel"],
.modal input[type="text"],
.modal input[type="time"],
.modal input[type="url"],
.modal input[type="week"],
.modal select,
.modal textarea {
  background-color: color-mix(in srgb, var(--ke-surface) 82%, transparent);
  border: 1px solid rgba(88, 102, 135, 0.24);
  border-radius: var(--ke-radius-control);
  color: var(--ke-text);
}

.page-body input:not([type]),
.page-body input[type="date"],
.page-body input[type="datetime-local"],
.page-body input[type="email"],
.page-body input[type="month"],
.page-body input[type="number"],
.page-body input[type="password"],
.page-body input[type="search"],
.page-body input[type="tel"],
.page-body input[type="text"],
.page-body input[type="time"],
.page-body input[type="url"],
.page-body input[type="week"],
.page-body select:not([multiple]),
.modal input:not([type]),
.modal input[type="date"],
.modal input[type="datetime-local"],
.modal input[type="email"],
.modal input[type="month"],
.modal input[type="number"],
.modal input[type="password"],
.modal input[type="search"],
.modal input[type="tel"],
.modal input[type="text"],
.modal input[type="time"],
.modal input[type="url"],
.modal input[type="week"],
.modal select:not([multiple]) {
  min-height: 2.35rem;
}

.page-body select[multiple],
.modal select[multiple] {
  min-height: 7rem;
}

.page-body input::placeholder,
.page-body textarea::placeholder,
.modal input::placeholder,
.modal textarea::placeholder {
  color: var(--ke-text-subtle);
}

.page-body input:focus,
.page-body select:focus,
.page-body textarea:focus,
.modal input:focus,
.modal select:focus,
.modal textarea:focus {
  border-color: var(--ke-brand-500);
  box-shadow: 0 0 0 0.25rem rgba(12, 142, 235, 0.18);
  outline: 0;
}

.page-body input:disabled,
.page-body select:disabled,
.page-body textarea:disabled,
.modal input:disabled,
.modal select:disabled,
.modal textarea:disabled {
  opacity: 0.68;
}

.page-body select option,
.modal select option {
  background: var(--ke-canvas);
  color: var(--ke-text);
}

.form-control:focus,
.form-select:focus {
  border-color: var(--ke-brand-500);
  box-shadow: 0 0 0 0.25rem rgba(12, 142, 235, 0.18);
}

.form-control::placeholder {
  color: var(--ke-text-subtle);
}

.form-check-input:checked {
  background-color: var(--ke-brand-600);
  border-color: var(--ke-brand-600);
}

/* Stage 40 squared corners: Bootstrap's compiled dist ships a literal
   `border-radius: .25em` on `.form-check-input[type="checkbox"]` that the
   zeroed --bs-border-radius* tokens don't reach, so it needs an explicit
   override. `.form-switch .form-check-input` is the one documented
   exception (D4, toggle switches keep their pill shape) — it carries equal
   selector specificity to the checkbox rule above, so it must come after it
   here to win the cascade. Radio inputs are untouched (`border-radius: 50%`
   stays a true circle). */
.form-check-input[type="checkbox"] {
  border-radius: 0;
}

.form-switch .form-check-input {
  border-radius: 2em;
}

.app-shell {
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Row below the full-width topbar: fixed sidebar rail + scrolling main area. */
.app-body {
  flex: 1 1 auto;
  min-height: 0;
}

.app-main-wrap {
  height: 100%;
  overflow: hidden;
}

@media (min-width: 992px) {
  .app-main-wrap {
    margin-left: var(--ke-sidebar-collapsed-width);
    transition: margin-left 0.2s ease;
  }

  html.sidebar-expanded .app-main-wrap {
    margin-left: var(--ke-sidebar-width);
  }
}

/* Two departures from the shared glass treatment, both for the project
   switcher menu, which opens position: fixed (see .app-sidebar-project-menu):

   - No overflow-x: hidden — it would clip the open menu back into the rail.
     Each child block clips its own horizontal spill instead: the nav carries
     overflow-x-hidden and the footer overflow-hidden in the template.
   - No backdrop-filter on the aside itself — engines that follow the spec
     make a backdrop-filtered element the containing block for fixed
     descendants, which re-bases Popper's viewport coordinates and drops the
     menu a topbar-height too low. The ::before pseudo-element below blurs
     the same pixels without making the aside a containing block. */
.app-sidebar {
  width: var(--ke-sidebar-width);
  background: var(--ke-sidebar-bg, rgba(255, 255, 255, 0.96)) !important;
  border-right: 1px solid var(--ke-chrome-border) !important;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.app-sidebar::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

.app-sidebar .nav-pills .nav-link {
  border-radius: 0;
}

.app-sidebar .nav-pills .nav-link.active {
  background-color: rgba(var(--bs-primary-rgb), 0.12);
  color: var(--ke-brand-700);
}

[data-bs-theme="dark"] .app-sidebar .nav-pills .nav-link.active {
  background-color: rgba(12, 142, 235, 0.16);
  color: var(--ke-brand-300);
}

/* The sidebar nav scrolls when there are more modules than fit; the scrollbar
   is hidden in favour of the chevron affordance in the sticky footer. */
.app-sidebar-nav {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.app-sidebar-nav::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* Boundary between the portfolio-level entries and the project-scoped
   sections beneath the project selector — a double rule (two crisp 1px
   lines), deliberately heavier than the single-line dividers between
   ordinary sections. */
.app-sidebar-section-break {
  height: 0.25rem;
  border-top: 1px solid var(--bs-border-color);
  border-bottom: 1px solid var(--bs-border-color);
}

/* Project switcher menu: opened with Popper's fixed strategy (see
   sidebar.html) so it escapes the nav's scroll-container clipping and keeps
   its full width over the page even when the rail is collapsed. The width is
   an absolute floor, never a percentage: under the fixed strategy 100% would
   resolve against the containing block, not the switcher row. Long project
   names wrap onto further lines (the BUTTON label truncates; the open menu
   is where the full name is always readable). */
.app-sidebar-project-menu {
  min-width: calc(var(--ke-sidebar-width) - 1rem);
  max-width: min(20rem, 90vw);
}

.app-sidebar-project-menu .dropdown-item {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Absolutely positioned so toggling it never changes the nav's own height —
   an in-flow hint would resize the scroll area and could oscillate at the
   point where the list only just overflows. It sits inside the sticky
   section's top padding. */
.app-sidebar-scroll-hint {
  position: absolute;
  top: 0.0625rem;
  left: 50%;
  transform: translate(-50%, 0);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 0.875rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--bs-secondary-color);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s ease;
}

.app-sidebar-scroll-hint:hover,
.app-sidebar-scroll-hint:focus-visible {
  color: var(--bs-body-color);
}

.app-sidebar-scroll-hint.is-visible {
  opacity: 1;
  visibility: visible;
  animation: ke-sidebar-scroll-hint 1.8s ease-in-out infinite;
}

@keyframes ke-sidebar-scroll-hint {
  0%,
  100% {
    transform: translate(-50%, 0);
  }
  50% {
    transform: translate(-50%, 0.125rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .app-sidebar-scroll-hint.is-visible {
    animation: none;
  }
}

@media (min-width: 992px) {
  .app-sidebar {
    position: fixed;
    inset: var(--ke-topbar-height) auto 0 0;
    z-index: var(--ke-z-sidebar);
    width: var(--ke-sidebar-collapsed-width);
    transform: none !important;
    visibility: visible !important;
    transition: width 0.2s ease;
  }

  html.sidebar-expanded .app-sidebar {
    width: var(--ke-sidebar-width);
  }

  .app-sidebar [data-sidebar-nav-link] {
    width: 100%;
    flex-direction: column;
    gap: 0.125rem !important;
    justify-content: center;
    padding: 0.5rem 0.25rem;
  }

  html.sidebar-expanded .app-sidebar [data-sidebar-nav-link] {
    flex-direction: row;
    gap: 1rem !important;
    justify-content: flex-start;
    padding: 0.5rem 1rem;
  }

  .app-sidebar [data-sidebar-label] {
    font-size: 0.6875rem;
    line-height: 1.2;
    text-align: center;
    white-space: normal;
    flex-grow: 0 !important;
  }

  /* Module labels are short, but the project switcher carries an unbounded
     project name: on the collapsed rail, clamp it rather than letting it
     stack the rail row tall. (Expanded, .text-truncate ellipsizes it.) */
  html:not(.sidebar-expanded) .app-sidebar #sidebar-project-switcher [data-sidebar-label] {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
    overflow-wrap: anywhere;
  }

  html.sidebar-expanded .app-sidebar [data-sidebar-label] {
    font-size: inherit;
    text-align: start;
    white-space: nowrap;
    flex-grow: 1 !important;
  }

  .app-sidebar [data-sidebar-expanded] {
    opacity: 0;
    pointer-events: none;
    max-width: 0;
    overflow: hidden;
    white-space: nowrap;
    transition: opacity 0.12s ease;
  }

  html.sidebar-expanded .app-sidebar [data-sidebar-expanded] {
    opacity: 1;
    pointer-events: auto;
    max-width: 12rem;
  }

  /* Nav count badge (components/nav_badge.html): inline pill after the label
     when expanded; floated over the icon's top-right corner when the rail is
     collapsed, mirroring the topbar bell badge. */
  .app-sidebar [data-sidebar-nav-link] {
    position: relative;
  }

  .app-sidebar .app-nav-badge {
    position: absolute;
    top: 0.125rem;
    left: 50%;
    margin-left: 0.375rem;
  }

  html.sidebar-expanded .app-sidebar .app-nav-badge {
    position: static;
    margin-left: 0;
  }

  /* First nav row carries the rail toggle: beside the link when expanded,
     stacked above it when collapsed (DOM order is link-then-button, so the
     collapsed state uses column-reverse). */
  .app-sidebar-lead {
    display: flex;
    flex-direction: column-reverse;
    align-items: center;
    gap: 0.25rem;
    /* The nav <ul> is a wrapping column flex container, so its single line
       stretches every <li> to the widest one's content. Pinning this row to the
       list width stops the added toggle widening all of them past the
       sidebar's overflow-x: hidden edge. */
    width: 100%;
    min-width: 0;
  }

  .app-sidebar-lead > [data-sidebar-nav-link] {
    width: 100%;
  }

  html.sidebar-expanded .app-sidebar-lead {
    flex-direction: row;
  }

  html.sidebar-expanded .app-sidebar-lead > [data-sidebar-nav-link] {
    flex: 1 1 auto;
    min-width: 0;
    width: auto;
  }
}

.app-topbar {
  position: relative;
  z-index: var(--ke-z-topbar);
  min-height: var(--ke-topbar-height);
  border-bottom: 1px solid var(--ke-chrome-border);
}

@media (min-width: 992px) {
  /* Locked to the token the fixed sidebar offsets itself by, so the rail
     always starts exactly under the topbar. */
  .app-topbar {
    height: var(--ke-topbar-height);
  }
}

.app-topbar-brand {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  height: 100%;
  padding-inline: 1rem;
  border-right: 1px solid var(--ke-chrome-border);
}

.app-topbar-brand img {
  max-height: 3.5rem;
  /* Below lg there is no sidebar to match and the topbar still has to fit the
     offcanvas hamburger, search and user controls, so the lockup runs small. */
  max-width: 4.5rem;
}

@media (min-width: 992px) {
  /* Fixed at the expanded sidebar width so the divider lines up exactly with
     the sidebar's right edge. */
  .app-topbar-brand {
    width: var(--ke-sidebar-width);
  }

  /* Pins the lockup to the size it rendered at in the old sidebar header,
     where the collapse toggle shared the row; the brand block is wider than
     that, so without the cap the logo would grow. */
  .app-topbar-brand img {
    max-width: 9.4rem;
  }
}

.content-container {
  width: min(100%, 90rem);
  margin-inline: auto;
}

.page-body > .p-4.p-md-4 {
  padding: 0 !important;
}

.page-body > .p-4.p-md-4.ke-max-sm,
.page-body > .p-4.p-md-4.ke-max-md,
.page-body > .p-4.p-md-4.ke-max-lg,
.page-body > .p-4.p-md-4.ke-max-xl,
.page-body > .p-4.p-md-4.ke-max-form {
  padding: 0 !important;
}

.page-body a.d-block {
  color: inherit;
  text-decoration: none;
}

.page-body a:not(.text-decoration-underline):not([class*="link-underline"]),
.modal a:not(.text-decoration-underline):not([class*="link-underline"]) {
  text-decoration: none;
}

.page-body a:not(.text-decoration-underline):not([class*="link-underline"]):hover,
.page-body a:not(.text-decoration-underline):not([class*="link-underline"]):focus-visible,
.modal a:not(.text-decoration-underline):not([class*="link-underline"]):hover,
.modal a:not(.text-decoration-underline):not([class*="link-underline"]):focus-visible {
  text-decoration: none;
}

.page-body a.glass-surface,
.page-body a.glass-card,
.modal a.glass-surface,
.modal a.glass-card {
  color: inherit;
  text-decoration: none;
}

.page-body a.glass-surface:hover,
.page-body a.glass-surface:focus-visible,
.page-body a.glass-card:hover,
.page-body a.glass-card:focus-visible,
.modal a.glass-surface:hover,
.modal a.glass-surface:focus-visible,
.modal a.glass-card:hover,
.modal a.glass-card:focus-visible {
  color: inherit;
  text-decoration: none;
}

.page-body a.d-inline-flex[class*="rounded-"].border,
.page-body a.d-flex[class*="rounded-"].border,
.modal a.d-inline-flex[class*="rounded-"].border,
.modal a.d-flex[class*="rounded-"].border {
  text-decoration: none;
}

.page-body a.d-inline-flex[class*="rounded-"].border:hover,
.page-body a.d-inline-flex[class*="rounded-"].border:focus-visible,
.page-body a.d-flex[class*="rounded-"].border:hover,
.page-body a.d-flex[class*="rounded-"].border:focus-visible,
.modal a.d-inline-flex[class*="rounded-"].border:hover,
.modal a.d-inline-flex[class*="rounded-"].border:focus-visible,
.modal a.d-flex[class*="rounded-"].border:hover,
.modal a.d-flex[class*="rounded-"].border:focus-visible {
  text-decoration: none;
}

/* Scrollable underline tabs — hides the scrollbar and keeps the underline on the
   container border like the old ke-tabs component. */
.app-tabs {
  --bs-nav-underline-link-active-color: var(--bs-primary);
  scrollbar-width: none;
  margin-bottom: -1px;
}
.app-tabs::-webkit-scrollbar { display: none; }
.app-tabs .nav-link { color: var(--bs-secondary-color); }
.app-tabs .nav-link:hover, .app-tabs .nav-link:focus-visible { color: var(--ke-brand-500); }

/* Settings sub-navigation — the vertical list of pages beside the settings
   content, under the section tabs above (templates/settings/base.html). The
   shell's flex-column stacks it full width above the content on small
   viewports; from lg up it becomes a fixed column beside it. */
.settings-subnav .nav-link {
  padding: 0.375rem 0.75rem;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--ke-text-muted);
  border-radius: calc(var(--ke-radius-control) - 4px);
}
.settings-subnav .nav-link:hover,
.settings-subnav .nav-link:focus-visible {
  background: var(--ke-track);
  color: var(--bs-primary);
}
.settings-subnav .nav-link.active {
  background: rgba(var(--bs-primary-rgb), 0.1);
  color: var(--bs-primary);
  font-weight: 600;
}
@media (min-width: 992px) {
  .settings-subnav {
    flex-basis: 200px;
  }
}

/* ---------------------------------------------------------------------------
   Icon rail — compact vertical section navigation for detail pages. Collapsed
   to a 46px strip of icon buttons with CSS tooltips; pin-to-expand reveals the
   labels (state persisted in localStorage by initIconRails in app.js). All
   colours flow from design tokens so light/dark themes come free.
   --------------------------------------------------------------------------- */
.app-rail-layout {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
}
.app-rail-panels {
  flex: 1 1 auto;
  min-width: 0;
}
.app-rail {
  flex: 0 0 auto;
  width: 46px;
  /* Sit in the page's left padding gutter, closer to the primary sidebar,
     without shifting the panels out of alignment with the page header. */
  margin-left: -1.5rem;
  position: sticky;
  top: 1rem;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 4px;
  background: var(--ke-surface);
  border: 1px solid var(--ke-border);
  border-radius: var(--ke-radius-control);
  box-shadow: var(--ke-shadow-sm);
  overflow: visible;
  transition: width 0.18s ease;
  z-index: 2;
}
.app-rail.is-expanded {
  width: 188px;
}
/* Rails nested inside a card keep a breathing gap from the card border. */
.glass-surface .app-rail {
  margin-left: -0.75rem;
}
.app-rail-top {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.app-rail-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 6px;
  border: 0;
  background: transparent;
  color: var(--ke-text-muted);
  border-radius: calc(var(--ke-radius-control) - 4px);
  cursor: pointer;
}
.app-rail-btn:hover,
.app-rail-btn:focus-visible {
  background: var(--ke-track);
  color: var(--bs-primary);
}
.app-rail.is-expanded [data-rail-expand] .ke-icon {
  transform: scaleX(-1);
}
.app-rail-sep {
  height: 1px;
  background: var(--ke-border);
  margin: 3px 4px;
}
.app-rail-items {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.app-rail-item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.625rem;
  min-height: 36px;
  padding: 6px;
  border: 0;
  background: transparent;
  color: var(--ke-text-muted);
  text-align: left;
  text-decoration: none;
  border-radius: calc(var(--ke-radius-control) - 4px);
  cursor: pointer;
  white-space: nowrap;
}
.app-rail-item:hover,
.app-rail-item:focus-visible {
  background: var(--ke-track);
  color: var(--bs-primary);
}
.app-rail-item.active {
  background: rgba(var(--bs-primary-rgb), 0.1);
  color: var(--bs-primary);
  font-weight: 600;
}
.app-rail.is-expanded .app-rail-item {
  justify-content: flex-start;
  padding: 6px 10px;
}
.app-rail-label,
.app-rail-count {
  display: none;
}
.app-rail.is-expanded .app-rail-label {
  display: inline;
  overflow: hidden;
  text-overflow: ellipsis;
}
.app-rail.is-expanded .app-rail-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  margin-left: auto;
  padding: 0 0.375rem;
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1.25rem;
  color: var(--bs-primary);
  background: rgba(var(--bs-primary-rgb), 0.12);
  border-radius: 0;
}
/* Collapsed tooltip — the dark (light-theme) pill to the right of the icon.
   Only shown while the rail is collapsed; the visible label replaces it once
   expanded. */
.app-rail-tip {
  position: absolute;
  left: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%);
  z-index: var(--ke-z-fixed);
  padding: 0.25rem 0.5rem;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--ke-tooltip-fg);
  background: var(--ke-tooltip-bg);
  border-radius: 0;
  box-shadow: var(--ke-shadow-sm);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.app-rail:not(.is-expanded) .app-rail-item:hover .app-rail-tip,
.app-rail:not(.is-expanded) .app-rail-item:focus-visible .app-rail-tip {
  opacity: 1;
}
/* Type-to-jump search popover — floats to the right of the rail. */
.app-rail-search {
  position: absolute;
  left: calc(100% + 12px);
  top: 0;
  z-index: var(--ke-z-fixed);
  width: 250px;
  padding: 0.5rem;
  background: var(--ke-dropdown-bg);
  border: 1px solid var(--ke-dropdown-border);
  border-radius: var(--ke-radius-control);
  box-shadow: var(--ke-shadow);
}
.app-rail-search[hidden] {
  display: none;
}
.app-rail-search-results {
  max-height: 280px;
  overflow-y: auto;
  margin-top: 0.5rem;
}
.app-rail-search-results .app-rail-item {
  justify-content: flex-start;
  padding: 6px 10px;
  color: var(--ke-text);
}
.app-rail-search-results .app-rail-label {
  display: inline;
}
@media (max-width: 767.98px) {
  .app-rail {
    width: 46px !important;
  }
  .app-rail .app-rail-label,
  .app-rail .app-rail-count {
    display: none !important;
  }
}
@media (prefers-reduced-motion: reduce) {
  .app-rail {
    transition: none;
  }
}

/* Per-day timesheet tabs. The active panel is selected by CSS from the
   persistent [data-active-tab] wrapper rather than Alpine x-show: the panels
   live inside #ts-week, which htmx re-renders wholesale on add/delete, and
   x-show does not reliably re-apply to swapped-in nodes. The wrapper itself is
   never swapped, so its attribute is always current and the right panel shows
   immediately on every render. Panels 0–6 are days; 7 is Attachments. */
.ts-day-tabs .ts-day-panel {
  display: none;
}
.ts-day-tabs[data-active-tab="0"] .ts-day-panel[data-tab-panel="0"],
.ts-day-tabs[data-active-tab="1"] .ts-day-panel[data-tab-panel="1"],
.ts-day-tabs[data-active-tab="2"] .ts-day-panel[data-tab-panel="2"],
.ts-day-tabs[data-active-tab="3"] .ts-day-panel[data-tab-panel="3"],
.ts-day-tabs[data-active-tab="4"] .ts-day-panel[data-tab-panel="4"],
.ts-day-tabs[data-active-tab="5"] .ts-day-panel[data-tab-panel="5"],
.ts-day-tabs[data-active-tab="6"] .ts-day-panel[data-tab-panel="6"],
.ts-day-tabs[data-active-tab="7"] .ts-day-panel[data-tab-panel="7"] {
  display: block;
}

.ke-action-card {
  min-height: 5.875rem;
  padding: 1.25rem;
  color: inherit;
  text-decoration: none;
  transition: background-color 0.15s ease, transform 0.15s ease;
}

.ke-action-card:hover {
  color: inherit;
  background: var(--ke-surface-hover);
  transform: translateY(-1px);
}

/* Selectable role card in the invite modal's role grid. The whole card is the
   label for its checkbox, so clicking anywhere toggles it; the selected state
   is driven by an Alpine :class binding rather than :has(). */
.ke-role-card {
  height: 100%;
  background: var(--ke-surface);
  border: 1px solid var(--ke-border);
  border-radius: var(--ke-radius-card);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.ke-role-card:hover {
  background: var(--ke-surface-hover);
}

.ke-role-card.is-selected {
  background: var(--ke-surface-hover);
  border-color: var(--ke-brand-600);
}

.ke-role-card:focus-within {
  outline: 2px solid var(--ke-brand-600);
  outline-offset: 2px;
}

/* One staged invitation awaiting send. `.has-error` marks a row the server
   rejected on the last submit. */
.ke-invite-row {
  background: var(--ke-surface);
  border: 1px solid var(--ke-border);
  border-radius: var(--ke-radius-card);
}

.ke-invite-row.has-error {
  border-color: var(--bs-danger);
}

.ke-stat-card {
  min-height: 5.875rem;
  padding: 1.25rem;
}

.ke-section-card {
  padding: 1.25rem;
}

.ke-timesheet-row {
  padding: 0.75rem;
}

/* Bordered list-row link — hover treatment for clickable dashboard rows. */
.ke-action-row {
  transition: background-color 0.15s ease;
}

.ke-action-row:hover {
  background: var(--ke-surface-hover);
}

/* Dashboard module-card lists — rows share one hairline rule between them
   instead of each sitting in its own bordered box, so a card reads as a list
   rather than a stack of nested panels. */
.ke-dashboard-list > * + * {
  border-top: 1px solid var(--bs-border-color);
}

.ke-dashboard-row {
  padding-block: 0.375rem;
}

/* Clickable rows keep a little inline padding so the hover fill is not flush
   against the text; the negative margin keeps the text aligned with the
   section heading above. */
.ke-dashboard-row.ke-action-row {
  padding-inline: 0.5rem;
  margin-inline: -0.5rem;
}

/* Stacked tables — on phones (<md) each row becomes a stacked card: the
   thead is hidden and every cell renders as a "LABEL value" line, the label
   coming from the cell's data-label attribute (cells without one span the
   full line). Cells holding a select or text/number input switch to
   label-above so the control gets the full row width. Works on DataTables-
   enhanced tables, which keep the original <table> markup, so table search
   and pagination keep working. */
@media (max-width: 767.98px) {
  .ke-table-stack thead { display: none; }
  .ke-table-stack,
  .ke-table-stack tbody { display: block; width: 100%; }
  .ke-table-stack tbody tr {
    display: block;
    padding: 0.75rem 0.25rem;
  }
  .ke-table-stack tbody tr + tr { border-top: 1px solid var(--bs-border-color); }
  .ke-table-stack tbody td {
    display: block;
    padding: 0.25rem 0 !important;
    border: 0 !important;
    white-space: normal !important;
    text-align: start !important;
  }
  .ke-table-stack tbody td[data-label] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
  }
  .ke-table-stack tbody td[data-label]::before {
    content: attr(data-label);
    flex-shrink: 0;
    font-size: 0.75rem;
    font-weight: 500;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
  }
  .ke-table-stack tbody td[data-label]:has(.form-select, input.form-control) {
    display: block;
  }
  .ke-table-stack tbody td[data-label]:has(.form-select, input.form-control)::before {
    display: block;
    margin-bottom: 0.25rem;
  }
}

/* On phones the truncated KPI sublabels and action-row titles lose too much
   to the ellipsis — let them wrap instead (utilities need !important to win
   over Bootstrap's .text-truncate). */
@media (max-width: 575.98px) {
  .ke-kpi-card .text-truncate,
  .ke-action-row .text-truncate {
    white-space: normal !important;
    overflow: visible !important;
    text-overflow: clip !important;
  }
}

.operational-settings-layout {
  display: grid;
  gap: 1.25rem;
  align-items: start;
}

.operational-settings-nav {
  min-width: 0;
}

.operational-settings-nav .nav {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.operational-settings-nav .nav > hr,
.operational-settings-nav .nav > .small {
  grid-column: 1 / -1;
}

.operational-settings-nav .nav > hr {
  width: 100%;
  margin-block: 0.35rem;
}

.operational-settings-nav .nav-link {
  position: relative;
  min-width: 0;
  border-radius: 0;
  padding: 0.5rem 0.5rem;
  font-size: 0.8125rem;
}

.operational-settings-nav .nav-link.active {
  background: var(--ke-muted-surface);
  box-shadow: inset 2px 0 0 var(--ke-brand-500);
  color: var(--ke-brand-500);
  font-weight: 700;
}

.operational-settings-nav .nav-link.active:hover,
.operational-settings-nav .nav-link.active:focus-visible {
  color: var(--ke-brand-400);
}

.operational-settings-content {
  min-width: 0;
}

/* Icon rails nested in a settings child (Skills catalogue) pull left with a
   negative margin sized for a full page's p-4 gutter; inside the settings
   grid that would overlap the vertical nav column. */
.operational-settings-content .app-rail {
  margin-left: 0;
}

.operational-form-card {
  padding: 1rem;
}

.operational-settings-form .mb-3 {
  margin-bottom: 0 !important;
}

.settings-edit-table {
  min-width: 44rem;
  border-collapse: separate;
  border-spacing: 0;
}

.settings-edit-table thead th {
  background: var(--ke-muted-surface);
}

.settings-edit-table th,
.settings-edit-table td {
  padding: 0.55rem 0.75rem;
  border-bottom: 1px solid rgba(88, 102, 135, 0.14);
}

[data-bs-theme="dark"] .settings-edit-table th,
[data-bs-theme="dark"] .settings-edit-table td {
  border-bottom-color: rgba(255, 255, 255, 0.06);
}

.settings-edit-table .form-control,
.settings-edit-table .form-select {
  min-height: 2rem;
  border-radius: 0;
  font-size: 0.8125rem;
  padding: 0.3rem 0.55rem;
}

.settings-edit-table .form-check-input {
  margin-top: 0.15rem;
}

.settings-edit-table .form-control-color {
  width: 2.25rem;
  min-height: 2rem;
  padding: 0.2rem;
}

.settings-delete-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--ke-z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background-color: rgba(0, 0, 0, 0.58);
}

.settings-delete-dialog {
  width: min(100%, 28rem);
  padding: 1.5rem;
}

@media (min-width: 768px) {
  .operational-settings-layout {
    grid-template-columns: minmax(10rem, 11rem) minmax(0, 1fr);
  }

  .operational-settings-nav {
    position: sticky;
    top: 1rem;
  }
}

/* Split pane — job overview Deliverables + resizable/collapsible Milestones.
   Width is driven by --ke-split-side-w (set inline by app.js from localStorage);
   collapse hides the panel body at every breakpoint. At lg+ the collapsed side
   column overlays the row's top-right corner (level with the Deliverables
   heading) so the main column spans the full row width; .ke-split-main-title
   reserves the overlay's footprint so a title-row action button can't sit
   underneath it. Below lg the columns stack (col-12) and the drag handle stays
   hidden. */
.ke-split-handle-col {
  display: none;
}

.ke-split-collapsed [data-split-body] {
  display: none;
}

.ke-split-chevron {
  transition: transform 0.15s ease;
}

.ke-split-collapsed .ke-split-chevron {
  transform: rotate(180deg);
}

@media (min-width: 992px) {
  .ke-split {
    position: relative;
  }

  .ke-split > .ke-split-main {
    flex: 1 1 0;
    width: auto;
    max-width: none;
    min-width: 0;
  }

  .ke-split > .ke-split-side {
    flex: 0 0 var(--ke-split-side-w, 33.3333%);
    width: var(--ke-split-side-w, 33.3333%);
    max-width: none;
    min-width: 0;
  }

  .ke-split.ke-split-collapsed > .ke-split-side {
    position: absolute;
    top: 0;
    right: 0;
    flex: none;
    width: auto;
  }

  .ke-split.ke-split-collapsed > .ke-split-main .ke-split-main-title {
    padding-right: 11rem;
  }

  .ke-split:not(.ke-split-collapsed) > .ke-split-handle-col {
    display: flex;
    flex: 0 0 auto;
    width: 1rem;
    padding: 0;
    align-items: stretch;
    justify-content: center;
  }

  .ke-split-handle {
    display: flex;
    width: 100%;
    align-items: stretch;
    justify-content: center;
    cursor: col-resize;
    touch-action: none;
  }

  .ke-split-handle::before {
    content: "";
    width: 3px;
    border-radius: 0;
    background: var(--bs-border-color);
    transition: background-color 0.15s ease;
  }

  .ke-split-handle:hover::before,
  .ke-split-handle:focus-visible::before,
  .ke-split-dragging .ke-split-handle::before {
    background: var(--bs-primary);
  }

  .ke-split-dragging,
  .ke-split-dragging * {
    user-select: none;
  }
}

.doc-row {
  transition: background-color 0.12s ease-in-out;
}

@keyframes doc-row-flash {
  0% {
    background-color: var(--bs-primary-bg-subtle);
  }
  100% {
    background-color: transparent;
  }
}

.doc-row-highlight {
  animation: doc-row-flash 2.5s ease-out 1;
}

/* Documents drop zone — dashed drop target at the top of a Documents tab.
   Uses Bootstrap CSS variables so it adapts to both light and dark themes. */
.document-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 1.5rem 1rem;
  text-align: center;
  cursor: pointer;
  border: 2px dashed var(--bs-border-color);
  border-radius: var(--bs-border-radius-xl, 1rem);
  background: var(--bs-tertiary-bg);
  color: var(--bs-body-color);
  transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

.document-dropzone:hover,
.document-dropzone:focus-visible {
  border-color: var(--bs-primary);
  outline: none;
}

.document-dropzone--active {
  border-color: var(--bs-primary);
  background: rgba(var(--bs-primary-rgb), 0.08);
}

/* Compact file-drop input — shared component templates/components/file_drop_input.html.
   The real file input is stretched invisibly over the face so click, focus and
   validation stay native; [data-file-drop] is the JS behaviour hook. */
.file-drop {
  position: relative;
  border: 2px dashed var(--bs-border-color);
  border-radius: var(--bs-border-radius-lg, 0.75rem);
  background: var(--bs-tertiary-bg);
  transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}
.file-drop:hover,
.file-drop:focus-within {
  border-color: var(--bs-primary);
}
.file-drop--invalid {
  border-color: var(--bs-form-invalid-border-color, var(--bs-danger));
}
.file-drop--has-files {
  border-style: solid;
}
.file-drop:has(.file-drop-input:disabled) {
  opacity: 0.6;
}
.file-drop-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
.file-drop-input:disabled { cursor: not-allowed; }
.file-drop-face {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.5rem 0.75rem;
  min-height: 2.75rem;
}
.file-drop-clear {
  position: relative;
  z-index: 2;
  margin-left: auto;
  flex-shrink: 0;
}
/* Drag-over highlight — standalone (not scoped under .file-drop) so bespoke
   zones opted into [data-file-drop] (e.g. the org logo uploader) get it too. */
.file-drop--active {
  border-color: var(--bs-primary) !important;
  background: rgba(var(--bs-primary-rgb), 0.08);
}

.line-item-inline-row .form-control,
.line-item-inline-row .form-select {
  min-height: 2.25rem;
  border-color: rgba(88, 102, 135, 0.28);
  border-radius: 0;
  background-color: color-mix(in srgb, var(--ke-surface) 86%, transparent);
  color: var(--ke-text);
  font-size: 0.8125rem;
  padding: 0.375rem 0.625rem;
}

.line-item-inline-row .form-control[type="number"] {
  text-align: right;
}

.line-item-inline-row .line-item-toggle-label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.line-item-inline-row .form-check-input {
  margin-top: 0;
  background-color: color-mix(in srgb, var(--ke-surface) 82%, transparent);
  border-color: rgba(148, 163, 184, 0.45);
}

.line-item-inline-row .form-check-input:checked {
  background-color: var(--ke-brand-600);
  border-color: var(--ke-brand-600);
}

.dt-container {
  color: var(--ke-text);
}

.dt-container .row {
  --bs-gutter-x: 0.5rem;
  --bs-gutter-y: 0.5rem;
}

.dt-container .dt-layout-row,
.dt-container > .row {
  margin-inline: 0;
  margin-block: 0 0.5rem;
}

.dt-container .dt-layout-row:last-child,
.dt-container > .row:last-child {
  margin-block: 0.5rem 0;
}

.dt-container .dt-length,
.dt-container .dt-search,
.dt-container .dt-info,
.dt-container .dt-paging {
  font-size: 0.75rem;
  color: var(--ke-text-muted);
}

.dt-container .dt-length label,
.dt-container .dt-search label {
  color: var(--ke-text-muted);
}

div.dt-container div.dt-layout-start > *:not(:last-child) {
  margin-right: 0.5rem;
}

div.dt-container div.dt-layout-end > *:not(:first-child) {
  margin-left: 0.5rem;
}

.dt-container .dt-input,
.dt-container .form-select,
.dt-container .form-control {
  min-height: 2rem;
  border-color: var(--bs-border-color);
  border-radius: 0;
  background-color: color-mix(in srgb, var(--ke-surface) 74%, transparent);
  color: var(--ke-text);
  font-size: 0.8125rem;
  padding-block: 0.25rem;
}

.dt-container .dt-search input {
  width: min(14rem, 100%);
}

.dt-container .dt-input:focus,
.dt-container .form-select:focus,
.dt-container .form-control:focus {
  border-color: var(--ke-brand-500);
  box-shadow: 0 0 0 0.2rem rgba(12, 142, 235, 0.16);
}

.dt-container .pagination {
  gap: 0.25rem;
  margin-bottom: 0;
}

.dt-container .page-link {
  min-width: 1.875rem;
  border-color: var(--bs-border-color);
  border-radius: 0 !important;
  background-color: transparent;
  color: var(--ke-text-muted);
  font-size: 0.75rem;
  padding: 0.25rem 0.5rem;
}

.dt-container .page-item.active .page-link {
  border-color: var(--ke-brand-600);
  background-color: var(--ke-brand-600);
  color: #fff;
}

.dt-container .page-item.disabled .page-link {
  background-color: transparent;
  color: var(--ke-text-subtle);
}

.dt-container table.dataTable {
  width: 100% !important;
  margin: 0 !important;
  border-top: 0 !important;
}

.dt-container table.dataTable.table-group-divider,
.dt-container table.dataTable > .table-group-divider {
  border-top: 0 !important;
}

.dt-container table.dataTable > thead > tr > th,
.dt-container table.dataTable > thead > tr > td {
  border-top: 0 !important;
  border-bottom: 1px solid var(--bs-border-color) !important;
  background: var(--ke-muted-surface);
  color: var(--ke-text-muted);
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1.25;
  padding: 0.65rem 1rem !important;
  text-transform: uppercase;
}

.dt-container table.table.dataTable > tbody > tr > *,
.dt-container table.table.dataTable > tfoot > tr > * {
  border-bottom-color: var(--bs-border-color);
  background-color: transparent;
  color: var(--ke-text);
  padding: 0.65rem 1rem !important;
  vertical-align: middle;
}

.dt-container table.dataTable > tbody > tr > th,
.dt-container table.dataTable > tbody > tr > td,
.dt-container table.dataTable > tfoot > tr > th,
.dt-container table.dataTable > tfoot > tr > td {
  line-height: 1.35;
}

.dt-container table.dataTable > tbody > tr:hover > * {
  background: var(--ke-muted-surface);
}

.dt-container table.dataTable thead > tr > th.dt-orderable-asc:hover,
.dt-container table.dataTable thead > tr > th.dt-orderable-desc:hover,
.dt-container table.dataTable thead > tr > td.dt-orderable-asc:hover,
.dt-container table.dataTable thead > tr > td.dt-orderable-desc:hover,
.dt-container table.dataTable thead > tr > th:focus,
.dt-container table.dataTable thead > tr > td:focus,
.dt-container table.dataTable thead > tr > th:focus-visible,
.dt-container table.dataTable thead > tr > td:focus-visible {
  outline: 0 !important;
  box-shadow: none !important;
}

.dt-container table.dataTable .dt-empty {
  padding-block: 2rem;
  color: var(--ke-text-muted);
  text-align: center;
}

/* ── DataTables controls on phones ───────────────────────────────────────
   The default DataTables Bootstrap layout lays the length menu, search, info
   and paging on justified rows (col-md-auto + m*-auto). Below md those cells
   collapse to content width and the auto margins centre them, so the "per
   page" select and the search box float awkwardly mid-screen (and the search
   input keeps its narrow 14rem cap). Stack every control to the full content
   width, left-aligned, with a full-width search field. The md breakpoint and
   up is untouched, so tablet and desktop keep the side-by-side strip. */
@media (max-width: 767.98px) {
  .dt-container .dt-layout-row {
    flex-wrap: wrap;
    row-gap: 0.5rem;
  }
  .dt-container .dt-layout-start,
  .dt-container .dt-layout-end,
  .dt-container .dt-layout-full {
    flex: 0 0 100%;
    max-width: 100%;
    width: 100%;
    margin-inline: 0 !important;
    justify-content: flex-start;
  }
  .dt-container .dt-length,
  .dt-container .dt-search {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    margin: 0;
  }
  .dt-container .dt-search input {
    flex: 1 1 auto;
    width: 100%;
  }
  /* The DataTables Bootstrap theme centres the info text and the pager below
     md (text-align:center; the pager uses justify-content:center !important).
     Match its selector specificity to pull both back to the start edge so the
     whole strip reads as one left-aligned column. */
  div.dt-container div.dt-info,
  div.dt-container div.dt-paging {
    text-align: start;
  }
  div.dt-container div.dt-paging ul.pagination {
    flex-wrap: wrap;
    justify-content: flex-start !important;
  }
}

/* Filter-strip tables (data-datatable-filters, class added by app.js) move the
   table search to the row start, where it is the page's only search box. The
   shrink-to-fit start cell squeezes it to ~10rem and clips the placeholder, so
   give it room from md up — below md the stacking rules above already take it
   to full width. */
@media (min-width: 768px) {
  .dt-container.dt-filter-strip .dt-search input {
    width: 18rem;
  }
}

.ke-icon {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}

.ke-icon-xs {
  width: 0.75rem;
  height: 0.75rem;
}

.ke-icon-sm {
  width: 0.875rem;
  height: 0.875rem;
}

.ke-icon-md {
  width: 1rem;
  height: 1rem;
}

.ke-icon-lg {
  width: 1.25rem;
  height: 1.25rem;
}

.ke-icon-xl {
  width: 1.5rem;
  height: 1.5rem;
}

.htmx-indicator {
  display: none;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  display: inline-flex;
}

.htmx-request .htmx-hide-on-request {
  display: none !important;
}

.sortable-drop-target {
  outline: 2px dashed rgba(12, 142, 235, 0.45);
  outline-offset: 0.25rem;
}

.sortable-ghost {
  opacity: 0.35;
}

.sortable-chosen {
  transform: scale(1.01);
}

.toast-container {
  z-index: var(--ke-z-toast);
}

.skip-link {
  position: absolute;
  top: 1rem;
  left: 1rem;
  z-index: var(--ke-z-skip-link);
  transform: translateY(-150%);
  transition: transform 0.15s ease;
}

.skip-link:focus {
  transform: translateY(0);
}

/* Honeypot field (apps.users.forms.HoneypotFormMixin) — deliberately NOT
   Bootstrap's .visually-hidden, which screen readers still announce and would
   give a real user a field to fill in.

   Uses display:none rather than off-screen positioning: browsers and password
   managers DO autofill off-screen inputs, and on the identifier-first password
   step the honeypot was the only fillable text input beside the password, so
   managers filled it and every real login was rejected as a bot. display:none
   is skipped by autofill. It is marginally easier for a sophisticated bot to
   detect, which is the right trade: the real throttles are the per-IP and
   per-email rate limits on login step 1 plus allauth's own login rate limits
   and lockouts, and a honeypot that locks legitimate users out is worse than a
   slightly more detectable one. */
.aux-field {
  display: none !important;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* ── Rostering board ──────────────────────────────────────────────────────
   Slot rows on shift cards, the picker rows, and the eligibility reason chips. */
/* Let day columns shrink below their content's min-content width so the
   nowrap card text (titles, contact line) truncates instead of forcing the
   column wide — otherwise a full 5–7 day week won't fit on one row. */
.roster-day-col {
  min-width: 0;
}
.roster-slot {
  background: var(--bs-tertiary-bg);
}
.roster-slot-empty {
  border: 1px dashed var(--bs-warning-border-subtle);
  background: var(--bs-warning-bg-subtle);
}
.roster-pick {
  background: var(--bs-tertiary-bg);
}
.roster-pick-blocked {
  opacity: 0.7;
}
.reason-chip {
  display: inline-block;
  padding: 0.05rem 0.4rem;
  border-radius: 0;
  font-size: 0.7rem;
  line-height: 1.2;
}
.reason-chip-warn {
  background: var(--bs-warning-bg-subtle);
  color: var(--bs-warning-text-emphasis);
}
.reason-chip-block {
  background: var(--bs-danger-bg-subtle);
  color: var(--bs-danger-text-emphasis);
}

@media print {
  .app-sidebar,
  .app-topbar,
  .btn,
  .toast-container,
  [data-print-hidden] {
    display: none !important;
  }

  /* The Gantt prints as one wide chart rather than as its scrolled window. */
  .gantt-scroll {
    overflow-x: visible;
  }

  /* With the roster left-nav + header (data-print-hidden) removed, let the
     content span the full page instead of sitting in a grid column. */
  .operational-settings-layout {
    display: block !important;
  }

  .roster-print-day,
  .roster-print-shift {
    break-inside: avoid;
  }

  .roster-print-table {
    border-collapse: collapse;
  }
  .roster-print-table th,
  .roster-print-table td {
    border: 1px solid #999;
    padding: 0.2rem 0.4rem;
  }

  body,
  .canvas-bg {
    background: #fff !important;
    color: #000 !important;
  }

  .glass-card,
  .glass-surface,
  .card {
    box-shadow: none !important;
    border-color: #ddd !important;
    background: #fff !important;
  }
}

/* ── Official Bootstrap-extension utility set ─────────────────────────────
   App-owned fixed-size helpers kept after the Tailwind→Bootstrap migration.
   Bootstrap has no fixed-rem sizing utilities, so these stay. Policy: a class
   lives here only while it has 3+ template usages — one-offs are inline
   styles. Do not add new ke-grid/Tailwind-port utilities; use native
   Bootstrap classes (row/col, gap-*, fst-italic, …) instead. */
.ke-w-1 { width: 0.25rem; }
.ke-h-1 { height: 0.25rem; }
.ke-h-2 { height: 0.5rem; }
.ke-w-3 { width: 0.75rem; }
.ke-h-3 { height: 0.75rem; }
.ke-w-4 { width: 1rem; }
.ke-h-4 { height: 1rem; }
.ke-h-5 { height: 1.25rem; }
.ke-h-6 { height: 1.5rem; }
.ke-w-8 { width: 2rem; }
.ke-h-8 { height: 2rem; }
.ke-w-20 { width: 5rem; }
.ke-w-24 { width: 6rem; }
.ke-w-48 { width: 12rem; }
.ke-field-compact { flex: 0 0 auto; width: 6.5rem; }
.ke-size-4 { width: 1rem; height: 1rem; }
.ke-size-5 { width: 1.25rem; height: 1.25rem; }
.ke-size-7 { width: 1.75rem; height: 1.75rem; }
.ke-size-8 { width: 2rem; height: 2rem; }
.ke-size-9 { width: 2.25rem; height: 2.25rem; }
.ke-size-10 { width: 2.5rem; height: 2.5rem; }
.ke-size-12 { width: 3rem; height: 3rem; }
.ke-size-16 { width: 4rem; height: 4rem; }
.ke-size-3-5 { width: 0.875rem; height: 0.875rem; }
.ke-min-h-0 { min-height: 0; }
.ke-max-h-80 { max-height: 20rem; }
.ke-max-sm { max-width: 24rem; }
.ke-max-md { max-width: 32rem; }
.ke-max-lg { max-width: 48rem; }
.ke-max-xl { max-width: 72rem; }
.ke-max-form { max-width: 42rem; }
.ke-max-none { max-width: none; }
.ke-min-w-48 { min-width: 12rem; }
.crm-dashboard-widget {
  min-height: 10.625rem;
  padding: 1rem !important;
}

.crm-dashboard-widget > .d-flex {
  margin-bottom: 0.875rem !important;
}

.crm-dashboard-table {
  margin-bottom: 0;
}

.crm-dashboard-table thead {
  display: none;
}

.crm-dashboard-table tbody tr + tr,
.crm-dashboard-list > li + li {
  border-top: 1px solid var(--bs-border-color);
}

.crm-dashboard-table td {
  padding: 0.5rem 1rem 0.5rem 0 !important;
  vertical-align: top;
}

.crm-dashboard-table td:last-child {
  padding-right: 0 !important;
  text-align: right;
}

.crm-dashboard-table td:first-child {
  width: 100%;
}

.crm-dashboard-list > li {
  padding-block: 0.5rem !important;
}

.inventory-dashboard-grid {
  display: grid;
  align-items: stretch;
  gap: 1rem !important;
}

.inventory-dashboard-side {
  display: grid;
  align-content: start;
  gap: 1rem !important;
}

/* Side cards sit 2-up between sm and lg (was ke-grid-cols-sm-2); the lg block
   below re-stacks them into the narrow side column. */
@media (min-width: 576px) {
  .inventory-dashboard-side {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.inventory-filter-actions {
  align-self: end;
}

.inventory-filter-grid .inventory-filter-actions,
.inventory-inline-filter-grid .inventory-filter-actions {
  grid-column: 1 / -1;
}

@media (min-width: 768px) {
  .inventory-inline-filter-grid {
    grid-template-columns: minmax(0, 2fr) minmax(12rem, 1fr);
  }
}

@media (min-width: 992px) {
  .inventory-dashboard-page .ke-stat-card {
    min-height: 5.25rem;
    padding: 1rem 1.25rem;
  }

  .inventory-dashboard-grid {
    grid-template-columns: minmax(0, 1.5fr) minmax(19rem, 1fr);
  }

  .inventory-dashboard-grid > .glass-surface {
    padding: 1.25rem !important;
  }

  .inventory-dashboard-grid > .glass-surface > .d-flex,
  .inventory-dashboard-grid > .glass-surface .inventory-dashboard-kpis {
    margin-bottom: 1rem !important;
  }

  .inventory-dashboard-grid > .glass-surface .glass-muted {
    padding: 0.75rem 1rem !important;
  }

  .inventory-dashboard-grid > .glass-surface .vstack a,
  .inventory-dashboard-grid > .glass-surface > .rounded-4.border {
    padding: 0.75rem 1rem !important;
  }

  .inventory-dashboard-side {
    grid-template-columns: minmax(0, 1fr);
  }

  .inventory-dashboard-side > .glass-surface {
    padding: 1.25rem !important;
  }

  .inventory-dashboard-side > .glass-surface .mt-5 {
    margin-top: 1rem !important;
  }
}

.ke-timeline-line {
  left: 1rem;
  width: 1px;
  background: var(--bs-border-color);
}
.min-w-0 { min-width: 0; }
.whitespace-pre-line { white-space: pre-line !important; }
.fs-7 { font-size: 0.75rem !important; }
.border-dashed { border-style: dashed !important; }
.ke-line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ke-cursor-grab { cursor: grab; }
.ke-cursor-not-allowed { cursor: not-allowed; }
.ke-cursor-help { cursor: help; }
/* Error-toned callout — the briefing card's "needs attention now" section. */
.ke-section-danger {
  background:
    radial-gradient(circle at 0% 0%, rgba(239, 68, 68, 0.1) 0%, transparent 60%),
    var(--ke-surface);
}

/* Chevron on an inline collapse toggle flips while its target is open. */
.ke-collapse-toggle .ke-icon {
  transition: transform 0.15s ease;
}

.ke-collapse-toggle[aria-expanded="true"] .ke-icon {
  transform: rotate(180deg);
}

/* ---------------------------------------------------------------------------
 * Ported utilities — used across templates but with no Bootstrap equivalent.
 * Defined once here so the markup that references them styles correctly,
 * rather than swapping each call site. See docs/styling-compat-audit.md.
 * ------------------------------------------------------------------------- */
/* Multi-checkbox group (CheckboxSelectMultiple) rendered by the bound-field
 * partial — a contained, scrollable list styled like a form control. */
.ke-checkbox-group {
  max-height: 14rem;
  overflow-y: auto;
  border: 1px solid rgba(88, 102, 135, 0.24);
  border-radius: var(--ke-radius-control);
  padding: 0.6rem 0.75rem;
  background-color: color-mix(in srgb, var(--ke-surface) 82%, transparent);
}
/* The position number beside each child in a sequenced checkbox group — wide
 * enough for two digits and the spinner, narrow enough to keep the label on
 * one line. See components/forms.html#sequenced-checkbox-group. */
.ke-sequence-input {
  width: 5rem;
  flex: 0 0 auto;
}
/* A form laid out as a Bootstrap grid (components/forms.html#field-grid).
 * The row's g-3 gutters do the spacing, so the bottom margin #bound-field
 * bakes into every field would double it up on each line — same technique as
 * .operational-settings-form above. Native row/col only: this is a scoped
 * component rule, not a new grid utility. */
.ke-form-grid .mb-3 {
  margin-bottom: 0 !important;
}
/* A multi-checkbox group flowed into two columns (the wizard's stage-scheme
 * picker). Bootstrap has no column-count utility and the group's own vstack
 * cannot be overridden from the call site; below md it stays a single
 * column, so it never squeezes on a phone. */
@media (min-width: 768px) {
  .ke-checkbox-columns .ke-checkbox-group {
    display: block;
    column-count: 2;
    column-gap: 1.5rem;
  }

  .ke-checkbox-columns .ke-checkbox-group .form-check {
    break-inside: avoid;
  }
}
.whitespace-pre-wrap { white-space: pre-wrap !important; }
.scroll-mt-5 { scroll-margin-top: 3rem; }
.scroll-mt-24 { scroll-margin-top: 6rem; }

/* ---------------------------------------------------------------------------
 * Rich-text editor (Tiptap) — central component behind RichTextFormField.
 * Built from frontend/src/editor; mounted by static/dist/editor.bundle.js.
 * Styled to read as a native form-control and to theme automatically via the
 * --ke- and --bs- tokens (no per-theme overrides needed). `.rich-text-content`
 * shares the typography for HTML rendered back on detail pages.
 * ------------------------------------------------------------------------- */

/* Plain textarea fallback: a normal control until the editor mounts and hides
 * it; if JS never runs, authors still get a usable textarea. */
.rich-text-source { display: block; }

.rich-text-editor {
  background-color: color-mix(in srgb, var(--ke-surface) 82%, transparent);
  border: 1px solid rgba(88, 102, 135, 0.24);
  border-radius: var(--ke-radius-control);
  overflow: hidden;
}
.rich-text-editor:focus-within {
  border-color: var(--ke-brand-500);
  box-shadow: 0 0 0 0.25rem rgba(12, 142, 235, 0.18);
}

.rich-text-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.125rem;
  padding: 0.375rem 0.5rem;
  background: var(--ke-muted-surface);
  border-bottom: 1px solid rgba(88, 102, 135, 0.24);
}

.rich-text-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--ke-text-muted);
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.rich-text-btn:hover {
  background: var(--ke-surface-hover);
  color: var(--ke-text);
}
.rich-text-btn:focus-visible {
  outline: 2px solid var(--ke-brand-500);
  outline-offset: -1px;
}
.rich-text-btn.is-active {
  background: var(--ke-brand-600);
  color: #fff;
}

.rich-text-divider {
  width: 1px;
  align-self: stretch;
  margin: 0.25rem 0.125rem;
  background: rgba(88, 102, 135, 0.24);
}

/* The Tiptap/ProseMirror editing surface. Taller by default to read like a
 * document, and user-resizable vertically (drag the bottom-right handle). */
.rich-text-editor .ProseMirror {
  min-height: 18rem;
  padding: 0.6rem 0.75rem;
  color: var(--ke-text);
  outline: none;
  overflow-wrap: anywhere;
  overflow: auto;
  resize: vertical;
}

/* Shared typography for the editing surface and rendered detail content. */
.rich-text-editor .ProseMirror,
.rich-text-content {
  line-height: 1.6;
}
.rich-text-editor .ProseMirror > :first-child,
.rich-text-content > :first-child { margin-top: 0; }
.rich-text-editor .ProseMirror > :last-child,
.rich-text-content > :last-child { margin-bottom: 0; }
.rich-text-editor .ProseMirror p,
.rich-text-content p { margin: 0 0 0.5rem; }
.rich-text-editor .ProseMirror h1,
.rich-text-content h1 { font-size: 1.5rem; font-weight: 600; margin: 0.75rem 0 0.5rem; }
.rich-text-editor .ProseMirror h2,
.rich-text-content h2 { font-size: 1.25rem; font-weight: 600; margin: 0.75rem 0 0.5rem; }
.rich-text-editor .ProseMirror h3,
.rich-text-content h3 { font-size: 1.1rem; font-weight: 600; margin: 0.75rem 0 0.5rem; }
.rich-text-editor .ProseMirror ul,
.rich-text-editor .ProseMirror ol,
.rich-text-content ul,
.rich-text-content ol { margin: 0 0 0.5rem; padding-left: 1.5rem; }
.rich-text-editor .ProseMirror a,
.rich-text-content a { color: var(--bs-link-color, var(--ke-brand-600)); text-decoration: underline; }

/* ── Module dashboards — KPI strips, segmented bars, chart cards ───────── */
/* Tighter-than-g-3 gutters for dashboard KPI strips (rows, not CSS grid). */
.ke-dashboard-kpis {
  --bs-gutter-x: 0.75rem;
  --bs-gutter-y: 0.75rem;
}

.ke-kpi-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 5.5rem;
  padding: 1rem 1.25rem;
  transition: background-color 0.15s ease, transform 0.15s ease;
}

.page-body a:hover > .ke-kpi-card,
.page-body a:focus-visible > .ke-kpi-card {
  background: var(--ke-surface-hover);
  transform: translateY(-1px);
}

/* Segmented status bar — one flex track, segments sized by flex-grow. */
.ke-segbar {
  display: flex;
  width: 100%;
  height: 0.875rem;
  border-radius: 0;
  overflow: hidden;
  background: var(--ke-track);
}

.ke-segbar > span {
  display: block;
  height: 100%;
  min-width: 0;
}

.ke-dot {
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  flex-shrink: 0;
}

.ke-chart-card {
  display: flex;
  flex-direction: column;
  padding: 1.25rem;
}

.ke-chart-card .ke-chart-body {
  position: relative;
  flex-grow: 1;
  min-height: 0;
}

/* Roster dashboard — multi-week status overview day strip */
.roster-dashboard-days {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.5rem;
}

@media (max-width: 767.98px) {
  .roster-dashboard-days {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.roster-dashboard-day {
  display: block;
  border: 1px solid var(--bs-border-color);
  border-radius: 0;
  padding: 0.5rem 0.625rem;
  color: inherit;
  min-height: 4.25rem;
}

.roster-dashboard-day:hover {
  border-color: var(--bs-primary);
}

.roster-dashboard-day--filled {
  background: var(--bs-success-bg-subtle);
  border-color: var(--bs-success-border-subtle);
}

.roster-dashboard-day--partial {
  background: var(--bs-warning-bg-subtle);
  border-color: var(--bs-warning-border-subtle);
}

.roster-dashboard-day--unfilled {
  background: var(--bs-danger-bg-subtle);
  border-color: var(--bs-danger-border-subtle);
}

.roster-dashboard-day--neutral {
  background: var(--bs-secondary-bg);
}

/* --empty keeps the default plain border */
/* ── Utilisation bars (Business Utilisation Dashboard) ────────────────────
   Pure-CSS horizontal capacity bars used by the machine and staff panels.
   The track represents 100% of capacity. Segment widths are passed inline as
   percentages of capacity (allocated + downtime + absence + idle == 100); an
   overload segment is positioned ABSOLUTELY past the right edge so it visibly
   spills beyond the 100% line. Colours are design-token driven, dark-mode
   aware via [data-bs-theme="dark"], and print-safe. No JavaScript. */
.ke-util-bar {
  position: relative;
  display: flex;
  width: 100%;
  height: 1rem;
  border-radius: 0;
  overflow: visible;
  background: var(--ke-track);
}

.ke-util-bar__track {
  display: flex;
  width: 100%;
  height: 100%;
  border-radius: 0;
  overflow: hidden;
}

.ke-util-bar__seg {
  display: block;
  height: 100%;
  min-width: 0;
}

.ke-util-bar__seg--allocated {
  background: var(--ke-brand-500);
}

/* Confirmed roster commitment — a distinct committed layer alongside
   allocation (staff only). Uses the info token so it reads apart from the
   brand-coloured allocation segment in both light and dark themes. */
.ke-util-bar__seg--rostered {
  background: var(--bs-info, #0dcaf0);
}

.ke-util-bar__seg--downtime {
  background: #9ca3af;
  background-image: repeating-linear-gradient(
    45deg,
    rgba(0, 0, 0, 0.12) 0,
    rgba(0, 0, 0, 0.12) 3px,
    transparent 3px,
    transparent 6px
  );
}

.ke-util-bar__seg--absence {
  background: #f59e0b;
  background-image: repeating-linear-gradient(
    45deg,
    rgba(255, 255, 255, 0.45) 0,
    rgba(255, 255, 255, 0.45) 3px,
    transparent 3px,
    transparent 6px
  );
}

.ke-util-bar__seg--idle {
  background: transparent;
}

/* Overload spills past the 100% line: a thin marker sitting just outside the
   track's right edge in an alarm colour. */
.ke-util-bar__overload {
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 100%;
  background: var(--bs-danger, #ef4444);
  border-radius: 0;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06);
}

/* The 100% reference line. */
.ke-util-bar__limit {
  position: absolute;
  top: -3px;
  bottom: -3px;
  right: 0;
  width: 2px;
  background: var(--ke-text-subtle);
  opacity: 0.5;
}

/* Legend swatch. */
.ke-util-swatch {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 0;
  flex-shrink: 0;
  vertical-align: middle;
}

.ke-util-swatch--allocated {
  background: var(--ke-brand-500);
}

.ke-util-swatch--rostered {
  background: var(--bs-info, #0dcaf0);
}

.ke-util-swatch--downtime {
  background: #9ca3af;
}

.ke-util-swatch--absence {
  background: #f59e0b;
}

.ke-util-swatch--idle {
  background: var(--ke-track);
  border: 1px solid var(--bs-border-color);
}

.ke-util-swatch--overload {
  background: var(--bs-danger, #ef4444);
}

.ke-util-swatch--pipeline {
  background: var(--ke-brand-300);
  opacity: 0.55;
}

/* Pipeline (unconfirmed demand) overlay segment — translucent so it reads as
   provisional next to the confirmed allocation. */
.ke-util-bar__seg--pipeline {
  background: var(--ke-brand-300);
  opacity: 0.5;
}

/* Roster grid cells — one per person per day. */
.ke-roster-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 0;
  font-size: 0.6875rem;
  font-weight: 600;
}

.ke-roster-cell--working {
  background: var(--bs-success-bg-subtle);
  color: var(--bs-success-text-emphasis);
}

.ke-roster-cell--holiday {
  background: var(--bs-primary-bg-subtle);
  color: var(--bs-primary-text-emphasis);
}

.ke-roster-cell--sick {
  background: var(--bs-danger-bg-subtle);
  color: var(--bs-danger-text-emphasis);
}

.ke-roster-cell--absent {
  background: var(--bs-warning-bg-subtle);
  color: var(--bs-warning-text-emphasis);
}

.ke-roster-cell--non_working,
.ke-roster-cell--bank_holiday {
  background: var(--ke-muted-surface);
  color: var(--ke-text-subtle);
}

/* ── Tools floating action button ────────────────────────────────────────
   Launches the context-aware Tools offcanvas. Sits below the offcanvas
   (1045) and its backdrop (1040) so it hides behind the panel when open. */
.tools-fab {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  width: 56px;
  height: 56px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Contrast ring (1px) keeps the edge visible on both light-grey and
     dark-navy canvases; brand gradient fill makes the disc pop on either. */
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: 50%;
  background: linear-gradient(150deg, var(--ke-brand-500), var(--ke-brand-600));
  color: #fff;
  box-shadow:
    0 6px 20px rgba(12, 142, 235, 0.45),
    var(--ke-shadow);
  z-index: var(--ke-z-fixed);
  transition: filter 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.tools-fab > svg { color: #fff; }
.tools-fab:hover {
  filter: brightness(1.06);
  transform: translateY(-1px);
  box-shadow:
    0 8px 24px rgba(12, 142, 235, 0.55),
    var(--ke-shadow);
}
.tools-fab:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* Slightly stronger ring in dark mode so the disc separates from the canvas. */
[data-bs-theme="dark"] .tools-fab { border-color: rgba(255, 255, 255, 0.28); }

@media print {
  /* Keep bar fills visible on paper (browsers strip backgrounds by default). */
  .ke-util-bar,
  .ke-util-bar__seg,
  .ke-util-bar__overload,
  .ke-util-swatch,
  .ke-roster-cell {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  .ke-util-bar {
    border: 1px solid #999;
  }
}

/* ── Planning backlog hierarchy ─────────────────────────────────────── */

/* Tier 1: customer band — tinted full-bleed band with brand accent. */
.ke-backlog-customer-header {
  padding: 0.375rem 0.75rem;
  box-shadow: inset 0.1875rem 0 0 var(--ke-brand-600);
}
[data-bs-theme="dark"] .ke-backlog-customer-header {
  box-shadow: inset 0.1875rem 0 0 var(--ke-brand-400);
}

/* Tier 2: job / quote row — indented one step. */
.ke-backlog-parent {
  padding: 0.3125rem 0.75rem 0.3125rem 1.5rem;
}

/* Tier 3: deliverable + its lines — indented rail group. */
.ke-backlog-group {
  margin: 0.25rem 0 0.375rem 1.5rem;
  border-left: 2px solid var(--bs-border-color);
}
.ke-backlog-deliverable {
  padding: 0.25rem 0.75rem 0.125rem;
  letter-spacing: 0.04em;
}

/* Tier 4: line rows — quietest, most compact. */
.ke-backlog-row {
  padding: 0.25rem 0.75rem;
}
.ke-backlog-row + .ke-backlog-row {
  border-top: 1px solid var(--bs-border-color);
}
.ke-backlog-row:hover {
  background: var(--ke-muted-surface);
}
.ke-backlog-hours {
  min-width: 3.5rem;
}
.ke-backlog-row .btn {
  --bs-btn-padding-y: 0.125rem;
  --bs-btn-padding-x: 0.5rem;
  --bs-btn-font-size: 0.75rem;
}

/* ── Entity reports (order/job Reports tab) ───────────────────────────── */
.report-stats {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
    gap: 0.75rem 1.5rem;
}
.report-stats .report-stat {
    min-width: 0;
}
.report-stats dt {
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--bs-secondary-color);
    margin-bottom: 0.125rem;
}
.report-stats dd {
    margin-bottom: 0;
}
.report-stats .report-stat--wide {
    grid-column: 1 / -1;
}
.report-section h3 {
    font-size: 0.9375rem;
    font-weight: 600;
    margin: 1rem 0 0.5rem;
}
.report-section .table {
    font-size: 0.875rem;
}
.report-section .table > :not(caption) > * > * {
    padding-block: 0.35rem;
}

/* ==========================================================================
   Analytics dashboards (Finance → Analytics)
   Pipeline funnel, stage board, per-customer bars and the customer ledger
   matrix. Stage colour tokens are shared across light + dark (validated for
   both surfaces); layout leans on Bootstrap utilities.
   ========================================================================== */
:root {
    --ke-stage-draft: #a98a3a;
    --ke-stage-sent: var(--ke-brand-600);
    --ke-stage-accepted: #2f9e77;
    --ke-stage-converted: #b3599d;
    --ke-stage-lost: #9aa3b2;
}

/* Stage colour fills (funnel segments, bars, swatches). */
.ke-an-seg-draft { background-color: var(--ke-stage-draft); }
.ke-an-seg-sent { background-color: var(--ke-stage-sent); }
.ke-an-seg-accepted { background-color: var(--ke-stage-accepted); }
.ke-an-seg-converted { background-color: var(--ke-stage-converted); }
.ke-an-seg-lost { background-color: var(--ke-stage-lost); }

/* Funnel band. */
.ke-an-funnel {
    display: flex;
    gap: 2px;
    height: 46px;
    border-radius: 0;
    overflow: hidden;
}
.ke-an-funnel-seg {
    min-width: 2.5rem;
    border-radius: 0;
}
/* Funnel label-row cells — same min-width as the segments so the two bands
   stay aligned even for tiny non-zero stages. */
.ke-an-funnel-cell {
    min-width: 2.5rem;
}

/* Small colour swatch dot. */
.ke-an-swatch {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 0;
    flex-shrink: 0;
}

/* Stage board columns. */
.ke-an-board-col {
    background-color: var(--ke-track);
    border-radius: 0;
    padding: 0.75rem;
}
.ke-an-board-card {
    transition: border-color 0.15s ease;
}
.ke-an-board-card:hover {
    border-color: var(--ke-stage-sent);
}

/* Horizontal stacked / value bars. */
.ke-an-bar-track {
    display: flex;
    gap: 2px;
    height: 16px;
    border-radius: 0;
    background-color: var(--ke-track);
    overflow: hidden;
}
.ke-an-bar-seg {
    height: 100%;
    border-radius: 0;
}
.ke-an-cust-name { width: 8rem; flex-shrink: 0; }
.ke-an-cust-total { width: 6rem; flex-shrink: 0; }

/* Live order-book progress bar. */
.ke-an-progress { height: 12px; }

/* Ledger matrix. Bootstrap's cell rule (.table>:not(caption)>*>*) paints every
   cell with the opaque --bs-table-bg at 0-1-1 specificity, so heat tints must
   target the td at equal specificity and the customer band/hover must ride the
   --bs-table-bg-type/-state inset-shadow channel — a tr background never shows. */
.ke-an-matrix-row { --bs-table-bg-type: var(--ke-track); }
.ke-an-matrix-row:hover { --bs-table-bg-state: color-mix(in srgb, var(--bs-emphasis-color) 11%, transparent); }
.ke-an-caret { display: inline-block; }
td.ke-an-t1 { background-color: color-mix(in srgb, var(--ke-brand-600) 10%, var(--bs-table-bg)); }
td.ke-an-t2 { background-color: color-mix(in srgb, var(--ke-brand-600) 18%, var(--bs-table-bg)); }
td.ke-an-t3 { background-color: color-mix(in srgb, var(--ke-brand-600) 26%, var(--bs-table-bg)); }
.ke-an-detail > td {
    background-color: color-mix(in srgb, #fff 60%, var(--bs-table-bg));
}
/* Dark surfaces already read clearly without the band; keep their tint mixes. */
[data-bs-theme="dark"] .ke-an-matrix-row { --bs-table-bg-type: initial; }
[data-bs-theme="dark"] td.ke-an-t1 { background-color: color-mix(in srgb, var(--ke-brand-600) 12%, transparent); }
[data-bs-theme="dark"] td.ke-an-t2 { background-color: color-mix(in srgb, var(--ke-brand-600) 20%, transparent); }
[data-bs-theme="dark"] td.ke-an-t3 { background-color: color-mix(in srgb, var(--ke-brand-600) 30%, transparent); }
[data-bs-theme="dark"] .ke-an-detail > td {
    background-color: color-mix(in srgb, var(--ke-brand-600) 8%, transparent);
}

/* Ledger sparkline. */
.ke-an-spark {
    width: 90px;
    height: 22px;
    vertical-align: middle;
}
.ke-an-spark polyline {
    stroke: var(--ke-brand-600);
    fill: none;
    stroke-width: 1.6;
}

/* ---------------------------------------------------------------------------
   Production tracker (stage 38)
   Board columns, card state stripes, the claimed tick, the table row tints,
   the ten-week timeline grid and the capacity bars. Every colour comes from an
   existing token (--ke-*) or a Bootstrap subtle/emphasis variable, so light and
   dark are both covered without a new palette.
--------------------------------------------------------------------------- */

/* The meeting modal is fullscreen, so unlike every centred modal it extends
   under the app sidebar (--ke-z-sidebar, which sits above the Bootstrap modal
   default of 1055). Lift just this modal above it. */
#modal-production-meeting {
  z-index: var(--ke-z-modal-fullscreen);
}

/* Board — a horizontally scrolling row of fixed-width stage columns. */
.production-col {
  width: 17.5rem;
}
.production-col-body {
  /* Keeps an empty column a valid SortableJS drop target. */
  min-height: 3.5rem;
}
.production-col-unstaged {
  background-color: color-mix(in srgb, var(--bs-warning-bg-subtle) 40%, var(--ke-track));
}

/* Cards carry a left stripe for the two states that need chasing. An inset
   shadow, not a border: Bootstrap's .border utility is !important and would
   win over any border-left declared here. */
.production-card-blocked {
  box-shadow: inset 3px 0 0 var(--ke-status-running-late), var(--ke-shadow-sm);
}
.production-card-overdue {
  box-shadow: inset 3px 0 0 var(--ke-status-overdue), var(--ke-shadow-sm);
}
.production-status-line {
  line-height: 1.2;
}

/* Claimed % sits on the built-% track as a tick, not a second bar. */
.production-claimed-tick {
  position: absolute;
  top: -0.15rem;
  bottom: -0.15rem;
  width: 2px;
  margin-left: -1px;
  border-radius: 0;
  background-color: var(--bs-emphasis-color);
  opacity: 0.65;
}

.production-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 999px;
}

/* Delta chips — one tone per change type. */
.production-delta {
  background-color: var(--bs-secondary-bg-subtle);
  color: var(--bs-secondary-text-emphasis);
}
.production-delta-new {
  background-color: var(--bs-primary-bg-subtle);
  color: var(--bs-primary-text-emphasis);
}
.production-delta-moved {
  background-color: var(--bs-info-bg-subtle);
  color: var(--bs-info-text-emphasis);
}
.production-delta-slipped {
  background-color: var(--bs-warning-bg-subtle);
  color: var(--bs-warning-text-emphasis);
}
.production-delta-completed {
  background-color: var(--bs-success-bg-subtle);
  color: var(--bs-success-text-emphasis);
}

/* Table view — tint the cells, never the row (an opaque --bs-table-bg on every
   cell would hide a tr background). */
/* --bs-table-bg is only defined inside a .table, and these tables are bare, so
   it needs a transparent fallback or the whole declaration is dropped. */
td.production-cell-blocked {
  background-color: color-mix(in srgb, var(--bs-warning-bg-subtle) 60%, var(--bs-table-bg, transparent));
}
td.production-cell-overdue {
  background-color: color-mix(in srgb, var(--bs-danger-bg-subtle) 55%, var(--bs-table-bg, transparent));
}
.production-mini-bar {
  display: inline-block;
  width: 4rem;
  height: 0.4rem;
  overflow: hidden;
}
.production-mini-bar > span {
  display: block;
  height: 100%;
}

/* Timeline — a --production-weeks column grid with absolutely placed bars. */
.production-timeline {
  min-width: 46rem;
}
.production-timeline-row {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) minmax(0, 1fr);
  align-items: center;
  gap: 0.75rem;
}
.production-timeline-label {
  min-width: 0;
}
.production-timeline-site {
  margin: 0.75rem 0 0.25rem;
}
.production-timeline-track {
  position: relative;
  height: 1.75rem;
}
.production-timeline-head .production-timeline-track {
  height: 1.25rem;
}
.production-timeline-weeks {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(var(--production-weeks, 10), minmax(0, 1fr));
}
.production-timeline-week {
  border-left: 1px solid var(--ke-border);
  padding-left: 0.25rem;
  overflow: hidden;
}
.production-timeline-week.is-current {
  background-color: var(--ke-track);
}
.production-timeline-today {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background-color: var(--ke-status-overdue);
  opacity: 0.7;
}
.production-timeline-bar {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  height: 1.2rem;
  min-width: 1.5rem;
  display: flex;
  align-items: center;
  border-radius: 0;
  overflow: hidden;
  text-decoration: none;
}
.production-timeline-bar.is-clipped-start {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.production-timeline-bar.is-clipped-end {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.production-timeline-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background-color: color-mix(in srgb, var(--bs-emphasis-color) 14%, transparent);
}
.production-timeline-bar-label {
  position: relative;
  padding: 0 0.4rem;
  white-space: nowrap;
}
.production-timeline-nobar {
  position: absolute;
  top: 50%;
  left: 0.25rem;
  transform: translateY(-50%);
}

/* Capacity — one bar per department week and per machine. */
.production-capacity-bar {
  display: block;
  width: 100%;
  height: 0.5rem;
  overflow: hidden;
}
.production-capacity-bar > span {
  display: block;
  height: 100%;
}

/* ---------------------------------------------------------------------------
   Project timeline (Gantt) — templates/projects/timeline.html.

   One horizontally-scrolling canvas: a sticky label column and tracks whose
   bars, diamonds and dots are placed by percentages the server computed
   (apps.projects.selectors.project_timeline_snapshot), so the chart draws in
   full with JavaScript off. app.js only scrolls the canvas to today.

   Stacking inside the canvas: the month/today grid sits at 1, bars and their
   markers at 2, and the label column at 3 so rows scroll cleanly beneath it.
   --------------------------------------------------------------------------- */
.gantt {
  overflow: hidden;
}

.gantt-scroll {
  overflow-x: auto;
}

@media (prefers-reduced-motion: no-preference) {
  .gantt-scroll {
    scroll-behavior: smooth;
  }
}

.gantt-canvas {
  position: relative;
  width: 100%;
}

/* Two overlays, both inset past the label column so their percentages share
   the tracks' own axis, and neither ever a pointer target: the month
   gridlines recede behind the bars (z 1), the today line paints over them
   (z 2, and last in the canvas so it wins the tie against the bars). */
.gantt-grid,
.gantt-today-layer {
  position: absolute;
  inset: 0 0 0 var(--gantt-label-w);
  pointer-events: none;
}

.gantt-grid {
  z-index: 1;
}

.gantt-today-layer {
  z-index: 2;
}

.gantt-gridline {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--ke-chrome-border);
  opacity: 0.6;
}

.gantt-today {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--ke-status-overdue);
}

/* A solid flag, not a wash: it sits on top of the month header's own label,
   and only full contrast keeps both readable. */
.gantt-today-pill {
  position: absolute;
  top: 0;
  left: 0;
  padding: 0.05rem 0.3rem;
  background: var(--ke-status-overdue);
  color: var(--bs-white);
  white-space: nowrap;
}

.gantt-row {
  display: grid;
  grid-template-columns: var(--gantt-label-w) minmax(0, 1fr);
  border-top: 1px solid var(--ke-chrome-border);
}

.gantt-row--head {
  border-top: 0;
}

.gantt-row--band {
  background: var(--ke-muted-surface);
}

.gantt-label {
  position: sticky;
  left: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  padding: 0 0.75rem;
  /* Opaque rather than the translucent glass token — bars scroll under it. */
  background: var(--ke-dropdown-bg);
  border-inline-end: 1px solid var(--ke-chrome-border);
}

.gantt-label-text {
  min-width: 0;
}

.gantt-track {
  position: relative;
  height: var(--gantt-row-h);
}

.gantt-row--head .gantt-track,
.gantt-row--band .gantt-track {
  height: 1.75rem;
}

.gantt-band-meta {
  display: flex;
  align-items: center;
  padding-inline: 0.5rem;
  white-space: nowrap;
}

.gantt-month {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  padding-inline: 0.5rem;
  border-inline-start: 1px solid var(--ke-chrome-border);
  white-space: nowrap;
  overflow: hidden;
}

.gantt-band-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ke-text);
  letter-spacing: 0.06em;
}

.gantt-band-toggle[aria-expanded="false"] .gantt-chevron {
  transform: rotate(-90deg);
}

@media (prefers-reduced-motion: no-preference) {
  .gantt-chevron {
    transition: transform 0.15s ease;
  }
}

/* Bars — squared like every other surface; the status fills double as the
   legend swatches, so a colour is stated once here and nowhere else. */
.gantt-bar {
  position: absolute;
  top: 50%;
  z-index: 2;
  height: 0.875rem;
  transform: translateY(-50%);
  border-radius: 0;
}

.gantt-bar--planned,
.gantt-bar--not-started {
  background: rgba(var(--bs-primary-rgb), 0.16);
}

.gantt-bar--in-progress {
  background: var(--ke-brand-600);
}

.gantt-bar--complete {
  background: var(--ke-status-on-time);
}

.gantt-bar--held {
  background: repeating-linear-gradient(
    45deg,
    color-mix(in srgb, var(--ke-status-running-late) 55%, transparent) 0 5px,
    color-mix(in srgb, var(--ke-status-running-late) 18%, transparent) 5px 10px
  );
}

/* The overrun runs from the missed target to where the work actually got to.
   It starts exactly where the planned bar ends, so it is inset by the 2px the
   surface system keeps between two fills that touch. */
.gantt-bar-overrun {
  position: absolute;
  top: 50%;
  z-index: 2;
  height: 0.875rem;
  margin-inline-start: 2px;
  transform: translateY(-50%);
}

.gantt-bar-overrun,
.gantt-key-swatch--overrun {
  background:
    repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.35) 0 4px, transparent 4px 8px),
    var(--ke-status-overdue);
}

.gantt-delay-chip {
  position: absolute;
  top: 50%;
  left: 100%;
  margin-inline-start: 0.3rem;
  transform: translateY(-50%);
  color: var(--ke-status-overdue);
  white-space: nowrap;
}

/* An overrun ending at the right edge has no room after it for the chip, so
   the chip moves inside the segment — white, because it is now on the red. */
.gantt-delay-chip--flip {
  left: auto;
  right: 0.25rem;
  margin-inline-start: 0;
  color: var(--bs-white);
}

/* Delivery markers: a hollow diamond is the plan, a solid one is what
   happened. Squares rotated 45deg, centred on their own percentage. */
.gantt-diamond {
  position: absolute;
  top: 50%;
  z-index: 2;
  width: 10px;
  height: 10px;
  background: transparent;
  border: 2px solid var(--ke-brand-600);
  transform: translate(-50%, -50%) rotate(45deg);
}

.gantt-diamond--planned {
  border-color: rgba(var(--bs-primary-rgb), 0.55);
}

.gantt-diamond--in-progress {
  border-color: var(--ke-brand-600);
}

.gantt-diamond--complete {
  border-color: var(--ke-status-on-time);
}

.gantt-diamond--held {
  border-color: var(--ke-status-running-late);
}

.gantt-diamond--actual {
  background: var(--ke-status-on-time);
  border-color: var(--ke-status-on-time);
}

.gantt-diamond--late {
  background: var(--ke-status-overdue);
  border-color: var(--ke-status-overdue);
}

.gantt-issue-dot {
  position: absolute;
  top: 50%;
  z-index: 2;
  width: 10px;
  height: 10px;
  /* True circles are the sanctioned exception to the squared radius system. */
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

.gantt-issue-dot--critical {
  background: var(--ke-status-overdue);
}

.gantt-issue-dot--warning {
  background: var(--ke-status-running-late);
}

.gantt-issue-dot--observation {
  background: var(--ke-text-subtle);
}

.gantt-chip {
  position: absolute;
  top: 50%;
  left: 0.25rem;
  padding: 0.05rem 0.4rem;
  transform: translateY(-50%);
  color: var(--ke-text-muted);
}

/* Legend keys — the same fills as the bars they name, never restated. */
.gantt-key-swatch {
  display: inline-block;
  width: 1.5rem;
  height: 0.625rem;
}

.gantt-key-diamond {
  display: inline-block;
  width: 10px;
  height: 10px;
  border: 2px solid var(--ke-brand-600);
  transform: rotate(45deg);
}

.gantt-key-today {
  display: inline-block;
  width: 2px;
  height: 0.875rem;
  background: var(--ke-status-overdue);
}

.gantt-bar:focus-visible,
.gantt-diamond:focus-visible,
.gantt-issue-dot:focus-visible {
  outline: 2px solid var(--bs-focus-ring-color);
  outline-offset: 2px;
}

/* Below md the label column takes too much of a narrow viewport; the labels
   truncate instead. */
@media (max-width: 767.98px) {
  .gantt {
    --gantt-label-w: 9.5rem;
  }
}

/* ---------------------------------------------------------------------------
   Colour picker — organisation branding (onboarding Branding step + Org
   Settings → Edit Organisation, _colour_picker.html). A compact colour
   square and an eyedropper icon form one clickable unit with the native
   <input type="color"> stretched invisibly over both; the icon sits outside
   the square so it stays visible whatever colour is chosen. The hex text
   input beside them is the field that actually submits. Sizing lives here
   rather than inline because Alpine string :style bindings replace the whole
   style attribute. Squared corners per Stage 40 — no border-radius. */
.colour-picker-control {
  cursor: pointer;
}

.colour-swatch {
  width: 3rem;
  min-height: 2.5rem;
  border: 1px solid var(--bs-border-color);
  background-color: var(--bs-body-bg);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.colour-picker-control:hover .colour-swatch {
  border-color: var(--bs-secondary-color);
}

/* The native colour input is opacity-0, so surface keyboard focus on the
   square instead. */
.colour-picker-control:focus-within .colour-swatch {
  border-color: var(--ke-brand-600);
  box-shadow: 0 0 0 0.25rem var(--bs-focus-ring-color);
}

/* No valid hex yet — checkerboard placeholder so "empty" doesn't read as
   white. Theme-aware via --bs-secondary-bg. */
.colour-swatch.is-empty {
  background-image:
    conic-gradient(var(--bs-secondary-bg) 25%, transparent 0 50%,
                   var(--bs-secondary-bg) 0 75%, transparent 0);
  background-size: 0.75rem 0.75rem;
}

.colour-picker-icon {
  line-height: 0;
}

.colour-hex-input {
  flex: 0 0 10rem;
  width: 10rem;
}

/* ---------------------------------------------------------------------------
   Evidence Time Lens — the project dashboard (projects/dashboard.html and its
   three partials) and the shared timeline the work package dashboard embeds.

   Density and layout are a literal port of the approved mockup
   (golden-thread-mockups-source/apps/02-project-golden-thread/src/styles.css):
   every px figure noted in a comment below is that file's figure, written here
   in rem against the 16px root. Its palette is NOT ported — colours are --ke-
   and Bootstrap tokens throughout, so the dark theme needs no rules of its
   own. The one exception is the dark surface behind the IFC render and the
   site map, which carries its own light level and cannot be expressed as a
   token. Squared corners per Stage 40 — no border-radius.

   A fixed lane-label column sits beside a horizontally scrolling track; every
   record card, gridline and lens is absolutely positioned by the `left`
   percentage apps.evidence.selectors computes, so the only inline styles in
   the markup are those per-instance offsets. Where the portfolio dashboard
   already carries the right rule — the page heading, the select box, the
   filter strip, the bottom-left map overlay, the segmented tab item — the
   .evidence-* selector joins that rule below instead of cloning it. */

.evidence-page { padding-bottom: 1.75rem; } /* 28px */

/* The element picker is a GET form of its own inside the shared filter strip
   (.portfolio-filterbar): the verified switch beside it is an htmx control,
   not a field of this form. */
.evidence-filter-form {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* 12px between panels is the mockup's whole vertical rhythm; the page
   heading's own padding is the gap above the first one. */
.evidence-panel { margin-top: 0.75rem; }

/* --- Timeline panel ----------------------------------------------------- */

/* The panel carries no padding of its own: the mockup's four bands — header,
   month scale, lane body, legend — run edge to edge and draw their own
   seams. */
.evidence-timeline-panel { overflow: hidden; }

/* 64px in the mockup, but a floor rather than a fixed height: on a narrow
   window the scrubber wraps under the title instead of being squeezed. */
.evidence-panel-header {
  min-height: 4rem;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.4375rem 1rem 0.3125rem 1.25rem; /* 7px 16px 5px 20px */
  border-bottom: 1px solid var(--ke-track);
}

.evidence-panel-header > .min-w-0 { flex: 1 1 14rem; }

/* Overview scrubber — 18px of years over the track they scope. */
.evidence-overview {
  flex: 1 1 18rem;
  max-width: 32rem;
  min-height: 2.75rem;
  display: grid;
  grid-template-rows: 1.125rem 1fr; /* 18px */
}

/* One column per year, however many years the record range pads out to. */
.evidence-overview-years {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}

.evidence-overview-years > span.is-active {
  color: var(--bs-primary);
  font-weight: 700;
}

.evidence-overview-track {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--ke-chrome-border);
  background: var(--ke-muted-surface);
}

.evidence-overview-dots {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-around;
}

.evidence-overview-dots i {
  width: 0.25rem; /* 4px */
  height: 0.25rem;
  border-radius: 50%;
  background: var(--ke-text-subtle);
}

/* The window is the span the track below actually draws — an indicator, not
   a handle, so it takes no pointer events. */
.evidence-overview-window {
  position: absolute;
  top: -1px;
  bottom: -1px;
  min-width: 0.75rem;
  color: var(--bs-primary);
  border: 1px solid var(--bs-primary);
  background: rgba(var(--bs-primary-rgb), 0.07);
  pointer-events: none;
}

/* --- Month scale, lanes and track --------------------------------------- */

.evidence-timeline {
  /* An 88px lane is exactly two 42px cards plus the 4px that keeps a stacked
     pair apart — see .evidence-card--upper / --lower below, which sit flush
     to the top and bottom of the lane rather than at 30/70. */
  --evidence-lane-height: 5.5rem;
  --evidence-card-height: 2.625rem;
  --evidence-tick-height: 1.875rem; /* 30px */
  /* Coupled to apps.evidence.selectors.CARD_FOOTPRINT_PCT together with
     .evidence-track's min-width — change one, change all three. */
  --evidence-card-width: 9.5rem;
}

.evidence-lane-labels {
  width: 13rem;
  border-right: 1px solid var(--ke-chrome-border);
}

.evidence-lane-label {
  height: var(--evidence-lane-height);
  border-color: var(--bs-border-color);
}

.evidence-track-scroll {
  min-width: 0;
}

.evidence-track {
  position: relative;
  min-width: 100rem;
}

/* Month scale above the lanes; also the row that carries the label column's
   "Lane" caption, which is what keeps the two columns level. */
.evidence-tick-row {
  position: relative;
  height: var(--evidence-tick-height);
}

.evidence-tick {
  position: absolute;
  bottom: 0.25rem;
  transform: translateX(-50%);
  padding-inline: 0.25rem;
  white-space: nowrap;
}

/* One hairline per month. A repeating gradient cannot draw this: months are
   unequal in length, so no single background-size lines up with the ticks. */
.evidence-gridline {
  position: absolute;
  top: var(--evidence-tick-height);
  bottom: 0;
  width: 1px;
  background: var(--ke-track);
}

.evidence-lane {
  position: relative;
  height: var(--evidence-lane-height);
  border-color: var(--bs-border-color);
}

/* The connector line the lane's records hang from — the mockup's defining
   mark. It stops short of the right edge so it runs up to the golden thread
   terminus rather than under it. */
.evidence-lane::before {
  content: "";
  position: absolute;
  left: 0;
  right: 5.5%;
  top: 50%;
  height: 1px;
  background: var(--ke-brand-600);
  opacity: 0.76;
}

/* Change and assurance is the one lane that is not provenance, so its thread
   takes the danger token — as its cards and its legend swatch already do. */
.evidence-lane--change::before {
  background: var(--bs-danger);
}

.evidence-lane-empty {
  position: absolute;
  top: 50%;
  left: 1rem;
  transform: translateY(-50%);
}

/* The mockup's node: a lane/status icon column, three lines of copy and the
   verification glyph. Fixed height, because it is what the lane height and
   the stacked sub-rows are both measured against. */
.evidence-card {
  position: absolute;
  top: 50%;
  width: var(--evidence-card-width);
  height: var(--evidence-card-height);
  transform: translate(-50%, -50%);
  display: grid;
  grid-template-columns: 1.1875rem 1fr 0.75rem; /* 19px icon, 12px status */
  align-items: center;
  gap: 0.25rem;
  padding: 0.1875rem 0.3125rem; /* 3px 5px */
  overflow: hidden;
  /* Opaque, not frosted. Where two records fall within
     apps.evidence.selectors.CARD_FOOTPRINT_PCT of each other the selector
     forces them into one sub-row and nudges the later one clear
     (FORCED_NUDGE_PCT), so cards genuinely cascade over one another — with
     the .glass-muted surface the card underneath read straight through as
     doubled title text. --ke-dropdown-bg is the platform's "sits over other
     content" surface and is opaque in both themes. */
  background: var(--ke-dropdown-bg) !important;
  border-color: var(--bs-border-color) !important;
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}

.evidence-card-icon {
  display: grid;
  place-items: center;
}

.evidence-card-copy {
  min-width: 0;
  display: grid;
}

.evidence-card-title {
  font-size: 0.6875rem; /* 11px */
  line-height: 1.05;
}

.evidence-card-meta {
  font-size: 0.625rem; /* 10px */
  line-height: 1.1;
}

.evidence-card-title,
.evidence-card-meta {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.evidence-card-status {
  display: inline-flex;
}

/* Sub-rows within a lane: the selector staggers near-coincident records so
   they don't cover each other. Because a stacked pair is horizontally close
   by construction, the two rows sit flush to the top and bottom of the lane
   rather than at 30/70 — that is the only split a 42px card clears in an
   88px lane. Lanes without collisions stay centred. */
.evidence-card--upper {
  top: 0;
  transform: translate(-50%, 0);
}

.evidence-card--lower {
  top: auto;
  bottom: 0;
  transform: translate(-50%, 0);
}

/* Hover and the milestone tint both mix into the opaque base rather than
   sitting on it as a translucent wash, so a raised card stays opaque over
   the one it cascades onto. */
.evidence-card:hover,
.evidence-card:focus-visible {
  background: color-mix(in srgb, var(--ke-brand-600) 8%, var(--ke-dropdown-bg)) !important;
  box-shadow: var(--ke-shadow-sm);
  z-index: 2;
}

/* Selected card. Outlined rather than filled so the card's own status glyphs
   keep their meaning. */
.evidence-card.is-selected {
  border-color: var(--ke-brand-600) !important;
  box-shadow: 0 0 0 1px var(--ke-brand-600);
  z-index: 1;
}

.evidence-card-change {
  border-color: rgba(var(--bs-danger-rgb), 0.5) !important;
}

.evidence-card-milestone {
  background: color-mix(in srgb, var(--bs-success) 12%, var(--ke-dropdown-bg)) !important;
  border-color: rgba(var(--bs-success-rgb), 0.5) !important;
}

/* The time lens — the selected record's moment, drawn across every lane. It
   never eats a card click, and it moves client-side (see the x-bind:style in
   projects/partials/evidence_timeline.html) so the band keeps up with a
   selection that costs no round trip. */
.evidence-lens {
  position: absolute;
  top: var(--evidence-tick-height);
  bottom: 0;
  width: 3.5rem; /* 56px */
  transform: translateX(-50%);
  border-left: 2px solid var(--bs-primary);
  border-right: 1px solid rgba(var(--bs-primary-rgb), 0.22);
  background: rgba(var(--bs-primary-rgb), 0.09);
  pointer-events: none;
  transition: left 0.25s ease;
}

/* Sits in the month scale above the band. The label is on a solid brand
   fill, not on the page surface, so its text colour is theme-independent by
   construction. */
.evidence-lens-label {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -100%);
  padding: 0.125rem 0.375rem;
  white-space: nowrap;
  color: #fff;
  background: var(--bs-primary);
}

/* Golden thread terminus — the end of the chain, pinned to the right of the
   site lane because that is where the physical work finishes. */
.evidence-terminus {
  position: absolute;
  top: 50%;
  right: 1%;
  z-index: 5;
  width: 8.5rem; /* 136px */
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  padding: 0.5rem;
}

.evidence-terminus--complete {
  border-color: var(--bs-success) !important;
}

/* --- Legend ------------------------------------------------------------- */

/* A centred strip along the foot of the panel, as in the mockup, rather than
   a row hanging under it. */
.evidence-legend {
  min-height: 1.75rem; /* 25px */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1.25rem;
  padding: 0.25rem 0.75rem;
  border-top: 1px solid var(--ke-track);
}

/* Legend swatches — the same three link types the chain of custody lists,
   plus the selection outline. */
.evidence-legend-swatch {
  display: inline-block;
  width: 1.5rem;
  height: 0;
  border-top: 2px solid var(--bs-secondary-color);
}

.evidence-legend-provenance {
  border-top-color: var(--ke-brand-600);
}

.evidence-legend-reference {
  border-top-style: dashed;
  border-top-color: var(--bs-secondary-color);
}

.evidence-legend-change {
  border-top-color: var(--bs-danger);
}

.evidence-legend-selected {
  height: 0.85rem;
  border: 1px solid var(--ke-brand-600);
}

/* Chain-of-custody rows, colour-coded by link type. */
.evidence-link-provenance {
  color: var(--bs-link-color);
}

.evidence-link-reference {
  color: var(--bs-secondary-color);
}

.evidence-link-change {
  color: var(--bs-danger);
}

/* --- Lower workspace ---------------------------------------------------- */

/* The mockup's 47.5 / 52.5 split. 30rem is a floor, not a height: on a
   two-column layout the panel grows to the foot of the viewport — see
   .evidence-page--fill below. */
.evidence-workspace {
  height: auto;
  min-height: 30rem;
  display: grid;
  grid-template-columns: 47.5% 52.5%;
  overflow: hidden;
}

.evidence-surface {
  min-width: 0;
  min-height: 0;
  display: grid;
  border-right: 1px solid var(--ke-chrome-border);
}

/* --- Record inspector --------------------------------------------------- */

.evidence-inspector {
  min-width: 0;
  min-height: 0;
  display: grid;
}

/* The htmx target, which keeps its id and gains nothing else — the pane
   inside it is a four-row grid, so this row has to pass the height on. */
.evidence-inspector > div {
  min-height: 0;
  display: grid;
}

/* Header, tab strip, scrolling body, footer — the body is the only row that
   gives, so the footer stays pinned to the foot of the panel. */
.evidence-pane {
  min-height: 0;
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr) auto;
}

.evidence-pane-header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.625rem 1rem; /* 10px 16px */
  border-bottom: 1px solid var(--ke-track);
}

/* 235px context column. The aside is beside every tab, not inside the
   Documents pane: it describes the record, not its documents. */
.evidence-pane-body {
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 14.6875rem;
  overflow: hidden;
}

.evidence-pane-main {
  min-width: 0;
  overflow: auto;
}

.evidence-pane-context {
  min-width: 0;
  overflow: auto;
  padding: 0.75rem 0.875rem; /* 12px 14px */
  border-left: 1px solid var(--ke-chrome-border);
  background: var(--ke-muted-surface);
}

.evidence-pane-footer {
  min-height: 1.75rem; /* 28px */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.25rem 0.8125rem; /* 13px */
  border-top: 1px solid var(--ke-chrome-border);
}

/* --- Documents table ---------------------------------------------------- */

/* Five columns need room: narrower than the table's floor the wrapper
   scrolls sideways inside the pane rather than widening the page. */
.evidence-doc-scroll { overflow-x: auto; }

.evidence-doc-table {
  width: 100%;
  min-width: 34rem;
  font-size: 0.75rem;
  border-collapse: collapse;
}

.evidence-doc-table th,
.evidence-doc-table td {
  padding: 0.375rem 0.625rem;
  text-align: left;
  vertical-align: middle;
}

.evidence-doc-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  height: 1.875rem; /* 30px */
  padding-block: 0;
  font-weight: 500;
  color: var(--ke-text-muted);
  background: var(--ke-muted-surface);
  border-bottom: 1px solid var(--ke-track);
}

/* The group caption row — one <tbody> per document group, so the caption is
   the group's own header cell rather than a styled data row. */
.evidence-doc-table tbody th {
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--ke-text-muted);
  background: var(--ke-muted-surface);
}

.evidence-doc-table tbody td { border-top: 1px solid var(--ke-track); }

.evidence-doc-table tbody td:first-child { max-width: 14rem; }

/* --- Narrow viewports ---------------------------------------------------- */

/* The inspector is barely half the content width, so the context column has
   to give up its 235px well before the mockup's own 1120px breakpoint —
   otherwise the documents table spends every desktop width in a horizontal
   scroll. Below 190px the label/value pair no longer fits side by side, so
   the list stacks instead of clipping "Responsible" to "Responsibl". */
@media (max-width: 1400px) {
  .evidence-pane-body { grid-template-columns: minmax(0, 1fr) 11.875rem; } /* 190px */

  .evidence-pane-context dl.row > dt,
  .evidence-pane-context dl.row > dd {
    flex: 0 0 100%;
    max-width: 100%;
    width: 100%;
  }

  .evidence-pane-context dl.row > dt { padding-bottom: 0; }
  .evidence-pane-context dl.row > dd { padding-top: 0; }
}

@media (max-width: 860px) {
  .evidence-workspace {
    height: auto;
    grid-template-columns: 1fr;
  }

  .evidence-surface {
    height: 25rem; /* 400px */
    border-right: 0;
    border-bottom: 1px solid var(--ke-chrome-border);
  }

  .evidence-inspector { min-height: 32.5rem; } /* 520px */

  /* The context aside drops under the tab content rather than squeezing it,
     and the pane scrolls as one column instead of two. */
  .evidence-pane-body {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr) auto;
    overflow-y: auto;
  }

  .evidence-pane-context {
    border-left: 0;
    border-top: 1px solid var(--ke-chrome-border);
  }
}

/* --- Legacy model-viewer placeholder ------------------------------------ */

/* Still the viewer on the work package and building dashboards, which have
   no imagery of their own yet. The wireframe SVG is inlined (see
   projects/partials/building_placeholder.svg) so its strokes inherit
   currentColor and the highlighted floor band can be filled from a token. */
.evidence-viewer {
  min-height: 22rem;
}

.evidence-viewer-svg {
  display: block;
  width: 100%;
  height: 100%;
  max-height: 30rem;
  margin-inline: auto;
}

.evidence-viewer-highlight {
  fill: rgba(var(--bs-primary-rgb), 0.5);
  stroke: var(--ke-brand-600);
}

.evidence-viewer-pin {
  position: absolute;
  top: 30%;
  left: 50%;
  max-width: 60%;
  transform: translateX(-50%);
}

/* ============================================================
   Portfolio dashboard (core/dashboard.html)
   Density and layout are a literal port of the approved mockup
   (portfolio-golden-thread-dashboard/src/styles.css): every px
   figure noted in a comment below is that file's figure, written
   here in rem against the 16px root. Its palette is NOT ported —
   colours are --ke- and Bootstrap tokens throughout, so the dark
   theme needs no rules of its own. The one exception is the map
   photograph, which carries its own light level and cannot be
   expressed as a token.
   ============================================================ */

.portfolio-page { padding-bottom: 1.75rem; } /* 28px */

/* --- Fill-the-viewport dashboard --------------------------------------- */

/* The dashboard (and only the dashboard — projects/project_list.html shares
   .portfolio-page) runs the lens down to the foot of the window: the filter
   strip, heading, summary strip and horizon keep their natural heights and
   .portfolio-workspace takes whatever is left, so the map shows more ground
   and the inspector gets the depth its detail wants.

   That needs an unbroken flex chain from the scroll container down, hence
   the :has() hooks on the two shared shells. Every step grows but none of
   them shrink: a window too short for the strips plus the workspace's 30rem
   floor must overflow .app-main and scroll, exactly as before, rather than
   compress the strips into each other. Two-column layouts only — under 860px
   the workspace stacks and keeps the fixed heights set in the breakpoint
   below. */
@media (min-width: 861px) {
  .app-main:has(> .page-body > .portfolio-page--fill),
  .app-main:has(> .page-body > .evidence-page--fill) {
    display: flex;
    flex-direction: column;
  }

  .app-main > .page-body:has(> .portfolio-page--fill),
  .app-main > .page-body:has(> .evidence-page--fill),
  .portfolio-page--fill,
  .evidence-page--fill,
  .portfolio-page--fill .portfolio-lens-scope {
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
  }

  .portfolio-page--fill > *,
  .evidence-page--fill > *,
  .portfolio-page--fill .portfolio-lens-scope > * { flex-shrink: 0; }

  /* The evidence timeline panel keeps its natural height, so the workspace
     underneath is the row that takes the extra. */
  .portfolio-page--fill .portfolio-workspace,
  .evidence-page--fill .evidence-workspace { flex-grow: 1; }

  /* The workspace is sized by the space left over, not by its own content:
     the record panel scrolls internally, so with the default auto basis a
     long document list would stretch the row and crop the viewer beside it
     into a portrait sliver. Its 30rem min-height still holds, so a window
     too short for it overflows and scrolls exactly as everything else here
     does. */
  .evidence-page--fill .evidence-workspace { flex-basis: 0; }

  /* The header tile takes a measured share of the new height — enough that
     it keeps pace with the panel, capped so it never crowds out the detail
     beneath it. */
  .portfolio-page--fill .portfolio-visual { height: clamp(8.25rem, 14vh, 11rem); }

  /* Packed at the top, not spread: `justify-content: space-between` used to
     bank the row's extra height into a gap after *every* child, so a tall
     window (the fill-viewport workspace stretches to whatever is left of it)
     read as coverage bar, stat row, decision tile and meta rows floating
     hundreds of pixels apart (plan 11 §5.1 Phase 4). The default
     `flex-start` keeps every island on the margins already set below it —
     the header tile, the bar, the stat row, the decision tile, the meta rows
     and the CTA stay tightly stacked whatever the panel's own height is, and
     any leftover space simply sits below the CTA rather than inside the
     content. flex-shrink is still off so a short window clips nothing either
     — the row keeps its content height and the main column scrolls instead. */
  .portfolio-page--fill .portfolio-inspector-body {
    display: flex;
    flex-direction: column;
  }

  .portfolio-page--fill .portfolio-inspector-body > * { flex-shrink: 0; }

  /* Nothing to show still reads as a panel, not a header with a void below. */
  .portfolio-page--fill .portfolio-inspector-empty { align-content: center; }
}

/* 12px between panels is the mockup's whole vertical rhythm. The summary
   strip is deliberately excluded — the page heading's own padding is its
   gap, exactly as in the mockup. */
.portfolio-panel { margin-top: 0.75rem; }

/* --- Filter strip -------------------------------------------------------
   Renders as the page header's right-hand action (see .portfolio-heading
   below), not a standalone strip, so it carries no height/padding of its
   own — it sizes to its controls and right-aligns inside the header slot. */

.portfolio-filterbar {
  min-height: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem;
  padding: 0;
}

/* Right-aligning three wrapped rows of controls under a stacked heading just
   reads as ragged — on a phone the form runs from the left like everything
   else on the page. */
@media (max-width: 575.98px) {
  .portfolio-filterbar { justify-content: flex-start; }
}

.portfolio-select,
.evidence-select {
  height: 2rem; /* 32px */
  min-width: 9.375rem;
  display: flex;
  align-items: center;
  border: 1px solid var(--ke-chrome-border);
  background: var(--ke-surface);
}

.portfolio-select-prefix,
.evidence-select-prefix { padding-left: 0.5625rem; }

/* Beats the `.page-body select` chrome (its own border and 2.35rem
   min-height) so the select reads as the inner half of the box rather than
   a second control sitting inside it. */
.page-body .portfolio-select-input,
.page-body .evidence-select-input {
  flex: 1;
  min-width: 0;
  height: 100%;
  min-height: 0;
  padding: 0 0.375rem;
  border: 0;
  background: transparent;
  color: var(--ke-text);
}

.page-body .portfolio-select-input:focus,
.page-body .evidence-select-input:focus {
  box-shadow: none;
  outline: 2px solid var(--bs-primary);
  outline-offset: -2px;
}

.portfolio-filter-apply {
  height: 2rem;
  display: inline-flex;
  align-items: center;
  padding-block: 0;
}

/* --- Page heading ------------------------------------------------------ */

.portfolio-heading,
.evidence-heading {
  min-height: 0;
  padding: 0 0 0.75rem;
}

.portfolio-kicker,
.evidence-kicker {
  color: var(--ke-brand-700);
  letter-spacing: 0.11em;
}

/* components/layout.html#page-header carries its own .mb-4 and .mt-1 utility
   spacing; the mockup's heading is 3px-tight, so both are pulled back here
   rather than by forking the shared partial. */
.portfolio-heading > div,
.delivery-heading > div,
.evidence-heading > div { margin-bottom: 0 !important; }

.portfolio-heading h1,
.delivery-heading h1,
.evidence-heading h1 { margin: 0.1875rem 0 !important; }

.portfolio-heading > div p,
.delivery-heading > div p,
.evidence-heading > div p {
  margin-top: 0.1875rem !important;
  font-size: 0.75rem;
}

/* The page-header's action slot is flex-shrink-0 by default, which would let
   the filter form overflow the header. Letting the row wrap instead keeps the
   two blocks whole: neither is ever squeezed, and when the filter form (its
   controls are 747px of nowrap content) no longer fits beside the title it
   drops onto its own line. 18rem is the basis that keeps both on one row down
   to ~1230px — the title's subtitle takes a second line long before that. The
   auto margin is what holds the form on the right once it has wrapped, since
   space-between puts a lone item on a line at the start.

   Scoped to the shared partial's own flex-sm-row breakpoint: below it the
   header is a column, where a flex-basis would be read as a height and open
   an 18rem hole between the subtitle and the filters. */
@media (min-width: 576px) {
  .portfolio-heading > div > div,
  .evidence-heading > div > div { flex-wrap: wrap; }

  .portfolio-heading div:has(> h1),
  .evidence-heading div:has(> h1) { flex: 1 1 18rem; min-width: 0; }

  /* !important because the partial hard-codes Bootstrap's .flex-shrink-0 on
     this slot and that utility is itself !important; without matching it the
     form cannot shrink onto its own line and overflows the page instead of
     wrapping its controls. */
  .portfolio-heading div:has(> .portfolio-filterbar),
  .evidence-heading div:has(> .portfolio-filterbar) {
    flex: 0 1 auto !important;
    min-width: 0;
    margin-left: auto;
  }
}

/* --- Summary strip ----------------------------------------------------- */

.portfolio-summary {
  display: grid;
  grid-template-columns: 0.85fr 1fr 1.25fr 1.05fr 1.05fr;
  min-height: 5.5rem; /* 88px */
  overflow: hidden;
}

/* The delivery dashboards' summary strip is the same cell, so it shares
   these rules rather than restating them; only the strip's own grid differs
   (see .delivery-summary). */
.portfolio-summary-item,
.delivery-summary-item {
  min-width: 0;
  display: grid;
  grid-template-columns: 2rem 1fr auto;
  align-items: center;
  gap: 0.5625rem; /* 9px */
  padding: 0.8125rem 0.875rem; /* 13px 14px */
  border-right: 1px solid var(--ke-track);
}

.portfolio-summary-item:last-child,
.delivery-summary-item:last-child { border-right: 0; }

.portfolio-summary-icon,
.delivery-summary-icon {
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
}

.portfolio-summary-copy,
.delivery-summary-copy {
  min-width: 0;
  display: grid;
  gap: 1px;
}

.portfolio-summary-copy > span,
.delivery-summary-copy > span {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* --- Percent bars (summary strip, lens panel, table) ------------------- */

.portfolio-bar {
  display: grid;
  grid-template-columns: 1fr 2.1875rem; /* value column 35px */
  align-items: center;
  gap: 0.5rem;
  min-width: 6.875rem;
}

.portfolio-bar--compact {
  grid-template-columns: 1fr 1.875rem;
  min-width: 5.9375rem;
}

/* Handed to components/progress.html#progress-bar as its progress_height, so
   the track and its fill share one 5px square-ended height. */
.portfolio-bar-track { height: 0.3125rem; }

/* --- Assurance horizon ------------------------------------------------- */

/* --horizon-lane-step is a whole milestone block (dot + days + name + title,
   70px) plus 2px of air, so a bumped milestone clears the one above it
   outright rather than tucking under its last line. The panel and its track
   (below) both grow by one step for every lane past the first, so a crowded
   strip only takes the extra height it actually needs. Only the building
   dashboard's strip still stacks lanes this way — the portfolio dashboard's
   compact variant (below) pre-merges same-day markers instead (plan 11
   §5.1 Phase 4) and never sets `--horizon-lanes`/`--lane`, so both formulas
   below simply resolve to their one-lane default for it. */
.portfolio-horizon-panel {
  --horizon-lane-step: 4.5rem; /* 72px */
  min-height: calc(9.125rem + (var(--horizon-lanes, 1) - 1) * var(--horizon-lane-step)); /* 146px */
  overflow: hidden;
}

/* min-height, not height: on a phone the legend wraps under the heading and
   the row has to grow for it rather than clip it. Same wrap-don't-squeeze
   rule as the page heading and the lens header. */
.portfolio-horizon-header {
  min-height: 2.875rem; /* 46px */
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.4375rem 0.875rem;
  border-bottom: 1px solid var(--ke-track);
}

.portfolio-horizon-header > .min-w-0 { flex: 1 1 14rem; }

.portfolio-horizon-legend { display: flex; gap: 0.75rem; }

/* The show/hide toggles for the strip's two kinds of milestone. Chips rather
   than buttons: they sit in a header of 12px captions, so they borrow its
   scale and only the pressed state carries any weight. */
.portfolio-horizon-toggles { display: flex; gap: 0.25rem; }

.portfolio-horizon-toggle {
  display: flex;
  align-items: center;
  gap: 0.3125rem;
  padding: 0.1875rem 0.4375rem;
  color: var(--bs-body-color);
  border: 0;
  background: var(--ke-muted-surface);
  white-space: nowrap;
}

/* An off toggle has to read as off in both themes, so it drops to the same
   subtle text colour the rest of the chrome mutes with rather than to an
   opacity, which would fade its background too. */
.portfolio-horizon-toggle[aria-pressed="false"] {
  color: var(--ke-text-subtle);
  background: transparent;
}

.portfolio-horizon-legend > span {
  display: flex;
  align-items: center;
  gap: 0.3125rem;
  white-space: nowrap;
}

/* The floor is what HORIZON_LANE_GAP_PERCENT is calibrated against: at 66rem
   the track is 916px and a 88px label is 9.6% of it, so the 10% lane gap
   clears a neighbour at every width from here up. Narrower than this the
   strip scrolls rather than compressing, which would put two labels back on
   top of each other however many lanes they were spread over. */
.portfolio-horizon-scroll { overflow-x: auto; }
.portfolio-horizon-inner { min-width: 66rem; }

.portfolio-horizon-axis {
  height: 1.3125rem; /* 21px */
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  padding-left: 12%;
}

.portfolio-horizon-axis > span {
  padding: 0.3125rem 0 0 0.3125rem;
  border-left: 1px solid var(--ke-track);
}

/* The track's top border IS the timeline; the gradient draws the 30/60-day
   gridlines behind the milestones. */
.portfolio-horizon-track {
  position: relative;
  height: calc(4.6875rem + (var(--horizon-lanes, 1) - 1) * var(--horizon-lane-step)); /* 75px */
  margin: 0 3.4375rem 0 5.3125rem; /* 0 55px 0 85px */
  border-top: 1px solid var(--ke-chrome-border);
  background-image: linear-gradient(
    90deg,
    transparent calc(33.33% - 1px),
    var(--ke-track) 33.33%,
    transparent calc(33.33% + 1px)
  );
  background-size: 33.33% 100%;
}

.portfolio-today-line {
  position: absolute;
  top: -0.3125rem;
  bottom: 0.4375rem;
  left: 0;
  border-left: 2px solid var(--bs-primary);
}

.portfolio-today-line::before {
  content: "";
  position: absolute;
  top: -0.1875rem;
  left: -0.25rem;
  width: 0.4375rem;
  height: 0.4375rem;
  border-radius: 50%;
  background: var(--bs-primary);
}

.portfolio-milestone {
  position: absolute;
  top: calc(-0.5rem + var(--lane, 0) * var(--horizon-lane-step));
  width: 5.5rem; /* 88px */
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  gap: 1px;
  text-align: center;
  text-decoration: none;
}

/* The hairline that ties a staggered milestone's dot back to the timeline.
   The milestone's own top is offset by lane*step (above), and its dot centre
   sits ~0.4rem below that, so the line runs from the track down to the dot;
   lane 0 resolves to zero height and draws nothing. */
.portfolio-milestone::before {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(0.5rem - var(--lane, 0) * var(--horizon-lane-step));
  height: max(0px, calc(var(--lane, 0) * var(--horizon-lane-step) - 0.125rem));
  border-left: 1px solid var(--ke-track);
}

/* One `color` drives the fill and its outer ring, so a decision type only has
   to restate that. The inner ring is the page surface, which flips by theme. */
.portfolio-milestone-dot {
  width: 0.5625rem; /* 9px */
  height: 0.5625rem;
  color: var(--ke-status-on-time);
  border: 2px solid var(--bs-body-bg);
  border-radius: 50%;
  background: currentcolor;
  box-shadow: 0 0 0 1px currentcolor;
}

.portfolio-milestone--hold .portfolio-milestone-dot { color: var(--bs-primary); }
.portfolio-milestone--action .portfolio-milestone-dot { color: var(--ke-status-overdue); }
/* Programme milestones (dated stage targets) sit beside the assurance ones on
   the same strip, so they need a colour no decision type uses — the brand blue
   is already the hold point's. */
.portfolio-milestone--programme .portfolio-milestone-dot {
  color: var(--bs-info, #0dcaf0);
}

.portfolio-milestone.is-selected .portfolio-milestone-dot {
  width: 0.8125rem; /* 13px */
  height: 0.8125rem;
  margin-top: -2px;
  color: var(--bs-primary);
  box-shadow: 0 0 0 4px rgba(var(--bs-primary-rgb), 0.17);
}

.portfolio-milestone-days { margin-top: 0.25rem; }

/* The building dashboard's strip still labels its markers inline (one
   project's decisions never cluster the way the portfolio's do), so these two
   stay even though the portfolio strip has stopped using them. */
.portfolio-milestone-name,
.portfolio-milestone-title {
  max-width: 5.5rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* --- Compact horizon (portfolio dashboard) ------------------------------
   Single lane (plan 11 §5.1 Phase 4): eight projects' milestones landing on
   the same few dates — the demo seed alone puts eight on one day — used to
   be solved by stacking lanes; apps.core.views._group_horizon_by_day now
   solves it before the template ever sees the rows, merging same-day,
   same-kind markers into one count badge. A marker here is therefore always
   one lane deep — a day count, a dot and one truncated "<project> ·
   <decision>" line — never a hover-only pop-out. Scoped to the modifier
   because the building dashboard shares every base rule above and keeps its
   own two-line labelled markers. */
.portfolio-horizon-panel--compact {
  min-height: 7.875rem; /* 126px: header + the one-lane marker stack below */
}

/* 55px: enough for the day pill, the dot and one truncated label line with
   the marker's own -0.5rem top offset (base rule, `--lane` now always 0). */
.portfolio-horizon-panel--compact .portfolio-horizon-track {
  height: 3.4375rem;
}

/* The day count now leads the marker (day above the dot, plan 11 §5.1), so
   the base rule's margin — sized for it trailing the dot on the building
   strip — does not apply here. */
.portfolio-horizon-panel--compact .portfolio-milestone-days {
  margin-top: 0;
}

/* The single combined "<project> · <decision>" line a solo marker shows, or
   the "N decisions" count badge a merged one shows instead — one line,
   ellipsis-truncated exactly like the building strip's name/title pair, so a
   crowded date reads as a count rather than running text off the marker. */
.portfolio-milestone-label {
  max-width: 5.5rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* --- Portfolio workspace (lens) ---------------------------------------- */

/* 30rem is the floor, not the height: it clears the inspector's natural
   content (the mockup's fixed 414px clipped the meta list) and keeps the map
   usable on a short window. On a two-column layout the panel grows past it to
   the foot of the viewport — see .portfolio-page--fill above. */
.portfolio-workspace {
  height: auto;
  min-height: 30rem;
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(22.5rem, 0.7fr);
  overflow: hidden;
}

.portfolio-surface {
  min-width: 0;
  display: grid;
  /* 52px header, but a floor rather than a fixed row: on a narrow column the
     subtitle wraps to two lines and a fixed row let it paint over the map. */
  grid-template-rows: minmax(3.25rem, auto) 1fr;
  border-right: 1px solid var(--ke-chrome-border);
}

/* Same wrap-don't-squeeze rule as the page heading above: the tab group is
   512px of nowrap content, so on a narrow surface column it would shrink the
   heading to a two-word-per-line sliver. It drops below the heading instead,
   held right by the auto margin. The delivery pages share the header, so
   they share the behaviour. */
.portfolio-surface-header,
.delivery-surface-header {
  /* Without this the header's min-content width — the un-splittable tab
     group — becomes its floor as a grid item, and it overflows the panel
     instead of letting the group scroll. */
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem 0.5rem 0.875rem;
  border-bottom: 1px solid var(--ke-track);
}

.portfolio-surface-header > .min-w-0,
.delivery-surface-header > .min-w-0 { flex: 1 1 14rem; }

/* Wrapping is settled before shrinking, so the group drops below the heading
   while it still wants its full 512px; only once it is alone on that line —
   and the surface column is narrower still — does it shrink and scroll its
   own tabs, rather than being clipped by the panel's overflow. */
.portfolio-view-tabs {
  min-width: 0;
  margin-left: auto;
  overflow-x: auto;
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--ke-chrome-border);
  background: var(--ke-muted-surface);
}

.portfolio-view-tab {
  height: 1.8125rem; /* 29px */
  display: flex;
  align-items: center;
  gap: 0.3125rem;
  padding: 0 0.5625rem;
  white-space: nowrap;
}

/* The portfolio lens' tabs are real links (?view= is their no-JS fallback), so
   they shed the link colour and underline and read as the tabs beside them. */
a.portfolio-view-tab {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

/* Two mechanisms, one look: pages whose tabs are inert spans mark the live one
   with .is-active, while the lens' tabs mark it with aria-current — an
   attribute Alpine can bind over the server-rendered value, which a class
   binding cannot do. */
.portfolio-view-tab.is-active,
.portfolio-view-tab[aria-current="page"] {
  color: var(--bs-primary);
  background: var(--ke-surface);
  box-shadow: var(--ke-shadow-sm);
}

.portfolio-surface-body,
.delivery-surface-body {
  min-height: 0;
  overflow: hidden;
}

/* --- Portfolio assurance matrix ---------------------------------------- */

/* The map's sibling pane inside .portfolio-surface-body, which is a fixed
   height with overflow hidden — so the matrix scrolls itself rather than
   pushing the lens card open. */
.portfolio-matrix-view {
  width: 100%;
  height: 100%;
  overflow: auto;
}

/* Compact by construction: a portfolio of eight projects across seven
   requirement types has to stay readable in the same box the map fills, so no
   cell wraps and the header stays put while the rows scroll under it. */
.portfolio-matrix-table {
  width: 100%;
  border-collapse: collapse;
}

.portfolio-matrix-table th,
.portfolio-matrix-table td {
  padding: 0.375rem 0.625rem;
  border-bottom: 1px solid var(--ke-track);
  white-space: nowrap;
}

.portfolio-matrix-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bs-body-bg);
  border-bottom: 1px solid var(--ke-chrome-border);
}

.portfolio-matrix-table tbody tr:hover:not(.is-selected) {
  background: var(--ke-muted-surface);
}

/* The selected row is marked the way the map marker and the horizon milestone
   are — a brand tint plus a 2px leading edge, so all three selections read as
   the same act. */
.portfolio-matrix-table tbody tr.is-selected {
  background: var(--bs-primary-bg-subtle);
  box-shadow: inset 2px 0 var(--bs-primary);
}

.portfolio-matrix-project {
  display: block;
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;
}

/* The RAG tint itself comes from Bootstrap utilities in the template — this
   only gives the pair of numbers its chip shape. */
.portfolio-matrix-cell {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.125rem 0.375rem;
}

/* --- Portfolio map ----------------------------------------------------- */

.portfolio-map-view,
.delivery-map-view {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--ke-muted-surface);
}

/* MAP_LEFT_* / MAP_TOP_* in apps/projects/selectors.py are percentages of
   this box, so the crop moves every marker — and since .portfolio-workspace
   now grows with the viewport, the crop is no longer fixed and the pins are
   approximate by construction. Deliberate: the photograph is a placeholder
   for a real interactive map, and the fit is not worth re-deriving for it. */
.portfolio-map-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 46% 50%;
  filter: saturate(0.72) contrast(0.96) brightness(1.03);
}

[data-bs-theme="dark"] .portfolio-map-image {
  filter: saturate(0.72) contrast(0.96) brightness(1.03) brightness(0.7)
    saturate(0.55);
}

.portfolio-marker {
  position: absolute;
  z-index: 3;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 0.25rem;
  text-decoration: none;
}

/* Health colour comes from the shared .portfolio-dot--* map below. */
.portfolio-marker-pin {
  width: 1.625rem; /* 26px */
  height: 1.625rem;
  display: grid;
  place-items: center;
  color: #fff;
  border: 3px solid var(--bs-body-bg);
  border-radius: 50%;
  box-shadow: var(--ke-shadow-sm);
}

.portfolio-marker.is-selected .portfolio-marker-pin {
  width: 2.125rem; /* 34px */
  height: 2.125rem;
  background: var(--bs-primary);
  box-shadow: 0 0 0 5px rgba(var(--bs-primary-rgb), 0.15), var(--ke-shadow-sm);
}

/* The name chip belongs to every marker; CSS shows it for the hovered or the
   selected one, so it survives an htmx lens swap without any JS of its own. */
.portfolio-marker-chip {
  display: none;
  padding: 0.3125rem 0.4375rem;
  white-space: nowrap;
}

.portfolio-marker:hover .portfolio-marker-chip,
.portfolio-marker:focus-visible .portfolio-marker-chip,
.portfolio-marker.is-selected .portfolio-marker-chip { display: block; }

.portfolio-map-density,
.delivery-map-density {
  position: absolute;
  top: 0.6875rem;
  left: 0.75rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.625rem;
}

.portfolio-map-key {
  position: absolute;
  left: 0.75rem;
  bottom: 0.625rem;
  display: flex;
  gap: 0.625rem;
  padding: 0.4375rem 0.5625rem;
}

.portfolio-map-key > span {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

/* --- Project inspector (core/partials/portfolio_lens.html) ------------- */

.portfolio-inspector {
  min-width: 0;
  display: grid;
  grid-template-rows: auto 1fr;
}

/* The no-selection branch renders a single child, which must fill the column
   rather than sit in the hero's row. */
.portfolio-inspector-empty { grid-row: 1 / -1; }

/* No per-project photograph any more (plan 11 §5.1 Phase 4) — a project that
   used to borrow the demo IFC model's image now gets an honest neutral tile:
   a diagonal brand-token gradient, dark enough at its bottom-left corner
   (where the title sits) in either theme by construction, since this is a
   deliberate graphic rather than the page surface. */
.portfolio-visual {
  position: relative;
  height: 8.25rem; /* 132px */
  overflow: hidden;
  background: linear-gradient(45deg, var(--ke-brand-900), var(--ke-brand-600));
}

/* The project's initials — the tile's one piece of per-project identity
   besides its name in the title block below. Top-left so it never collides
   with the health pill (top-right) or the title block (bottom). */
.portfolio-visual-mark {
  position: absolute;
  top: 0.75rem;
  left: 0.875rem;
  width: 2.375rem; /* 38px */
  height: 2.375rem;
  display: grid;
  place-items: center;
  color: #fff;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.32);
  border-radius: 50%;
  letter-spacing: 0.03em;
}

.portfolio-visual-title {
  position: absolute;
  left: 0.875rem;
  right: 0.875rem;
  bottom: 0.75rem;
}

/* A brand tint light enough for the gradient in either theme, which is why
   it names the palette step rather than a themed text token. */
.portfolio-visual-kicker {
  color: var(--ke-brand-200);
  letter-spacing: 0.08em;
}

.portfolio-health-pill {
  position: absolute;
  top: 0.625rem;
  right: 0.625rem;
  display: flex;
  align-items: center;
  gap: 0.3125rem;
  padding: 0.3125rem 0.4375rem;
  color: #fff;
  background: color-mix(in srgb, var(--ke-status-on-time) 90%, transparent);
}

.portfolio-health-pill--attention {
  background: color-mix(in srgb, var(--ke-status-running-late) 90%, transparent);
}

.portfolio-health-pill--at_risk {
  background: color-mix(in srgb, var(--ke-status-overdue) 90%, transparent);
}

/* No min-height: 0 and no overflow here: both let this grid row shrink below
   its content, which is what cut the meta list off. Left visible, the row
   grows and .portfolio-workspace's auto height follows it. */
.portfolio-inspector-body {
  padding: 0.6875rem 0.8125rem; /* 11px 13px */
}

.portfolio-inspector-primary {
  display: grid;
  grid-template-columns: 7.375rem 1fr; /* 118px */
  align-items: end;
  gap: 0.625rem;
}

.portfolio-mini-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  margin-top: 0.5625rem;
  border-top: 1px solid var(--ke-track);
  border-bottom: 1px solid var(--ke-track);
}

.portfolio-mini-stats > div {
  min-width: 0;
  display: grid;
  align-content: center;
  min-height: 3.5rem; /* 56px */
  padding: 0.4375rem 0.5rem;
  border-right: 1px solid var(--ke-track);
}

.portfolio-mini-stats > div:first-child { padding-left: 0; }
.portfolio-mini-stats > div:last-child { border-right: 0; }

.portfolio-next-decision {
  min-height: 3.25rem; /* 52px */
  display: grid;
  grid-template-columns: 2.125rem 1fr auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4375rem 0;
  border-bottom: 1px solid var(--ke-track);
}

.portfolio-decision-icon {
  width: 1.9375rem; /* 31px */
  height: 1.9375rem;
  display: grid;
  place-items: center;
}

/* 120px label column: wide enough for "Accountable person" and "Building
   safety lead" to sit on one line each, which is what the inspector column's
   22.5rem floor is sized for. */
.portfolio-project-meta {
  display: grid;
  grid-template-columns: 7.5rem 1fr; /* 120px */
  gap: 0.3125rem;
  margin: 0.5rem 0;
}

.portfolio-project-meta dd {
  min-width: 0;
  margin: 0;
}

.portfolio-open-thread {
  width: 100%;
  height: 2.125rem; /* 34px */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4375rem;
  padding-block: 0;
}

.portfolio-open-thread > .ke-icon:last-child { margin-left: auto; }

/* --- Portfolio table ---------------------------------------------------- */

/* The table moved off the dashboard: it now lists every project, at all
   statuses, on projects/project_list.html. */
.portfolio-table-panel { overflow: hidden; }

.portfolio-panel-header {
  height: 3.25rem; /* 52px */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.875rem;
  border-bottom: 1px solid var(--ke-chrome-border);
}

.portfolio-columns-button {
  height: 1.75rem; /* 28px */
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  padding: 0 0.5rem;
  border: 1px solid var(--ke-chrome-border);
}

/* Bootstrap's default --bs-table-bg is the opaque canvas, which would paint a
   flat slab inside the frosted panel; the card is the surface here. */
.portfolio-table {
  --bs-table-bg: transparent;
  min-width: 62rem;
  margin-bottom: 0;
}

/* Bootstrap compiles the cell padding as a literal, so tightening the rows to
   the mockup's density means restating the whole shorthand. */
.portfolio-table > :not(caption) > * > * { padding: 0.375rem 0.5rem; }

.portfolio-table thead th {
  height: 1.875rem; /* 30px */
  padding-block: 0;
  background: var(--ke-muted-surface);
  border-bottom-color: var(--ke-track);
}

.portfolio-table tbody tr { height: 3.0625rem; } /* 49px */
.portfolio-table tbody td { border-bottom-color: var(--ke-track); }

.portfolio-table tbody tr:hover:not(.portfolio-row-selected) {
  --bs-table-bg-state: var(--ke-muted-surface);
}

/* Selected table row — tint via Bootstrap's table state token, never a
   background-color override. Bootstrap paints that tint with an inset
   9999px shadow on each cell, so the 2px selected edge has to be layered in
   front of it rather than replace it. */
.portfolio-row-selected { --bs-table-bg-state: var(--bs-primary-bg-subtle); }

.portfolio-row-selected > td:first-child {
  box-shadow: inset 2px 0 var(--bs-primary), inset 0 0 0 9999px var(--bs-table-bg-state);
}

/* --- Shared traffic-light dots ----------------------------------------- */

/* Used by the map pins, the horizon legend, the map key and the table's
   health column. */
.portfolio-dot--on_track,
.portfolio-dot--gateway { background: var(--ke-status-on-time); }
.portfolio-dot--attention { background: var(--ke-status-running-late); }
.portfolio-dot--at_risk,
.portfolio-dot--action { background: var(--ke-status-overdue); }
.portfolio-dot--hold { background: var(--bs-primary); }
.portfolio-dot--programme { background: var(--bs-info, #0dcaf0); }

/* Health is a live-work judgement, so a project that is not ACTIVE gets a
   neutral dot in the table and lets its status pill do the talking. */
.portfolio-status-dot--inactive { background: var(--ke-text-subtle); }

/* --- Narrow viewports --------------------------------------------------- */

/* 1500px, not the 1180px this strip used to break at: everywhere between the
   two the five fr columns still fit, but only by clipping their own labels
   ("Golden threa…", "Gateways in next 45 d…"). The compact treatment — no
   inline coverage bar, no trend glyph, smaller icons — buys back exactly the
   width those labels need, and 1500px is where the coverage column stops
   being able to carry both its bar and its label. */
@media (max-width: 1500px) {
  /* 12.5rem is what the longest KPI actually needs — icon, gap, "Gateways in
     next 45 days" and the item's own padding. Below 9rem the captions clipped
     silently; now the strip scrolls (as it already could) rather than lying
     about what it says. */
  .portfolio-summary {
    grid-template-columns: repeat(5, minmax(12.5rem, 1fr));
    overflow-x: auto;
  }

  .portfolio-summary-item,
  .delivery-summary-item {
    grid-template-columns: 1.6875rem 1fr;
    padding-inline: 0.5625rem;
  }

  .portfolio-summary-item > .portfolio-bar,
  .portfolio-summary-item > .ke-icon,
  .delivery-summary-item > .portfolio-bar { display: none; }

  .portfolio-summary-icon,
  .delivery-summary-icon {
    width: 1.6875rem;
    height: 1.6875rem;
  }

  /* The inspector keeps its full floor here — it is the width the meta
     list's one-line labels need, whatever the viewport. */
  .portfolio-workspace {
    grid-template-columns: minmax(0, 1.3fr) minmax(22.5rem, 0.7fr);
  }
}

/* Below ~1200px five 12.5rem KPIs no longer fit the content column. The icon
   is the one part of an item that says nothing its label doesn't — dropping
   it takes each KPI to 10.25rem and keeps all five in view down to ~920px,
   which beats scrolling the fifth one out of sight. The critical-issue signal
   survives: the count itself is already red. */
@media (max-width: 1200px) {
  .portfolio-summary { grid-template-columns: repeat(5, minmax(10.25rem, 1fr)); }
  .portfolio-summary-item { grid-template-columns: 1fr; }
  .portfolio-summary-icon { display: none; }
}

@media (max-width: 860px) {
  .portfolio-workspace {
    height: auto;
    grid-template-columns: 1fr;
  }

  .portfolio-surface {
    height: 26.875rem;
    border-right: 0;
    border-bottom: 1px solid var(--ke-chrome-border);
  }

  .portfolio-inspector { min-height: 23.75rem; }
}

/* ============================================================
   Delivery dashboards — Site / Building / Work package
   Shared chrome for the three delivery levels, ported from the
   approved mockup (golden-thread-mockups-source/apps/
   03-site-building-work-package/src/styles.css): every px figure
   noted in a comment below is that file's figure, written here in
   rem against the 16px root. Its palette is NOT ported — colours
   are --ke- and Bootstrap tokens throughout, so the dark theme
   needs no rules of its own. The one exception is the site-plan
   photograph, which carries its own light level and cannot be
   expressed as a token.

   The vocabulary is deliberately generic: the Building and Work
   package dashboards reuse these names rather than growing a
   second set. Where the portfolio dashboard already carries the
   right rule — the summary cell, the surface header, the map
   overlays, the percent bars, the segmented tabs, the
   traffic-light dots — the .delivery-* selector joins that rule
   above instead of cloning it.
   ============================================================ */

.delivery-page { padding-bottom: 1.75rem; } /* 28px */

/* 12px between panels is the mockup's whole vertical rhythm. The summary
   strip is deliberately excluded — the page heading's own padding is its
   gap, exactly as on the portfolio dashboard. */
.delivery-panel { margin-top: 0.75rem; }

/* The requirements trio's first card is the shared people partial, whose
   root cannot carry h-100 without also stretching it on the work package
   page — the site page's own wrapper stretches it here instead. */
#site-competency-requirements {
  display: flex;
  flex-direction: column;
  height: 100%;
}

#site-competency-requirements > .glass-card { flex: 1 1 auto; }

/* --- Page heading ------------------------------------------------------ */

.delivery-heading {
  min-height: 5.125rem; /* 82px */
  padding: 0.75rem 0;
}

.delivery-kicker {
  color: var(--ke-brand-700);
  letter-spacing: 0.11em;
}

/* --- Summary strip ----------------------------------------------------- */

/* Five equal cells, unlike the portfolio's weighted columns: every delivery
   level shows the same five metric shapes. The cell itself is
   .portfolio-summary-item's rule. */
.delivery-summary {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  min-height: 5.5rem; /* 88px */
  overflow: hidden;
}

/* --- Workspace (surface + inspector) ----------------------------------- */

/* The inspector's natural height drives the panel, as on the portfolio
   dashboard; min-height keeps the surface a usable size when a building has
   little detail to show. */
.delivery-workspace {
  height: auto;
  min-height: 26.875rem; /* 430px */
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(20rem, 0.62fr); /* 320px floor */
  overflow: hidden;
}

.delivery-surface {
  min-width: 0;
  display: grid;
  grid-template-rows: 3.25rem 1fr; /* 52px header */
  border-right: 1px solid var(--ke-chrome-border);
}

/* --- Site plan --------------------------------------------------------- */

/* object-position is the mockup's own crop of this image. No markers ride on
   it: buildings carry no coordinates, so there is nothing to calibrate
   against (contrast .portfolio-map-image, whose crop is load-bearing). */
.delivery-map-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 61% 53%;
  filter: saturate(0.72) contrast(0.96) brightness(1.02);
}

[data-bs-theme="dark"] .delivery-map-image {
  filter: saturate(0.72) contrast(0.96) brightness(1.02) brightness(0.7)
    saturate(0.55);
}

/* Two rows rather than the portfolio key's single one: the legend sits above
   the note explaining why the plan carries no building markers. */
.delivery-map-key {
  position: absolute;
  left: 0.75rem;
  bottom: 0.625rem;
  display: grid;
  gap: 0.25rem;
  padding: 0.4375rem 0.5625rem;
}

.delivery-map-key > div {
  display: flex;
  gap: 0.625rem;
}

.delivery-map-key > div > span {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

/* --- Inspector (projects/partials/site_building_inspector.html) --------- */

.delivery-inspector {
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-rows: auto 1fr;
}

/* The no-selection branch renders a single child, which must fill the column
   rather than sit in the hero's row. */
.delivery-inspector-empty { grid-row: 1 / -1; }

.delivery-inspector-hero {
  min-height: 5.5rem; /* 88px */
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.625rem;
  padding: 1rem; /* 16px */
  border-bottom: 1px solid var(--ke-track);
  background: var(--ke-muted-surface);
}

.delivery-inspector-body {
  min-height: 0;
  overflow-y: auto;
  padding: 0.6875rem 0.8125rem; /* 11px 13px */
}

.delivery-inspector-metrics {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  margin-top: 0.5625rem;
  border-top: 1px solid var(--ke-track);
  border-bottom: 1px solid var(--ke-track);
}

.delivery-inspector-metrics > div {
  min-width: 0;
  min-height: 3.5rem; /* 56px */
  display: grid;
  align-content: center;
  padding: 0.6875rem 0.75rem; /* 11px 12px */
  border-right: 1px solid var(--ke-track);
}

.delivery-inspector-metrics > div:first-child { padding-left: 0; }
.delivery-inspector-metrics > div:last-child { border-right: 0; }

/* 120px label column (the mockup's 104px), wide enough for "Building safety
   lead" to stay on one line at the inspector column's 20rem floor. */
.delivery-meta {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: 0.375rem 0.5rem; /* 6px 8px */
  margin: 0.5rem 0;
}

.delivery-meta dd {
  min-width: 0;
  margin: 0;
}

/* --- Delivery board ---------------------------------------------------- */

.delivery-board { overflow: hidden; }

.delivery-row {
  display: grid;
  grid-template-columns: 1.6fr 1.15fr 1fr 0.8fr 1.15fr 1.75rem; /* 28px arrow */
  align-items: center;
  gap: 0.875rem; /* 14px */
  min-height: 3.3125rem; /* 53px */
  padding: 0 0.75rem;
  border-bottom: 1px solid var(--ke-track);
}

.delivery-row:last-child { border-bottom: 0; }

.delivery-row-head {
  min-height: 1.875rem; /* 30px */
  background: var(--ke-muted-surface);
}

a.delivery-row:hover:not(.is-selected) { background: var(--ke-muted-surface); }

/* Selected row — the same 2px primary edge on a subtle tint the portfolio
   table draws, without Bootstrap's table-state shadow to layer around. */
a.delivery-row.is-selected {
  background: var(--bs-primary-bg-subtle);
  box-shadow: inset 2px 0 var(--bs-primary);
}

.delivery-entity-cell {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* The asset board's own columns. .delivery-row's grid belongs to the site
   dashboard's building board, whose columns must not shift, so the building
   dashboard's six columns arrive as a modifier rather than an edit. */
.delivery-row--assets {
  grid-template-columns: 1.5fr 1fr 0.6fr 1.2fr 0.8fr 1.75rem; /* 28px arrow */
}

/* --- Work package pane (buildings/building_detail.html) ----------------- */

/* The building workspace is the same panel with the mockup's own figures: a
   taller surface and a wider right column, since the package pane holds
   cards rather than the site inspector's stacked metrics. */
.delivery-workspace--building {
  min-height: 35rem; /* 560px */
  grid-template-columns: minmax(0, 1.42fr) minmax(24.375rem, 0.78fr); /* 390px floor */
}

/* Three rows, not the mockup's four: its selected-package footer lives in
   the model surface fragment, so one htmx swap moves the viewer and the
   footer together. The header row is .portfolio-panel-header's 52px. */
.delivery-package-pane {
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-rows: 3.25rem 2.4375rem minmax(0, 1fr); /* 52px 39px */
  overflow: hidden;
}

.delivery-filter-chips {
  display: flex;
  align-items: center;
  gap: 0.3125rem; /* 5px */
  padding: 0.3125rem 0.625rem; /* 5px 10px */
  overflow-x: auto;
  border-bottom: 1px solid var(--ke-track);
}

.delivery-filter-chip {
  height: 1.5625rem; /* 25px */
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0 0.5rem;
  white-space: nowrap;
  border: 1px solid var(--ke-chrome-border);
  border-radius: 99px;
}

.delivery-filter-chip.is-active {
  color: var(--bs-primary);
  border-color: var(--bs-primary);
  background: var(--bs-primary-bg-subtle);
  font-weight: 600;
}

.delivery-package-list {
  min-height: 0;
  overflow-y: auto;
  padding: 0.5rem; /* 8px */
}

.delivery-package-card {
  display: grid;
  gap: 0.5rem; /* 8px */
  margin-bottom: 0.4375rem; /* 7px */
  padding: 0.625rem; /* 10px */
  border: 1px solid var(--ke-chrome-border);
  background: var(--ke-surface);
}

.delivery-package-card:last-child { margin-bottom: 0; }

.delivery-package-card:hover:not(.is-selected) {
  border-color: var(--bs-primary-border-subtle);
}

/* Selected card — the same 2px primary edge on a subtle tint that
   a.delivery-row.is-selected draws on the boards. */
.delivery-package-card.is-selected {
  border-color: var(--bs-primary);
  background: var(--bs-primary-bg-subtle);
  box-shadow: inset 2px 0 var(--bs-primary);
}

.delivery-package-card-head {
  display: grid;
  grid-template-columns: 1.8125rem 1fr auto; /* 29px icon tile */
  align-items: center;
  gap: 0.4375rem; /* 7px */
}

.delivery-package-icon {
  width: 1.8125rem;
  height: 1.8125rem;
  display: grid;
  place-items: center;
}

.delivery-package-meta {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0.375rem;
}

/* Sits under the model viewer inside the swapped fragment, not in the pane
   — see buildings/partials/building_model_surface.html. */
.delivery-package-footer {
  min-height: 5.3125rem; /* 85px */
  display: grid;
  grid-template-columns: 1fr 10rem; /* 160px action */
  align-items: center;
  gap: 0.625rem;
  padding: 0.625rem 0.75rem; /* 10px 12px */
  border-top: 1px solid var(--ke-chrome-border);
  background: var(--ke-muted-surface);
}

/* --- Narrow viewports --------------------------------------------------- */

@media (max-width: 1180px) {
  /* The inspector keeps its full floor — it is the width the meta list's
     one-line labels need, whatever the viewport. */
  .delivery-workspace {
    grid-template-columns: minmax(0, 1.25fr) minmax(20rem, 0.62fr);
  }

  /* Same trade on the building dashboard: the surface gives up the width,
     the package pane keeps the floor its cards are sized for. */
  .delivery-workspace--building {
    grid-template-columns: minmax(0, 1.1fr) minmax(24.375rem, 0.78fr);
  }
}

@media (max-width: 860px) {
  .delivery-summary {
    grid-template-columns: repeat(5, 11.25rem); /* 180px */
    overflow-x: auto;
  }

  .delivery-workspace {
    height: auto;
    grid-template-columns: 1fr;
  }

  .delivery-surface {
    height: 25rem; /* 400px */
    border-right: 0;
    border-bottom: 1px solid var(--ke-chrome-border);
  }

  .delivery-workspace--building { grid-template-columns: 1fr; }

  .delivery-inspector { min-height: 26.875rem; }
  .delivery-package-pane { min-height: 28.125rem; } /* 450px */

  .delivery-board { overflow-x: auto; }
  .delivery-table { min-width: 48rem; }
}

/* ============================================================
   Work package assurance dashboard
   (work_packages/work_package_detail.html and its assurance
   panel partial)
   Density and layout are a literal port of the approved mockup
   (golden-thread-mockups-source/apps/03-site-building-work-package/
   src/styles.css — .package-controls, .package-lower,
   .assurance-panel, .assurance-tabs, .overview-*, .split-tab,
   .requirement-list, .evidence-row, .supplier-*): every px figure
   noted in a comment below is that file's figure, written here in
   rem against the 16px root. Its palette is NOT ported — colours
   are --ke- and Bootstrap tokens throughout, so the dark theme
   needs no rules of its own. The page's level bar, heading,
   summary strip and viewer are the shared .delivery-* / .evidence-*
   cells defined with the site delivery dashboard above.
   ============================================================ */

/* --- Package selector ---------------------------------------------------
   The old controls bar is gone (plan 11's header consolidation moved the
   selector into the page header's own actions row, beside Complete and
   Edit — templates/work_packages/partials/header_controls.html); this rule
   is what still styles it there. */

/* The package selector is the only item in the header actions row that may
   shrink; the buttons beside it keep their natural width. */
.delivery-package-select {
  flex: 0 1 17.5rem; /* 280px */
  min-width: 12.5rem;
}

/* --- Tab strip --------------------------------------------------------- */

.delivery-tabs {
  display: flex;
  align-items: flex-end;
  gap: 0.125rem;
  padding: 0 0.75rem;
  border-bottom: 1px solid var(--ke-track);
  overflow-x: auto;
}

.delivery-tab {
  height: 2.1875rem; /* 35px */
  display: inline-flex;
  align-items: center;
  padding: 0 0.6875rem;
  color: var(--ke-text-muted);
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}

.delivery-tab:hover { color: var(--ke-text); }

.delivery-tab.is-active {
  color: var(--bs-primary);
  border-bottom-color: var(--bs-primary);
}

/* --- Overview: score tiles --------------------------------------------- */

.delivery-score {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-bottom: 1px solid var(--ke-track);
}

.delivery-score > span {
  min-height: 5.125rem; /* 82px */
  min-width: 0;
  display: grid;
  grid-template-columns: 1.6875rem 1fr; /* 27px icon column */
  grid-template-rows: 1fr 1fr;
  align-items: end;
  gap: 0 0.5rem;
  padding: 0.75rem;
  border-right: 1px solid var(--ke-track);
}

.delivery-score > span:last-child { border-right: 0; }

.delivery-score > span > svg {
  grid-row: 1 / 3;
  align-self: center;
}

.delivery-score > span > span {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* --- Requirement and action rows --------------------------------------- */

/* One row shape for both list bodies in the panel: the requirement schedule
   (copy + status badge) and the overview's assurance actions (severity icon
   + copy + arrow). */
.delivery-requirement-row {
  min-height: 2.1875rem; /* 35px */
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.375rem 0.625rem;
  border-bottom: 1px solid var(--ke-track);
}

a.delivery-requirement-row:hover { background: var(--ke-muted-surface); }

/* --- Evidence document table ------------------------------------------- */

.delivery-doc-table {
  height: 100%;
  overflow: auto;
}

/* Five columns need room: narrower than this the table scrolls sideways
   inside the panel rather than crushing every cell to an ellipsis. */
.delivery-doc-row {
  min-width: 41rem;
  min-height: 2.375rem; /* 38px */
  display: grid;
  grid-template-columns: 1.6fr 0.8fr 1.05fr 0.9fr 0.8fr;
  align-items: center;
  gap: 0.5rem;
  padding: 0 0.625rem;
  border-bottom: 1px solid var(--ke-track);
}

.delivery-doc-row > span { min-width: 0; }

.delivery-doc-head {
  position: sticky;
  top: 0;
  z-index: 1;
  min-height: 1.875rem; /* 30px */
  background: var(--ke-muted-surface);
}

/* --- Supplier tab ------------------------------------------------------ */

.delivery-supplier-identity {
  min-height: 5.625rem; /* 90px */
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.8125rem;
  border-bottom: 1px solid var(--ke-track);
}

.delivery-supplier-stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  border-bottom: 1px solid var(--ke-track);
}

.delivery-supplier-stats > div {
  min-height: 4.6875rem; /* 75px */
  min-width: 0;
  display: grid;
  align-content: center;
  gap: 0.125rem;
  padding: 0.625rem 0.75rem;
  border-right: 1px solid var(--ke-track);
}

.delivery-supplier-stats > div:last-child { border-right: 0; }

/* --- Panel footer ------------------------------------------------------ */

.delivery-panel-footer {
  min-height: 1.6875rem; /* 27px */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.25rem 0.6875rem;
  border-top: 1px solid var(--ke-track);
}

/* --- Responsive -------------------------------------------------------- */

@media (max-width: 620px) {
  .delivery-supplier-stats { grid-template-columns: repeat(2, 1fr); }

  .delivery-supplier-stats > div:nth-child(2n) { border-right: 0; }
}

/* --- Shared list table --------------------------------------------------
   The chrome around templates/components/list_table.html. It replaces the
   DataTables strips above with the same geometry: a control strip under the
   panel header, the table edge to edge, an info/paging strip closing the
   card. Both strips are inset 0.875rem to line up with
   .portfolio-panel-header; the table itself is not, so its first column sits
   where the DataTables one did.

   No dark-mode variants here on purpose — every colour is a --ke-* token or
   a Bootstrap component that already follows data-bs-theme (07). */

.list-table-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 0.75rem;
  padding: 0.375rem 0.875rem;
}

/* The search box takes the width it needs and no more — the strip is a
   toolbar, not a form. */
.list-table-search {
  min-width: 0;
  flex: 0 1 18rem;
}

.list-table-per-page { width: auto; }

/* The strip that closes the card, carrying the rule the panel header carries
   at the top. */
.list-table-footer {
  padding: 0.375rem 0.875rem;
  border-top: 1px solid var(--ke-track);
}

/* The sprite has no chevron-up, so ascending is the down chevron turned
   over rather than a second near-identical glyph in icons.svg. */
.list-table-sort-icon { flex-shrink: 0; }
.list-table-sort-icon--asc { transform: rotate(180deg); }

/* Below the small breakpoint the two halves of the toolbar stack, and the
   search box takes the full width rather than sitting in a 18rem column
   beside an empty gap. */
@media (max-width: 575.98px) {
  .list-table-toolbar { flex-direction: column; align-items: stretch; }

  .list-table-search { flex: 1 1 auto; }

  /* The paging strip is wider than a phone: let the record count and the
     controls stack, and the controls themselves wrap, rather than pushing
     Next past the panel edge. */
  .list-table-footer nav { row-gap: 0.5rem; justify-content: center; }

  .list-table-footer nav > div:last-child { flex-wrap: wrap; justify-content: center; }
}

/* Page tabs — horizontal glass tab bar (components/tabs.html#page-tabs) */

.page-tabs {
  position: sticky;
  z-index: 4;
  top: 0;
  overflow: hidden;
  background: color-mix(in srgb, var(--ke-surface) 94%, transparent);
}

.page-tabs .app-tabs {
  gap: 0;
  padding: 0 0.4rem;
  border-bottom: 0;
}

.page-tabs .nav-link {
  display: inline-flex;
  min-height: 3.35rem;
  align-items: center;
  gap: 0.5rem;
  padding: 0.72rem 1.15rem;
  border-bottom-width: 3px;
  color: var(--ke-text-muted);
  font-size: 0.78rem;
  font-weight: 650;
  white-space: nowrap;
}

.page-tabs .nav-link:hover { background: var(--ke-surface-hover); color: var(--ke-text); }
.page-tabs .nav-link.active { background: rgba(var(--bs-primary-rgb), 0.055); color: var(--ke-brand-700); }
[data-bs-theme="dark"] .page-tabs .nav-link.active { color: var(--ke-brand-300); }

.page-tab-count {
  display: inline-grid;
  min-width: 1.35rem;
  height: 1.35rem;
  padding: 0 0.3rem;
  place-items: center;
  background: var(--ke-muted-surface);
  color: var(--ke-text-muted);
  font-size: 0.62rem;
}

.page-tabs .nav-link.active .page-tab-count { background: var(--bs-primary-bg-subtle); color: var(--ke-brand-700); }
[data-bs-theme="dark"] .page-tabs .nav-link.active .page-tab-count { color: var(--ke-brand-300); }

.page-tab-content { min-width: 0; }
.page-tab-panel { display: flex; min-width: 0; flex-direction: column; gap: 0.75rem; }

@media (max-width: 767.98px) {
  .page-tabs .nav-link { min-height: 3rem; padding: 0.65rem 0.8rem; }
}

/* Preview drawer — shared quick-look offcanvas (components/drawer.html#preview-drawer) */

.preview-drawer {
  --bs-offcanvas-width: min(26rem, 94vw);
  border-left: 1px solid var(--ke-chrome-border);
}

.preview-drawer-header { min-height: 5rem; border-bottom: 1px solid var(--ke-chrome-border); }

.preview-drawer-kicker {
  color: var(--ke-brand-700);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-size: 0.6875rem;
  font-weight: 700;
}
[data-bs-theme="dark"] .preview-drawer-kicker { color: var(--ke-brand-300); }

.preview-drawer-item { min-height: calc(100vh - 5rem); }

.preview-drawer-hero {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  padding: 1.15rem;
  border-bottom: 1px solid var(--ke-chrome-border);
  background: rgba(var(--bs-primary-rgb), 0.045);
}

.preview-drawer-icon {
  display: inline-grid;
  width: 2.75rem;
  height: 2.75rem;
  flex: 0 0 auto;
  place-items: center;
  background: var(--bs-primary-bg-subtle);
  color: var(--ke-brand-700);
}

[data-bs-theme="dark"] .preview-drawer-icon { color: var(--ke-brand-300); }
.preview-drawer-hero > div { min-width: 0; }
.preview-drawer-hero > div > span { color: var(--ke-text-muted); font-size: 0.68rem; font-weight: 650; letter-spacing: 0.07em; text-transform: uppercase; }
.preview-drawer-hero h3 { margin: 0.2rem 0 0; color: var(--ke-text); font-size: 1.15rem; line-height: 1.2; }
.preview-drawer-hero p { margin: 0.3rem 0 0; color: var(--ke-text-muted); font-size: 0.75rem; }

.preview-drawer-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; }
.preview-drawer-facts > div { min-width: 0; min-height: 4.4rem; padding: 0.75rem 0.9rem; border-right: 1px solid var(--ke-track); border-bottom: 1px solid var(--ke-track); }
.preview-drawer-facts > div:nth-child(2n) { border-right: 0; }
.preview-drawer-facts dt { color: var(--ke-text-muted); font-size: 0.65rem; font-weight: 500; }
.preview-drawer-facts dd { margin: 0.18rem 0 0; overflow-wrap: anywhere; color: var(--ke-text); font-size: 0.78rem; font-weight: 650; }

.preview-drawer-note { margin: 1rem; padding: 0.9rem; border-left: 3px solid var(--ke-brand-600); background: var(--ke-muted-surface); }
.preview-drawer-note strong { color: var(--ke-text); font-size: 0.72rem; }
.preview-drawer-note p { margin: 0.3rem 0 0; color: var(--ke-text-muted); font-size: 0.72rem; line-height: 1.5; }

.preview-drawer-actions { display: flex; gap: 0.5rem; padding: 1rem; border-top: 1px solid var(--ke-chrome-border); }
.preview-drawer-actions .btn { display: inline-flex; flex: 1 1 0; align-items: center; justify-content: center; gap: 0.4rem; }

@media (max-width: 767.98px) {
  .preview-drawer-facts { grid-template-columns: 1fr; }
  .preview-drawer-facts > div { border-right: 0; }
  .preview-drawer-actions { flex-direction: column-reverse; }
}

/* ============================================================
   Supplier detail
   (suppliers/supplier_detail.html and suppliers/partials/*)
   The hero is a port of the approved mockup
   (golden-thread-mockups-source/apps/05-supplier-details/
   src/styles.css — .supplier-hero, .supplier-logo,
   .supplier-meta, .hero-score): every px figure noted in a
   comment below is that file's figure, written here in rem
   against the 16px root. Its palette is NOT ported — colours
   are --ke- and Bootstrap tokens throughout, so the dark theme
   needs no rules of its own.

   Everything else on the page — the summary strip, the tab
   strip, the board rows, the inspector shell, the requirement
   rows, the document table — is a .delivery-* / .portfolio-*
   cell defined above, so only the shapes those cannot express
   appear here.
   ============================================================ */

/* --- Hero -------------------------------------------------------------- */

.supplier-hero {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9375rem; /* 15px */
  padding: 0.9375rem 0 1.25rem; /* 15px / 20px */
}

.supplier-hero-logo {
  width: 3.4375rem; /* 55px */
  height: 3.4375rem;
  flex-shrink: 0;
  display: grid;
  place-items: center;
}

.supplier-hero-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 1rem; /* 16px between items */
  margin-top: 0.5625rem; /* 9px */
}

.supplier-hero-meta > span {
  min-width: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
}

/* The mockup's .hero-score. It is not a summary cell: it sits inside the
   hero's flex row and carries the left edge that divides it from the title
   block. Wider than the mockup's 118px number tile because it holds a
   sentence and a Soon pill rather than a score. */
.supplier-hero-score {
  min-width: 11.25rem; /* 180px */
  display: grid;
  gap: 0.1875rem;
  padding-left: 1.25rem; /* 20px */
  border-left: 1px solid var(--ke-track);
}

.supplier-hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

/* --- Workspace (package board + inspector) ----------------------------- */

/* Not .portfolio-workspace: that grid carries a 30rem floor and, below
   860px, a fixed-height surface — both of which suit a map but would clip a
   board as long as the supplier's package list. */
.supplier-workspace {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(20rem, 0.62fr); /* 320px floor */
  overflow: hidden;
}

.supplier-workspace > :first-child {
  min-width: 0;
  border-right: 1px solid var(--ke-chrome-border);
}

/* --- Package board ----------------------------------------------------- */

.supplier-package-board { overflow-x: auto; }

/* .delivery-row's own columns belong to the site dashboard's building board,
   whose widths must not shift, so the supplier board's five arrive as a
   modifier rather than an edit. The compact variant — the overview card,
   which shares its width with the inspector — renders the first three cells
   only. */
.supplier-package-row {
  min-width: 44rem;
  grid-template-columns: minmax(0, 1.7fr) 1fr 1fr 0.9fr 0.9fr;
}

.supplier-package-row--compact {
  min-width: 26rem;
  grid-template-columns: minmax(0, 1.7fr) 1fr 1fr;
}

/* --- Package inspector stats ------------------------------------------- */

/* Two rows of two. .delivery-inspector-metrics is a single row of two, so
   its border rules cannot express the second row. */
.supplier-package-stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  margin-top: 0.5625rem;
  border-top: 1px solid var(--ke-track);
}

.supplier-package-stats > div {
  min-width: 0;
  min-height: 3.5rem; /* 56px */
  display: grid;
  align-content: center;
  padding: 0.6875rem 0.75rem; /* 11px 12px */
  border-bottom: 1px solid var(--ke-track);
}

.supplier-package-stats > div:nth-child(odd) {
  padding-left: 0;
  border-right: 1px solid var(--ke-track);
}

/* --- Four-cell metric strip (compliance and documents tabs) ------------- */

/* The page summary strip's cell (.delivery-summary-item, which brings its
   own narrow-viewport rules) in a four-column frame — .delivery-summary
   itself is fixed at five. */
.supplier-metric-strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  min-height: 5.5rem; /* 88px */
  overflow: hidden;
}

/* --- Document table ---------------------------------------------------- */

/* .delivery-doc-row's columns are the work package panel's. The supplier
   library needs room for up to three badges in its status cell and a fixed
   track for the download link. */
.supplier-doc-row { grid-template-columns: 1.7fr 1.2fr 1fr 0.8fr 5.5rem; }

/* --- Narrow viewports --------------------------------------------------- */

/* 1200px, not the 992px this used to break at: everywhere between the two the
   two columns still fit, but only by clipping the inspector's own content —
   its health badge, its acceptance pills and the golden-thread button all ran
   under the workspace's overflow:hidden edge. The hero has the same tell,
   wrapping its score tile onto the action row with a stray divider still
   drawn. Both stop at the width the inspector needs, so both break here. */
@media (max-width: 1200px) {
  /* The score tile is on its own line by now, so the edge that divided it
     from the title block has nothing left to divide. */
  .supplier-hero-score {
    padding-left: 0;
    border-left: 0;
  }

  .supplier-workspace { grid-template-columns: 1fr; }

  .supplier-workspace > :first-child {
    border-right: 0;
    border-bottom: 1px solid var(--ke-chrome-border);
  }
}

@media (max-width: 860px) {
  .supplier-metric-strip {
    grid-template-columns: repeat(4, 11.25rem); /* 180px */
    overflow-x: auto;
  }
}

/* ============================================================
   Interactive maps
   The generic map component (templates/components/map.html +
   the SilasMap module in static/js/app.js). One vocabulary for
   every map surface — portfolio pins, the site location picker,
   site plans — so no page grows map CSS of its own.

   Markers reuse the portfolio traffic-light palette rather than
   restating it: the health values are the ones the dashboard
   legend and the health column already use (on_track /
   attention / at_risk), plus a neutral --default for a project
   with no health to report.
   ============================================================ */

.silas-map {
  position: relative;
  border-radius: var(--ke-radius-card);
  overflow: hidden;
  /* Also what a map that never finishes loading looks like — a flat panel
     rather than a hole in the layout. */
  background: var(--ke-muted-surface);
}

/* --- Heights ------------------------------------------------------------ */

.silas-map--panel { height: 26.25rem; } /* 420px — dashboard/detail panels */
.silas-map--tall { height: 35rem; } /* 560px — a map that is the page */
.silas-map--input { height: 17.5rem; } /* 280px — the form-field picker */

/* --- Markers ------------------------------------------------------------ */

/* Real <button>s (see app.js): keyboard-reachable and named, so the pins are
   not a mouse-only affordance. The design mirrors the placeholder map's
   .portfolio-marker-pin, which is what users learned to read: a solid badge
   in the same RAG palette as the assurance-horizon dots, with the shared
   map-pin sprite glyph in white — a drop on a disc, not a subtle dot. */
.silas-map-marker {
  position: relative;
  width: 2rem; /* 32px */
  height: 2rem;
  padding: 0;
  display: grid;
  place-items: center;
  color: #fff; /* the glyph strokes with currentColor */
  border: 3px solid var(--bs-body-bg);
  border-radius: 999px;
  cursor: pointer;
  box-shadow: var(--ke-shadow-sm);
}

.silas-map-marker-glyph {
  width: 1.125rem; /* 18px */
  height: 1.125rem;
  pointer-events: none;
}

.silas-map-marker:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
}

/* The same theme-independent RAG values the assurance horizon and the
   portfolio legend use — a pin and its legend dot must never disagree. */
.silas-map-marker--on_track { background: var(--ke-status-on-time); }
.silas-map-marker--attention { background: var(--ke-status-running-late); }
.silas-map-marker--at_risk { background: var(--ke-status-overdue); }
.silas-map-marker--site { background: var(--bs-primary); }
/* Legend counterpart of the --site marker, for ke-dot key rows. */
.silas-map-dot--site { background: var(--bs-primary); }
.silas-map-marker--default { background: var(--ke-text-subtle); }

.silas-map-marker.is-selected {
  z-index: 2;
  width: 2.375rem; /* 38px — the selected pin steps forward, as the
                      placeholder's did */
  height: 2.375rem;
  background: var(--bs-primary);
  box-shadow: 0 0 0 0.3125rem rgba(var(--bs-primary-rgb), 0.25), var(--ke-shadow-sm);
}

/* The marker's name, as the placeholder map's chip behaved: revealed by
   hover, keyboard focus or selection — pure CSS, so it needs no JS after an
   htmx swap. pointer-events off so it never steals the marker's click. */
.silas-map-marker-chip {
  position: absolute;
  left: calc(100% + 0.5rem);
  top: 50%;
  transform: translateY(-50%);
  padding: 0.1875rem 0.5rem;
  border-radius: var(--ke-radius-control);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--ke-text);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms ease;
}

.silas-map-marker:hover .silas-map-marker-chip,
.silas-map-marker:focus-visible .silas-map-marker-chip,
.silas-map-marker.is-selected .silas-map-marker-chip {
  opacity: 1;
}

/* --- Fallbacks ---------------------------------------------------------- */

/* Hidden until the module gives up (no WebGL, a vendored file that did not
   load). The <noscript> copy of the same message is always shown, because
   with JavaScript off nothing is ever there to add the failed class. */
.silas-map-fallback {
  display: none;
  position: absolute;
  inset: 0;
  z-index: 2;
  margin: 0;
  padding: 1.5rem;
  align-items: center;
  justify-content: center;
  text-align: center;
  color: var(--ke-text-muted);
  background: var(--ke-muted-surface);
}

.silas-map--failed .silas-map-fallback,
.silas-map-fallback--noscript { display: flex; }

/* --- MapLibre chrome ---------------------------------------------------- */

/* The attribution is a licence requirement (ODbL, and OGL once Zoomstack
   lands), so it is never hidden — only sized down to the platform's fine
   print. The controls square off with everything else. */
.silas-map .maplibregl-ctrl-attrib { font-size: 0.6875rem; } /* 11px */

.silas-map .maplibregl-ctrl-group { border-radius: var(--ke-radius-control); }

/* MapLibre's chrome is a fixed light palette drawn partly with embedded SVG
   data URIs, so no token can reach its zoom glyphs. Inverting the whole
   control in dark mode is what keeps them visible without hardcoding a
   colour; hue-rotate puts the (achromatic) surface back where it started. */
[data-bs-theme="dark"] .silas-map .maplibregl-ctrl-group,
[data-bs-theme="dark"] .silas-map .maplibregl-ctrl-attrib {
  filter: invert(1) hue-rotate(180deg);
}

/* --- Zones -------------------------------------------------------------- */

/* Only rendered when the drawing tools are on (FEATURE_MAP_ZONES plus
   projects.manage_zones): the wrapper exists to position the save card over
   the map, and takes the mount's own height so it changes no layout. */
.silas-map-zone-editor {
  position: relative;
  height: 100%;
}

/* The save card — a small float in the map's bottom-left corner, clear of
   MapLibre's own chrome (zoom top-right, drawing tools top-left,
   attribution bottom-right). Hidden by the `hidden` attribute until the
   module has a drawn shape or a clicked zone to talk about. */
.silas-map-zone-form {
  position: absolute;
  left: 0.75rem;
  bottom: 0.75rem;
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: 15rem; /* 240px — a name and a type, no wider */
  padding: 0.75rem;
  border-radius: var(--ke-radius-card);
}

.silas-map-zone-form[hidden] { display: none; }

/* ============================================================
   Project command dashboard
   A visual index across programme, place, delivery, documents
   and compliance. It deliberately reuses the portfolio/delivery
   dashboard's square, compact glass language.
   ============================================================ */

.project-command-page {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding-bottom: 1.75rem;
}

.project-dashboard-single-panel { min-width: 0; }

.project-pulse-button {
  width: 100%;
  border: 0;
  border-radius: 0;
  background: rgba(var(--bs-primary-rgb), 0.035);
  font: inherit;
}

.project-pulse-button:hover { background: rgba(var(--bs-primary-rgb), 0.08); }

.project-command-heading,
.project-command-panel-header,
.project-command-actions,
.project-command-window,
.project-pulse-item,
.project-pulse-copy,
.project-outcome,
.project-programme-label,
.project-action-item,
.project-action-copy,
.project-spatial-overlay,
.project-live-indicator,
.project-scope-item,
.project-scope-copy,
.project-command-footer-link,
.project-package-legend,
.project-package-name,
.project-package-scope,
.project-compliance-hero,
.project-compliance-links a,
.project-document-name,
.project-command-empty {
  display: flex;
}

.project-command-heading {
  min-height: 5.25rem;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 0.25rem 0 0.75rem;
}

.project-command-kicker,
.project-command-section-kicker {
  color: var(--ke-brand-700);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-size: 0.6875rem;
  font-weight: 700;
}

[data-bs-theme="dark"] .project-command-kicker,
[data-bs-theme="dark"] .project-command-section-kicker { color: var(--ke-brand-300); }

.project-command-title {
  color: var(--ke-text);
  font-size: clamp(1.75rem, 2.4vw, 2.35rem);
  font-weight: 680;
  letter-spacing: -0.035em;
  line-height: 1.05;
}

.project-command-subtitle {
  color: var(--ke-text-muted);
  font-size: 0.875rem;
}

.project-command-actions { align-items: center; gap: 0.5rem; }

.project-command-panel {
  min-width: 0;
  overflow: hidden;
  background: var(--ke-surface);
}

.project-command-panel-header {
  min-height: 4.25rem;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.875rem 1rem;
  border-bottom: 1px solid var(--ke-chrome-border);
}

.project-command-panel-header h2 {
  color: var(--ke-text);
  letter-spacing: -0.018em;
}

.project-command-window { align-items: center; gap: 0.4rem; white-space: nowrap; }

.project-pulse {
  display: grid;
  grid-template-columns: 1.08fr 0.82fr 1.05fr 1.25fr 0.8fr 1.1fr;
  min-height: 6.5rem;
  overflow: hidden;
}

.project-pulse-item {
  min-width: 0;
  flex-direction: column;
  justify-content: center;
  gap: 0.2rem;
  padding: 0.85rem 1rem;
  border-right: 1px solid var(--ke-chrome-border);
  color: var(--ke-text);
}

.project-pulse-item:last-child { border-right: 0; }

.project-pulse-item--health,
.project-pulse-item--action {
  flex-direction: row;
  align-items: center;
  gap: 0.7rem;
}

.project-pulse-item--action { background: rgba(var(--bs-primary-rgb), 0.035); }
.project-pulse-item--action:hover { background: rgba(var(--bs-primary-rgb), 0.08); }

.project-pulse-copy { min-width: 0; flex-direction: column; }

.project-pulse-copy strong,
.project-pulse-value {
  color: var(--ke-text);
  font-size: 1.3rem;
  font-weight: 680;
  letter-spacing: -0.025em;
  line-height: 1.2;
}

.project-pulse-value--text {
  overflow: hidden;
  font-size: 0.92rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-pulse-icon,
.project-outcome-icon,
.project-action-icon,
.project-scope-entity-icon,
.project-spatial-pin {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
}

.project-pulse-icon {
  width: 2.25rem;
  height: 2.25rem;
  background: var(--ke-muted-surface);
  color: var(--ke-text-muted);
}

.project-pulse-icon--at_risk { color: var(--bs-danger); background: var(--bs-danger-bg-subtle); }
.project-pulse-icon--attention { color: var(--bs-warning-text-emphasis); background: var(--bs-warning-bg-subtle); }
.project-pulse-icon--on_track { color: var(--bs-success); background: var(--bs-success-bg-subtle); }

/* A pulse strip of an arbitrary count of equal-weight metrics (the
   compliance module's stat strips, e.g. assurance/overview.html) rather
   than the dashboard's fixed six asymmetric columns above (plan 09 §7). */
.project-pulse--stats { grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); }

.project-command-grid { display: grid; min-width: 0; gap: 0.75rem; }
.project-command-grid--programme { grid-template-columns: minmax(0, 2.35fr) minmax(18rem, 0.9fr); }
.project-command-grid--spatial { grid-template-columns: minmax(0, 1.72fr) minmax(20rem, 0.86fr); }
.project-command-grid--controls { grid-template-columns: minmax(0, 1.45fr) minmax(22rem, 0.9fr); }

.project-dashboard-health-body {
  display: grid;
  grid-template-columns: minmax(20rem, 0.85fr) minmax(0, 2fr);
}

.project-dashboard-health-body .project-compliance-hero {
  border-right: 1px solid var(--ke-chrome-border);
  border-bottom: 0;
}

.project-dashboard-health-body .project-guarantees { align-content: stretch; }

.project-outcomes {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  border-bottom: 1px solid var(--ke-chrome-border);
}

.project-outcome {
  position: relative;
  min-width: 0;
  min-height: 7.65rem;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 1rem;
  border-right: 1px solid var(--ke-chrome-border);
}

.project-outcome:last-child { border-right: 0; }
.project-outcome--attention { background: color-mix(in srgb, var(--bs-warning-bg-subtle) 42%, transparent); }

.project-outcome-icon { width: 2.25rem; height: 2.25rem; }

.project-outcome > div:not(.project-outcome-progress) {
  min-width: 0;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
}

.project-outcome-label {
  margin-bottom: 0.28rem;
  color: var(--ke-text-muted);
  font-size: 0.6875rem;
  font-weight: 650;
  letter-spacing: 0.075em;
  text-transform: uppercase;
}

.project-outcome strong {
  overflow: hidden;
  color: var(--ke-text);
  font-size: 0.9rem;
  line-height: 1.25;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-outcome > div > span:last-child {
  margin-top: 0.18rem;
  color: var(--ke-text-muted);
  font-size: 0.75rem;
  line-height: 1.35;
}

.project-outcome > a { margin-left: auto; color: var(--ke-text-muted); }

.project-outcome-progress {
  position: absolute;
  right: 1rem;
  bottom: 0.85rem;
  left: 1rem;
  height: 0.25rem;
  overflow: hidden;
  background: var(--ke-track);
}

.project-outcome-progress > span { display: block; height: 100%; background: var(--ke-brand-600); }
.project-outcome-progress > .project-outcome-progress--success { background: var(--bs-success); }
.project-outcome-progress > .project-outcome-progress--warning { background: var(--bs-warning); }
.project-outcome-progress > .project-outcome-progress--danger { background: var(--bs-danger); }

.project-programme-rails { min-width: 0; padding: 0.25rem 0; }

.project-programme-row {
  display: grid;
  grid-template-columns: 11.25rem minmax(0, 1fr);
  min-width: 0;
  border-bottom: 1px solid var(--ke-track);
}

.project-programme-row:last-child { border-bottom: 0; }

.project-programme-label {
  min-width: 0;
  flex-direction: column;
  justify-content: center;
  padding: 0.9rem 1rem;
  border-right: 1px solid var(--ke-chrome-border);
}

.project-programme-label strong {
  overflow: hidden;
  color: var(--ke-text);
  font-size: 0.82rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-programme-track {
  display: flex;
  min-width: 0;
  margin: 0;
  padding: 0.75rem 0.7rem 0.65rem;
  overflow-x: auto;
  list-style: none;
}

.project-programme-stage {
  position: relative;
  display: flex;
  flex: 1 0 6.75rem;
  min-width: 0;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.project-programme-stage::before {
  position: absolute;
  top: 0.64rem;
  right: 50%;
  left: -50%;
  height: 1px;
  background: var(--ke-chrome-border);
  content: "";
}

.project-programme-stage:first-child::before { display: none; }
.project-programme-stage--complete::before { background: var(--bs-success); }

.project-programme-node {
  position: relative;
  z-index: 1;
  display: inline-flex;
  width: 1.35rem;
  height: 1.35rem;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--ke-chrome-border);
  background: var(--ke-dropdown-bg);
  color: var(--ke-text-muted);
}

.project-programme-stage--complete .project-programme-node { border-color: var(--bs-success); background: var(--bs-success); color: #fff; }
.project-programme-stage--in_progress .project-programme-node { border-color: var(--ke-brand-600); box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.12); }
.project-programme-stage--overdue .project-programme-node { border-color: var(--bs-danger); background: var(--bs-danger); color: #fff; }
.project-programme-node-dot { width: 0.35rem; height: 0.35rem; background: var(--ke-brand-600); }

.project-programme-stage-copy {
  display: flex;
  width: 100%;
  min-width: 0;
  flex-direction: column;
  padding: 0.42rem 0.25rem 0;
}

.project-programme-stage-copy strong {
  display: -webkit-box;
  overflow: hidden;
  color: var(--ke-text);
  font-size: 0.69rem;
  line-height: 1.2;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.project-programme-stage-copy span { color: var(--ke-text-muted); font-size: 0.65rem; }

.project-action-centre { display: flex; flex-direction: column; }

.project-action-count {
  display: inline-grid;
  width: 1.75rem;
  height: 1.75rem;
  place-items: center;
  background: var(--ke-muted-surface);
  color: var(--ke-text-muted);
  font-size: 0.75rem;
  font-weight: 700;
}

.project-action-count.is-live { background: var(--bs-danger-bg-subtle); color: var(--bs-danger); }
.project-action-list { flex: 1 1 auto; }

.project-action-item {
  min-height: 4.4rem;
  align-items: center;
  gap: 0.65rem;
  padding: 0.72rem 0.85rem;
  border-bottom: 1px solid var(--ke-track);
  border-left: 3px solid transparent;
  color: var(--ke-text);
}

.project-action-item:hover { background: var(--ke-surface-hover); }
.project-action-item--danger { border-left-color: var(--bs-danger); }
.project-action-item--warning { border-left-color: var(--bs-warning); }
.project-action-item--primary { border-left-color: var(--ke-brand-600); }
.project-action-item--danger .project-action-icon { color: var(--bs-danger); background: var(--bs-danger-bg-subtle); }
.project-action-item--warning .project-action-icon { color: var(--bs-warning-text-emphasis); background: var(--bs-warning-bg-subtle); }
.project-action-item--primary .project-action-icon { color: var(--ke-brand-700); background: var(--bs-primary-bg-subtle); }

.project-action-icon { width: 2rem; height: 2rem; }
.project-action-copy { min-width: 0; flex: 1 1 auto; flex-direction: column; }
.project-action-copy strong { overflow: hidden; color: var(--ke-text); font-size: 0.78rem; text-overflow: ellipsis; white-space: nowrap; }
.project-action-copy > span:last-child { overflow: hidden; color: var(--ke-text-muted); font-size: 0.68rem; text-overflow: ellipsis; white-space: nowrap; }
.project-action-eyebrow { color: var(--ke-text-subtle); font-size: 0.6rem; font-weight: 700; letter-spacing: 0.075em; text-transform: uppercase; }
.project-action-chevron { flex: 0 0 auto; color: var(--ke-text-subtle); }

.project-action-clear {
  display: grid;
  min-height: 18rem;
  padding: 2rem;
  place-content: center;
  justify-items: center;
  text-align: center;
}

.project-action-clear > span { display: grid; width: 3rem; height: 3rem; margin-bottom: 0.75rem; place-items: center; background: var(--bs-success-bg-subtle); color: var(--bs-success); }
.project-action-clear p { max-width: 17rem; margin: 0.25rem 0 0; color: var(--ke-text-muted); font-size: 0.75rem; }

.project-command-footer-link {
  min-height: 2.75rem;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.7rem 0.9rem;
  border-top: 1px solid var(--ke-chrome-border);
  color: var(--bs-link-color);
  font-size: 0.75rem;
  font-weight: 650;
  text-decoration: none;
}

.project-command-footer-link:hover { background: var(--ke-surface-hover); }

.project-spatial-header { min-height: 4.25rem; }
.project-spatial-stage { position: relative; height: 27rem; overflow: hidden; background: #11191f; }
.project-spatial-model { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 49%; }
.project-spatial-map { position: absolute; inset: 0; }

.project-spatial-overlay {
  position: absolute;
  z-index: 3;
  align-items: center;
  gap: 0.6rem;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(15, 22, 31, 0.84);
  color: rgba(255, 255, 255, 0.82);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.22);
}

.project-spatial-overlay--top { top: 0.75rem; left: 0.75rem; padding: 0.42rem 0.6rem; font-size: 0.66rem; }
.project-spatial-overlay--bottom { right: 0.75rem; bottom: 0.75rem; left: 0.75rem; min-height: 4.1rem; padding: 0.65rem 0.75rem; }
.project-spatial-overlay--bottom > span:nth-child(2) { display: flex; min-width: 0; flex: 1 1 auto; flex-direction: column; }
.project-spatial-overlay--bottom strong { color: #fff; font-size: 0.82rem; }
.project-spatial-overlay--bottom span span { overflow: hidden; font-size: 0.7rem; text-overflow: ellipsis; white-space: nowrap; }
.project-spatial-overlay--bottom a { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 0.35rem; color: #fff; font-size: 0.7rem; font-weight: 650; text-decoration: none; }
.project-spatial-detail-button { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 0.35rem; padding: 0.35rem 0.45rem; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.06); color: #fff; font-size: 0.7rem; font-weight: 650; }
.project-spatial-detail-button:hover { background: rgba(255, 255, 255, 0.14); }
.project-spatial-pin { width: 2.25rem; height: 2.25rem; background: rgba(12, 142, 235, 0.22); color: #7cc6fd; }
.project-live-indicator { gap: 0.35rem; color: #fff; font-weight: 650; }
.project-live-indicator > span:first-child { width: 0.42rem; height: 0.42rem; flex: 0 0 auto; background: #10b981; box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.18); }

.project-scope { display: flex; min-height: 31.25rem; flex-direction: column; }
.project-scope-tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--ke-chrome-border); }
.project-scope-tabs button { min-width: 0; padding: 0.7rem 0.45rem; border: 0; border-right: 1px solid var(--ke-track); border-bottom: 2px solid transparent; background: transparent; color: var(--ke-text-muted); font-size: 0.7rem; font-weight: 650; }
.project-scope-tabs button:last-child { border-right: 0; }
.project-scope-tabs button.is-active { border-bottom-color: var(--ke-brand-600); background: rgba(var(--bs-primary-rgb), 0.045); color: var(--ke-brand-700); }
[data-bs-theme="dark"] .project-scope-tabs button.is-active { color: var(--ke-brand-300); }
.project-scope-tabs button span { margin-left: 0.2rem; color: var(--ke-text-subtle); font-size: 0.63rem; }
.project-scope-list { display: flex; min-height: 0; flex: 1 1 auto; flex-direction: column; }

.project-scope-item {
  min-height: 4.3rem;
  align-items: center;
  gap: 0.62rem;
  padding: 0.65rem 0.8rem;
  border-bottom: 1px solid var(--ke-track);
  color: var(--ke-text);
}

.project-scope-item:hover { background: var(--ke-surface-hover); }
.project-scope-select { width: 100%; border: 0; border-left: 3px solid transparent; background: transparent; font: inherit; }
.project-scope-select.is-selected { border-left-color: var(--ke-brand-600); background: rgba(var(--bs-primary-rgb), 0.065); }
.project-scope-entity-icon { width: 2rem; height: 2rem; background: var(--bs-primary-bg-subtle); color: var(--ke-brand-700); }
[data-bs-theme="dark"] .project-scope-entity-icon { color: var(--ke-brand-300); }
.project-scope-copy { min-width: 0; flex: 1 1 auto; flex-direction: column; }
.project-scope-copy strong { overflow: hidden; color: var(--ke-text); font-size: 0.76rem; text-overflow: ellipsis; white-space: nowrap; }
.project-scope-copy span { overflow: hidden; color: var(--ke-text-muted); font-size: 0.67rem; text-overflow: ellipsis; white-space: nowrap; }
.project-scope-health { width: 0.55rem; height: 0.55rem; flex: 0 0 auto; border-radius: 50%; background: var(--ke-text-subtle); }
.project-scope-health--at_risk { background: var(--bs-danger); }
.project-scope-health--attention { background: var(--bs-warning); }
.project-scope-health--on_track { background: var(--bs-success); }
.project-scope-hrb { padding: 0.12rem 0.28rem; background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis); font-size: 0.56rem; font-weight: 700; letter-spacing: 0.06em; }
.project-scope-empty { padding: 2rem 1rem; color: var(--ke-text-muted); font-size: 0.75rem; text-align: center; }
.project-scope-list > .project-command-footer-link { margin-top: auto; }

.project-package-summary { padding: 0.9rem 1rem; border-bottom: 1px solid var(--ke-chrome-border); }
.project-package-segments { display: flex; height: 0.45rem; overflow: hidden; background: var(--ke-track); }
.project-package-segments span { display: block; height: 100%; }
.project-package-segments .is-complete,
.project-package-legend .is-complete { background: var(--bs-success); }
.project-package-segments .is-progress,
.project-package-legend .is-progress { background: var(--ke-brand-600); }
.project-package-segments .is-hold,
.project-package-legend .is-hold { background: var(--bs-warning); }
.project-package-segments .is-not-started,
.project-package-legend .is-not-started { background: var(--ke-text-subtle); }
.project-package-segments .is-empty { background: var(--ke-muted-surface); }
.project-package-legend .is-overdue { background: var(--bs-danger); }

.project-package-legend {
  flex-wrap: wrap;
  align-items: center;
  gap: 0.55rem 1rem;
  margin-top: 0.62rem;
  color: var(--ke-text-muted);
  font-size: 0.67rem;
}

.project-package-legend > span { display: inline-flex; align-items: center; gap: 0.28rem; }
.project-package-legend i { width: 0.42rem; height: 0.42rem; border-radius: 50%; }
.project-package-legend strong { color: var(--ke-text); }

.project-package-table { min-width: 0; }
.project-package-row { display: grid; grid-template-columns: minmax(9rem, 1.2fr) minmax(9rem, 1fr) 7rem 5.5rem 1.25rem; min-height: 3.9rem; align-items: center; gap: 0.75rem; padding: 0.55rem 1rem; border-bottom: 1px solid var(--ke-track); color: var(--ke-text); }
.project-preview-row { width: 100%; border-top: 0; border-right: 0; border-left: 0; border-radius: 0; background: transparent; font: inherit; }
.project-package-row:hover { background: var(--ke-surface-hover); }
.project-package-row.is-overdue { box-shadow: inset 3px 0 0 var(--bs-danger); }
.project-package-row--head { min-height: 2.25rem; background: var(--ke-muted-surface); color: var(--ke-text-muted); font-size: 0.63rem; font-weight: 650; letter-spacing: 0.04em; text-transform: uppercase; }
.project-package-name,
.project-package-scope { min-width: 0; flex-direction: column; }
.project-package-name strong,
.project-package-scope strong { overflow: hidden; color: var(--ke-text); font-size: 0.72rem; text-overflow: ellipsis; white-space: nowrap; }
.project-package-name span,
.project-package-scope span { overflow: hidden; color: var(--ke-text-muted); font-size: 0.66rem; text-overflow: ellipsis; white-space: nowrap; }
.project-package-date { color: var(--ke-text-muted); font-size: 0.7rem; }

.project-compliance-hero { align-items: stretch; border-bottom: 1px solid var(--ke-chrome-border); }
.project-compliance-hero > div:first-child { display: flex; min-width: 9.25rem; flex-direction: column; justify-content: center; padding: 1rem; border-right: 1px solid var(--ke-chrome-border); }
.project-compliance-hero > div:first-child > span:last-child { color: var(--ke-text-muted); font-size: 0.66rem; }
.project-compliance-score { color: var(--ke-text); font-size: 2rem; font-weight: 700; letter-spacing: -0.05em; line-height: 1; }
.project-compliance-facts { display: grid; flex: 1 1 auto; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.project-compliance-facts span { display: flex; flex-direction: column; justify-content: center; padding: 0.65rem 0.8rem; border-right: 1px solid var(--ke-track); border-bottom: 1px solid var(--ke-track); color: var(--ke-text-muted); font-size: 0.65rem; }
.project-compliance-facts span:nth-child(2n) { border-right: 0; }
.project-compliance-facts span:nth-last-child(-n+2) { border-bottom: 0; }
.project-compliance-facts strong { color: var(--ke-text); font-size: 1rem; }

.project-guarantees { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.project-guarantee { padding: 0.78rem 0.9rem; border-right: 1px solid var(--ke-track); border-bottom: 1px solid var(--ke-track); }
.project-guarantee:nth-child(2n) { border-right: 0; }
.project-guarantee-heading { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; font-size: 0.7rem; }
.project-guarantee-track { height: 0.25rem; margin: 0.45rem 0; overflow: hidden; background: var(--ke-track); }
.project-guarantee-track span { display: block; height: 100%; }
.project-guarantee p { margin: 0; color: var(--ke-text-muted); font-size: 0.62rem; line-height: 1.35; }

.project-compliance-links { display: grid; grid-template-columns: repeat(3, 1fr); }
.project-compliance-links a { min-width: 0; align-items: center; gap: 0.45rem; padding: 0.7rem; border-right: 1px solid var(--ke-track); color: var(--ke-text); text-decoration: none; }
.project-compliance-links a:last-child { border-right: 0; }
.project-compliance-links a:hover { background: var(--ke-surface-hover); }
.project-compliance-links a > span { display: flex; min-width: 0; flex: 1 1 auto; flex-direction: column; }
.project-compliance-links strong { overflow: hidden; font-size: 0.68rem; text-overflow: ellipsis; white-space: nowrap; }
.project-compliance-links small { overflow: hidden; color: var(--ke-text-muted); font-size: 0.59rem; text-overflow: ellipsis; white-space: nowrap; }

.project-document-upload {
  padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--ke-chrome-border);
  background: color-mix(in srgb, var(--ke-muted-surface) 58%, transparent);
}
.project-document-upload #upload-zone {
  display: grid !important;
  grid-template-columns: minmax(18rem, 1.4fr) minmax(24rem, 1fr);
  align-items: center;
  gap: 0.75rem !important;
}
.project-document-upload .file-drop {
  border-width: 1px;
  border-radius: 0;
  background: var(--ke-surface);
}
.project-document-upload .file-drop-face { min-height: 3rem; }
.project-document-upload #upload-zone > .d-flex {
  flex-wrap: nowrap !important;
  justify-content: flex-end !important;
}
.project-document-upload #document-upload-hint { max-width: 22rem; }
.project-document-upload #upload-zone > .alert { grid-column: 1 / -1; }
.project-document-layout { display: grid; grid-template-columns: minmax(14rem, 0.78fr) minmax(0, 2.5fr); }
.project-document-stats { display: grid; grid-template-columns: repeat(2, 1fr); border-right: 1px solid var(--ke-chrome-border); }
.project-document-stats > div { display: flex; min-height: 5.6rem; flex-direction: column; justify-content: center; padding: 0.8rem 1rem; border-right: 1px solid var(--ke-track); border-bottom: 1px solid var(--ke-track); }
.project-document-stats > div:nth-child(2n) { border-right: 0; }
.project-document-stats > div:nth-last-child(-n+2) { border-bottom: 0; }
.project-document-stats span { color: var(--ke-text-muted); font-size: 0.67rem; }
.project-document-stats strong { color: var(--ke-text); font-size: 1.4rem; line-height: 1.2; }
.project-document-stats small { color: var(--ke-text-subtle); font-size: 0.6rem; }
.project-document-list { min-width: 0; }
.project-document-row { display: grid; grid-template-columns: minmax(12rem, 1.7fr) minmax(7rem, 0.75fr) 7.5rem 6rem 1.2rem; min-height: 3.6rem; align-items: center; gap: 0.7rem; padding: 0.45rem 0.9rem; border-bottom: 1px solid var(--ke-track); color: var(--ke-text); font-size: 0.7rem; }
.project-document-row:last-child { border-bottom: 0; }
.project-document-row:hover { background: var(--ke-surface-hover); }
.project-document-row--head { min-height: 2.2rem; background: var(--ke-muted-surface); color: var(--ke-text-muted); font-size: 0.61rem; font-weight: 650; letter-spacing: 0.04em; text-transform: uppercase; }
.project-document-name { min-width: 0; align-items: center; gap: 0.55rem; }
.project-document-name i { display: grid; width: 1.8rem; height: 1.8rem; flex: 0 0 auto; place-items: center; background: var(--bs-primary-bg-subtle); color: var(--ke-brand-700); font-style: normal; }
.project-document-name > span { display: flex; min-width: 0; flex-direction: column; }
.project-document-name strong,
.project-document-name small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-document-name small { color: var(--ke-text-muted); font-size: 0.62rem; }

.project-command-empty { min-height: 7rem; align-items: center; gap: 0.8rem; padding: 1rem; color: var(--ke-text-muted); }
.project-command-empty > div { display: flex; min-width: 0; flex: 1 1 auto; flex-direction: column; }
.project-command-empty strong { color: var(--ke-text); font-size: 0.78rem; }
.project-command-empty span { font-size: 0.7rem; }
.project-command-empty--compact { min-height: 5rem; }

.project-command-page a:focus-visible,
.project-command-page button:focus-visible { outline: 3px solid var(--bs-focus-ring-color); outline-offset: -2px; }

@media (max-width: 1399.98px) {
  .project-pulse { grid-template-columns: repeat(3, 1fr); }
  .project-pulse-item:nth-child(3) { border-right: 0; }
  .project-pulse-item:nth-child(-n+3) { border-bottom: 1px solid var(--ke-chrome-border); }
  .project-command-grid--programme { grid-template-columns: minmax(0, 1.9fr) minmax(18rem, 0.85fr); }
  .project-outcome { min-height: 8.4rem; }
}

@media (max-width: 1199.98px) {
  .project-command-grid--programme,
  .project-command-grid--spatial,
  .project-command-grid--controls { grid-template-columns: 1fr; }
  .project-action-centre { min-height: auto; }
  .project-spatial-stage { height: 24rem; }
  .project-scope { min-height: 0; }
  .project-scope-list { min-height: 14rem; }
  .project-dashboard-health-body { grid-template-columns: 1fr; }
  .project-dashboard-health-body .project-compliance-hero { border-right: 0; border-bottom: 1px solid var(--ke-chrome-border); }
  .project-document-upload #upload-zone { grid-template-columns: 1fr; }
  .project-document-upload #upload-zone > .d-flex { justify-content: space-between !important; }
  .project-document-layout { grid-template-columns: 1fr; }
  .project-document-stats { grid-template-columns: repeat(4, 1fr); border-right: 0; border-bottom: 1px solid var(--ke-chrome-border); }
  .project-document-stats > div { border-right: 1px solid var(--ke-track); border-bottom: 0; }
  .project-document-stats > div:nth-child(2n) { border-right: 1px solid var(--ke-track); }
  .project-document-stats > div:last-child { border-right: 0; }
}

@media (max-width: 767.98px) {
  .project-command-heading { align-items: flex-start; flex-direction: column; }
  .project-command-actions { width: 100%; }
  .project-command-actions .btn { flex: 1 1 0; justify-content: center; }
  .project-pulse { grid-template-columns: repeat(2, 1fr); }
  .project-pulse-item:nth-child(3) { border-right: 1px solid var(--ke-chrome-border); }
  .project-pulse-item:nth-child(2n) { border-right: 0; }
  .project-pulse-item:nth-child(-n+4) { border-bottom: 1px solid var(--ke-chrome-border); }
  .project-outcomes { grid-template-columns: 1fr; }
  .project-outcome { min-height: 7.2rem; border-right: 0; border-bottom: 1px solid var(--ke-chrome-border); }
  .project-outcome:last-child { border-bottom: 0; }
  .project-programme-row { grid-template-columns: 1fr; }
  .project-programme-label { border-right: 0; border-bottom: 1px solid var(--ke-track); }
  .project-command-window { display: none; }
  .project-spatial-header { align-items: flex-start; flex-direction: column; }
  .project-spatial-stage { height: 21rem; }
  .project-spatial-overlay--bottom a { display: none; }
  .project-spatial-detail-button { padding: 0.35rem; }
  .project-package-row { grid-template-columns: minmax(0, 1fr) 6.5rem 1.25rem; }
  .project-package-row > :nth-child(2),
  .project-package-row > :nth-child(4) { display: none; }
  .project-compliance-hero { flex-direction: column; }
  .project-compliance-hero > div:first-child { border-right: 0; border-bottom: 1px solid var(--ke-chrome-border); }
  .project-compliance-links { grid-template-columns: 1fr; }
  .project-compliance-links a { border-right: 0; border-bottom: 1px solid var(--ke-track); }
  .project-document-upload { padding: 0.65rem; }
  .project-document-upload #upload-zone > .d-flex { align-items: flex-start !important; flex-direction: column; }
  .project-document-stats { grid-template-columns: repeat(2, 1fr); }
  .project-document-stats > div { border-bottom: 1px solid var(--ke-track); }
  .project-document-stats > div:nth-last-child(-n+2) { border-bottom: 0; }
  .project-document-row { grid-template-columns: minmax(0, 1fr) 7.25rem 1.2rem; }
  .project-document-row > :nth-child(2),
  .project-document-row > :nth-child(4) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .project-command-page * { scroll-behavior: auto; transition: none !important; }
}

/* ============================================================
   3D model viewer
   The generic viewer component (templates/components/model_viewer.html
   + the SilasModelViewer module in static/js/app.js). One
   vocabulary for every model surface — the building Model tab,
   a work package's elements, the evidence timeline — so no page
   grows viewer CSS of its own.

   Organised like the map block above it: the mount, its heights,
   the chrome the module drives, the fallbacks, then the dark
   theme. The renderer paints the canvas itself; everything here
   is the furniture around it.
   ============================================================ */

.silas-viewer {
  position: relative;
  border-radius: var(--ke-radius-card);
  overflow: hidden;
  /* Also what a model that never finishes loading looks like — a flat panel
     rather than a hole in the layout. */
  background: var(--ke-muted-surface);
  /* The emissive tint the module puts on highlighted elements. A token rather
     than a literal so a surface can retune it, and so the two themes can
     differ: the light value is lost against a pale canvas in dark mode. */
  --ke-viewer-highlight: var(--ke-brand-500);
}

/* --- Heights ------------------------------------------------------------ */

.silas-viewer--panel { height: 26.25rem; } /* 420px — detail-page panels */
.silas-viewer--tall { height: 35rem; } /* 560px — a viewer that is the page */

/* The canvas host: the module appends the renderer's <canvas> here, so the
   toolbar and the label stay siblings of it rather than children of a box
   three.js owns. */
.silas-viewer-canvas {
  position: absolute;
  inset: 0;
}

.silas-viewer-canvas canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* --- Chrome ------------------------------------------------------------- */

/* The toolbar floats over the model's top edge, squared off like every other
   control surface. It is rendered by the component, not built in JS, so it is
   there and readable before three.js has finished loading. */
.silas-viewer-toolbar {
  position: absolute;
  top: 0.75rem;
  left: 0.75rem;
  right: 0.75rem;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
  padding: 0.375rem;
}

.silas-viewer-tool-group {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

/* An axis button holds its state while that plane is the live cut. */
.silas-viewer-toolbar .btn.is-active {
  background: rgba(var(--bs-primary-rgb), 0.16);
  color: var(--bs-primary);
}

.silas-viewer-section-offset {
  width: 8rem;
}

/* The picked element's name, over the model — an overlay card in the corner
   opposite the toolbar so the two never collide. pointer-events off so the
   card can never swallow the next pick. */
.silas-viewer-label {
  position: absolute;
  right: 0.75rem;
  bottom: 0.75rem;
  z-index: 2;
  max-width: 16rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  pointer-events: none;
  padding: 0.5625rem 0.6875rem; /* 9px 11px */
  border-left: 3px solid var(--bs-primary) !important;
}

.silas-viewer-label[hidden] { display: none; }

/* --- List rows ---------------------------------------------------------- */

/* Any row wired to a viewer (plan 05 §6.3). The flash is what says a model
   pick landed on a row that was already on screen; the rule stays with the
   selected row afterwards, so the answer survives the animation. */
[data-viewer-highlight] {
  cursor: pointer;
}

@keyframes silas-viewer-row-flash {
  0% {
    background-color: rgba(var(--bs-primary-rgb), 0.24);
  }
  100% {
    /* Ends on the resting tint the rule below holds, so the flash eases into
       the persistent selection colour instead of snapping to it at 1.2s. */
    background-color: rgba(var(--bs-primary-rgb), 0.08);
  }
}

[data-viewer-highlight].is-active {
  animation: silas-viewer-row-flash 1.2s ease-out 1;
  box-shadow: inset 3px 0 0 var(--bs-primary);
  /* The flash ends after 1.2s and the selection does not: a faint tint keeps
     the row that owns the highlighted element identifiable in a long list.
     rgba over the token reads on both themes, which a fixed colour would not. */
  background-color: rgba(var(--bs-primary-rgb), 0.08);
}

@media (prefers-reduced-motion: reduce) {
  [data-viewer-highlight].is-active { animation: none; }
}

/* --- Element groups ----------------------------------------------------- */

/* The storey/class accordion beside the viewer
   (templates/bim/partials/element_list.html). Native <details>, so all that is
   left for CSS is what utility classes cannot say: the gap between stacked
   groups, and a summary that reads as something to click. */
.silas-element-group + .silas-element-group {
  margin-top: 0.5rem;
}

.silas-element-group > summary {
  cursor: pointer;
}

/* A flex summary loses its marker in Chrome and Firefox but not in Safari —
   dropped there too, so the three engines render the same row. */
.silas-element-group > summary::-webkit-details-marker { display: none; }

/* --- Fallbacks ---------------------------------------------------------- */

/* Hidden until the module gives up (no WebGL, a vendored file or a GLB that
   did not load). The <noscript> copy of the same message is always shown,
   because with JavaScript off nothing is ever there to add the failed class. */
.silas-viewer-fallback {
  display: none;
  position: absolute;
  inset: 0;
  z-index: 3;
  margin: 0;
  padding: 1.5rem;
  align-items: center;
  justify-content: center;
  text-align: center;
  color: var(--ke-text-muted);
  background: var(--ke-muted-surface);
}

.silas-viewer--failed .silas-viewer-fallback,
.silas-viewer-fallback--noscript { display: flex; }

/* A failed mount keeps nothing but its message: the toolbar would offer tools
   for a model that is not there. */
.silas-viewer--failed .silas-viewer-toolbar,
.silas-viewer--failed .silas-viewer-label { display: none; }

/* --- Dark theme --------------------------------------------------------- */

/* The lighter brand tone: the mid-blue emissive of the light theme reads as
   grey against the dark canvas the scene background takes. */
[data-bs-theme="dark"] .silas-viewer {
  --ke-viewer-highlight: var(--ke-brand-300);
}
