/* ============================================
   SAHAJ COOLING ERP — Design System
   Light sidebar, Shadcn/Radix-inspired layout
   ============================================ */

:root {
  /* shadcn CSS variables (used by login page) */
  --radius: 0.65rem;
  --background: hsl(0 0% 100%);
  --foreground: hsl(222.2 84% 4.9%);
  --primary: hsl(221.2 83.2% 53.3%);
  --primary-foreground: hsl(210 40% 98%);
  --muted-foreground: hsl(215.4 16.3% 46.9%);
  --accent: hsl(210 40% 96.1%);
  --accent-foreground: hsl(222.2 47.4% 11.2%);
  --destructive: hsl(0 84.2% 60.2%);
  --border: hsl(214.3 31.8% 91.4%);
  --input: hsl(214.3 31.8% 91.4%);
  --ring: hsl(221.2 83.2% 53.3%);

  /* Brand Colors */
  --s-primary: #2C4190;
  --s-primary-light: #3a56b0;
  --s-primary-dark: #1e2d66;
  --s-accent: #E42626;
  --s-accent-light: #ff4444;
  --s-accent-dark: #b81e1e;

  /* Semantic Colors */
  --s-success: #2e7d32;
  --s-success-light: #e8f5e9;
  --s-warning: #ed6c02;
  --s-warning-light: #fff3e0;
  --s-error: #d32f2f;
  --s-error-light: #ffebee;
  --s-info: #0288d1;
  --s-info-light: #e1f5fe;

  /* Neutrals */
  --s-bg: #f5f5f5;
  --s-surface: #ffffff;
  --s-border: #e0e0e0;
  --s-text: #212121;
  --s-text-secondary: #666666;
  --s-text-disabled: #9e9e9e;
  --s-hover: #f0f0f0;
  --s-selected: #e8eaf6;

  /* Sidebar — light theme tokens */
  --s-sidebar-width: 16rem;
  --s-sidebar-collapsed: 3rem;
  --s-sidebar-bg: #E3F2FD;
  --s-sidebar-text: #222222;
  --s-sidebar-text-muted: rgba(34,34,34,0.5);
  --s-sidebar-accent: #d6e4f0;
  --s-sidebar-accent-fg: #1e3a5f;
  --s-sidebar-border: #e8e8e8;
  --s-sidebar-ring: rgba(44,65,144,0.3);
  --s-header-height: 4rem;

  /* Spacing */
  --s-xs: 4px;
  --s-sm: 8px;
  --s-md: 16px;
  --s-lg: 24px;
  --s-xl: 32px;
  --s-2xl: 48px;

  /* ---- Safe area and the end-of-page gutter ----
     --s-safe-bottom is whatever the device reserves at the bottom of the screen: the
     home indicator on an iPhone, the gesture bar on Android. It reads 0 on a desktop and
     on any phone without one, so everything built on it is inert there. It only reports a
     real value because App.razor asks for viewport-fit=cover.

     --s-page-bottom-gutter is the single place the "room after the last thing on the page"
     is set. .s-content spends it, the phone breakpoint below raises it, and any future
     scroll container that ends a page should use it rather than inventing its own number.
     Deliberately *not* spacing between components — only after the whole page. */
  --s-safe-bottom: env(safe-area-inset-bottom, 0px);
  --s-page-bottom-gutter: calc(var(--s-md) + var(--s-safe-bottom));

  /* Typography */
  --s-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --s-font-mono: 'JetBrains Mono', 'Consolas', monospace;

  /* Shadows */
  --s-shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --s-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
  --s-shadow-md: 0 4px 6px rgba(0,0,0,0.1);
  --s-shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
  --s-shadow-inset: 0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04);

  /* Radius */
  --s-radius: 0.625rem;
  --s-radius-sm: 0.375rem;
  --s-radius-md: 0.625rem;
  --s-radius-lg: 0.75rem;
  --s-radius-xl: 1rem;
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { margin: 0; height: 100%; overflow: hidden; }
html { font-size: 16px; -webkit-tap-highlight-color: transparent; }
body {
  font-family: var(--s-font); color: var(--s-text); background: var(--s-bg);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
a { color: var(--s-primary); text-decoration: none; transition: color 0.15s ease; }
a:hover { text-decoration: underline; }

/* ---- Typography ---- */
h1 { font-size: 1.75rem; font-weight: 600; }
h2 { font-size: 1.5rem; font-weight: 600; }
h3 { font-size: 1.25rem; font-weight: 600; }
h4 { font-size: 1.125rem; font-weight: 600; }
h5 { font-size: 1rem; font-weight: 600; }
h6 { font-size: 0.875rem; font-weight: 600; }
.text-caption { font-size: 0.75rem; color: var(--s-text-secondary); }
.text-body2 { font-size: 0.875rem; }
.text-overline { font-size: 0.625rem; text-transform: uppercase; letter-spacing: 1px; color: var(--s-text-secondary); }

/* ---- Grid System (12-col) ---- */
.s-grid { display: grid; gap: var(--s-md); }
.s-grid-2 { grid-template-columns: repeat(2, 1fr); }
.s-grid-3 { grid-template-columns: repeat(3, 1fr); }
.s-grid-4 { grid-template-columns: repeat(4, 1fr); }
.s-grid-6 { grid-template-columns: repeat(6, 1fr); }
.s-grid-12 { grid-template-columns: repeat(12, 1fr); }
.s-col-1 { grid-column: span 1; }
.s-col-2 { grid-column: span 2; }
.s-col-3 { grid-column: span 3; }
.s-col-4 { grid-column: span 4; }
.s-col-6 { grid-column: span 6; }
.s-col-8 { grid-column: span 8; }
.s-col-12 { grid-column: span 12; }
@media (max-width: 768px) {
  .s-grid-2, .s-grid-3, .s-grid-4, .s-grid-6 { grid-template-columns: 1fr; }
  .s-col-1, .s-col-2, .s-col-3, .s-col-4, .s-col-6, .s-col-8 { grid-column: span 1; }
}

/* ---- Flex Utilities ---- */
.s-stack { display: flex; flex-direction: column; gap: var(--s-md); }
.s-row { display: flex; flex-direction: row; gap: var(--s-md); align-items: center; }
.s-row-between { display: flex; justify-content: space-between; align-items: center; }
.s-gap-sm { gap: var(--s-sm); }
.s-gap-xs { gap: var(--s-xs); }
.s-wrap { flex-wrap: wrap; }

/* ---- Spacing ---- */
.s-p-sm { padding: var(--s-sm); }
.s-p-md { padding: var(--s-md); }
.s-p-lg { padding: var(--s-lg); }
.s-p-xl { padding: var(--s-xl); }
.s-mb-sm { margin-bottom: var(--s-sm); }
.s-mb-md { margin-bottom: var(--s-md); }
.s-mb-lg { margin-bottom: var(--s-lg); }
.s-mt-md { margin-top: var(--s-md); }

/* ---- Buttons ---- */
.s-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 16px; border: 1px solid transparent;
  font-family: var(--s-font); font-size: 0.875rem; font-weight: 500;
  cursor: pointer; transition: background 0.15s, color 0.15s; line-height: 1.4;
  background: var(--s-primary); color: #fff; border-radius: var(--s-radius);
}
.s-btn:hover { background: var(--s-primary-light); }
.s-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.s-btn--outline { background: transparent; color: var(--s-primary); border-color: var(--s-primary); }
.s-btn--outline:hover { background: var(--s-selected); }
.s-btn--text { background: transparent; color: var(--s-primary); border: none; padding: 8px 12px; }
.s-btn--text:hover { background: var(--s-hover); }
.s-btn--danger { background: var(--s-error); }
.s-btn--danger:hover { background: var(--s-accent-dark); }
.s-btn--text.s-btn--danger { background: transparent; color: var(--s-error); }
.s-btn--text.s-btn--danger:hover { background: color-mix(in srgb, var(--s-error) 10%, transparent); color: var(--s-error); }
.s-btn--success { background: var(--s-success); }
.s-btn--sm { padding: 4px 10px; font-size: 0.8125rem; }
.s-btn--icon { padding: 6px; min-width: 32px; justify-content: center; }

/* ---- Autocomplete ---- */
.s-autocomplete { position: relative; }
.s-autocomplete-dropdown {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 1050;
  max-height: 220px; overflow-y: auto;
  background: #fff; border: 1px solid var(--s-border);
  border-radius: var(--s-radius); box-shadow: var(--s-shadow-md);
  margin-top: 2px;
}
.s-autocomplete-option {
  padding: 8px 12px; font-size: 0.84rem; cursor: pointer;
  transition: background 0.1s ease; background: #fff;
}
.s-autocomplete-option:hover, .s-autocomplete-option--active { background: #e8f0fe; }

/* Allow dropdowns/autocomplete to escape parent clipping — only when open */
td:has(.s-autocomplete), td:has(.s-multiselect), td:has(.s-custom-date) { overflow: visible !important; }
.s-table:has(.s-autocomplete), .s-table-edit:has(.s-autocomplete), .s-table:has(.s-multiselect), .s-table-edit:has(.s-multiselect) { overflow: visible !important; }
/* SSelect (`.s-custom-select-*`) renders its dropdown as position:fixed with
   JS-computed coordinates, so it doesn't need overflow:visible on ancestors.
   Removing the s-custom-select-backdrop entries here keeps the list page's
   sticky pagination contained when an inline status SSelect opens in a row. */
/* SAutocomplete now positions its list the same way, so .s-autocomplete-dropdown is
   deliberately NOT in the two rules below any more.
   Flipping a scroll pane to overflow:visible resets its scrollLeft to 0. On a phone that
   was a real bug: swipe a BOD line grid right to reach the IDU picker, tap it, and the
   pane jumped back to column one — carrying the list that had just opened off screen with
   it (measured: scrollLeft 220 → 0, dropdown at left 452 in a 360px viewport). Harmless on
   a desktop only because the grid fits and there is no scroll position to lose.
   .s-multiselect stays: its dropdown is still absolutely positioned, and it is the same
   trap waiting for whoever puts one in a wide grid. */
.s-table-wrap:has(.s-multiselect-backdrop) { overflow: visible !important; }
.s-card:has(.s-multiselect-backdrop) { overflow: visible !important; }
.lead-panel:has(.s-autocomplete), .lead-panel:has(.s-multiselect), .lead-panel:has(.s-custom-date) { overflow: visible !important; }
.lead-panel-body:has(.s-autocomplete), .lead-panel-body:has(.s-multiselect), .lead-panel-body:has(.s-custom-date) { overflow: visible !important; }
/* ---- Custom Themed Dropdowns ---- */
/* The scrollbar is hidden three ways because no single property covers every engine:
   scrollbar-width (Firefox, and Safari 18.2+), -ms-overflow-style (legacy Edge) and the
   ::-webkit-scrollbar rule below (Chrome, older Safari, Samsung Internet). Linters flag
   scrollbar-width as unsupported in Safari — the webkit rule is what covers it there, so
   don't drop either one. */
.s-multiselect, .s-custom-select { position: relative; }
.s-multiselect-dropdown, .s-custom-select-dropdown { position: absolute; top: 100%; left: 0; right: 0; z-index: 1050; max-height: 200px; overflow-y: auto; background: #fff; border: 1px solid var(--s-border); border-radius: var(--s-radius); box-shadow: var(--s-shadow-md); margin-top: 2px; padding: 0; scrollbar-width: none; -ms-overflow-style: none; }
.s-multiselect-dropdown::-webkit-scrollbar, .s-custom-select-dropdown::-webkit-scrollbar { display: none; }
.s-multiselect-backdrop, .s-custom-select-backdrop { position: fixed; inset: 0; z-index: 1049; }
.s-custom-select-trigger { display: flex; align-items: center; justify-content: space-between; gap: 6px; cursor: pointer; padding: 8px 12px; border: 1px solid var(--s-border); font-family: var(--s-font); font-size: 0.875rem; background: var(--s-surface); color: var(--s-text); border-radius: var(--s-radius); transition: border-color 0.15s, box-shadow 0.15s; width: 100%; -webkit-user-select: none; user-select: none; min-height: 38px; }
.s-custom-select-trigger > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.s-custom-select-trigger:hover { border-color: #bbb; }
.s-custom-select-trigger.open { border-color: var(--s-primary); box-shadow: 0 0 0 2px rgba(44,65,144,0.15); }
.s-custom-select-trigger svg { width: 14px; height: 14px; color: var(--s-text-secondary); flex-shrink: 0; transition: transform 0.15s; }
.s-custom-select-trigger.open svg { transform: rotate(180deg); }
.s-custom-select-option { display: flex; align-items: center; gap: 8px; padding: 7px 12px; cursor: pointer; font-size: 0.84rem; color: var(--s-text); transition: background 0.1s; }
.s-custom-select-option:hover { background: var(--s-hover); }
.s-custom-select-option.selected { background: var(--s-selected); color: var(--s-primary); font-weight: 500; }
.s-custom-select-option .check-icon { width: 14px; height: 14px; color: var(--s-primary); }

/* ---- Custom Themed Date Picker ---- */
.s-custom-date { position: relative; }
.s-custom-date-trigger { display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 8px 12px; border: 1px solid var(--s-border); font-family: var(--s-font); font-size: 0.875rem; background: var(--s-surface); color: var(--s-text); border-radius: var(--s-radius); transition: border-color 0.15s, box-shadow 0.15s; width: 100%; -webkit-user-select: none; user-select: none; }
.s-custom-date-trigger:hover { border-color: #bbb; }
.s-custom-date-trigger.open { border-color: var(--s-primary); box-shadow: 0 0 0 2px rgba(44,65,144,0.15); }
.s-custom-date-trigger svg { width: 16px; height: 16px; color: var(--s-text-secondary); flex-shrink: 0; }
.s-custom-date-trigger .date-text { flex: 1; }
.s-custom-date-trigger .date-placeholder { color: var(--s-text-disabled); }
.s-custom-date-popup { position: absolute; top: 100%; left: 0; z-index: 1050; background: #fff; border: 1px solid var(--s-border); border-radius: var(--s-radius); box-shadow: var(--s-shadow-md); margin-top: 2px; padding: 12px; min-width: 280px; }
.s-cal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.s-cal-header span { font-size: 0.875rem; font-weight: 600; color: var(--s-text); }
.s-cal-nav { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; background: none; cursor: pointer; border-radius: var(--s-radius-sm); color: var(--s-text-secondary); font-size: 16px; }
.s-cal-nav:hover { background: var(--s-hover); color: var(--s-text); }
/* Month + year as dropdowns between the arrows. Native <select>s on purpose: the browser draws
   and scrolls the option list itself, which is what makes reaching 1990 one gesture instead of
   430 clicks on the ‹ arrow. They are styled to read as the old static "January 2026" label. */
.s-cal-period { display: flex; align-items: center; gap: 4px; min-width: 0; }
.s-cal-period-select {
  border: 1px solid transparent; background: none; cursor: pointer;
  font-family: var(--s-font); font-size: 0.875rem; font-weight: 600; color: var(--s-text);
  padding: 2px 4px; border-radius: var(--s-radius-sm); max-width: 104px;
}
.s-cal-period-select:hover { background: var(--s-hover); }
.s-cal-period-select:focus { outline: none; border-color: var(--s-primary); }
.s-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; text-align: center; }
.s-cal-dow { font-size: 0.7rem; font-weight: 600; color: var(--s-text-disabled); padding: 4px 0; }
.s-cal-day { font-size: 0.8rem; padding: 6px 0; border-radius: var(--s-radius-sm); cursor: pointer; color: var(--s-text); transition: background 0.1s; border: none; background: none; }
.s-cal-day:hover { background: var(--s-hover); }
.s-cal-day.other-month { color: var(--s-text-disabled); }
.s-cal-day.today { font-weight: 700; color: var(--s-primary); }
.s-cal-day.selected { background: var(--s-primary); color: #fff; font-weight: 600; }
.s-cal-day.selected:hover { background: var(--s-primary-light); }
.s-cal-today-btn { display: block; width: 100%; margin-top: 8px; padding: 6px; border: none; background: none; color: var(--s-primary); font-size: 0.78rem; font-weight: 500; cursor: pointer; border-radius: var(--s-radius-sm); }
.s-cal-today-btn:hover { background: var(--s-selected); }
/* .lead-panel-head { background: var(--s-sidebar-bg) !important; } */

/* ---- 12-hour Time Picker (STimePicker / SDateTimePicker) ---- */
/* Three dropdowns rather than <input type="time">: the native control follows the OS
   locale and renders 24-hour on many machines, and no CSS reaches it. */
/* Three rules that have to be read together, because fixing one alone breaks another.

   min-width:0 -- this is routinely a grid or flex item, and such an item defaults to
   min-width:auto: it refuses to shrink below its content and overflows its track instead.
   The row only exceeds its track once the clear button appears, so that overflow shows on a
   time that has been filled in and not on an empty one.

   flex:0 0 auto on the selects -- min-width:0 alone just moves the damage. The row then
   shrinks the dropdowns below the width their own text needs and the browser clips it, so
   PM renders as PN and a minute loses a digit. A clipped time is worse than an overflowing
   box: it is still readable, just wrong. The selects therefore never shrink.

   flex-wrap -- with the selects fixed, a track too narrow for the row has to give somewhere.
   Wrapping drops the clear button to a second line, which stays inside the box. Nothing here
   relies on it at the sizes we ship; it is the floor under an unusually wide font.

   The width the row needs is font-dependent -- the stack falls back to Segoe UI off Inter,
   which is wider -- so leave real headroom rather than tuning to the pixel. */
.s-time-picker { display: flex; align-items: center; gap: 4px; min-width: 0; flex-wrap: wrap; }
.s-time-picker select.s-input { width: auto; min-width: 62px; flex: 0 0 auto; padding-left: 8px; padding-right: 22px; font-variant-numeric: tabular-nums; }
.s-time-picker__meridiem { margin-left: 4px; }
.s-time-picker__sep { color: var(--s-text-secondary); font-weight: 600; }
/* Narrower than the 32px .s-btn--icon default: inline in the row, that width is space the
   dropdowns beside it need more than a times glyph does. */
.s-time-picker__clear { color: var(--s-text-secondary); min-width: 24px; padding: 4px; }
.s-datetime-picker { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; }
.s-datetime-picker .s-custom-date { min-width: 150px; }
.s-datetime-picker .s-field { margin-bottom: 0; }
@media (max-width: 640px) {
    .s-time-picker select.s-input { min-width: 58px; padding-right: 18px; }
    .s-datetime-picker { width: 100%; }
    .s-datetime-picker > * { flex: 1 1 auto; }
}

/* ---- Shared Panel / Form Layout ---- */
.lead-form { display: flex; flex-direction: column; gap: 12px; }
.lead-panel { margin-bottom: 6px; background: #fff; border: 1px solid var(--s-border); border-radius: 8px; overflow: hidden; }
.lead-panel-head { background: #f8f9fb; padding: 8px 16px; border-bottom: 1px solid var(--s-border); font-size: 0.8rem; font-weight: 600; color: var(--s-primary); text-transform: uppercase; letter-spacing: 0.4px; }
.lead-panel-body { padding: 12px 16px; }
.lead-panel-body .s-grid { gap: 8px 12px; }
.lead-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.lead-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 10px 16px; background: #f8f9fb; border: 1px solid var(--s-border); border-radius: 8px; }
.detail-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid #f0f1f3; font-size: 0.85rem; }
.detail-label { color: var(--s-text-secondary); font-weight: 500; }
.detail-value { color: var(--s-text); font-weight: 600; text-align: right; }
@media (max-width: 900px) { .lead-two-col { grid-template-columns: 1fr; } }

/* ---- Form Controls ---- */
.s-field { display: flex; flex-direction: column; gap: 6px; }
.s-label { font-size: 0.8125rem; font-weight: 500; color: var(--s-text-secondary); }
.s-input, .s-select, .s-textarea {
  padding: 8px 12px; border: 1px solid var(--s-border);
  font-family: var(--s-font); font-size: 0.875rem;
  background: var(--s-surface); color: var(--s-text);
  transition: border-color 0.15s, box-shadow 0.15s; width: 100%;
  border-radius: var(--s-radius);
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.s-input:focus, .s-select:focus, .s-textarea:focus {
  outline: none; border-color: var(--s-primary); box-shadow: 0 0 0 2px rgba(44,65,144,0.15);
}
.s-input:disabled, .s-select:disabled, .s-textarea:disabled {
  background: #fff; cursor: not-allowed; color: var(--s-text-disabled);
}
.s-input:read-only, .s-textarea:read-only {
  background: #fff; cursor: default;
}
.s-btn:focus-visible, .s-tab:focus-visible, .s-pagination button:focus-visible, .s-nav-link:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--s-surface), 0 0 0 4px rgba(44,65,144,0.3);
  transition: box-shadow 0.15s;
}
@keyframes s-shake {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-4px); }
  40%, 80% { transform: translateX(4px); }
}
.s-input--error { border-color: var(--s-error); animation: s-shake 0.3s ease; }
.s-input--changed { background-color: #fef3c7; border-color: #f59e0b; font-weight: 800; color: #78350f; letter-spacing: 0.01em; }
.s-helper { font-size: 0.75rem; color: var(--s-text-secondary); }
.s-error-text { font-size: 0.75rem; color: var(--s-error); }
.s-textarea { min-height: 80px; resize: vertical; }
.s-checkbox-row { display: flex; align-items: center; gap: 8px; }
.s-section-title {
  font-size: 0.9375rem; font-weight: 600; color: var(--s-text);
  margin: 0; padding-bottom: 0.25rem;
}
.s-switch { position: relative; width: 40px; height: 22px; }

/* ---- Cards / Paper ---- */
.s-card, .s-paper {
  background: var(--s-surface); border: 1px solid var(--s-border);
  padding: var(--s-md); box-shadow: var(--s-shadow-sm);
  border-radius: var(--s-radius);
  transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.s-card--elevated { box-shadow: var(--s-shadow); }
.s-card--elevated:hover { box-shadow: var(--s-shadow-md); transform: translate3d(0, -1px, 0); }
.s-card--form { padding: var(--s-lg); }

.s-list-page > .s-card { padding: 0; border-radius: var(--s-radius); }

/* ---- Editable Table (Excel-like) ---- */
.s-table-edit { width: 100%; border-collapse: collapse; font-size: 0.82rem; table-layout: auto; }
.s-table-edit th {
  padding: 10px 10px; background: #f0f0f0; border: 1px solid #d0d0d0;
  font-weight: 600; font-size: 0.75rem; color: var(--s-text-secondary);
  text-align: center; white-space: nowrap; -webkit-user-select: none; user-select: none;
}
.s-table-edit td {
  padding: 4px 6px; border: 1px solid #d0d0d0; vertical-align: middle;
  position: relative; text-align: center; min-height: 40px;
}
.s-table-edit td .s-input,
.s-table-edit td .s-textarea,
.s-table-edit td input,
.s-table-edit td textarea {
  border: none; border-radius: 0; background: transparent;
  padding: 8px 10px; width: 100%; font-size: 0.82rem;
  outline: none; box-shadow: none; text-align: center;
}
.s-table-edit td .s-select,
.s-table-edit td select {
  border: none; border-radius: 0; background: #fff;
  padding: 8px 10px; width: 100%; font-size: 0.82rem;
  outline: none; box-shadow: none; text-align: center;
  -webkit-appearance: menulist; appearance: menulist;
}
.s-table-edit td select option { background: #fff; color: #333; }
.s-table-edit td input:focus,
.s-table-edit td select:focus,
.s-table-edit td .s-input:focus {
  background: #e8f0fe; outline: 2px solid var(--s-primary); outline-offset: -2px;
}
.s-table-edit td .s-field { gap: 0; }
.s-table-edit td .s-label { display: none; }
.s-table-edit td .s-custom-select-trigger { min-height: unset; padding: 4px 8px; font-size: 0.82rem; border: none; border-radius: 0; box-shadow: none; }
.s-table-edit td .s-custom-select-trigger:hover { border: none; }
.s-table-edit td .s-custom-select-trigger.open { border: none; box-shadow: none; outline: none; }
.s-table-edit td span { padding: 8px 10px; display: block; font-size: 0.82rem; }
.s-table-edit td .s-autocomplete { position: relative; }
.s-table-edit td .s-autocomplete .s-input { border: none; border-radius: 0; }
.s-table-edit td input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--s-primary); }
.s-table-edit td .s-btn--icon { padding: 4px; }
.s-table-edit tbody tr:hover { background: #f8f9fa; }
.s-table-edit tbody tr:nth-child(even) { background: #fafbfc; }
.s-table-edit tbody tr:nth-child(even):hover { background: #f0f1f2; }
.s-table-edit tfoot td { background: #f0f0f0; font-weight: 600; padding: 6px 8px; text-align: center; }
.s-table-edit--hs th { background: #E3F2FD; color: #1565C0; }
.s-table-edit--hs tfoot td { background: #E3F2FD; }
.s-table-edit--ls th { background: #FFF3E0; color: #E65100; }
.s-table-edit--ls tfoot td { background: #FFF3E0; }

/* ---- Tables ---- */
.s-table-wrap { overflow-x: auto; }

/* ---- Wide grids: horizontal scrolling only ----
   The BOM grid runs to thirteen columns, wider than any laptop, so it scrolls sideways inside
   its own box — the page body must never scroll horizontally. `scroll` rather than `auto` keeps
   the bar drawn instead of fading out when idle, and it is deliberately fatter than the 6px
   global one because here it is signposting that more columns exist, not just scrolling.

   Vertically the box is not capped at all: it grows to its full height and the *page* scrolls,
   so every row is reachable by scrolling the window like any other page. It used to carry a
   `calc(100dvh - 180px)` cap, which put the horizontal bar at the bottom of the visible box but
   cost a second, nested scrollbar for the rows. overflow-y is `hidden` rather than `visible`
   because a box that scrolls on one axis cannot be visible on the other — with the height left
   to the content there is nothing to clip. */
.s-table-wrap--wide {
  overflow-x: scroll;
  overflow-y: hidden;
}
/* Chrome/Edge/Safari. Setting `scrollbar-color` or `scrollbar-width` on this element would make
   Chrome ignore every rule below and fall back to a thin overlay bar that reserves no space and
   is invisible until you already know to scroll — which is the whole problem. So the standard
   properties are kept off this selector and applied to Firefox on its own, further down. */
.s-table-wrap--wide::-webkit-scrollbar { height: 9px; width: 9px; }
.s-table-wrap--wide::-webkit-scrollbar-track { background: #f1f3f7; border-radius: 999px; }
.s-table-wrap--wide::-webkit-scrollbar-thumb { background: #b3bac7; border-radius: 999px; }
.s-table-wrap--wide::-webkit-scrollbar-thumb:hover { background: #8b94a5; }
.s-table-wrap--wide::-webkit-scrollbar-corner { background: transparent; }
/* Firefox has no ::-webkit-scrollbar; the standard properties are all it understands, and they
   are safe to set here because this block never reaches Chrome. */
@supports (-moz-appearance: none) {
  .s-table-wrap--wide { scrollbar-width: thin; scrollbar-color: #b3bac7 #f1f3f7; }
}

/* The header row used to be `position: sticky` against this box. With the height cap gone the
   box no longer scrolls vertically, so sticky here would resolve against a scrollport that never
   moves and do nothing — and it cannot be made to follow the window instead, because an ancestor
   that scrolls on any axis becomes the scrollport. The rules are dropped rather than left in as
   a no-op. Opaque header backgrounds stay on .s-table th / .s-table-edit th below. */
.s-table { width: 100%; border-collapse: collapse; font-size: 0.84rem; table-layout: auto; border: 1px solid var(--s-border); }
.s-table th {
  text-align: center; padding: 9px 12px; background: #fafafa;
  border: 1px solid var(--s-border); font-weight: 700;
  color: #000000; font-size: 0.8rem;
  white-space: nowrap; cursor: default; -webkit-user-select: none; user-select: none;
}
.s-table td { padding: 8px 12px; border: 1px solid var(--s-border); text-align: center; vertical-align: middle; color: var(--s-text); font-size: 0.78rem; font-weight: 400; }

/* Actions column (last) centered */
.s-table th:last-child,
.s-table td:last-child { text-align: center; }

.s-table tbody tr { transition: background 0.15s ease; cursor: pointer; }
.s-table tbody tr:hover { background: var(--s-hover); }
.s-table tbody tr.selected { background: var(--s-selected); }
.s-table--empty { padding: 40px; text-align: center; color: var(--s-text-secondary); }
.s-table .s-actions { display: flex; flex-direction: row; align-items: center; gap: var(--s-xs); justify-content: center; }

/* ---- Row actions ----
   Icon-only View / Edit / Delete on a single line. See RowActions.razor for why icons.

   The nowrap rule below is the blunt half of the fix and it is deliberate: action buttons are
   scattered through ~40 list pages as bare children of a <td>, so a cell with three of them wrapped
   and one row of data rendered three rows tall. Every one of those cells is now a single line
   without touching a single page. Pages that adopt RowActions get the icons as well. */
.s-row-actions { display: flex; flex-direction: row; align-items: center; gap: 4px; justify-content: center; white-space: nowrap; }
.s-table td:last-child { white-space: nowrap; }
.s-table td:last-child > .s-btn + .s-btn { margin-left: 4px; }

.s-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  /* 30px, not the 44px touch minimum: a data grid is a pointer surface and three 44px targets in
     a cell is taller than the row. The phone layouts stack these pages into cards instead. */
  width: 30px; height: 30px; padding: 0; flex-shrink: 0;
  border: 1px solid var(--s-border); border-radius: 6px;
  background: var(--s-surface, #fff); color: var(--s-text-secondary);
  cursor: pointer; text-decoration: none; transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.s-icon-btn:hover { background: var(--s-hover); color: var(--s-primary); border-color: var(--s-primary); }
.s-icon-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.s-icon-btn:disabled:hover { background: var(--s-surface, #fff); color: var(--s-text-secondary); border-color: var(--s-border); }
/* Colour only on the one action that cannot be undone. */
.s-icon-btn--danger:hover { background: var(--s-error-light); color: var(--s-error); border-color: var(--s-error); }

/* ---- Pagination ---- */
.s-pagination { display: flex; align-items: center; gap: 4px; justify-content: center; padding: var(--s-sm) 0; }
.s-pagination button {
  min-width: 32px; height: 32px; border: 1px solid var(--s-border);
  background: var(--s-surface); cursor: pointer; font-size: 0.8125rem;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--s-radius); transition: background 0.15s ease, color 0.15s ease;
}
.s-pagination button:hover { background: var(--s-hover); }
.s-pagination button.active { background: var(--s-primary); color: #fff; border-color: var(--s-primary); }
.s-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }

/* ---- Badges / Chips ---- */
.s-badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 10px; font-size: 0.75rem; font-weight: 600;
  border-radius: var(--s-radius-sm); text-transform: uppercase; letter-spacing: 0.5px;
}
.s-badge--success { background: var(--s-success-light); color: var(--s-success); }
.s-badge--warning { background: var(--s-warning-light); color: var(--s-warning); }
.s-badge--error { background: var(--s-error-light); color: var(--s-error); }
.s-badge--info { background: var(--s-info-light); color: var(--s-info); }
.s-badge--default { background: #f5f5f5; color: var(--s-text-secondary); }
.s-badge--primary { background: var(--s-selected); color: var(--s-primary); }

/* ---- Tabs ---- */
.s-tabs { display: flex; flex-direction: column; }
.s-tabs-header { display: flex; border-bottom: 2px solid var(--s-border); gap: 0; }
.s-tab-btn {
  padding: 10px 20px; cursor: pointer; font-size: 0.875rem; font-weight: 500;
  color: var(--s-text-secondary); border: none; background: none;
  border-bottom: 2px solid transparent; margin-bottom: -2px;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
  font-family: var(--s-font); white-space: nowrap;
}
.s-tab-btn:hover { color: var(--s-primary); background: #f5f5f5; }
.s-tab-btn--active { color: var(--s-primary); border-bottom-color: var(--s-primary); font-weight: 600; }
.s-tabs-content { padding: var(--s-md) 0; }

/* ---- Dialogs / Modals ---- */
.s-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.5);
  display: flex; align-items: center; justify-content: center;
  z-index: 1000; animation: fadeIn 0.2s ease-out;
  overflow: hidden;
}
.s-dialog {
  background: var(--s-surface); min-width: 400px; max-width: 90vw;
  max-height: 90vh; max-height: 90dvh; display: flex; flex-direction: column;
  box-shadow: var(--s-shadow-lg); border-radius: var(--s-radius-lg);
  animation: zoomIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
  /* Note: do NOT add `will-change: transform` or `transform` here — it would
     turn .s-dialog into the containing block for position:fixed children
     (e.g. SSelect dropdowns), pinning their popovers to the dialog corner. */
}
.s-dialog-title {
  padding: 16px 20px; font-size: 1.125rem; font-weight: 600;
  border-bottom: 1px solid var(--s-border);
}

/* Size modifiers. .s-dialog is shrink-to-fit with only a 90vw ceiling, so a dialog holding a
   12-column grid stretches to fill the screen: the New Employee form measured 1145px on a 1366px
   viewport, which put ~530px behind every half-width control and left "Gender" and a 10-digit
   mobile marooned in whitespace. --lg was already being asked for in the markup and had never
   been defined anywhere, so it was inert.

   820px is a two-column form: ~370px per field, wide enough for the helper lines under Role and
   Work Location without turning a short input into a runway. */
.s-dialog--sm { width: 460px; max-width: 94vw; }
.s-dialog--md { width: 640px; max-width: 94vw; }
.s-dialog--lg { width: 820px; max-width: 94vw; }
.s-dialog--xl { width: 1100px; max-width: 94vw; }

/* Grouped form sections inside a dialog — "Role", "Contact", "Login", "Shift & leave".
   These were bold text floating in whitespace, which is what made a busy form read as sparse:
   with nothing dividing them the eye cannot tell a heading from a field label. The rule does the
   grouping so the gaps do not have to, which buys back vertical space too. */
.s-form-section { margin-top: 2px; padding-top: 10px; border-top: 1px solid var(--s-border); }
.s-form-section:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.s-form-section-title { font-size: 0.875rem; font-weight: 600; color: var(--s-text); }
.s-form-section-hint { font-size: 0.75rem; color: var(--s-text-secondary); margin-top: 2px; }

/* Tighter rows inside a dialog than on a full page. The column gap stays at 16px — that one
   separates two controls sitting side by side and still has to. */
.s-dialog-content .s-grid, .s-dialog-body .s-grid { gap: 12px 16px; }

/* Confirmation dialogs (SConfirmDialog, ~101 of the hosted dialogs in the app). The component
   renders inside the host's .s-dialog-content, so its actions bar has to be pulled back out to
   the dialog edges for its top rule to actually divide the question from the answer. */
/* ---- Card header: tabs + toolbar as one band ----
   A tabbed list page used to stack three separate strips down the screen — a tab row, a toolbar
   row, then the table card — so the controls read as floating debris above the content rather than
   as part of it. This makes them the card's own header: tabs on the left, search and column
   controls on the right, one rule under both, and the table directly beneath. The tab strip keeps
   its own border off; .s-card-head carries it, and .s-tab-btn's existing -2px margin lets the
   active underline sit on top of it. */
.s-card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; flex-shrink: 0;
  /* Vertical padding, not zero: the tabs are outlined chips now, and a chip touching the table's
     grey header below it is what made them look like they were sliding under the table. */
  padding: 8px 12px; background: var(--s-surface);
  border-bottom: 1px solid var(--s-border);
  border-top-left-radius: var(--s-radius); border-top-right-radius: var(--s-radius);
}
.s-card-head > .s-tabs-header { border-bottom: none; gap: 6px; }

/* The open tab is outlined, not merely underlined. On an approval queue the tab you are on decides
   what Approve and Reject will act on, so which one is open has to be unmistakable rather than a
   two-pixel colour change.

   It is a self-contained chip with a border on all four sides, NOT a folder tab. The folder trick
   — paint the bottom edge in the surface colour and pull it down over the header rule — needs the
   panel directly beneath to be that colour, and here it is the table's grey sticky header. The tab
   read as though it were sliding underneath the table. A closed shape has nothing to blend into
   and cannot go wrong whatever sits below it.

   Scoped to .s-card-head on purpose: nine other screens use .s-tab-btn and were not part of this
   change. Any page adopting the card-head pattern gets this for free. */
.s-card-head .s-tab-btn {
  border: 1px solid transparent;
  border-radius: var(--s-radius-sm);
  margin-bottom: 0;
  padding: 7px 16px;
}
.s-card-head .s-tab-btn:hover { background: var(--s-hover); }
.s-card-head .s-tab-btn--active {
  border-color: var(--s-primary);
  background: var(--s-selected);
  color: var(--s-primary);
  font-weight: 600;
}
.s-card-head__tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.s-card-head__tools .s-toolbar { margin-bottom: 0; }

/* The table is no longer the top of the card, so it must not round its top corners or the header
   above it would appear to float on a notched edge. */
.s-list-page > .s-card > .s-card-head ~ .s-table-wrap { border-top-left-radius: 0; border-top-right-radius: 0; }
.s-list-page > .s-card > .s-card-head ~ .s-table-wrap > .s-table thead tr:first-child th:first-child,
.s-list-page > .s-card > .s-card-head ~ .s-table-wrap > .s-table thead tr:first-child th:last-child {
  border-radius: 0;
}

@media (max-width: 720px) {
  /* The search has a 250px floor, so on a phone the tools drop to their own full-width line
     under the tabs rather than squeezing the tab labels to nothing. */
  .s-card-head { padding: 0 10px; }
  .s-card-head__tools { width: 100%; padding-bottom: 10px; }
  .s-card-head__tools .s-search { flex: 1 1 0; min-width: 0; }
}

/* A table whose declared column widths are binding. Auto layout treats a th width as a suggestion
   and lets cell content override it, which is how the approval queues ended up 35–94px wider than
   their own scroll container and clipped the Approve/Reject column off the right edge — the widest
   offender being the Decision cell, whose reviewer name and timestamp simply refused to shrink.
   With fixed layout the widths hold and the text wraps instead. Columns left without a width share
   whatever is left, so exactly one free column (the remark/reason) absorbs the slack.

   overflow-wrap is the necessary companion: a single long unbroken token would otherwise still
   spill out of its cell. */
.s-table--fixed { table-layout: fixed; width: 100%; }
.s-table--fixed td, .s-table--fixed th { overflow-wrap: anywhere; }

/* The base sheet sets `white-space: nowrap` on the last cell and on every th, which suits a row of
   action buttons and short headers. It does not suit a Decision cell carrying a reviewer's name, a
   timestamp and a quoted note: that is one unbreakable line, and it was the whole reason only the
   Reviewed tab overflowed while Pending sat exactly on the container width. Let both wrap here. */
.s-table--fixed td:last-child, .s-table--fixed th { white-space: normal; }

/* ...but a button label is not prose. Letting the cell wrap made "Approve" break as "Approv e",
   which is worse than the overflow it was fixing. Text wraps, controls do not. */
.s-table--fixed td:last-child .s-btn { white-space: nowrap; }

/* ---- Frozen actions column (pointer screens only) ----
   The last column is the actions column by convention across ~40 list pages — the base sheet
   already assumes it, with `td:last-child { white-space: nowrap }` and the button-spacing rule
   beside it. On a wide table the reader scrolls right to read the data and loses View / Edit /
   Delete off the left, or scrolls back to reach them. Pinning that column keeps the actions under
   the cursor at every scroll position.

   Desktop only, deliberately: the phone layout already pins the FIRST column (see the mobile
   block near the end of this file) so the row stays identifiable while scrolling, and pinning both
   edges of a 360px screen would leave almost nothing between them.

   The pattern mirrors that mobile rule exactly, because the same three things have to be true:
   an opaque background, the row's hover/selected state repainted on the pinned cell (its own
   background would otherwise cut the row's short), and an edge line saying the table continues.

   `.s-list-page` carries it automatically because that convention holds there. A report page has
   no such guarantee — its last column is usually data (Vehicles, Locations, a note) and pinning
   that would be nonsense — so those opt in per table with `.s-table--sticky-actions`. */
@media (min-width: 1024px) {
  .s-list-page .s-table tbody td:last-child,
  .s-table--sticky-actions tbody td:last-child {
    position: sticky; right: 0; z-index: 2;
    background: var(--s-surface);
    box-shadow: -1px 0 0 var(--s-border);
  }
  /* Above the sticky thead's z-index: 50, which is declared !important. */
  .s-list-page .s-table thead th:last-child,
  .s-table--sticky-actions thead th:last-child {
    position: sticky !important; right: 0; z-index: 51 !important;
    box-shadow: inset 0 -1px 0 var(--s-border), inset 0 1px 0 var(--s-border),
                -1px 0 0 var(--s-border) !important;
  }
  .s-list-page .s-table tbody tr:hover td:last-child,
  .s-table--sticky-actions tbody tr:hover td:last-child { background: var(--s-hover); }
  .s-list-page .s-table tbody tr.selected td:last-child,
  .s-table--sticky-actions tbody tr.selected td:last-child { background: var(--s-selected); }

  /* The "nothing matched" row spans every column, so pinning it would park the message on the
     right-hand edge instead of across the table. */
  .s-list-page .s-table tbody td.s-table--empty,
  .s-table--sticky-actions tbody td.s-table--empty { position: static; box-shadow: none; }
}

/* Empty state for a list/queue with nothing in it. Distinct from .s-table--empty, which is the
   "nothing matched your filter" row *inside* a table — an empty queue and a filtered-out one are
   different facts and the reader has to be able to tell which they are looking at. */
.s-empty-state { padding: 2.5rem 1.5rem; text-align: center; }
.s-empty-state__icon { font-size: 1.75rem; line-height: 1; margin-bottom: 10px; opacity: 0.75; }
.s-empty-state__title { font-size: 0.95rem; font-weight: 600; color: var(--s-text); margin-bottom: 4px; }
.s-empty-state__hint {
  font-size: 0.8125rem; color: var(--s-text-secondary);
  max-width: 46ch; margin: 0 auto; line-height: 1.5;
}

.s-confirm-message { margin: 0; line-height: 1.55; color: var(--s-text); font-size: 0.9rem; }
.s-confirm-actions { margin: 20px -20px -20px; }
@media (max-width: 640px) {
  .s-confirm-actions { margin: 16px -16px -16px; }
}
.s-dialog-inner { display: flex; flex-direction: column; max-height: 90vh; max-height: 90dvh; }
.s-dialog-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--s-border); }
/* min-height:0 is load-bearing, not decoration: a flex child defaults to min-height:auto, which
   refuses to shrink below its content, so `flex: 1; overflow-y: auto` alone lets a tall form push
   the dialog past its max-height instead of scrolling inside it. Pages were each patching this
   with an inline style; it belongs here. */
.s-dialog-body { padding: 20px; overflow-y: auto; flex: 1; min-height: 0; }
.s-dialog-content { padding: 20px; overflow-y: auto; flex: 1; min-height: 0; }
.s-dialog-actions { padding: 12px 20px; display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--s-border); }

/* ---- Utility ---- */
.s-form { width: 100%; }
.text-danger { color: var(--s-error); }

/* ---- Toasts (Sonner richColors) ---- */
.s-toast-container {
  position: fixed;
  bottom: 32px;
  right: 32px;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 356px;
  max-width: calc(100vw - 32px);

  /* The container is a 356px-wide fixed box pinned to the bottom-right corner at z-index 2000 —
     exactly where every form page puts its save button (.lead-actions, .prof-panel-actions and
     the dialog action rows are all bottom-right). Without this it stays in the hit-test tree even
     while empty of anything the user wants to click, so any click landing inside those 356px is
     swallowed by the container instead of reaching the button underneath.

     That made the commonest recovery in the app silently fail: get a validation error, fix the
     field, click Save again while the error toast is still on screen — and nothing happens. The
     button looks fine and answers no clicks, which reads as a dead form rather than a covered one.

     pointer-events:none lets clicks fall straight through the container; .s-toast puts them back
     on for the toasts themselves, so their own dismiss button still works. */
  pointer-events: none;
}
.s-toast {
  position: relative;
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 16px;
  padding-right: 36px;
  width: 100%;
  border-radius: 8px;
  border: 1px solid;
  font-family: var(--s-font);
  font-size: 13px;
  line-height: 1.5;
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  animation: s-toast-in 0.35s cubic-bezier(0.21,1.02,0.73,1) forwards;
  will-change: transform, opacity;
}
.s-toast--exiting {
  animation: s-toast-out 0.2s ease-in forwards;
}

/* richColors per severity */
.s-toast--success {
  background: #fff;
  border-color: hsl(0 0% 89.8%);
  color: #0a0a0a;
}
.s-toast--error {
  background: hsl(359, 100%, 97%);
  border-color: hsl(359, 100%, 94%);
  color: hsl(360, 100%, 45%);
}
.s-toast--warning {
  background: hsl(49, 100%, 97%);
  border-color: hsl(49, 91%, 91%);
  color: hsl(31, 92%, 45%);
}
.s-toast--info {
  background: hsl(208, 100%, 97%);
  border-color: hsl(221, 91%, 91%);
  color: hsl(210, 92%, 45%);
}

/* Icon */
.s-toast__icon {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 1px;
}
.s-toast__icon svg { display: block; }

/* Content */
.s-toast__content { flex: 1; min-width: 0; }
.s-toast__title {
  font-size: 13px;
  font-weight: 600;
}
.s-toast__desc {
  font-size: 13px;
  font-weight: 400;
  opacity: 0.9;
  margin-top: 2px;
}

/* Action button */
.s-toast__action {
  flex-shrink: 0;
  padding: 2px 10px;
  height: 24px;
  font-size: 12px;
  font-weight: 500;
  font-family: var(--s-font);
  background: rgba(0,0,0,0.15);
  color: inherit;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
  white-space: nowrap;
  align-self: center;
}
.s-toast__action:hover { background: rgba(0,0,0,0.25); }

/* Close button — always visible, top-right */
.s-toast__close {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: none;
  background: transparent;
  color: inherit;
  cursor: pointer;
  border-radius: 50%;
  opacity: 0.5;
  transition: opacity 0.15s;
  padding: 0;
}
.s-toast__close:hover { opacity: 1; }

/* Loading spinner */
.s-toast__spinner {
  width: 16px;
  height: 16px;
  animation: spin 0.8s linear infinite;
}
.s-toast__spinner circle {
  stroke: currentColor;
  stroke-width: 2.5;
  fill: none;
  stroke-dasharray: 40;
  stroke-dashoffset: 12;
  stroke-linecap: round;
}

/* Animations */
@keyframes s-toast-in {
  from { opacity: 0; transform: translateY(100%) scale(0.96); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes s-toast-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to { opacity: 0; transform: translateY(100%) scale(0.96); }
}

@media (max-width: 600px) {
  .s-toast-container {
    bottom: 16px;
    right: 16px;
    left: 16px;
    width: auto;
  }
}

/* ---- Alerts ---- */
.s-alert {
  padding: 12px 16px; display: flex; align-items: flex-start; gap: 10px;
  font-size: 0.875rem; border-left: 4px solid;
  border-radius: var(--s-radius);
}
.s-alert--success { background: var(--s-success-light); border-left-color: var(--s-success); }
.s-alert--error { background: var(--s-error-light); border-left-color: var(--s-error); }
.s-alert--warning { background: var(--s-warning-light); border-left-color: var(--s-warning); }
.s-alert--info { background: var(--s-info-light); border-left-color: var(--s-info); }

/* ---- Progress ---- */
.s-progress { height: 6px; background: var(--s-border); overflow: hidden; border-radius: var(--s-radius); }
.s-progress-bar { height: 100%; background: var(--s-primary); transition: width 0.3s; }
.s-progress-bar--success { background: var(--s-success); }

/* ---- Spinner ---- */
.s-spinner {
  width: 24px; height: 24px; border: 3px solid var(--s-border);
  border-top-color: var(--s-primary); border-radius: 50%;
  animation: spin 0.7s linear infinite;
  will-change: transform;
}
.s-spinner--lg { width: 40px; height: 40px; border-width: 4px; }
.s-spinner-center { display: flex; justify-content: center; padding: var(--s-xl); }

/* ---- Divider ---- */
.s-divider { border: none; border-top: 1px solid var(--s-border); margin: var(--s-md) 0; }

/* ---- Tooltip ---- */
.s-tooltip { position: relative; display: inline-block; }
.s-tooltip::after {
  content: attr(data-tooltip); position: absolute; bottom: 100%;
  left: 50%; transform: translateX(-50%); padding: 4px 8px;
  background: #333; color: #fff; font-size: 0.75rem; white-space: nowrap;
  opacity: 0; pointer-events: none; transition: opacity 0.15s;
  border-radius: var(--s-radius-sm);
}
.s-tooltip:hover::after { opacity: 1; }

/* ============================================
   App Shell — Inset Sidebar Layout
   All collapse/expand driven by .s-collapsed on .s-app
   ============================================ */
.s-app {
  display: flex;

  /* dvh, not vh. On a phone browser 100vh is the *large* viewport — the height the page
     would have if the URL bar were retracted — so the app was laid out ~110px taller than
     the screen actually showed. html/body are overflow:hidden and the shell owns the only
     scroll container, so that bottom strip could not be scrolled to by any means: the
     pagination footer, the Save row at the end of a form and the last dashboard card were
     simply gone. vh stays first as the fallback for browsers without dvh; on a desktop the
     two are identical, which is why this never showed up there. */
  height: 100vh;
  height: 100dvh;

  overflow: hidden;
  background: var(--s-sidebar-bg);
}

/* ---- Sidebar ---- */
.s-sidebar {
  width: var(--s-sidebar-width);
  background: var(--s-sidebar-bg);
  color: var(--s-sidebar-text);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 100;
  overflow: hidden;
  transition: width 200ms cubic-bezier(0.4, 0, 0.2, 1);
  will-change: width;
}

.s-collapsed .s-sidebar {
  width: var(--s-sidebar-collapsed);
}

/* Sidebar Header (logo area) */
.s-sidebar-header {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem 0.75rem;
  min-height: var(--s-header-height);
  flex-shrink: 0;
}

.s-sidebar-logo-img {
  max-height: calc(var(--s-header-height) - 16px);
  max-width: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
  transition: max-width 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

.s-collapsed .s-sidebar-header {
  padding: 0.5rem 0.25rem;
}

.s-collapsed .s-sidebar-logo-img {
  max-height: calc(var(--s-header-height) - 18px);
  max-width: calc(var(--s-sidebar-collapsed) - 16px);
  object-fit: contain;
  object-position: center;
}

.s-collapsed .s-sidebar-content {
  padding: 0.25rem 0;
}

/* Sidebar Content (scrollable nav area — hidden scrollbar like Ticket-Final) */
.s-sidebar-content {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0.25rem 0.5rem;
  /* Three properties for one effect — see the note above .s-custom-select. Dropping
     scrollbar-width because a linter calls it Safari-only would un-hide it on Firefox. */
  -ms-overflow-style: none;       /* legacy Edge */
  scrollbar-width: none;          /* Firefox, Safari 18.2+ */
}
.s-sidebar-content::-webkit-scrollbar {
  display: none;                  /* Chrome, older Safari, Samsung Internet */
}

/* Nav Sections */
.s-nav-section {
  padding: 0.25rem 0;
}

.s-nav-section-title {
  padding: 0.375rem 0.5rem;
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #1e3a5f;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  height: auto;
  transition: opacity 200ms, height 200ms, padding 200ms;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  -webkit-user-select: none;
  user-select: none;
}
.s-nav-section-title:hover {
  background: rgba(0,0,0,0.06);
  border-radius: var(--s-radius-sm);
  color: #0d2340;
}
.s-nav-section-title .s-chevron {
  width: 12px; height: 12px; transition: transform 200ms; flex-shrink: 0;
}
.s-nav-section.s-section-collapsed .s-chevron { transform: rotate(-90deg); }
.s-nav-section.s-section-collapsed .s-nav-link { display: none; }

.s-collapsed .s-nav-section-title {
  display: none;
}

/* Nav Links */
.s-nav-link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0 0.5rem;
  height: 2rem;
  color: var(--s-sidebar-text);
  font-size: 0.875rem;
  transition: background 0.15s, color 0.15s, padding 200ms;
  cursor: pointer;
  text-decoration: none;
  border-radius: var(--s-radius-sm);
  white-space: nowrap;
  overflow: hidden;
}

.s-nav-link:hover {
  background: var(--s-sidebar-accent);
  color: var(--s-sidebar-accent-fg);
  text-decoration: none;
}

.s-nav-link.active {
  background: var(--s-sidebar-accent);
  color: var(--s-sidebar-accent-fg);
  font-weight: 500;
}

.s-nav-link svg {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}

/* Shown in place of the sidebar when a role can open nothing, so an empty sidebar always
   explains itself instead of looking broken. See NavMenu. */
.s-nav-notice {
  padding: 0.85rem 1rem;
  font-size: 0.78rem;
  color: var(--s-text-secondary);
  line-height: 1.55;
}

.s-nav-link span {
  overflow: hidden;
  opacity: 1;
  transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
  backface-visibility: hidden;
}

/* ---- The collapsed rail was completely empty ----
   Two rules that are each correct on their own cancelled the sidebar out entirely:

     .s-nav-section.s-section-collapsed .s-nav-link { display: none }   (accordion)
     .s-collapsed .s-nav-section-title              { display: none }   (icon rail)

   Every section renders collapsed to begin with, so all 61 links are hidden and the
   expanded sidebar shows just the 10 section titles you click to open. Collapse the rail
   and those titles are hidden too — leaving 0 of 61 links and 0 of 10 titles visible, i.e.
   a blank 48px strip with nothing in it and no way to navigate.

   In the rail there is no section header to click, so a section's accordion state is
   meaningless there and must not suppress its links: the icons *are* the navigation. */
.s-collapsed .s-nav-section.s-section-collapsed .s-nav-link { display: flex; }

.s-collapsed .s-nav-link {
  justify-content: center;
  padding: 0;
  gap: 0;
  width: 100%;
  position: relative;
}

.s-collapsed .s-nav-link span {
  display: none;
}

.s-collapsed .s-nav-link:hover::after {
  content: attr(title);
  position: absolute;
  left: calc(100% + 0.5rem);
  top: 50%;
  transform: translateY(-50%);
  background: var(--s-text);
  color: var(--s-surface);
  padding: 0.25rem 0.5rem;
  border-radius: var(--s-radius-sm);
  font-size: 0.75rem;
  white-space: nowrap;
  z-index: 300;
  pointer-events: none;
  opacity: 1;
}

/* Sidebar Footer */
.s-sidebar-footer {
  padding: 0.5rem;
  border-top: 1px solid var(--s-sidebar-border);
  flex-shrink: 0;
}

/* ---- Sidebar Inset (main content wrapper) ---- */
.s-sidebar-inset {
  margin-left: var(--s-sidebar-width);
  margin-top: 0.5rem;
  margin-right: 0.5rem;
  margin-bottom: 0.5rem;
  flex: 1;
  display: flex;
  flex-direction: column;
  /* Must track .s-app's unit exactly — see the note there. Left at vh this stayed the
     taller of the two and re-created the unreachable strip on its own. */
  height: calc(100vh - 1rem);
  height: calc(100dvh - 1rem);
  transition: margin-left 200ms cubic-bezier(0.4, 0, 0.2, 1);
  will-change: margin-left;
  background: var(--s-surface);
  border-radius: var(--s-radius-xl);
  border: 1px solid var(--s-border);
  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
  overflow: hidden;
}

.s-collapsed .s-sidebar-inset {
  margin-left: calc(var(--s-sidebar-collapsed) + 0.5rem);
}

/* ---- Header ---- */
.s-header {
  height: var(--s-header-height);
  background: #ffffff;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--s-lg);
  position: sticky;
  top: 0;
  /* Must be ABOVE sticky table thead (z-index 50) so the notification panel
     descending from the header stacks above tables on every page. */
  z-index: 200;
  transition: box-shadow 200ms;
  border-bottom: 1px solid transparent;
}

.s-header.scrolled {
  box-shadow: 0 1px 3px rgba(0,0,0,0.06);
  border-bottom-color: var(--s-border);
}

/* Toggle Button */
.s-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: none;
  background: transparent;
  color: var(--s-text);
  cursor: pointer;
  border-radius: var(--s-radius-sm);
  transition: background 0.15s;
}

.s-toggle-btn:hover {
  background: var(--s-hover);
}

.s-toggle-btn svg {
  width: 1.125rem;
  height: 1.125rem;
}

/* ---- Content Area ---- */
.s-content {
  /* The app's only page-level scroll container — html/body are overflow:hidden and every
     route renders inside this one <main>. That makes it the right and the *only* place to
     put the end-of-page gutter: set it here and every current and future page inherits it
     with no per-page markup. Do not push this onto individual pages.

     The bottom is a token rather than var(--s-md) so the phone breakpoint can raise it and
     the device's safe-area inset can ride along. On a desktop the inset is 0 and this
     computes to exactly the 16px it always was. */
  padding: var(--s-md);
  padding-bottom: var(--s-page-bottom-gutter);

  /* Landscape notch: viewport-fit=cover lets content sit under a display cutout on the
     long edge. Both are 0 in portrait and on every desktop. */
  padding-left: calc(var(--s-md) + env(safe-area-inset-left, 0px));
  padding-right: calc(var(--s-md) + env(safe-area-inset-right, 0px));

  flex: 1;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
}

/* ---- Full-height list page layout ---- */
.s-list-page {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.s-list-page > .s-card {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* .s-list-page IS FOR ONE CARD. It exists so a single .s-card can grow to fill the viewport
   and scroll its own table, and it does that by making every direct .s-card child
   `flex: 1; min-height: 0`. Give it four cards and they become four equal-height flex
   siblings: each crushed to a quarter of the viewport, clipping whatever does not fit, with
   no scrollbar to reach it. On a phone the overflow then draws straight over the card below.
   That is what the Work Order view and Reserve Stock did until 2026-08-26 — 434px of content
   in a 114px card at 360px wide.

   A page that is a COLUMN OF CARDS — a detail view, a form — uses .lead-form / .lead-panel
   instead (see above), which is what every other view and form page in this app already does. */

/* ---- A table that stops being a table on a phone ----
   Add .s-table--stack to an .s-table and give every <td> a data-label. Below 640px each row
   becomes a labelled card. Same shape as .bom-receipt__table, which proved it at a site gate:
   a sideways scroll on a narrow screen means half the row is simply never read. */
@media (max-width: 640px) {
  .s-table--stack thead { display: none; }
  .s-table--stack, .s-table--stack tbody, .s-table--stack tr, .s-table--stack td { display: block; width: 100%; }
  .s-table--stack tfoot, .s-table--stack tfoot tr, .s-table--stack tfoot td { display: block; width: 100%; }
  .s-table--stack tr {
    border: 1px solid var(--s-border);
    border-radius: 8px;
    margin-bottom: 10px;
    padding: 6px 10px;
    background: var(--s-surface, #fff);
  }
  .s-table--stack td {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    text-align: right;
    border: none;
    padding: 7px 2px;
    min-height: 38px;
  }
  .s-table--stack td::before {
    content: attr(data-label);
    font-size: .72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--s-text-secondary);
    flex-shrink: 0;
    text-align: left;
  }
  /* A cell with nothing to say is noise once it is a labelled line of its own. */
  .s-table--stack td[data-label=""]::before { content: none; }

  /* The empty-state row is a message, not a field: no label, and centred across the card.
     Without this it draws an empty ::before box and shoves the text to the right. */
  .s-table--stack td.s-table--empty {
    display: block;
    text-align: center;
    min-height: 0;
  }
  .s-table--stack td.s-table--empty::before { content: none; }
  .s-table--stack td .s-input,
  .s-table--stack td .s-select,
  .s-table--stack td .s-autocomplete { max-width: 62%; }
}

.s-list-page > .s-card > .s-table-wrap {
  overflow-x: auto;
  flex: 1;
  border-top-left-radius: var(--s-radius);
  border-top-right-radius: var(--s-radius);
}

/* Match the rounded card on the sticky thead so its grey background doesn't
   square off the card's rounded top corners. */
.s-list-page > .s-card > .s-table-wrap > .s-table thead tr:first-child th:first-child {
  border-top-left-radius: var(--s-radius);
}
.s-list-page > .s-card > .s-table-wrap > .s-table thead tr:first-child th:last-child {
  border-top-right-radius: var(--s-radius);
}

/* Solid sticky header — applies to every list-page table */
.s-table thead,
.s-table thead tr {
  background: #dde3eb !important;
}
.s-table thead th {
  position: sticky !important;
  top: 0 !important;
  z-index: 50 !important;
  background: #dde3eb !important;
  box-shadow: inset 0 -1px 0 var(--s-border), inset 0 1px 0 var(--s-border) !important;
}


/* Report pages: the .s-card directly wraps the <table> with no .s-table-wrap,
   so the page itself is the scroll container and the sticky thead has no close
   scrolling ancestor to attach to. Adding overflow + max-height here turns the
   card into the scroll region; the .s-table thead th sticky rule above then
   keeps the header pinned to the top of the card while rows scroll. */
.s-card.s-card--scroll {
  overflow: auto;
  max-height: calc(100vh - 240px);
  max-height: calc(100dvh - 240px);
}

/* Center the filter header divs */
.s-table th > div { justify-content: center; }

/* The master pages carry no pagination — they load every row and the table scrolls — so
   .s-list-count is all that sits in the footer slot there. Both stick to the bottom of the
   card so the footer stays put while the rows scroll under it. */
.s-list-page > .s-card > .s-pagination,
.s-list-page > .s-card > .s-list-count {
  position: sticky;
  bottom: 0;
  z-index: 3;
  border-top: 1px solid var(--s-border);
  margin: 0 -0px;
  padding: 8px 12px;
  background: #fff;
  box-shadow: 0 -2px 6px rgba(0,0,0,0.04);
  border-bottom-left-radius: var(--s-radius);
  border-bottom-right-radius: var(--s-radius);
}

.s-list-count {
  font-size: 12.5px;
  color: #6b7280;
  font-family: var(--s-font);
}

/* ---- Header popup backdrop ----
   Invisible click-catcher behind the notification panel and the profile menu, so a click
   anywhere outside them dismisses them. Deliberately transparent: these are menus, not modals,
   and a dimmed page would read as "the app is blocked". It must out-rank the sticky table
   thead (z-index 30) and the sidebar (200) but stay under its own panel. */
.s-menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 99998;
  background: transparent;
}

/* ---- Mobile Overlay ---- */
.s-sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.4);
  z-index: 150;
  opacity: 0;
  transition: opacity 200ms;
  pointer-events: none;
}

/* ---- Mobile Responsive ---- */
@media (max-width: 768px) {
  .s-sidebar {
    transform: translateX(-100%);
    width: var(--s-sidebar-width);
    z-index: 200;
    box-shadow: var(--s-shadow-lg);
    transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), width 200ms cubic-bezier(0.4, 0, 0.2, 1);
  }

  .s-collapsed .s-sidebar {
    width: var(--s-sidebar-width);
    transform: translateX(-100%);
  }

  .s-mobile-open .s-sidebar {
    transform: translateX(0);
  }

  .s-mobile-open .s-sidebar-backdrop {
    display: block;
    opacity: 1;
    pointer-events: auto;
  }

  .s-sidebar-inset,
  .s-collapsed .s-sidebar-inset {
    margin-left: 0;
    margin-top: 0;
    margin-right: 0;
    margin-bottom: 0;
    border-radius: 0;

    /* ---- THE bottom-of-every-page bug ----
       This rule used to say `min-height: 100vh`, and that one declaration cut the bottom off
       every page in the app on every phone.

       On a phone `vh` is the *large* viewport — the height the page would have if the browser
       retracted its URL bar — so 100vh is roughly the visible height PLUS the URL bar, about
       110px more than you can actually see. `.s-sidebar-inset` already sets
       `height: calc(100dvh - 1rem)` and `dvh` is the correct, currently-visible height. But a
       min-height floor always beats height, so the correct dvh height was overridden and the
       main content wrapper was laid out ~110px taller than the screen.

       Nothing could reach that strip. `.s-app` is `height: 100dvh; overflow: hidden` and this
       element is `overflow: hidden` too, so the extra height was clipped rather than scrolled
       — and `.s-content`, which is `flex: 1` inside here, inherited the overshoot. Scrolling
       .s-content to its very end therefore parked the last card, the pagination footer, the
       Save row and the end-of-page gutter inside a region below the visible screen. Every
       page, every module, and no amount of padding could fix it: the padding was landing in
       the clipped strip too, which is exactly why raising it changed nothing.

       No desktop browser can reproduce this — with no retractable URL bar `vh == dvh`, the
       floor equals the height and nothing overflows. That is why it survived the emulated
       360px checks and only ever showed up on real devices.

       The margins above are zeroed here, so the inset should simply be the full shell height.
       vh first as the fallback for browsers without dvh, matching .s-app and the base rule. */
    height: 100vh;
    height: 100dvh;
  }
}

/* ---- Page Header ---- */
.s-page-header {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: var(--s-lg);
}
.s-page-title { font-size: 1.5rem; font-weight: 600; border: none; outline: none; }
.s-page-subtitle { font-size: 0.875rem; color: var(--s-text-secondary); }

/* ---- Toolbar ---- */
.s-toolbar { display: flex; gap: var(--s-sm); flex-wrap: nowrap; align-items: center; margin-bottom: var(--s-md); }
.s-toolbar .s-select { width: auto; min-width: 0; padding: 6px 8px; font-size: 0.8125rem; }

/* ---- Search Box ---- */
.s-search { position: relative !important; display: inline-flex !important; align-items: center; min-width: 250px; }
.s-search .s-input { padding-left: 36px !important; width: 100% !important; }
.s-search > svg { position: absolute !important; left: 10px !important; top: 50% !important; transform: translateY(-50%) !important; color: var(--s-text-secondary); pointer-events: none; z-index: 1; width: 16px !important; height: 16px !important; overflow: visible !important; }

/* ---- Currency ---- */
.s-currency { font-variant-numeric: tabular-nums; }
.s-currency::before { content: '\20B9'; }

/* ---- Dropdown ---- */
.s-dropdown { position: relative; display: inline-block; }
.s-dropdown-menu {
  position: absolute; top: 100%; right: 0; min-width: 180px;
  background: var(--s-surface); border: 1px solid var(--s-border);
  box-shadow: var(--s-shadow-md); z-index: 500;
  display: none; border-radius: var(--s-radius);
}
.s-dropdown.open .s-dropdown-menu {
  display: block;
  animation: zoomIn 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}
.s-dropdown-item {
  padding: 8px 16px; cursor: pointer; font-size: 0.875rem;
  display: flex; align-items: center; gap: 8px;
  transition: background 0.15s ease;
}
.s-dropdown-item:hover { background: var(--s-hover); }

/* ---- Animations (GPU-accelerated with translate3d/scale3d) ---- */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes slideInRight { from { transform: translate3d(100%, 0, 0); opacity: 0; } to { transform: translate3d(0, 0, 0); opacity: 1; } }
@keyframes zoomIn { from { opacity: 0; transform: scale3d(0.95, 0.95, 1); } to { opacity: 1; transform: scale3d(1, 1, 1); } }
@keyframes zoomOut { from { opacity: 1; transform: scale3d(1, 1, 1); } to { opacity: 0; transform: scale3d(0.95, 0.95, 1); } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes s-filter-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }

/* ---- Scrollbar ---- */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; transition: background 0.2s; }
::-webkit-scrollbar-thumb:hover { background: #aaa; }
.s-sidebar::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); }

/* ---- Login Page ---- */
.login-page,
.login-page *,
.login-page *::before,
.login-page *::after {
  box-sizing: border-box;
}
.login-page h1,
.login-page h2,
.login-page h3,
.login-page p {
  margin: 0;
}
.login-page a {
  color: inherit;
  text-decoration: none;
  transition: none;
}
.login-page a:hover {
  text-decoration: none;
}
.login-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100svh;
  gap: 1.5rem;
  background: var(--background);
  color: var(--foreground);
  padding: 1.5rem;
  font-family: var(--s-font);
}
@media (min-width: 768px) {
  .login-page { padding: 2.5rem; }
}
.login-container {
  width: 100%;
  max-width: 24rem;
}
.login-form-wrapper {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.login-fieldgroup {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.login-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  text-align: center;
}
.login-logo-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  font-weight: 500;
  text-decoration: none;
  color: var(--foreground);
}
.login-logo-link:hover { text-decoration: none; }
.login-logo {
  display: flex;
  width: 2rem;
  height: 2rem;
  align-items: center;
  justify-content: center;
  border-radius: calc(var(--radius) - 2px);
  color: var(--foreground);
}
.login-logo svg { width: 1.5rem; height: 1.5rem; }
.login-logo img { height: 2.5rem; width: auto; }
.login-title {
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: -0.025em;
  color: var(--foreground);
  margin: 0;
}
.login-description {
  font-size: 0.875rem;
  color: var(--muted-foreground);
  margin: 0;
  text-align: center;
}
.login-description a {
  color: var(--foreground) !important;
  font-weight: 500;
  text-decoration: underline !important;
  text-underline-offset: 4px;
}
.login-description a:hover { text-decoration: underline !important; }
.login-field {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}
.login-label {
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1;
  color: var(--foreground);
}
.login-label-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.login-forgot {
  font-size: 0.8125rem;
  color: var(--foreground) !important;
  text-decoration: underline !important;
  text-underline-offset: 4px;
}
.login-forgot:hover { color: var(--foreground) !important; text-decoration: underline !important; }
.login-input {
  display: flex;
  height: 2.25rem;
  width: 100%;
  border-radius: calc(var(--radius) - 2px);
  border: 1px solid var(--input);
  background: transparent;
  padding: 0.375rem 0.75rem;
  font-size: 0.875rem;
  font-family: var(--s-font);
  color: var(--foreground);
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  transition: border-color 0.15s, box-shadow 0.15s;
  outline: none;
  box-sizing: border-box;
}
.login-input::placeholder {
  color: var(--muted-foreground);
}
.login-input:focus {
  border-color: var(--ring);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 15%, transparent);
}
.login-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  white-space: nowrap;
  border-radius: calc(var(--radius) - 2px);
  font-size: 0.875rem;
  font-weight: 500;
  font-family: var(--s-font);
  height: 2.25rem;
  padding: 0.375rem 1rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
  text-decoration: none;
  box-sizing: border-box;
  line-height: 1.25rem;
}
.login-btn svg {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}
.login-btn--primary {
  width: 100%;
  background: var(--primary);
  color: var(--primary-foreground);
  border: none;
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.login-btn--primary:hover {
  background: color-mix(in srgb, var(--primary) 90%, black);
}
.login-btn--outline {
  width: 100%;
  background: var(--background);
  color: var(--foreground);
  border: 1px solid var(--input);
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.login-btn--outline:hover {
  background: var(--accent);
  color: var(--accent-foreground);
}
.login-separator {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.75rem;
  color: var(--muted-foreground);
  text-transform: uppercase;
}
.login-separator::before,
.login-separator::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}
.login-social-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}
@media (min-width: 640px) {
  .login-social-grid { grid-template-columns: 1fr 1fr; }
}
.login-password-wrapper { position: relative; }
.login-password-wrapper .login-input { padding-right: 2.5rem; }
.login-password-toggle {
  position: absolute; right: 0.75rem; top: 50%; transform: translateY(-50%);
  background: none; border: none; cursor: pointer; color: var(--muted-foreground);
  padding: 0; display: flex; align-items: center; justify-content: center;
}
.login-password-toggle:hover { color: var(--foreground); }
.login-password-toggle svg { width: 1rem; height: 1rem; }
.login-terms {
  padding: 0 1.5rem;
  text-align: center;
  font-size: 0.75rem;
  color: var(--muted-foreground);
  text-wrap: balance;
  margin: 0;
  line-height: 1.6;
}
.login-terms a {
  color: var(--foreground) !important;
  text-decoration: underline !important;
  text-underline-offset: 4px;
}
.login-terms a:hover { color: var(--foreground) !important; }
.login-alert {
  border-radius: calc(var(--radius) - 2px);
  padding: 0.625rem 0.75rem;
  font-size: 0.875rem;
}
.login-alert--error {
  background: color-mix(in srgb, var(--destructive) 10%, transparent);
  color: var(--destructive);
  border: 1px solid color-mix(in srgb, var(--destructive) 20%, transparent);
}
.login-alert--success {
  background: color-mix(in srgb, hsl(142 76% 36%) 10%, transparent);
  color: hsl(142 76% 36%);
  border: 1px solid color-mix(in srgb, hsl(142 76% 36%) 20%, transparent);
}

/* ---- Auth Pages (Login / Register) ---- */
.s-auth-page {
  /* Same trap as .s-sidebar-inset above, on the one page that does not use the app shell:
     html/body are overflow:hidden, so a 100vh floor on a phone puts ~110px of this page below
     the visible screen with no way to scroll to it — enough to hide the sign-in button on a
     short handset. dvh is the visible height; vh stays first as the fallback. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(to bottom, #eff6ff, #f3f4f6);
  padding: var(--s-md);
}
.s-auth-card {
  background: var(--s-surface); width: 100%; max-width: 28rem;
  border-radius: var(--s-radius-lg); box-shadow: var(--s-shadow-lg);
  overflow: hidden;
}
.s-auth-card-header {
  background: var(--s-primary-dark, #1e2d66); color: white;
  text-align: center; padding: 1.5rem;
}
.s-auth-card-header .s-auth-icon {
  width: 48px; height: 48px; display: flex; align-items: center;
  justify-content: center; margin: 0 auto 12px; color: white;
  background: none; border-radius: 0;
}
.s-auth-card-header .s-auth-icon svg { width: 48px; height: 48px; }
.s-auth-card-header .s-auth-heading {
  font-size: 1.5rem; font-weight: 700; text-align: center;
  margin: 0 0 var(--s-xs); color: white;
}
.s-auth-card-header .s-auth-subtitle {
  font-size: 0.875rem; text-align: center; color: rgba(255,255,255,0.7);
  margin: 0;
}
.s-auth-card-body { padding: 2rem; }
.s-auth-heading {
  font-size: 1.5rem; font-weight: 700; text-align: center;
  margin: 0 0 var(--s-xs); color: var(--s-text);
}
.s-auth-subtitle {
  font-size: 0.875rem; text-align: center; color: var(--s-text-secondary);
  margin: 0 0 var(--s-lg);
}
/* Password show/hide toggle */
.s-password-wrapper { position: relative; }
.s-password-wrapper .s-input { padding-right: 40px; }
.s-password-toggle {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  background: none; border: none; cursor: pointer; color: #9ca3af; padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.s-password-toggle:hover { color: #4b5563; }
.s-password-toggle svg { width: 16px; height: 16px; }
.s-auth-forgot {
  display: block; text-align: center; margin-top: var(--s-md);
  font-size: 0.8125rem; color: var(--s-primary); text-decoration: none;
}
.s-auth-forgot:hover { text-decoration: underline; }
.s-auth-divider {
  display: flex; align-items: center; gap: var(--s-md);
  margin: var(--s-lg) 0; font-size: 0.8125rem; color: var(--s-text-secondary);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.s-auth-divider::before,
.s-auth-divider::after {
  content: ''; flex: 1; border-top: 1px solid var(--s-border);
}
.s-auth-footer {
  text-align: center; margin-top: var(--s-lg); font-size: 0.875rem;
  color: var(--s-text-secondary);
}
.s-auth-footer a { color: var(--s-primary); font-weight: 600; text-decoration: none; }
.s-auth-footer a:hover { text-decoration: underline; }
.s-auth-name-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-md); }
.s-btn--block { width: 100%; justify-content: center; }
.s-btn--outline {
  background: var(--s-surface); color: var(--s-text); border: 1px solid var(--s-border);
}
.s-btn--outline:hover { background: var(--s-hover); }
.s-btn--outline:disabled { opacity: 0.5; cursor: not-allowed; }
@media (max-width: 480px) {
  .s-auth-card-body { padding: 1.5rem; }
}

/* ===== Reports module ===== */
.s-report-hero {
  background: linear-gradient(135deg, var(--s-primary) 0%, var(--s-primary-light) 60%, #5468c7 100%);
  color: #fff;
  border-radius: var(--s-radius-lg);
  padding: 1.75rem 2rem;
  margin-bottom: 1.25rem;
  position: relative;
  overflow: hidden;
  box-shadow: 0 12px 28px -16px rgba(44,65,144,0.55);
}
.s-report-hero::after {
  content: ""; position: absolute; right: -70px; top: -70px;
  width: 280px; height: 280px; border-radius: 50%;
  background: radial-gradient(circle at center, rgba(228,38,38,0.35), transparent 70%);
  pointer-events: none;
}
.s-report-hero__inner { position: relative; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.s-report-hero h1 { font-size: 1.5rem; font-weight: 700; margin: 0 0 0.35rem; letter-spacing: -0.01em; color: #fff; }
.s-report-hero h1:focus, .s-report-hero h1:focus-visible { outline: none; box-shadow: none; background: transparent; }
.s-report-hero p { margin: 0; opacity: 0.9; font-size: 0.9rem; }
.s-report-hero__meta { display: flex; gap: 1rem; font-size: 0.78rem; opacity: 0.9; align-items: center; }
.s-report-hero__meta b { font-weight: 600; }

.s-kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 1.25rem; }
.s-kpi {
  background: var(--s-surface); border: 1px solid var(--s-border);
  border-radius: var(--s-radius-md);
  padding: 1rem 1.1rem; position: relative; overflow: hidden;
  transition: transform 160ms ease, box-shadow 160ms ease;
}
.s-kpi:hover { transform: translateY(-2px); box-shadow: 0 10px 22px -12px rgba(0,0,0,0.18); }
.s-kpi::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 4px;
  background: var(--s-primary);
}
.s-kpi--accent::before { background: var(--s-accent); }
.s-kpi--success::before { background: #2e7d32; }
.s-kpi--warn::before { background: #ed6c02; }
.s-kpi--info::before { background: #0288d1; }
/* --danger was already being written on tiles by ManagerDashboard before it existed here, so
   "Overdue" and "Critical" drew the default blue bar of an ordinary tile — the one accent that
   most needed to be different was the one that was not. */
.s-kpi--danger::before { background: #E42626; }
/* A KPI that drills into the rows behind its number. It is a real <button> so it is
   keyboard-reachable; these resets stop the UA button styling from fighting the card. */
.s-kpi--link {
  display: block; width: 100%; text-align: left; font: inherit; color: inherit;
  cursor: pointer; padding-right: 2rem;
}
.s-kpi--link::after {
  content: "\203A"; position: absolute; right: 0.85rem; top: 50%; transform: translateY(-50%);
  font-size: 1.4rem; line-height: 1; color: var(--s-text-muted); opacity: 0.55;
}
.s-kpi--link:hover::after, .s-kpi--link:focus-visible::after { opacity: 1; color: var(--s-primary); }
.s-kpi--link:focus-visible { outline: 2px solid var(--s-primary); outline-offset: 2px; }

/* The tile currently focusing the table below it. Needs to be unmistakable — a filtered report
   that looks unfiltered reads as missing rows. */
.s-kpi--on {
  border-color: var(--s-primary);
  box-shadow: 0 0 0 2px rgba(44, 65, 144, 0.18);
  background: rgba(44, 65, 144, 0.04);
}
.s-kpi--on::after { content: "\00D7"; opacity: 1; color: var(--s-primary); font-size: 1.1rem; }

.s-kpi__label { font-size: 0.7rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--s-text-muted); font-weight: 600; }
.s-kpi__value { font-size: 1.55rem; font-weight: 700; color: var(--s-text); margin: 0.2rem 0 0.15rem; line-height: 1.1; letter-spacing: -0.02em; }
.s-kpi__sub { font-size: 0.76rem; color: var(--s-text-muted); }
.s-kpi__icon {
  position: absolute; right: 0.8rem; top: 0.85rem;
  width: 32px; height: 32px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(44,65,144,0.08); color: var(--s-primary);
}
.s-kpi--accent .s-kpi__icon { background: rgba(228,38,38,0.1); color: var(--s-accent); }
.s-kpi--success .s-kpi__icon { background: rgba(46,125,50,0.12); color: #2e7d32; }
.s-kpi--warn .s-kpi__icon { background: rgba(237,108,2,0.12); color: #ed6c02; }
.s-kpi--info .s-kpi__icon { background: rgba(2,136,209,0.12); color: #0288d1; }

/* ── Clickable KPI tiles ───────────────────────────────────────────────────
   The hover lift is scoped to .s-kpi--clickable rather than every .s-kpi: a tile
   that lifts under the cursor and then does nothing when clicked reads as broken,
   which is what the un-wired tiles used to do.
   The arrow appears on hover/focus and sits in the padding, so it never reflows
   the number above it. */
.s-kpi--clickable { cursor: pointer; }
.s-kpi--clickable:hover { transform: translateY(-2px); box-shadow: 0 10px 22px -12px rgba(0,0,0,0.18); }
.s-kpi--clickable:focus-visible {
  outline: 2px solid var(--s-primary); outline-offset: 2px;
}
.s-kpi--clickable::after {
  content: "→";
  position: absolute; right: 0.9rem; bottom: 0.7rem;
  font-size: 0.85rem; line-height: 1; color: var(--s-primary);
  opacity: 0; transform: translateX(-3px);
  transition: opacity 160ms ease, transform 160ms ease;
}
.s-kpi--clickable:hover::after,
.s-kpi--clickable:focus-visible::after { opacity: 0.75; transform: translateX(0); }

/* Names the dashboard filter a list page was opened with, and clears it. */
.s-kpi-view-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 6px 5px 10px; border-radius: 999px;
  background: var(--s-selected); color: var(--s-primary);
  border: 1px solid rgba(44,65,144,0.22);
  font-size: 12px; font-weight: 500; flex-shrink: 0;
}
.s-kpi-view-chip b { font-weight: 700; }
.s-kpi-view-chip__clear {
  border: 0; border-radius: 999px; cursor: pointer;
  padding: 2px 9px; font: inherit; font-size: 11px; font-weight: 600;
  background: var(--s-surface); color: var(--s-text-secondary);
  border: 1px solid var(--s-border);
}
.s-kpi-view-chip__clear:hover { color: var(--s-error); border-color: var(--s-error); }

.s-report-card-title { font-size: 0.95rem; font-weight: 600; margin: 0 0 0.15rem; display: flex; align-items: center; gap: 0.5rem; }
.s-report-card-sub { font-size: 0.78rem; color: var(--s-text-muted); margin: 0 0 0.85rem; }

/* Donut chart — see Components/UI/SPieChart.razor for why this is SVG and not a conic-gradient.
   The chart sits beside its legend and both wrap onto one column when the card gets narrow. */
.s-pie { --s-pie-size: 160px; display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
.s-pie__figure { position: relative; width: var(--s-pie-size); height: var(--s-pie-size); flex: 0 0 auto; }
.s-pie__figure svg { width: 100%; height: 100%; display: block; transform: rotate(0deg); }
.s-pie__track { fill: transparent; stroke: var(--s-surface-variant, #eee); stroke-width: 5; }
.s-pie__slice { fill: transparent; stroke-width: 5; transition: stroke-width 120ms ease, opacity 120ms ease; }
.s-pie__slice--link { cursor: pointer; }
.s-pie__slice--link:hover { stroke-width: 6.5; }
/* The hole is not a hit target — a click landing here would belong to no slice. */
.s-pie__hole { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center;
               justify-content: center; pointer-events: none; text-align: center; }
.s-pie__total { font-size: 1.45rem; font-weight: 700; line-height: 1.1; color: var(--s-text); font-variant-numeric: tabular-nums; }
.s-pie__total-label { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--s-text-muted); }
.s-pie__legend { list-style: none; margin: 0; padding: 0; flex: 1 1 170px; min-width: 0;
                 display: flex; flex-direction: column; gap: 0.3rem; }
.s-pie__legend button { display: flex; align-items: center; gap: 0.45rem; width: 100%; padding: 2px 4px;
                        background: none; border: 0; border-radius: 5px; font: inherit; font-size: 0.78rem;
                        color: var(--s-text-secondary); text-align: left; }
.s-pie__legend button:not(:disabled) { cursor: pointer; }
.s-pie__legend button:not(:disabled):hover { background: var(--s-hover); }
.s-pie__legend i { width: 10px; height: 10px; border-radius: 2px; flex: 0 0 auto; }
.s-pie__legend-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-pie__legend b { font-weight: 600; color: var(--s-text); font-variant-numeric: tabular-nums; }
.s-pie__legend-pct { color: var(--s-text-muted); font-variant-numeric: tabular-nums; min-width: 34px; text-align: right; }

/* Distribution bar */
.s-dist { display: flex; width: 100%; height: 10px; border-radius: 999px; overflow: hidden; background: var(--s-border); }
.s-dist > span { display: block; height: 100%; transition: width 300ms ease; }
.s-dist-legend { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.7rem; font-size: 0.75rem; color: var(--s-text-muted); }
.s-dist-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 0.35rem; vertical-align: middle; }
.s-dist-legend b { font-weight: 600; color: var(--s-text); margin-left: 0.2rem; }

/* Funnel */
.s-funnel { display: flex; flex-direction: column; gap: 0.55rem; }
.s-funnel__row { display: grid; grid-template-columns: 120px 1fr 70px; align-items: center; gap: 0.6rem; font-size: 0.82rem; }
.s-funnel__label { color: var(--s-text-secondary); font-weight: 500; }
.s-funnel__bar { height: 22px; background: var(--s-hover, #f5f5f5); border-radius: 6px; overflow: hidden; position: relative; }
.s-funnel__fill { height: 100%; background: linear-gradient(90deg, var(--s-primary), #5468c7); border-radius: 6px; transition: width 400ms ease; }
.s-funnel__fill--accent { background: linear-gradient(90deg, var(--s-accent), #ff6b6b); }
.s-funnel__fill--success { background: linear-gradient(90deg, #2e7d32, #66bb6a); }
.s-funnel__count { font-weight: 600; color: var(--s-text); text-align: right; font-variant-numeric: tabular-nums; }

/* Hub category card */
.s-rcat {
  position: relative; display: flex; flex-direction: column; gap: 0.6rem;
  padding: 1.25rem; background: var(--s-surface);
  border: 1px solid var(--s-border); border-radius: var(--s-radius-lg);
  transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
  min-height: 172px; text-decoration: none; color: inherit;
}
.s-rcat:hover { transform: translateY(-3px); box-shadow: 0 14px 28px -16px rgba(44,65,144,0.28); border-color: rgba(44,65,144,0.3); }
.s-rcat__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.5rem; }
.s-rcat__icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: rgba(44,65,144,0.08); color: var(--s-primary); flex: none; }
.s-rcat__title { font-size: 1rem; font-weight: 600; margin: 0.25rem 0 0; color: var(--s-text); }
.s-rcat__desc { font-size: 0.82rem; color: var(--s-text-muted); margin: 0; flex: 1; line-height: 1.5; }
.s-rcat__foot { display: flex; align-items: center; justify-content: space-between; margin-top: 0.25rem; }
.s-rcat__cta { color: var(--s-primary); font-weight: 600; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 0.3rem; }
.s-rcat__tag { font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; padding: 0.22rem 0.55rem; border-radius: 999px; background: rgba(46,125,50,0.12); color: #2e7d32; }
.s-rcat__tag--soon { background: rgba(158,158,158,0.18); color: #616161; }
.s-rcat__tag--beta { background: rgba(237,108,2,0.14); color: #ed6c02; }
.s-rcat--accent .s-rcat__icon { background: rgba(228,38,38,0.1); color: var(--s-accent); }
.s-rcat--success .s-rcat__icon { background: rgba(46,125,50,0.12); color: #2e7d32; }
.s-rcat--warn .s-rcat__icon { background: rgba(237,108,2,0.12); color: #ed6c02; }
.s-rcat--info .s-rcat__icon { background: rgba(2,136,209,0.12); color: #0288d1; }
.s-report-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; }

/* Coming-soon checklist */
.s-soon-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin-top: 1rem; }
.s-soon-item { display: flex; align-items: center; gap: 0.65rem; padding: 0.75rem 0.9rem; background: var(--s-surface); border: 1px dashed var(--s-border); border-radius: var(--s-radius-md); font-size: 0.85rem; color: var(--s-text-secondary); }
.s-soon-item svg { color: var(--s-text-muted); flex: none; }

/* Rank / leaderboard list */
.s-rank { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.55rem; }
.s-rank__item { display: grid; grid-template-columns: 24px 1fr auto; align-items: center; gap: 0.6rem; font-size: 0.85rem; }
.s-rank__pos { width: 22px; height: 22px; border-radius: 6px; background: rgba(44,65,144,0.08); color: var(--s-primary); font-weight: 700; font-size: 0.72rem; display: flex; align-items: center; justify-content: center; }
/* Matches the first row whether .s-rank__item is the <li> itself or a <button> inside one —
   a drillable rank list wraps the row in a button, which would make :nth-child(1) true for
   every row. */
.s-rank > li:first-child .s-rank__pos { background: rgba(228,38,38,0.12); color: var(--s-accent); }
.s-rank__label { color: var(--s-text); font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-rank__value { color: var(--s-text); font-weight: 600; font-variant-numeric: tabular-nums; }
.s-rank__bar { grid-column: 2 / 4; height: 4px; background: var(--s-border); border-radius: 999px; overflow: hidden; margin-top: -0.15rem; }
.s-rank__bar > span { display: block; height: 100%; background: linear-gradient(90deg, var(--s-primary), #5468c7); }

/* Trend chart */
.s-trend { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.4rem; align-items: end; height: 140px; padding: 0 0 0.25rem; }
.s-trend__col { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; height: 100%; justify-content: flex-end; }
.s-trend__bar { width: 100%; max-width: 28px; background: linear-gradient(180deg, var(--s-primary), #5468c7); border-radius: 6px 6px 0 0; min-height: 4px; transition: height 300ms ease; }
.s-trend__bar--accent { background: linear-gradient(180deg, var(--s-accent), #ff6b6b); }
.s-trend__label { font-size: 0.7rem; color: var(--s-text-muted); font-weight: 500; }
.s-trend__value { font-size: 0.72rem; color: var(--s-text); font-weight: 600; font-variant-numeric: tabular-nums; }

/* A chart element that opens the rows behind it. Real <button>s, so a keyboard reaches them.
   Deliberately sets no `display` — the layout class it is combined with (.s-trend__col,
   .s-funnel__row, .s-rank__item) owns that and would be overridden. */
.s-drillable {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; width: 100%; text-align: inherit; cursor: pointer;
}
.s-drillable:focus-visible { outline: 2px solid var(--s-primary); outline-offset: 3px; border-radius: 4px; }
.s-funnel__row.s-drillable:hover .s-funnel__label,
.s-rank__item.s-drillable:hover .s-rank__label { color: var(--s-primary); }
.s-funnel__row.s-drillable:hover .s-funnel__bar { box-shadow: inset 0 0 0 1px var(--s-primary); }
.s-trend__col.s-drillable:hover .s-trend__bar { filter: brightness(1.2); }
.s-trend__col.s-drillable:hover .s-trend__label { color: var(--s-primary); font-weight: 700; }
.s-dist-legend .s-drillable { width: auto; }
.s-dist-legend .s-drillable:hover { color: var(--s-primary); text-decoration: underline; }

/* Stat grid (inside cards) */
.s-stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 0.75rem; margin-top: 0.5rem; }
.s-stat { padding: 0.65rem 0.75rem; border-radius: var(--s-radius-md); background: var(--s-hover, #f7f9fc); border: 1px solid var(--s-border); }
.s-stat__label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--s-text-muted); font-weight: 600; }
.s-stat__value { font-size: 1.1rem; font-weight: 700; color: var(--s-text); margin-top: 0.2rem; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }

/* Dashboard hero overrides */
.s-dash-hero { padding: 2rem 1.75rem; min-height: 132px; display: flex; align-items: center; }
.s-dash-hero .s-report-hero__inner { align-items: center; gap: 1.25rem; flex-wrap: nowrap; width: 100%; }
.s-dash-hero__id { display: flex; align-items: center; gap: 1rem; min-width: 0; flex: 1; }
.s-dash-hero__id > div:last-child { min-width: 0; }
.s-dash-hero__id h1 { font-size: 1.4rem; margin: 0 0 0.35rem; line-height: 1.25; }
.s-dash-hero__id h1:focus, .s-dash-hero__id h1:focus-visible { outline: none; box-shadow: none; background: transparent; }
.s-dash-hero__id p { margin: 0; opacity: 0.9; font-size: 0.85rem; line-height: 1.45; }
.s-dash-hero__avatar {
  width: 52px; height: 52px; border-radius: 14px; flex: none;
  background: rgba(255,255,255,0.18);
  border: 1px solid rgba(255,255,255,0.35);
  color: #fff; display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 1.15rem; letter-spacing: 0.02em;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}
.s-dash-hero__date {
  text-align: right; flex: none; padding: 0.55rem 0.9rem;
  border-radius: 10px; background: rgba(255,255,255,0.12);
  border: 1px solid rgba(255,255,255,0.18);
}
.s-dash-hero__day { font-size: 0.78rem; opacity: 0.8; font-weight: 500; letter-spacing: 0.02em; }
.s-dash-hero__full { font-size: 0.95rem; font-weight: 600; letter-spacing: -0.005em; margin-top: 0.1rem; }
@media (max-width: 720px) {
  .s-dash-hero .s-report-hero__inner { flex-wrap: wrap; }
  .s-dash-hero__date { text-align: left; width: 100%; }
}

/* Bucket (progress segment) */
.s-bucket { display: flex; flex-direction: column; gap: 0.5rem; }
.s-bucket__row { display: grid; grid-template-columns: 90px 1fr 70px; gap: 0.6rem; align-items: center; font-size: 0.82rem; }
.s-bucket__label { color: var(--s-text-secondary); font-weight: 500; }
.s-bucket__track { height: 18px; background: var(--s-hover, #f5f5f5); border-radius: 4px; overflow: hidden; }
.s-bucket__fill { height: 100%; background: linear-gradient(90deg, var(--s-primary), #5468c7); transition: width 400ms ease; }
.s-bucket__count { text-align: right; font-weight: 600; color: var(--s-text); font-variant-numeric: tabular-nums; }

/* ---- Status & Priority badge tones ----
   Emitted by StatusBadge.razor (GetVariant), so these apply app-wide.
   Tailwind 100/700 scale so status and priority read as one system.
   .status-select--* below re-uses the same tones for the inline Status
   dropdown on the leads grid, which is a select rather than a badge. */

.s-badge--priority-high   { background: #ffedd5; color: #c2410c; }
.s-badge--priority-medium { background: #fef9c3; color: #a16207; }
.s-badge--priority-low    { background: #dcfce7; color: #15803d; }

/* Status badges — same tones as the .status-select dropdown below, for the
   lead view header and the BOD/BOQ tables in Related Documents. */
.s-badge--status-info    { background: #dbeafe; color: #1d4ed8; }
.s-badge--status-success { background: #dcfce7; color: #15803d; }
.s-badge--status-warning { background: #fef9c3; color: #a16207; }
.s-badge--status-error   { background: #fee2e2; color: #b91c1c; }
.s-badge--status-default { background: #f3f4f6; color: #4b5563; }

/* Lead-pipeline tones — one hue per LeadStatus so no two stages look alike.
   New=default grey, InProgress=info blue, Won=success, Lost=error, OnHold=warning;
   the four below fill the remaining stages. Same 100/700 scale as above. */
.s-badge--status-contacted   { background: #cffafe; color: #0e7490; } /* cyan   */
.s-badge--status-sitevisit   { background: #fce7f3; color: #be185d; } /* pink   */
.s-badge--status-quoted      { background: #ede9fe; color: #6d28d9; } /* violet */
.s-badge--status-negotiation { background: #ffedd5; color: #c2410c; } /* orange */

/* Width comes from the SSelect inside (grids pass max-width:140px; form
   fields stay full-bleed), so don't constrain it here. */
.status-select { display: block; }
.status-select .s-custom-select-trigger { font-weight: 600; border-color: transparent; }
/* Chevron defaults to grey — follow the pill's text colour instead. */
.status-select .s-custom-select-trigger svg { color: currentColor; }

/* InProgress (also Submitted/Sent on the BOD & BOQ grids) */
.status-select--info .s-custom-select-trigger    { background: #dbeafe; color: #1d4ed8; }
/* Won */
.status-select--success .s-custom-select-trigger { background: #dcfce7; color: #15803d; }
/* OnHold */
.status-select--warning .s-custom-select-trigger { background: #fef9c3; color: #a16207; }
/* Lost */
.status-select--error .s-custom-select-trigger   { background: #fee2e2; color: #b91c1c; }
/* New */
.status-select--default .s-custom-select-trigger { background: #f3f4f6; color: #4b5563; }
/* Contacted */
.status-select--contacted .s-custom-select-trigger   { background: #cffafe; color: #0e7490; }
/* SiteVisitDone */
.status-select--sitevisit .s-custom-select-trigger   { background: #fce7f3; color: #be185d; }
/* Quoted */
.status-select--quoted .s-custom-select-trigger      { background: #ede9fe; color: #6d28d9; }
/* Negotiation */
.status-select--negotiation .s-custom-select-trigger { background: #ffedd5; color: #c2410c; }

/* ---- My Profile (/my/profile) ----
   The page is built from the standard form classes (lead-form / lead-panel / s-grid /
   s-field / s-input / s-badge / lead-actions). Only the few things with no existing
   equivalent live here. Not a .razor.css because App.razor omits the scoped bundle.

   Everything is scoped under .prof-page (the wrapper around the whole page) so the
   responsive rules at the bottom of this block cannot reach the other form pages. */

/* This is a form page, not a data page: capped and centred so the fields stay a sane
   length on a wide monitor instead of a single input spanning 1600px. */
.prof-page { max-width: 1280px; margin-inline: auto; }
.prof-header { display: flex; align-items: center; justify-content: space-between;
               flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }

/* Submit row inside a panel, so the button stays attached to the fields it posts. Same
   look as .lead-actions, but as the panel's own footer rather than a detached bar. */
.prof-panel-actions { display: flex; justify-content: flex-end; gap: 10px;
                      padding: 10px 16px; background: #f8f9fb;
                      border-top: 1px solid var(--s-border); }

/* Column-flex the side-by-side panels so a footer pins to the bottom of the taller one
   instead of floating wherever its own content happens to end. */
.prof-page .lead-two-col > .lead-panel { display: flex; flex-direction: column; }
.prof-page .lead-two-col > .lead-panel > .lead-panel-body { flex: 1 1 auto; }
.prof-page .prof-panel-actions { margin-top: auto; }

/* A column of stacked panels as one half of a .lead-two-col row — Account with Username under it,
   beside Change Password. align-self:start keeps the pair at their natural height instead of the
   grid stretching them to match the taller password panel, which is what left a tall empty box
   under "Employee record" once the mobile and email rows moved into My Details. */
.prof-page .lead-two-col > .prof-col {
  display: flex; flex-direction: column; gap: 12px;
  align-self: start; min-width: 0;
}

/* Save bar for the My Details form. Deliberately NOT position:sticky.
   It was, briefly, so a change made in the top panel could be saved without scrolling — but a
   sticky element stays pinned for the whole height of its container, and that container is the
   entire five-panel form. So the bar floated over the Personal Information and Contact fields the
   moment you were anywhere above the end of the section: solid background, drawn on top, looking
   exactly like a rendering bug. A save bar belongs at the end of what it saves. */

/* My Details is one panel holding five headed groups, so each group needs a divider that reads as
   subordinate to .lead-panel-head rather than competing with it: a rule above, smaller and in the
   secondary colour. The first has no top border — it sits directly under the panel head, and a
   second line there would look like a doubled border. */
.prof-details .prof-group-head {
  margin: 18px 0 10px; padding-top: 14px;
  border-top: 1px solid var(--s-border);
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.5px;
  text-transform: uppercase; color: var(--s-text-secondary);
}
.prof-details .prof-group-head:first-child {
  margin-top: 0; padding-top: 0; border-top: none;
}

.prof-page .prof-save-note {
  margin-right: auto; align-self: center;
  font-size: 0.78rem; color: var(--s-text-secondary);
}

/* Identity block at the top of the Account panel. */
.prof-identity { display: flex; align-items: center; gap: 12px; padding-bottom: 12px;
                 margin-bottom: 4px; border-bottom: 1px solid #f0f1f3; }
.prof-avatar   { width: 44px; height: 44px; flex: none; border-radius: 50%;
                 background: var(--s-primary); color: #fff; display: flex;
                 align-items: center; justify-content: center;
                 font-size: 0.95rem; font-weight: 700; }
.prof-name     { font-size: 0.95rem; font-weight: 700; color: var(--s-text); margin-bottom: 4px; }

/* Password input + reveal button. .s-field is a flex column, so this is the row that
   positions the eye over the input. */
.prof-pw          { position: relative; }
.prof-pw .s-input { padding-right: 38px; }
.prof-eye         { position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
                    background: none; border: none; cursor: pointer;
                    color: var(--s-text-secondary); padding: 2px; display: flex; }
.prof-eye:hover   { color: var(--s-text); }
.prof-eye svg     { width: 17px; height: 17px; }

/* Live requirement ticks under the new-password field. auto-fit rather than a fixed
   two columns: this box sits in a half-width panel on a laptop and a full-width one on
   a tablet, so let it find its own column count instead of guessing per breakpoint. */
.prof-rules { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
              gap: 6px 12px;
              padding: 10px 12px; background: #f8f9fb;
              border: 1px solid var(--s-border); border-radius: 8px; }
.prof-rule  { display: flex; align-items: center; gap: 7px;
              font-size: 0.75rem; color: var(--s-text-secondary); }
.prof-tick  { display: inline-flex; width: 12px; justify-content: center;
              color: var(--s-border); font-weight: 700; }
.prof-rule.is-met            { color: var(--s-success); }
.prof-rule.is-met .prof-tick { color: var(--s-success); }

/* Hidden until the confirm field actually disagrees. */
.prof-mismatch { display: none; }

/* Result banner after a POST round-trip. */
.prof-alert       { padding: 9px 14px; border-radius: 8px; font-size: 0.82rem; border: 1px solid transparent; }
.prof-alert--ok   { background: var(--s-success-light); color: var(--s-success); border-color: var(--s-success); }
.prof-alert--err  { background: var(--s-error-light);   color: var(--s-error);   border-color: var(--s-error); }

/* A long username or email is one unbroken string; without this it pushes the row wider
   than the panel instead of wrapping. min-width:0 lets the flex child actually shrink. */
.prof-page .detail-value { min-width: 0; overflow-wrap: anywhere; }

/* A grid item's automatic minimum size is min-content, and the vehicles table's min-content
   is ~680px — so without this the track grows to fit the table and drags the whole page
   (identity panel and all) out to 680px on a phone. With it the track shrinks to the
   container and the table scrolls inside its own .s-table-wrap, which is the point of that
   wrapper. */
.prof-page .s-grid > * { min-width: 0; }

/* ---- Responsive ----
   The global rule near the top of this file collapses .s-col-* to `span 1` at <=768px but
   never re-declares .s-grid-12, so a .s-col-6 inside a 12-column grid becomes one twelfth
   of the width — the password fields render as ~60px slivers. These rules restate both
   halves together, which is the only way to keep them consistent.

   Tablet keeps two columns (the panels are full width by then, so a half is still ~340px);
   phones go to one. */
@media (max-width: 768px) {
  .prof-page .s-grid-12 { grid-template-columns: repeat(2, 1fr); }
  .prof-page .s-col-6   { grid-column: span 1; }
  .prof-page .s-col-12  { grid-column: span 2; }

  /* Comfortable touch target for the reveal toggle — the icon stays 17px, the hit area grows. */
  .prof-page .prof-pw .s-input { padding-right: 44px; }
  .prof-page .prof-eye { right: 2px; width: 40px; height: 40px; padding: 0;
                         align-items: center; justify-content: center; }
}

@media (max-width: 560px) {
  .prof-page .s-grid-12 { grid-template-columns: 1fr; }
  .prof-page .s-col-12  { grid-column: span 1; }

  /* Label above value: side-by-side wraps the label onto two lines as soon as the value is
     a sentence ("Not linked — My Portal pages are unavailable"). Beats !important-ing the
     inline text-align on every long row. */
  .prof-page .detail-row   { flex-direction: column; align-items: flex-start; gap: 2px; }
  .prof-page .detail-value { text-align: left !important; }

  /* Anything under 16px makes iOS Safari zoom the viewport on focus, which then leaves the
     page scrolled sideways. */
  .prof-page .s-input { font-size: 16px; }

  .prof-page .lead-panel-body     { padding: 12px; }
  .prof-page .prof-panel-actions  { padding: 10px 12px; }
  .prof-page .prof-panel-actions .s-btn { width: 100%; justify-content: center; }

  /* The save bar stacks rather than squeezing the note and the button onto one 360px row,
     where the button would shrink to a couple of words. */
  .prof-page .prof-save-bar { flex-wrap: wrap; padding: 10px 12px; }
  .prof-page .prof-save-note { margin-right: 0; width: 100%; }
  .prof-page .prof-save-bar .s-btn { width: 100%; justify-content: center; }
}


/* ============================================================================
   SITE VISIT — photo picker and detail page
   ---------------------------------------------------------------------------
   Lives here rather than in a .razor.css because scoped styles are not wired up
   in App.razor, so a component stylesheet would be inert.

   Two photo surfaces, deliberately styled apart:
     .svd-odo   two fixed slots, the evidence behind the KM figure and the fuel
                claim. Always exactly start + end.
     .spp/.svd-gallery   a growing gallery of the work itself.
   They are never merged. Someone auditing a travel claim needs to land on the
   two readings, not hunt for them among thirty site pictures.
   ========================================================================= */

/* ---- Page header ---- */
.svd-head { display: flex; align-items: flex-start; justify-content: space-between;
    gap: 12px; margin-bottom: 14px; }
.svd-head-main { min-width: 0; }
.svd-head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.svd-crumb { display: inline-flex; align-items: center; gap: 3px; font-size: 0.7rem;
    font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
    color: var(--s-text-secondary); text-decoration: none; margin-bottom: 3px; }
.svd-crumb:hover { color: var(--s-primary); }
.svd-title { margin: 0; font-size: 1.3rem; font-weight: 700; color: var(--s-text);
    letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; }
.svd-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
    font-size: 0.78rem; color: var(--s-text-secondary); margin-top: 3px; }
.svd-dot { width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: 0.5; }

/* Equal-height panels in the two-column row. Without this the shorter panel stretches and
   leaves a dead band of whitespace under its content. */
.svd-page .lead-two-col > .lead-panel { display: flex; flex-direction: column; }
.svd-page .lead-two-col > .lead-panel > .lead-panel-body { flex: 1 1 auto; }

/* ---- Definition rows ---- */
.svd-dl { margin: 0; }
.svd-dl-row { display: grid; grid-template-columns: 108px 1fr; gap: 10px; align-items: center;
    padding: 7px 0; border-bottom: 1px solid #f0f1f4; }
.svd-dl-row:last-child { border-bottom: none; padding-bottom: 0; }
.svd-dl-row:first-child { padding-top: 0; }
.svd-dl dt { font-size: 0.75rem; color: var(--s-text-secondary); font-weight: 500; }
.svd-dl dd { margin: 0; font-size: 0.85rem; font-weight: 600; color: var(--s-text);
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.svd-time { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.svd-muted { font-weight: 400; color: var(--s-text-secondary); }

/* ---- The odometer sum, written out ---- */
.svd-calc { display: flex; align-items: stretch; gap: 8px; }
.svd-calc-term { flex: 1; min-width: 0; text-align: center; background: #f8f9fb;
    border: 1px solid var(--s-border); border-radius: 8px; padding: 9px 6px; }
.svd-calc-term--total { background: rgba(44,65,144,0.06); border-color: rgba(44,65,144,0.25); }
.svd-calc-num { font-size: 1.15rem; font-weight: 700; color: var(--s-text);
    font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1.2; }
.svd-calc-term--total .svd-calc-num { color: var(--s-primary); }
.svd-calc-lbl { font-size: 0.5625rem; font-weight: 700; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--s-text-secondary); margin-top: 2px; }
.svd-calc-op { display: flex; align-items: center; font-size: 0.95rem; font-weight: 600;
    color: var(--s-text-secondary); flex-shrink: 0; }

.svd-warn { margin-top: 8px; font-size: 0.7rem; color: #b45309; background: #fffbeb;
    border: 1px solid #fde68a; border-radius: 6px; padding: 6px 9px; }

.svd-odo-head { font-size: 0.75rem; color: var(--s-text-secondary); font-weight: 500;
    margin: 14px 0 7px; padding-top: 12px; border-top: 1px solid #f0f1f4; }
.svd-odo-note { margin: 9px 0 0; font-size: 0.6875rem; color: var(--s-text-secondary); }

.spp-actions { display: flex; flex-wrap: wrap; gap: 8px; }
/* Applied to both a <label> (gallery) and a <button> (camera) — font-family: inherit is what
   keeps them looking like one pair, since a button otherwise takes the UA's own font. */
.spp-btn { display: inline-flex; align-items: center; justify-content: center; gap: 7px;
           padding: 10px 14px; background: #f8f9fb; border: 1px dashed var(--s-border);
           border-radius: 8px; cursor: pointer; font-size: 0.8125rem; font-weight: 600;
           font-family: inherit; color: var(--s-text); min-height: 42px; }
.spp-btn:hover { border-color: var(--s-primary); color: var(--s-primary); }
.spp-btn input[type="file"] { display: none; }
.spp-btn--off { opacity: 0.5; cursor: not-allowed; }
.spp-btn--off:hover { border-color: var(--s-border); color: var(--s-text); }

.spp-note { font-size: 0.6875rem; color: var(--s-text-secondary); margin-top: 6px; }
.spp-note--warn { color: #b45309; }

.spp-grid, .svd-gallery { display: grid; gap: 8px; margin-top: 10px;
    /* auto-fill, not auto-fit: with two photos auto-fit stretches each to half the
       panel. Thumbnails keep one size and the row simply fills up. */
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }

.spp-thumb, .svd-photo { position: relative; border: 1px solid var(--s-border);
    border-radius: 8px; overflow: hidden; background: #f8f9fb;
    transition: transform 140ms ease, box-shadow 140ms ease, border-color 140ms ease; }
.svd-photo:hover { transform: translateY(-2px); border-color: var(--s-primary);
    box-shadow: 0 8px 18px -10px rgba(0,0,0,0.25); }
.spp-thumb img, .svd-photo img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
    display: block; }
.svd-photo img { cursor: zoom-in; }

.spp-del, .svd-del { position: absolute; top: 5px; right: 5px; width: 24px; height: 24px;
    border-radius: 50%; background: rgba(0,0,0,0.55); color: #fff; border: none;
    cursor: pointer; font-size: 0.7rem; line-height: 1; display: flex;
    align-items: center; justify-content: center; }

.spp-src { position: absolute; bottom: 5px; left: 5px; background: rgba(0,0,0,0.6);
    color: #fff; border-radius: 4px; padding: 1px 5px; font-size: 0.5625rem;
    font-weight: 700; letter-spacing: 0.04em; }

.svd-photo-meta { padding: 5px 7px; font-size: 0.6875rem; color: var(--s-text-secondary);
    display: flex; align-items: center; justify-content: space-between; gap: 5px; }
.svd-late { position: absolute; top: 5px; left: 5px; background: #fffbeb; color: #b45309;
    border: 1px solid #fde68a; border-radius: 4px; padding: 1px 5px;
    font-size: 0.5625rem; font-weight: 700; letter-spacing: 0.03em; }

/* The two odometer slots. Fixed pair, visually distinct from the gallery above. */
.svd-odo { display: flex; flex-wrap: wrap; gap: 14px; }
.svd-odo-slot { text-align: center; }
.svd-odo-cap { font-size: 0.625rem; font-weight: 700; letter-spacing: 0.06em;
    color: var(--s-text-secondary); margin-top: 4px; }

/* Read-only fact, shaped like an SInput so a panel of facts lines up with a panel of fields. */
.svd-fact-label { display: block; font-size: 0.75rem; font-weight: 500;
    color: var(--s-text-secondary); margin-bottom: 4px; }
.svd-fact-value { font-size: 0.9375rem; font-weight: 600; color: var(--s-text);
    font-variant-numeric: tabular-nums; }
.svd-fact-sub { font-size: 0.6875rem; color: var(--s-text-secondary); margin-top: 2px; }

.svd-note { background: #f8f9fb; border: 1px solid var(--s-border); border-radius: 6px;
    padding: 9px 11px; font-size: 0.8125rem; white-space: pre-wrap; word-break: break-word; }

.svd-empty { border: 1px dashed var(--s-border); border-radius: 8px; padding: 20px;
    text-align: center; color: var(--s-text-secondary); font-size: 0.8125rem; }

/* Marks a panel whose contents were recorded at the punch and cannot be edited. Notes and site
   photos stay open; times, coordinates and odometer readings never do. Saying which is which
   beats leaving someone hunting for an edit button that does not exist. */
.svd-locked { display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0;
    font-size: 0.5625rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--s-text-secondary); background: #eef0f4; border: 1px solid var(--s-border);
    border-radius: 4px; padding: 2px 6px; }

@media (max-width: 560px) {
  .spp-grid, .svd-gallery { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
  .spp-btn { flex: 1; }
}


/* ============================================================================
   GLOBAL RESPONSIVE
   ---------------------------------------------------------------------------
   Last block in the file on purpose: these rules correct patterns defined much
   earlier, and relying on source order is clearer than an arms race of
   specificity selectors.

   Breakpoints match the rest of the app: 900px (sidebar still visible, panels
   narrow), 768px (sidebar becomes a drawer), 560px (phone).
   ========================================================================= */

/* ---- Grid children must be allowed to shrink ----
   A grid item's automatic minimum size is min-content, so one wide child — a
   table, a long unbroken string — makes its track wider than the container and
   drags the whole page sideways. This is what makes .s-table-wrap's overflow-x
   actually engage instead of being bypassed. */
.s-grid > * { min-width: 0; }

/* ---- 12-column form grids ----
   The 768px block near the top of this file collapses .s-col-* to `span 1` but
   never re-declares .s-grid-12, so a .s-col-6 inside a 12-track grid rendered at
   one twelfth width — roughly 60px, i.e. unusable. Track count and span have to
   change together, which is what these two blocks do. */
@media (max-width: 768px) {
  .s-grid-12 { grid-template-columns: repeat(2, 1fr); }
  .s-col-12  { grid-column: span 2; }
}
@media (max-width: 560px) {
  .s-grid-12 { grid-template-columns: 1fr; }
  .s-col-12  { grid-column: span 1; }
}

/* ---- Dialogs ----
   .s-dialog sets `min-width: 400px; max-width: 90vw`. min-width wins over
   max-width in CSS, so on any viewport under ~444px every dialog in the app was
   wider than the screen — and .s-overlay is overflow:hidden, so the parts that
   hung off the edge could not even be scrolled to. Several dialogs also carry an
   inline min-width (420-560px), which is why this needs !important: an inline
   style cannot be beaten any other way. */
@media (max-width: 640px) {
  .s-overlay { padding: 10px; align-items: flex-start; overflow-y: auto; }
  .s-dialog {
    min-width: 0 !important;
    width: 100%;
    max-width: 100%;
    max-height: calc(100dvh - 20px);
    margin: auto 0;
  }
  .s-dialog-inner { max-height: calc(100dvh - 20px); }

  .s-dialog-title, .s-dialog-header { padding: 14px 16px; font-size: 1rem; }
  .s-dialog-body,  .s-dialog-content { padding: 14px 16px; }

  /* Stacked action buttons get a full-width tap target rather than three
     cramped ones on a single line. */
  .s-dialog-actions { padding: 12px 16px; flex-wrap: wrap; }
  .s-dialog-actions .s-btn { flex: 1 1 130px; justify-content: center; }
}

/* ---- Toolbars, search and page headers ----
   .s-toolbar is flex-wrap:nowrap and .s-search has a 250px floor, so a toolbar
   with a search box plus two filters could not fit a phone and pushed the page
   wide. Wrapping is the whole fix; the search box just needs permission to
   shrink. */
@media (max-width: 900px) {
  .s-toolbar { flex-wrap: wrap; }
  .s-toolbar > * { min-width: 0; }
  .s-search { min-width: 0; flex: 1 1 220px; }

  .s-page-header { flex-wrap: wrap; gap: 10px; }
  .s-page-header > * { min-width: 0; }
}

/* ---- Search and Columns share the top line on a phone ----
   The search box had `flex: 1 1 100%` below 560px, so it claimed a whole line on its own
   and every list page in the app pushed Columns onto a second one — a 40px-tall strip
   holding a single right-aligned button, on 42 pages.

   Two changes make them a pair. The toggle collapses to an icon-only square, which is what
   frees the room: the label and the caret cost ~85px of the ~340px a 360px screen has, and
   the grid icon already reads as "columns" next to a magnifier. And `order` pins the pair
   to the first line regardless of source position — the Clear Filters / Clear Sort buttons
   are rendered *between* the two, so without it they would land between them on the line.
   Everything else in the toolbar keeps the old full-width stacking below the pair.

   !important throughout: every one of these properties is set inline on the component. */
@media (max-width: 640px) {
  .s-toolbar > * { order: 2; }
  .s-toolbar > .s-search { order: 0; flex: 1 1 0; min-width: 0; }
  .s-toolbar > *:not(.s-search):not(.s-coltoggle) { flex: 1 1 100%; }

  .s-coltoggle {
    order: 1;
    display: flex !important;
    margin-left: 0 !important;

    /* The width must be a definite number, not `auto` + aspect-ratio. Sizing the button
       from its own height is circular here: the height comes from `align-self: stretch`,
       which is only resolved *after* the toolbar has laid its line out, so the line budgets
       the bare icon (~21px) and the painted 38px button then hangs 7px off the right of a
       360px screen. 38px matches the search input; the stretch below keeps the two aligned
       even if that input's height changes. */
    flex: 0 0 38px;
    align-self: stretch;
  }
  .s-coltoggle-label, .s-coltoggle-caret { display: none !important; }
  .s-coltoggle-btn {
    width: 100% !important;
    height: 100% !important;
    padding: 0 !important;
    gap: 0 !important;
    justify-content: center;
  }
  .s-coltoggle-btn .s-coltoggle-icon { width: 18px !important; height: 18px !important; }
}

@media (max-width: 560px) {
  .s-page-title { font-size: 1.2rem; }

  /* Toolbar controls go full width and stack: side-by-side halves leave the
     labels of a date filter or a select clipped. The search box is the exception —
     it pairs with the Columns toggle above. */
  .s-toolbar .s-btn { flex: 1 1 auto; justify-content: center; }

  .s-pagination { flex-wrap: wrap; }

  /* iOS Safari zooms the viewport when a focused input is under 16px, and never
     zooms back out — leaving the page scrolled sideways. */
  .s-input, .s-select, .s-textarea, select, textarea,
  input[type="text"], input[type="password"], input[type="email"],
  input[type="number"], input[type="date"], input[type="tel"] { font-size: 16px; }
}

/* ---- Intrinsic form sub-grids ----
   Replaces the hand-written `style="display:grid;grid-template-columns:1fr 1fr"`
   that was scattered through the HR dialogs. auto-fit + minmax means the column
   count follows the space available, so the same markup gives two columns in a
   680px dialog and one on a phone with no breakpoint to keep in sync. */
.s-form-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; }
.s-form-grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.75rem; }
.s-form-grid-2 > *, .s-form-grid-3 > * { min-width: 0; }

/* ---- Page-header action clusters and pagination ----
   Both are inline-styled `display:flex` rows with no wrap, so their buttons ran
   off the right edge on a phone (measured ~490px of content in a 360px viewport
   on /sales/quotations). Only flex-wrap is set here — the inline styles own
   everything else, and none of them declare wrap. */
@media (max-width: 900px) {
  .s-page-header .s-row { flex-wrap: wrap; justify-content: flex-end; }
}
@media (max-width: 560px) {
  .s-page-header .s-row { width: 100%; }
  .s-page-header .s-row .s-btn { flex: 1 1 auto; justify-content: center; }

  .s-pagination, .s-pagination > div, .s-pagination > div > div { flex-wrap: wrap; min-width: 0; }
  .s-pagination { justify-content: center; row-gap: 6px; }

  /* Belt and braces for the 16px input rule above: a wider font must never let a
     control outgrow the column it sits in. */
  .s-input, .s-select, .s-textarea, select, textarea { max-width: 100%; }
}

/* ---- Tab strips ----
   Not inside a media query on purpose. A settings page with seven tabs overflows
   at 768px, a page with three does not until 400px, and picking one breakpoint
   gets it wrong for both. overflow-x:auto costs nothing when the tabs already
   fit — no scrollbar is shown — and turns the strip into its own scroll pane
   exactly when it does not. */
/* .bom-tabs is the BOM view/form's own copy of this strip — same shape, same -2px underline
   overhang, declared locally in those two pages rather than through STabs. It is listed here so
   there is one implementation of "a tab strip scrolls when it does not fit": with five tabs
   (BOM Items · Dispatch Notes · Raise a Return · Return Notes · Site Expense) it overflows well
   before 360px, and without this the last tab simply could not be reached on a phone. */
.s-tabs-header,
.bom-tabs {
  overflow-x: auto;

  /* Explicit, not left to the initial `visible`. Setting overflow on one axis forces the other
     from `visible` to `auto`, so this box had become scrollable vertically too — and .s-tab-btn
     carries `margin-bottom: -2px` to sit its underline over the header's border, which is exactly
     the kind of intentional 2px overhang that then reads as vertical overflow. */
  overflow-y: hidden;

  flex-wrap: nowrap;

  /* No visible scrollbar. A tab strip is one row of controls: a scrollbar track under it adds
     height, draws a second horizontal line right below the header's border, and appears even on
     the pages whose tabs fit and never scroll. Hiding the chrome keeps the strip swipeable and
     shift-wheel scrollable without any of that. This is what tab strips normally do. */
  scrollbar-width: none;          /* Firefox, Safari 18.2+ */
  -ms-overflow-style: none;       /* legacy Edge */

  /* Swiping the tabs to their end must not then drag the page or trigger back-navigation. */
  overscroll-behavior-x: contain;
}
.s-tabs-header::-webkit-scrollbar,
.bom-tabs::-webkit-scrollbar { display: none; }   /* Chrome, older Safari, Samsung Internet */

/* Tabs keep their own width instead of being squeezed. Without this the strip never overflows —
   the buttons just shrink until every label wraps to two or three lines and the row is unreadable,
   which is the failure the scroll pane above is meant to replace. */
.s-tab-btn,
.bom-tab { flex: 0 0 auto; white-space: nowrap; }

/* ── Margin summary (MarginSummary.razor) ───────────────────────────────────────────────
   Only rendered for roles holding CanViewMargin. Three cards sit in a responsive band. */
.s-margin-panel {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
@media (max-width: 720px) {
  .s-margin-panel { grid-template-columns: 1fr; }
}

.s-margin-card {
  border: 1px solid var(--s-border);
  border-left-width: 3px;
  border-radius: 8px;
  padding: 10px 14px;
  background: var(--s-surface, #fff);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.s-margin-card--pos { border-left-color: #2E7D32; }
.s-margin-card--neg { border-left-color: var(--s-error); }
.s-margin-card--total { background: var(--s-surface-variant, #f5f6fa); }

.s-margin-card-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--s-text-secondary);
}
.s-margin-card-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.s-margin-card-amount {
  font-size: 20px;
  font-weight: 700;
  line-height: 1.1;
}
.s-margin-card--pos .s-margin-card-amount { color: #2E7D32; }
.s-margin-card--neg .s-margin-card-amount { color: var(--s-error); }

.s-margin-card-pct {
  font-size: 12px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
  white-space: nowrap;
}
.s-margin-card--pos .s-margin-card-pct { color: #1B5E20; background: #E8F5E9; }
.s-margin-card--neg .s-margin-card-pct { color: #B71C1C; background: #FDECEA; }

.s-margin-card-sub {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 11px;
  color: var(--s-text-secondary);
  border-top: 1px dashed var(--s-border);
  padding-top: 4px;
  margin-top: 2px;
}
.s-margin-card-sub b { font-weight: 600; color: var(--s-text); }

/* Legacy inline margin rows kept for the quotation view's pricing table. */
.s-margin-pos { color: #2E7D32; }
.s-margin-neg { color: var(--s-error); }

/* ============================================================================
   Leave balance cards  —  /my/leave-balances
   ----------------------------------------------------------------------------
   These carry their own grid because .s-form-grid-2 cannot be reused here: it is
   `auto-fit minmax(180px, 1fr)`, and inside a ~250px card interior only one
   column fits, so every label/value pair collapsed onto two separate rows.
   .s-leave-row is an explicit `1fr auto` — label left, figure right, always.
   ============================================================================ */
.s-leave-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--s-md);
}

.s-leave-card {
  background: var(--s-surface);
  border: 1px solid var(--s-border);
  border-radius: var(--s-radius-lg);
  padding: 1.1rem 1.15rem 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  position: relative;
  overflow: hidden;
  transition: box-shadow 160ms ease, transform 160ms ease;
}
.s-leave-card:hover { transform: translateY(-2px); box-shadow: var(--s-shadow-md); }

/* Accent rail. The tone is the leave type's health, set by a --tone variable so
   one rule serves every state instead of four near-identical modifiers. */
.s-leave-card::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 4px;
  background: var(--tone, var(--s-primary));
}

.s-leave-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.s-leave-card__name {
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--s-text-secondary);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.s-leave-card__figure { display: flex; align-items: baseline; gap: 0.4rem; }
.s-leave-card__value {
  font-size: 2.125rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--tone, var(--s-primary));
}
.s-leave-card__unit { font-size: 0.8125rem; color: var(--s-text-secondary); }

.s-leave-card__meter { display: flex; flex-direction: column; gap: 0.3rem; }
.s-leave-card__meter .s-progress { border-radius: 999px; }
.s-leave-card__meter .s-progress-bar { background: var(--tone, var(--s-primary)); border-radius: 999px; }
.s-leave-card__legend {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.7rem;
  color: var(--s-text-secondary);
  font-variant-numeric: tabular-nums;
}

.s-leave-breakdown {
  border-top: 1px dashed var(--s-border);
  padding-top: 0.55rem;
  display: flex;
  flex-direction: column;
  gap: 0.28rem;
}
.s-leave-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 0.75rem;
  font-size: 0.75rem;
}
.s-leave-row > span:first-child { color: var(--s-text-secondary); }
.s-leave-row > span:last-child {
  font-weight: 600;
  color: var(--s-text);
  font-variant-numeric: tabular-nums;
}
.s-leave-row--used > span:last-child { color: var(--s-error); }
.s-leave-row--lapsed > span:last-child { color: var(--s-warning); }

/* The "unlimited / unpaid" type has no quota, so a bar and a remaining figure are
   both meaningless — it gets a plain note in their place. */
.s-leave-card__note {
  font-size: 0.72rem;
  color: var(--s-text-secondary);
  background: var(--s-hover);
  border-radius: var(--s-radius-sm);
  padding: 0.4rem 0.55rem;
  line-height: 1.35;
}

.s-leave-hint { font-size: 0.78rem; color: var(--s-text-secondary); }
.s-leave-hint a { color: var(--s-primary); font-weight: 600; text-decoration: none; }
.s-leave-hint a:hover { text-decoration: underline; }

@media (max-width: 640px) {
  .s-leave-grid { grid-template-columns: 1fr; }
}


/* ============================================================================
   MOBILE — app shell, list tables, dashboard hero
   ---------------------------------------------------------------------------
   Last block in the file, same reason as the GLOBAL RESPONSIVE section above:
   these rules correct patterns declared much earlier and source order is easier
   to follow than an escalating specificity war.

   Desktop is deliberately untouched. The one rule outside a media query is inert
   at laptop widths — it only bites on a box that is already being squeezed.
   ========================================================================= */

/* ---- Never squeeze a block whose height comes from its content ----
   .s-content is `display:flex; flex-direction:column`, so every page-level block
   is a flex item and gets shrunk before the column agrees to scroll. The
   dashboard hero lost 78px that way on both a phone and a tablet:
   `min-height:132px` became the floor it was crushed to and `overflow:hidden`
   swallowed the rest, so "Good morning, QA DIRECTOR" rendered as "DIRECTOR" with
   the greeting cropped off above it and the date chip sliced in half below.

   .s-list-page is the deliberate exception — it is the one child that *should*
   absorb the leftover height and scroll its table inside itself. */
.s-content > *:not(.s-list-page) { flex-shrink: 0; }

@media (max-width: 640px) {
  /* ---- List tables ----
     The table already scrolls inside .s-table-wrap, so nothing was unreachable,
     but it read as broken for two reasons. Every cell wrapped: "Project
     Engineer" over two lines, a date over three, which made an attendance row
     73px tall so four records filled the screen. And once you swiped right the
     name column was gone, leaving columns of numbers belonging to nobody.

     One line per cell plus a pinned identity column fixes both. The table gets
     *wider* as a result, which is the right trade — it is a scroll pane, and
     vertical space is what is scarce on a phone. */
  .s-table th, .s-table td { white-space: nowrap; }
  .s-table th { padding: 8px 10px; }
  .s-table td { padding: 7px 10px; font-size: 0.75rem; }

  /* First column stays put while the rest swipes under it. thead th is already
     `position: sticky !important` for the vertical stick, so this only adds the
     horizontal half; its z-index has to clear the 50 that rule uses. */
  .s-table tbody td:first-child {
    position: sticky; left: 0; z-index: 2;
    background: var(--s-surface);
  }
  /* !important is not decoration here: the sticky-thead rule sets `z-index: 50 !important`
     on every th, so the pinned cell tied with the ones scrolling past it and lost on paint
     order — it stayed in place but was covered, and the header read "Check Out" above a
     column of names. The body cells never had this problem: their siblings have no z-index
     at all. */
  .s-table thead th:first-child { left: 0; z-index: 51 !important; }

  /* The edge line is the affordance: it is what says the table continues past it,
     since a touch device shows no scrollbar until you drag. The header cell has to
     restate the two insets it already carried — that declaration is !important, so
     a plain box-shadow here would be ignored rather than merged. */
  .s-table tbody td:first-child { box-shadow: 1px 0 0 var(--s-border); }
  .s-table thead th:first-child {
    box-shadow: inset 0 -1px 0 var(--s-border), inset 0 1px 0 var(--s-border),
                1px 0 0 var(--s-border) !important;
  }

  /* Row states have to be repainted on the pinned cell — it carries its own
     opaque background, so the row's would otherwise stop at the second column. */
  .s-table tbody tr:hover td:first-child { background: var(--s-hover); }
  .s-table tbody tr.selected td:first-child { background: var(--s-selected); }

  /* Cells that opted into truncation keep it. /hr/site-visits sets its own
     max-width + ellipsis on the Location column; nowrap above is what makes that
     ellipsis appear rather than the text simply being cut. */
  .s-table td[style*="ellipsis"] { max-width: 160px; }

  /* ---- Pagination footer ----
     Three lines on a phone with the middle one half empty. The footer is
     space-between with two children — the record count, and a group holding both
     the page buttons and the Rows selector. That group wraps as a unit, so Rows
     was dragged onto a third line while the space beside "Page 1 of 3 | 54
     records" sat empty.

     display:contents dissolves the group: its two halves become items of the
     footer itself and can be placed independently. Rows fills the empty space,
     the buttons take the line below and centre. Two lines, no hole.
     :has() picks them by what they contain rather than by position, so a page
     that orders the two differently still lands right. */
  .s-pagination { flex-wrap: wrap; justify-content: space-between; row-gap: 8px; column-gap: 10px; }
  /* !important because that group carries an inline `display:flex` on all 12 HR pages
     plus the Settings lists, and an inline declaration cannot be beaten any other way.
     Without it the rule silently no-ops and the `order` values below sort the two halves
     *within* the group instead of within the footer — which moved the record count to the
     bottom and made the footer worse, not better. */
  .s-pagination > div:nth-child(2):has(select) { display: contents !important; }
  .s-pagination > div:first-child { order: 1; }
  .s-pagination > div > div:has(> select) { order: 2; margin-left: auto; }
  .s-pagination > div > div:has(> button) { order: 3; flex: 1 0 100%; justify-content: center; }
}

/* ---- Horizontal strips that must scroll rather than overflow ----
   Not in a media query, and not new behaviour: this is exactly what .s-tabs-header
   already does. A strip written as a plain flex div (RoleEditor's tab row) has no
   class to hang it on, so it gets this one. Costs nothing when the strip fits — no
   scrollbar is drawn — and turns it into its own scroll pane when it does not. */
.s-scroll-x {
  overflow-x: auto;
  overflow-y: hidden;
  flex-wrap: nowrap;
  scrollbar-width: none;          /* Firefox, Safari 18.2+ */
  -ms-overflow-style: none;       /* legacy Edge */
  overscroll-behavior-x: contain;
}
.s-scroll-x::-webkit-scrollbar { display: none; }   /* Chrome, older Safari, Samsung Internet */
.s-scroll-x > * { flex: 0 0 auto; }

@media (max-width: 640px) {
  /* ---- Detail-page headers ----
     `<div style="display:flex; align-items:center; justify-content:space-between;
     margin-bottom:12px">` with an action cluster inside it, repeated verbatim across
     ~35 view and create/edit pages in Sales, Service, Settings, Projects and Masters.
     Neither div declares flex-wrap, so the buttons ran off the right edge — 319px off
     on the quotation view, where Back, Download PDF, Create Revision and Edit share a
     row that will not break.

     Matching on the inline style is not pretty. It is also the only hook those 35
     copies share, and threading a class through all of them would be a far larger and
     riskier edit than the three declarations it would buy. Nothing but wrapping is set
     here; the inline styles still own layout, spacing and alignment. */
  .s-content > div[style*="space-between"][style*="margin-bottom"],
  .s-content > div[style*="space-between"][style*="margin-bottom"] > div { flex-wrap: wrap; }
  .s-content > div[style*="space-between"][style*="margin-bottom"] > div > .s-btn {
    flex: 1 1 auto; justify-content: center;
  }

  /* ---- Tables written without a .s-table-wrap ----
     The related-documents tables on the lead view sit straight inside .lead-panel,
     which is overflow:hidden — so 406px of BOD and BOQ columns, including the Grand
     Total, could not be reached at all. Only tables whose direct parent is not a wrap
     are touched, so every list page keeps the pane it already has.

     `display: block` on a <table> makes the table box itself the scroll container while
     its rows still lay out as a table. It is the one way to give a table a pane without
     wrapping it in new markup.

     Any unwrapped table, not only .s-table: the quotation's Pricing Summary is a bare
     <table> inside a panel that sets overflow:hidden explicitly, and it clipped too. */
  :not(.s-table-wrap) > table { display: block; overflow-x: auto; max-width: 100%; }
}

@media (max-width: 768px) {
  /* ---- Room at the end of a page ----
     There is deliberately NO phone-specific number here. The gutter is
     `var(--s-md) + env(safe-area-inset-bottom)` at every width — see the token in :root.

     This block used to raise it to 40px, and that was compensation for a bug rather than a
     layout decision: `min-height: 100vh` on .s-sidebar-inset was pushing the foot of every
     page below the screen, and padding was the wrong lever for it — the padding landed in the
     clipped strip too. With the shell fixed, the extra was pure white band, and on a list page
     it never scrolled away: .s-list-page is `flex: 1`, so the card always stretches to the
     padding edge and the gap sits under it permanently. Measured ~56px on a gesture-nav phone
     (40 + a 16px inset).

     What is left is the two things that are actually part of the layout: --s-md, the same
     16px this page already uses for its other gutters, and the device's own reserved strip,
     which is not breathing room but compensation and is 0 where there is none. Nothing else
     is anchored to the bottom of the viewport — this app has no bottom navigation and no
     fixed footer, and the header is a flex sibling above .s-content rather than an overlay,
     so neither needs paying for. */

  /* The list page hands its height to the card, so the gutter has to come off the card
     rather than after it — otherwise the sticky pagination footer is what sits against
     the edge. */
  .s-list-page { padding-bottom: 0; }

  /* The drawer is fixed to bottom:0, so its last nav link is the one thing the gutter
     above cannot reach. */
  .s-sidebar-content { padding-bottom: calc(0.25rem + var(--s-safe-bottom)); }

  /* Toasts and dialogs are the other two boxes anchored near the bottom edge. */
  .s-toast-container { bottom: calc(32px + var(--s-safe-bottom)); }
  .s-dialog-actions { padding-bottom: calc(12px + var(--s-safe-bottom)); }
}

@media (max-width: 560px) {
  /* Reclaim the gutter — 8px per side is a 5% wider table on a 360px screen. */
  .s-content {
    padding: 12px 10px var(--s-page-bottom-gutter);
    padding-left: calc(10px + env(safe-area-inset-left, 0px));
    padding-right: calc(10px + env(safe-area-inset-right, 0px));
  }

  /* ---- Dashboard / report hero ----
     Sized for a laptop: 2rem padding, a 132px floor and a row that refuses to
     wrap until 720px. On a phone that leaves the greeting fighting the date chip
     for 270px. */
  .s-report-hero { padding: 1.25rem 1.1rem; }
  .s-dash-hero { padding: 1.25rem 1.1rem; min-height: 0; }
  .s-dash-hero .s-report-hero__inner { flex-wrap: wrap; gap: 0.85rem; }
  .s-dash-hero__avatar { width: 44px; height: 44px; font-size: 1rem; border-radius: 12px; }
  .s-dash-hero__id { gap: 0.7rem; }
  .s-dash-hero__id h1 { font-size: 1.1rem; }
  .s-dash-hero__id p { font-size: 0.8rem; }
  .s-report-hero h1 { font-size: 1.25rem; }

  /* Stat and KPI strips: one 180px floor is wider than half a phone, so the
     tiles were already stacking one-up and wasting the row. 150px gives two. */
  .s-kpi-strip { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
  .s-kpi { padding: 0.8rem 0.85rem; }
  .s-kpi__value { font-size: 1.3rem; }

  /* The icon is absolutely positioned in the tile's corner — free space at 240px,
     directly on top of the label at 150px. Reserving the corner instead only moved
     the problem: "PENDING QUOTATIONS" and "OPEN SERVICE CALLS" then wrapped to two
     lines, which made every tile in that row taller and left the short-labelled
     ones half empty.

     So the icon goes at phone width. It is decoration that repeats what the label
     already says, and the coloured bar down the left edge still tells the tiles
     apart. Every label fits one line with it gone. */
  .s-kpi__icon { display: none; }
  .s-kpi__label { font-size: 0.68rem; }
  .s-kpi__sub { font-size: 0.72rem; }
  .s-stat-grid { grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 0.5rem; }

  /* Fixed label columns inside cards — 120px of a 300px card left the value with
     no room, so long values wrapped under a half-empty label. */
  .s-funnel__row { grid-template-columns: 92px 1fr 46px; gap: 0.45rem; font-size: 0.78rem; }
  .s-bucket__row { grid-template-columns: 76px 1fr 46px; gap: 0.45rem; font-size: 0.78rem; }
}

/* ============================================================================
   In-page camera (sahajCamera.capture)
   ----------------------------------------------------------------------------
   A live getUserMedia viewfinder, used everywhere a photo is taken: check-in
   odometer, site-visit odometers, expense receipts, vehicle number plate, site
   photos. It replaced <input type=file capture> as the primary path because a
   picked file arrives in the vendor's format — an iPhone hands over HEIC, which
   no engine decodes through an <img> tag, and the odometer preview silently
   never appeared. A frame off the video stream is drawn to a canvas and encoded
   by us, so every device produces the same JPEG.

   Built in plain DOM on document.body, outside Blazor's render tree: it must
   open on the tap without a circuit round-trip, and a re-render must not be
   able to detach the node the shutter handler is bound to.
   ========================================================================= */
.scam {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 3000;
  background: #000;
  flex-direction: column;
  align-items: stretch;
}
.scam--on { display: flex; }

.scam-video {
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
  /* contain, not cover: cropping a viewfinder hides the edge of the odometer
     the employee is trying to frame. */
  object-fit: contain;
  background: #000;
}

.scam-msg {
  color: #fff;
  font-family: var(--s-font);
  font-size: 0.875rem;
  text-align: center;
  padding: 0.75rem 1.25rem;
  min-height: 1.25rem;
}

.scam-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  /* The home indicator sits over the bottom strip on an iPhone, and the shutter
     is exactly what lands under it. */
  padding-bottom: calc(1rem + var(--s-safe-bottom));
  background: #000;
}

.scam-btn {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.4);
  color: #fff;
  font-family: var(--s-font);
  font-size: 0.8125rem;
  border-radius: 999px;
  padding: 0.5rem 1rem;
  cursor: pointer;
  /* 44px is the smallest target a thumb hits reliably on a phone. */
  min-height: 44px;
  min-width: 72px;
}

.scam-shutter {
  width: 68px;
  height: 68px;
  border-radius: 50%;
  background: #fff;
  border: 4px solid rgba(255, 255, 255, 0.35);
  background-clip: padding-box;
  cursor: pointer;
  flex: 0 0 auto;
}
.scam-shutter:active { opacity: 0.7; }

/* The escape hatch: shown only when the stream will not start (permission
   refused, no camera, or a plain-HTTP origin where getUserMedia is blocked).
   A real label wrapping a real input, so the tap is a genuine user gesture —
   a programmatic click after an awaited rejection is not, and iOS blocks it. */
.scam-file {
  display: none;
  margin: 0 1.25rem 1.5rem;
  padding: 0.875rem 1rem;
  text-align: center;
  border: 1px dashed rgba(255, 255, 255, 0.5);
  border-radius: var(--s-radius);
  color: #fff;
  font-family: var(--s-font);
  font-size: 0.875rem;
  cursor: pointer;
}
.scam--failed .scam-file { display: block; }
.scam--failed .scam-shutter { opacity: 0.35; pointer-events: none; }
.scam-file input { display: none; }

/* Landscape on a phone leaves almost no vertical room — put the controls beside
   the viewfinder rather than stealing half the height from it. */
@media (max-height: 480px) and (orientation: landscape) {
  .scam { flex-direction: row; }
  .scam-video { order: 1; }
  .scam-bar {
    order: 2;
    flex-direction: column-reverse;
    padding: 1rem;
    padding-bottom: 1rem;
    padding-right: calc(1rem + env(safe-area-inset-right, 0px));
  }
  .scam-msg { display: none; }
  .scam-shutter { width: 56px; height: 56px; }
}

/* ============================================================================
   Machine filter bar (MachineFilterBar.razor)
   ----------------------------------------------------------------------------
   Sits above a service call's machine table on the job sheet and the call view.
   One AMC contract on a hotel puts every unit in the building on one call, so
   the strip is what turns "read down 32 rows" into "3rd floor, room 204".
   ========================================================================= */
.mfb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--s-border);
  background: var(--s-surface-variant, #fafbfc);
}

/* The search box takes the slack; the pickers stay at a readable fixed width so
   a long product name cannot push the row into a second line on a laptop. */
.mfb-search { flex: 1 1 220px; min-width: 0; }
.mfb-pick { flex: 0 1 160px; min-width: 0; }

.mfb-count {
  font-size: 0.75rem;
  color: var(--s-text-secondary);
  white-space: nowrap;
}

.mfb-clear { flex-shrink: 0; }

/* On a phone the four controls stack rather than shrink to unusable widths —
   this strip is most useful in the field, on the smallest screen. */
@media (max-width: 640px) {
  .mfb { gap: 6px; }
  .mfb-search { flex: 1 1 100%; }
  .mfb-pick { flex: 1 1 calc(50% - 3px); }
}

/* ── BOM material flow: dispatch selection, receipt panel, returns ──────────────
   Here rather than in a .razor.css because App.razor does not include the scoped
   styles bundle — every .razor.css in this project is inert. */

/* Select and Dispatch scroll with the rest of the grid.
   They used to be pinned — select to the left edge, Dispatch to the right above 900px — so a line
   could still be ticked with the costing columns scrolled past. Unpinned on request: a pinned pair
   costs roughly a fifth of the width permanently, the two frozen edges made the middle of the grid
   feel narrower than it is, and the columns sat over the data rather than beside it. The opaque
   backgrounds went with them; they only existed because a sticky cell lets scrolled columns show
   through. Keep the width so the tick box still has a column of its own. */
.bom-col-select { width: 44px; }

/* Deliberately outside .s-table-wrap--wide — that box scrolls on x, which makes it
   the scrollport, so a sticky bar inside it slides away sideways with the columns. */
.bom-dispatch-bar {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 14px;
  margin-top: 8px;
  background: var(--s-surface, #fff);
  border: 1px solid var(--s-border);
  border-radius: 8px;
  box-shadow: 0 -4px 14px rgba(0, 0, 0, 0.08);
  font-size: 0.82rem;
}

.bom-receipt { padding: 12px 14px; }
.bom-receipt__notice,
.bom-receipt__meta {
  font-size: 0.76rem;
  padding: 8px 10px;
  border-radius: 6px;
  margin-bottom: 10px;
}
.bom-receipt__notice { background: #fffbeb; color: #92400e; }
.bom-receipt__meta { background: var(--s-surface-variant); color: var(--s-text-secondary); }
.bom-receipt__foot { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 12px; }
.bom-receipt__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 12px;
}

/* A document preview beside its print-time controls — the shape the quotation download has always
   used, now shared by the challan and the return receipt. They previously stacked the controls
   above the page inside an 820px dialog, which shrank an A4 sheet to about 70% and left the print
   unreadable. The page gets the room; the fields sit alongside it. */
/* Flush to the dialog edges: the preview is the content, and 20px of surface around a rendered
   page just eats height the page could have used. .s-dialog caps at 90vh, so the 70vh below plus
   the title and the actions bar is what keeps the whole thing on screen without an outer scroll. */
.s-dialog--xl .s-dialog-content { padding: 0; }
.s-dialog--xl .s-dialog-actions { margin-top: 0; }

.pdf-doc-body { display: flex; min-height: 0; }
.pdf-doc-preview { flex: 6; min-width: 0; }
.pdf-doc-preview iframe {
  display: block;
  width: 100%;
  height: 70vh;
  border: none;
  background: #e5e7eb;
}
.pdf-doc-preview__empty {
  height: 70vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f8f9fb;
  color: var(--s-text-muted);
}
.pdf-doc-side {
  flex: 4;
  min-width: 0;
  border-left: 1px solid var(--s-border);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow-y: auto;
}
.pdf-doc-side__head {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--s-primary);
  text-transform: uppercase;
  letter-spacing: 0.4px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--s-border);
}
.pdf-doc-side__hint { font-size: 0.72rem; color: var(--s-text-secondary); }

/* Below this the two panes cannot both be useful side by side, so the page goes on top and the
   fields underneath — the same breakpoint the quotation dialog uses. */
@media (max-width: 900px) {
  .pdf-doc-body { flex-direction: column; }
  .pdf-doc-side {
    flex: none;
    width: 100%;
    border-left: none;
    border-top: 1px solid var(--s-border);
    max-height: 34vh;
  }
  .pdf-doc-preview iframe,
  .pdf-doc-preview__empty { height: 46vh; }
}

/* The serial picker's dropdown on a dispatch note. It replaced a native <datalist>, which Chrome
   gives no caret and only opens once you type — fifty units in stock and the control read as an
   empty text box. Rows are clickable and the panel stays open, because picking five units for one
   line is the normal case. */
.dmn-serial-panel {
  border: 1px solid var(--s-border);
  border-radius: 6px;
  max-height: 150px;
  overflow-y: auto;
  background: var(--s-surface);
  margin-bottom: 4px;
  box-shadow: var(--s-shadow-sm, 0 2px 6px rgba(0, 0, 0, 0.08));
}
.dmn-serial-option {
  padding: 5px 10px;
  cursor: pointer;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.74rem;
  border-bottom: 1px solid var(--s-border);
}
.dmn-serial-option:last-child { border-bottom: none; }
.dmn-serial-option:hover { background: var(--s-primary); color: #fff; }
.dmn-serial-empty {
  padding: 6px 10px;
  font-size: 0.74rem;
  color: var(--s-text-secondary);
}

/* A dispatch row opens the note's own receipt page. It has to look clickable across the whole row,
   not just on the number — the number alone is a small target and nothing else hinted at it. */
.dmn-row { cursor: pointer; }
.dmn-row:hover { background: var(--s-surface-variant); }
.dmn-row__link { color: var(--s-primary); font-weight: 600; }
.dmn-row:hover .dmn-row__link { text-decoration: underline; }

.bom-returns__zone { padding: 4px 0 14px; }
.bom-returns__head {
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--s-primary);
  padding: 10px 12px 6px;
}
.bom-returns__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 12px;
}

/* At phone width the receipt becomes one card per line. It is used one-handed at a
   gate, where a sideways scroll means half the row is simply never read — so the
   table stops being a table rather than being made narrower. Targets stay at 44px. */
@media (max-width: 560px) {
  .bom-receipt__table thead { display: none; }
  .bom-receipt__table,
  .bom-receipt__table tbody,
  .bom-receipt__table tr,
  .bom-receipt__table td { display: block; width: 100%; }
  .bom-receipt__table tr {
    border: 1px solid var(--s-border);
    border-radius: 8px;
    margin-bottom: 10px;
    padding: 6px 8px;
  }
  .bom-receipt__table td {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    text-align: left !important;
    border: none;
    padding: 7px 2px;
    min-height: 44px;
  }
  .bom-receipt__table td::before {
    content: attr(data-label);
    font-size: 0.74rem;
    font-weight: 600;
    color: var(--s-text-secondary);
    flex-shrink: 0;
  }
  .bom-receipt__table td .s-input { max-width: 60%; }
}

/* ============================================================================
   Dynamic project-phase questionnaire (.pq-*)
   ----------------------------------------------------------------------------
   Lives here rather than in a page <style> block because these classes are used
   by several separate .razor files -- the phases page, the answer renderer, the
   question row and the Masters editor. (Scoped .razor.css is inert in this app:
   App.razor does not include the scoped-styles bundle.)
   ========================================================================== */

.pq-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--s-border);
}
.pq-row:last-child { border-bottom: none; }

/* Full-width rows: the BOD serial grid and anything else a two-column
   label/control split would squash. */
.pq-row--full { flex-direction: column; align-items: stretch; }

/* Applied after a failed Save so an unanswered required question is findable
   in a list of thirty. */
.pq-row--missing { background: rgba(220, 38, 38, .07); }

.pq-label { flex: 1; min-width: 0; font-size: 13px; color: var(--s-text); }
.pq-req { color: var(--s-error); margin-left: 2px; }

.pq-control {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

/* Radio groups cannot be un-selected natively, so single-choice and yes/no
   controls carry an explicit clear. */
.pq-clear {
    border: none;
    background: transparent;
    color: var(--s-text-secondary);
    cursor: pointer;
    font-size: 16px;
    line-height: 1;
    padding: 0 2px;
}
.pq-clear:hover { color: var(--s-error); }

.pq-hint { font-size: .72rem; color: var(--s-text-secondary); margin-top: 4px; }

.pq-checklist { display: flex; flex-wrap: wrap; gap: 8px 16px; }
.pq-checklist label {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: .84rem;
    cursor: pointer;
    white-space: nowrap;
}

.pq-opt-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    background: var(--s-surface-variant);
    border: 1px solid var(--s-border);
    border-radius: 12px;
    font-size: .75rem;
}

.pq-photos { display: flex; flex-wrap: wrap; gap: 8px; }

.pq-section-head {
    font-weight: 600;
    font-size: .8rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--s-primary);
}

/* "+ Add Phase" sits in the tab strip, so it is dashed to read as an action
   rather than as another phase. */
.phase-tab--add { color: var(--s-primary); font-weight: 600; border: 1px dashed var(--s-border); }

@media (max-width: 640px) {
    /* A label plus a date-time picker plus a file chip does not fit 360px side
       by side -- stack them instead of letting the row overflow. */
    .pq-row { flex-direction: column; align-items: stretch; gap: 6px; }
    .pq-label { font-size: .82rem; font-weight: 500; }
    .pq-control { width: 100%; }

    /* !important is required, not sloppy: the numeric answers carry an inline
       width, and inline style= beats the stylesheet. */
    .pq-control input.s-input { width: 100% !important; }

    /* With user-defined phases there can be many tabs. A horizontal scroller
       beats a three-line wrapped tab bar, and the page body still never
       scrolls sideways. */
    .phase-tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
    .phase-tab { flex: 0 0 auto; white-space: nowrap; }

    /* Thirty questions puts Save a long way from wherever the user is. */
    .phase-actions {
        position: sticky;
        bottom: 0;
        background: var(--s-surface);
        padding: 8px 0;
        border-top: 1px solid var(--s-border);
        z-index: 5;
    }
}

/* ============================================================
   Module dashboards  (ModuleDashboardView.razor)

   One renderer draws all seven, so these are the only classes
   the per-module pages need. Everything else on those pages is
   already-existing theme: .s-kpi, .s-funnel, .s-trend, .s-dist,
   .s-stat, .s-table--stack.
   ============================================================ */

.s-dash-panel { padding: 1.25rem; height: 100%; }

/* height:100% above exists so panels sharing a grid row match each other. A panel standing alone
   -- the targets board above the tiles, the module index below the tables -- has no sibling to
   match, and there that height resolves against the scroll container and leaves a third of a
   screen of white space under the last row. */
.s-dash-panel--auto { height: auto; }

/* ---------- Targets: what the module is aiming at, above its tiles ---------- */

.s-target-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 10px;
  margin-top: 0.5rem;
}

.s-target {
  border: 1px solid var(--s-border);
  border-left: 3px solid var(--s-muted, #9e9e9e);
  border-radius: var(--s-radius, 8px);
  padding: 0.625rem 0.75rem;
  background: var(--s-surface);
}

.s-target--success { border-left-color: #2e7d32; }
.s-target--info    { border-left-color: #0288d1; }
.s-target--danger  { border-left-color: #E42626; }

.s-target__head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  margin-bottom: 0.35rem;
}
.s-target__label {
  font-size: 0.8125rem; font-weight: 600; color: var(--s-text);
  overflow-wrap: anywhere;
}
.s-target__status {
  font-size: 0.64rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  white-space: nowrap; padding: 1px 6px; border-radius: 4px;
  background: var(--s-hover, #f0f0f0); color: var(--s-text-secondary);
}
.s-target--success .s-target__status { background: #e8f5e9; color: #2e7d32; }
.s-target--info .s-target__status    { background: #e1f5fe; color: #0277bd; }
.s-target--danger .s-target__status  { background: #fdecea; color: #c62828; }

/* The achieved figure is the number being read; the target is the thing it is read against, so it
   is present and quieter rather than the same weight. */
.s-target__figures { font-size: 0.95rem; color: var(--s-text); margin-bottom: 0.4rem; }
.s-target__figures b { font-size: 1.15rem; font-weight: 700; }
.s-target__figures span { font-size: 0.78rem; color: var(--s-text-secondary); }

.s-target__bar {
  height: 6px; border-radius: 3px; overflow: hidden;
  background: var(--s-hover, #f0f0f0);
}
.s-target__fill { height: 100%; border-radius: 3px; background: var(--s-primary); }
.s-target--success .s-target__fill { background: #2e7d32; }
.s-target--info .s-target__fill    { background: #0288d1; }
.s-target--danger .s-target__fill  { background: #E42626; }

.s-target__foot {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  margin-top: 0.35rem;
  font-size: 0.72rem; color: var(--s-text-secondary);
}
.s-target__foot b { color: var(--s-text); }

@media (max-width: 640px) {
  .s-target-grid { grid-template-columns: 1fr; gap: 8px; }
}

/* Title on the left, "open the full list" on the right. flex-end rather than centre because the
   title block is two lines and the button is one — centring pushes the button below the subtitle
   on a narrow card. */
.s-dash-panel__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.s-dash-panel__head .s-report-card-sub { margin-bottom: 0.6rem; }

/* "No open calls" is an answer, not an error — muted, centred, and given enough height that the
   card keeps the shape of the one beside it rather than collapsing to a caption. */
.s-dash-empty {
  padding: 1.75rem 0.5rem;
  text-align: center;
  color: var(--s-text-muted);
  font-size: 0.85rem;
}

/* A row or bar that opens the record behind it. */
.s-dash-row { cursor: pointer; }
.s-dash-row:hover { background: var(--s-hover, #f5f7fb); }
.s-funnel__row.s-dash-row:hover .s-funnel__label { color: var(--s-primary); font-weight: 600; }


/* The money a bar is also about, printed at the right-hand end of its track. It sits over the
   track rather than in the count column so the count column keeps meaning one thing — see the
   comment in ModuleDashboardView.

   The surface-coloured chip is not decoration: a full-length fill runs underneath this text, and
   the fill colour is chosen per series, so there is no single text colour that reads on all of
   them. The chip gives it a known background whatever it lands on. */
.s-dash-note {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--s-surface);
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--s-text-secondary);
  pointer-events: none;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* A dashboard table is a sample of eight rows, never a working grid, so it is deliberately
   tighter than .s-table elsewhere — eight rows and a header have to fit beside a chart. */
.s-dash-panel .s-table th,
.s-dash-panel .s-table td { padding: 0.5rem 0.6rem; font-size: 0.82rem; }

/* .s-table th is `white-space: nowrap` app-wide, which is right for a working grid inside a
   full-width page and wrong here: a dashboard table lives in a card that may be half the page, and
   an unwrappable header row sets a minimum width the card cannot meet. The table then overflowed
   into .s-table-wrap's horizontal scroller and the rightmost column — the status badge — was
   simply not on screen, with a scrollbar the reader had no reason to look for. Long document
   numbers get the same treatment for the same reason. */
.s-dash-panel .s-table th { white-space: normal; }
.s-dash-panel .s-table td { overflow-wrap: anywhere; }

@media (max-width: 640px) {
  /* .s-table--stack turns each row into a labelled card below 640px; the head then has to stack
     too, or the "open the full list" button sits alone on a line of its own. */
  .s-dash-panel__head { flex-direction: column; align-items: stretch; }
  .s-dash-panel { padding: 1rem; }
}

/* ── The quick-access index ───────────────────────────────────────────────────────────────────
   A grid of labelled page cards under a section heading, each carrying the sidebar's own glyph.
   Two consumers: the "Everything in <module>" card at the foot of every module dashboard, and the
   Ctrl+K palette's empty state. It lived in ModuleQuickLinks' own <style> block while there was
   one of them; a second copy in the palette would be the copy that drifts. */
.s-quick-group { margin-top: 1rem; }
.s-quick-group:first-of-type { margin-top: 0.5rem; }

.s-quick-group__title {
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--s-text-secondary);
  margin-bottom: 0.5rem;
}

/* auto-fill, not auto-fit: with four links in a wide card auto-fit stretches each one to a quarter
   of the row, which reads as four buttons rather than an index. A fixed track keeps a short module
   and a long one looking like the same thing. */
.s-quick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(196px, 1fr));
  gap: 8px;
}

.s-quick-link {
  display: flex; align-items: center; gap: 0.625rem;
  padding: 0.5rem 0.625rem;
  border: 1px solid var(--s-border); border-radius: var(--s-radius, 8px);
  background: var(--s-surface);
  color: var(--s-text); text-decoration: none;
  font-size: 0.8125rem; line-height: 1.2;
  transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
}

.s-quick-link:hover,
.s-quick-link:focus-visible {
  border-color: var(--s-primary);
  background: var(--s-hover, #f0f0f0);
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.07);
  outline: none;
}

.s-quick-link__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex: 0 0 28px;
  border-radius: 6px;
  background: var(--s-hover, #f0f0f0);
  color: var(--s-primary);
}
.s-quick-link:hover .s-quick-link__icon { background: var(--s-primary); color: #ffffff; }

/* The label wins the leftover width and the chevron keeps its own, so a two-line label pushes the
   arrow nowhere. anywhere rather than break-word: "Regularization Approvals" has no space narrow
   enough to break at on a 196px track. */
.s-quick-link__label { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

.s-quick-link__go { flex: 0 0 auto; color: var(--s-text-secondary); }
.s-quick-link:hover .s-quick-link__go { color: var(--s-primary); }

@media (max-width: 640px) {
  .s-quick-grid { grid-template-columns: 1fr 1fr; gap: 6px; }
  .s-quick-link { padding: 0.4375rem 0.5rem; font-size: 0.78rem; }
  .s-quick-link__icon { width: 24px; height: 24px; flex-basis: 24px; }
}

@media (max-width: 380px) {
  .s-quick-grid { grid-template-columns: 1fr; }
}

/* ══ The seven module dashboards ══════════════════════════════════════════════════════════════
   Everything here is scoped to .mdash, and that is not tidiness — it is the whole reason this
   block can exist. `.s-kpi` is worn by twenty components, `.s-report-hero` by the role router at
   "/", and `.s-dash-panel` by the quick-access index; restyling any of them globally would redraw
   the landing dashboard and sixteen report pages that nobody asked to have changed. The markup
   keeps every existing class so the guards that scrape `.s-kpi__value` still work.            */

/* The hero was 1.75rem/2rem of padding — 112px of gradient for a title and a clock. */
.mdash .s-report-hero { padding: 1.15rem 1.5rem; margin-bottom: 1rem; }
.mdash .s-report-hero h1 { font-size: 1.3rem; margin-bottom: 0.2rem; }
.mdash .s-report-hero p { font-size: 0.84rem; }

/* ── KPI tiles: a headline row, then an index ──
   Thirteen identically-weighted tiles is a wall, not a dashboard: "Open Leads" carried exactly the
   same emphasis as "BOQs", so nothing was read first. The lead tiles are the figures the handler puts
   first, drawn large; the rest are compact. It also fixes the ragged tail — six, six and a lone
   orphan on the third row. */
/* Exactly four across, not auto-fit. auto-fit fitted five of Procurement's six tiles on the first
   row and left the sixth alone on a second, which is the ragged tail this split exists to remove;
   a fixed four also means every module opens with the same shape rather than one deciding it has
   room for five. */
.mdash-kpi-lead {
  display: grid; gap: 12px; margin-bottom: 12px;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 1080px) { .mdash-kpi-lead { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.mdash-kpi-rest {
  display: grid; gap: 10px; margin-bottom: 1.25rem;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
}
.mdash-kpi-lead .s-kpi { padding: 1.05rem 1.15rem; }
.mdash-kpi-lead .s-kpi__value { font-size: 1.95rem; margin: 0.3rem 0 0.2rem; }
.mdash-kpi-rest .s-kpi { padding: 0.7rem 0.8rem; }
.mdash-kpi-rest .s-kpi__value { font-size: 1.2rem; margin: 0.15rem 0 0.1rem; }
.mdash-kpi-rest .s-kpi__label { font-size: 0.64rem; }
.mdash-kpi-rest .s-kpi__sub { font-size: 0.7rem; }

/* Colour where it means something, and nowhere else. The default rule was `--s-primary`, so a
   third of the wall wore a navy bar that encoded nothing and competed with the tiles that were
   genuinely flagging a number. Neutral by default; the five real tones keep their colour. */
.mdash .s-kpi::before { background: var(--s-border); width: 3px; }
.mdash .s-kpi--accent::before  { background: var(--s-accent); }
.mdash .s-kpi--success::before { background: #2e7d32; }
.mdash .s-kpi--warn::before    { background: #ed6c02; }
.mdash .s-kpi--info::before    { background: #0288d1; }
.mdash .s-kpi--danger::before  { background: #E42626; }

/* Lift on hover is for something you can open. A tile that goes nowhere should not pretend. */
.mdash .s-kpi:hover { transform: none; box-shadow: none; }
.mdash .s-kpi--clickable:hover { transform: translateY(-2px); box-shadow: 0 10px 22px -12px rgba(0,0,0,0.18); }

/* ── Panels: no stretched voids, no stranded halves ──
   `.s-dash-panel` is `height: 100%`, so in a two-up row a four-bar panel was stretched to the
   height of an eight-bar one and carried ~150px of empty white nobody could explain. And a lone
   half-width panel at the end of the list left half a row blank. Flowing them with auto-fit and
   starting them at the top fixes both: a short panel is short, and a last panel fills the row. */
.mdash-panels {
  display: grid; gap: 14px; margin-bottom: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
  align-items: start;
}
.mdash-panels > .s-col-6  { grid-column: auto; }
.mdash-panels > .s-col-12 { grid-column: 1 / -1; }
.mdash-panels .s-dash-panel { height: auto; }

/* Tables keep the twelve-column grid: `AddTable` forces Span 12 on anything five columns or wider
   precisely because `.s-table th` is nowrap, and an auto-fit track would put a five-column table
   back in a half-width card and hide its last column behind a scrollbar. Only the stretch goes. */
.mdash-tables { align-items: start; }
.mdash-tables .s-dash-panel { height: auto; }

.mdash .s-report-card-title { font-size: 0.95rem; }
.mdash .s-report-card-sub { margin-bottom: 0.9rem; }

/* ── Tables inside a dashboard card are not the working grid ──
   A filled grey header band is right on a list page where it pins above a hundred scrolling rows.
   On a five-row card it is a slab of grey competing with the card's own title. */
/* !important is not decoration here: `.s-table thead th` sets its grey with one, for the sticky
   header on a list page of a hundred scrolling rows. Nothing without one can reach it. */
.mdash .s-dash-panel .s-table thead,
.mdash .s-dash-panel .s-table thead tr { background: transparent !important; }
.mdash .s-dash-panel .s-table thead th {
  background: transparent !important;
  box-shadow: none !important;
  border: none;
  border-bottom: 1.5px solid var(--s-border);
  color: var(--s-text-secondary);
  font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase;
  text-align: left;
  padding: 0.35rem 0.6rem 0.5rem;
}

/* A full box border on every cell makes a spreadsheet. Inside a card, rows separated by a hairline
   read as a list, which is what these five-row extracts are. Alignment goes left for text and stays
   right for figures -- RightAlign is an inline style, and inline beats all of this. */
.mdash .s-dash-panel .s-table td {
  border: none;
  border-bottom: 1px solid var(--s-border);
  text-align: left;
  padding: 0.55rem 0.6rem;
}
.mdash .s-dash-panel .s-table th:last-child,
.mdash .s-dash-panel .s-table td:last-child { text-align: left; }
.mdash .s-dash-panel .s-table tbody tr:last-child td { border-bottom: none; }
.mdash .s-dash-panel .s-table tbody tr:hover { background: var(--s-hover, #f7f8fa); }

/* ── The trend chart had no frame at all ──
   Bars floated with their values above them and nothing to read a height against, so a column was
   only ever "taller than that one". Four faint gridlines and a baseline make it a chart. */
.mdash .s-trend {
  height: 170px;
  padding: 0.25rem 0 0;
  border-bottom: 1px solid var(--s-border);
  background-image: repeating-linear-gradient(
    to top,
    var(--s-border) 0, var(--s-border) 1px,
    transparent 1px, transparent 25%);
  background-size: 100% 100%;
}
.mdash .s-trend__bar { border-radius: 4px 4px 0 0; }
.mdash .s-trend__label { padding-top: 0.35rem; }

/* A zero column is a reading, not an absence — it needs to sit on the baseline and be visibly
   nothing, rather than showing the 4px stub that reads as a small value. */
.mdash .s-trend__col { justify-content: flex-end; }

@media (max-width: 640px) {
  .mdash-kpi-lead { grid-template-columns: 1fr 1fr; gap: 8px; }
  .mdash-kpi-rest { grid-template-columns: 1fr 1fr; gap: 8px; }
  .mdash-kpi-lead .s-kpi__value { font-size: 1.45rem; }
  .mdash .s-report-hero { padding: 1rem 1.1rem; }
  .mdash-panels { grid-template-columns: 1fr; }
}


/* ============================================================================
   A FIXED-LAYOUT TABLE NEEDS A FLOOR, AND A WAY TO STOP BEING A TABLE
   ---------------------------------------------------------------------------
   Two rules, one bug.

   .s-table--fixed is `table-layout: fixed` with a declared width on every column
   but one — the free column (a remark, a reason) absorbs the slack. That works
   while there IS slack. Below the sum of the declared widths there is none, and
   fixed layout does not overflow: it hands the free column whatever is left,
   which is ZERO. Its text then paints straight across its neighbours and the
   row looks like two rows drawn on top of each other.

   Measured on /hr/geofence-approvals before this block. Seven declared columns
   (638px) plus the action column (172px) is 810px, so:
     - at a 820px tablet the table pane is 520px, the Employee-remark cell laid
       out 0px wide, and its text rendered as a column of single letters spilling
       ~25px over the cells either side;
     - at 414px, where the MOBILE block forces `white-space: nowrap`, the same
       cell could not wrap at all and spilled 380px sideways, straight over the
       Approve and Reject buttons.
   Desktop was never affected, which is why this survived: at 1366 the pane is
   1066px and the free column gets its 256px.

   `min-width` on the th is not the fix and never was — fixed layout reads
   `width` and ignores `min-width` on a column. The floor has to be on the TABLE,
   so that the scroll container has something to scroll. Each table declares its
   own as the sum of its columns; a table that does not set --s-table-min is
   untouched by this rule. */
.s-table--fixed { min-width: var(--s-table-min, 0px); }

/* ---- ...and on a phone it must be allowed to wrap ----
   The MOBILE block above sets `white-space: nowrap` on every .s-table cell. That is right for an
   ordinary list table, whose columns are sized by their content: one line per cell, swipe to read
   the rest. It is actively wrong for a fixed-layout table, whose columns are sized by declaration.
   Here the free column is already only as wide as the floor allows, so forbidding it to wrap gives
   its text nowhere to go except across its neighbours - the 380px spill that made the geofence
   queue look like two rows drawn on top of each other at 414px.

   Cells that genuinely must stay on one line - a date, a clock reading, a badge - say so with an
   inline `white-space: nowrap`, and an inline declaration still beats this. So this only frees the
   ones that were never asked to hold a line, which is exactly the free column. */
@media (max-width: 640px) {
  .s-table--fixed td { white-space: normal; }
}
