/* Ported from the React build's src/styles/theme.css — color tokens are exact RGB values read
   from the real TSMPlus app's own CSS (colors are facts, not copyrightable expression). Only the
   outer shell (topbar/rail/login/cards/buttons) is ported here; module pages use real Telerik
   components + Telerik's own theme (loaded via CDN in index.html), not these utility classes. */
:root {
  --pacific-blue-50: rgb(239 249 252);
  --pacific-blue-100: rgb(215 238 246);
  --pacific-blue-400: rgb(54 154 193);
  --pacific-blue-500: rgb(41 137 177);
  --pacific-blue-600: rgb(37 110 149);
  --pacific-blue-950: rgb(18 41 58);

  --white-50: rgb(255 255 255);
  --white-100: rgb(239 239 239);
  --white-200: rgb(220 220 220);
  --white-600: rgb(101 101 101);
  --white-800: rgb(70 70 70);
  --white-900: rgb(61 61 61);

  --carnation-500: rgb(241 92 97);

  --yellow-warning-400: rgb(235 185 52);
  --yellow-warning-600: rgb(188 121 18);

  --green-success-400: rgb(80 195 160);
  --green-success-600: rgb(22 133 105);
  --purple-500: rgb(156 123 223);
  --purple-600: rgb(134 87 208);

  --fb-primary: var(--pacific-blue-400);
  --fb-primary-hover: var(--pacific-blue-500);
  --fb-primary-light: var(--pacific-blue-50);
  --fb-secondary: var(--white-200);
  --fb-danger: var(--carnation-500);
  --fb-warning: var(--yellow-warning-400);
  --fb-warning-light: rgb(252 250 234);
  --fb-dark: var(--white-800);
  --fb-body-bg: #f5f7f9;
  --fb-border: var(--white-200);
  --fb-text: var(--white-900);
  --fb-text-muted: var(--white-600);
  --fb-sidebar-bg: #1b2a38;
  --fb-radius: 6px;
  --fb-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 6px rgba(0, 0, 0, 0.04);
  /* Card/panel surface — was a hardcoded #fff at every call site (32 occurrences); this token
     lets [data-theme=dark] below flip them all in one place. */
  --fb-surface: #fff;
}

/* David: "the paragraph text is too faint, make contrast much higher" — Bootstrap's own
   `.text-muted` (and several Metronic form/placeholder styles) read Metronic's own
   `--kt-text-muted`/`--kt-text-gray-*`/`--kt-form-text-color`/`--kt-input-*-placeholder-color`
   tokens, which style.bundle.css sets to #A1A5B7 in light mode — ~2.5:1 against a white card,
   well under WCAG AA's 4.5:1 minimum for normal text. Confirmed real TSMPlus already carries this
   exact fix (firebird-dapr's own app-overrides.css) — ported its exact light-mode values verbatim
   rather than picking new ones, so trial matches real's already-chosen contrast rather than
   inventing a second, different "muted gray". Dark mode's own --kt-text-muted (#565674) already
   passes AA against Metronic's dark card bg, so only light mode needs the override. */
:root, [data-theme="light"] {
  --kt-text-muted: #4D5261;
  --kt-form-text-color: #4D5261;
  --kt-input-placeholder-color: #565E64;
  --kt-input-solid-placeholder-color: #565E64;
  --kt-text-gray-500: #6E7180;
  --kt-text-gray-600: #656D76;
}

/* Dark mode (David: "How does a user change it from light to dark mode?", ThemeService.cs +
   Topbar.razor's account-menu toggle). Metronic's own style.bundle.css already ships a complete
   [data-theme=dark] palette for its own --kt-* / --bs-* tokens — this block only bridges THIS app's
   own --fb-* custom tokens to the same real values Metronic already uses for the equivalent
   concept (card bg, border, gray text), so our components read as part of one consistent theme
   rather than a separately-invented dark palette. Brand/status hues (--fb-primary, --fb-danger,
   --fb-warning, --green-success-*, --purple-*) are left unchanged — they're facts matching real
   TSMPlus's own color values (see this file's top-of-file note) and stay legible on both grounds. */
[data-theme="dark"] {
  --fb-surface: #1e1e2d;        /* = Metronic --kt-card-bg */
  --fb-text: #cdcdde;           /* = Metronic --kt-gray-800 */
  --fb-text-muted: #92929f;     /* = Metronic --kt-gray-700 */
  --fb-border: #2b2b40;         /* = Metronic --bs-border-color / --kt-card-border-color */
  --fb-primary-light: rgba(54, 154, 193, 0.16); /* was a near-white blue tint; a light-on-dark hover highlight needs an alpha tint instead of a pale solid */
  /* Same contrast fix as the light-mode override above — Metronic's own stock dark-mode
     --kt-text-muted (#565674) is ~2.35:1 against its own dark card bg (#1e1e2d), also failing AA.
     Real's fix value ported verbatim (app-overrides.css: "Loaded last so it wins over the
     KeenThemes vendor bundle's [data-theme=dark] block" — same reasoning applies here). */
  --kt-text-muted: #8b949e;
}

* { box-sizing: border-box; }

html, body { height: 100%; }

/* Font-family intentionally NOT set here — let Metronic's Inter (from style.bundle.css) apply, so
   typography matches real TSMPlus. Only keep our background/colour tokens. */
body {
  margin: 0;
  color: var(--fb-text);
}

a { color: var(--fb-primary); text-decoration: none; }
a:hover { color: var(--fb-primary-hover); text-decoration: underline; }

/* Blazor renders into #app; make it a full-height flex column so Metronic's app-page fills the
   viewport and the app-footer pins to the bottom on short pages. */
#app { display: flex; flex-direction: column; min-height: 100vh; }

/* The footer carries the trial tools (TRIAL badge — moved out of the header 2026-07-13; the
   View-as role picker that used to live here too has since moved to the topbar's account/avatar
   dropdown, see TrialTools.razor's own header comment for why). Grid pages fill the viewport and
   capture wheel scroll internally, which would leave the footer below the fold — so pin it to the
   viewport bottom (sticky) with a solid background + top border + shadow, keeping it reachable. */
#kt_app_footer {
  position: sticky; bottom: 0; z-index: 90;
  background: var(--bs-body-bg, #fff);
  border-top: 1px solid var(--bs-border-color, #e4e6ef);
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
}

/* Collapsible icon-only sidebar (§S1). Self-driven (Metronic's KTApp minimize engine isn't
   bootstrapped in this standalone WASM build): the chevron in the sidebar header toggles
   body.fb-nav-collapsed (persisted in localStorage), which narrows the rail to icons only. Both the
   Metronic width var AND an explicit width are set so it wins over Metronic's .app-sidebar rule
   (which is why the earlier KTApp-based attempt failed).
   iPhone fix (2026-08-15) — the WHOLE block is now desktop-only (min-width:768px): on a phone the
   sidebar is a drawer, and the persisted collapsed state was turning the open drawer into a 74px
   icon-only strip with all labels hidden — David's live report: "menu is just icons and not
   responding". index.html's fbApplySidebar also refuses to apply the class below 768px, so this
   media query is the belt-and-braces layer for any pre-existing localStorage value. */
@media (min-width: 768px) {
.fb-nav-toggle { flex-shrink: 0; }
.fb-nav-toggle i { transition: transform .15s ease; }
body.fb-nav-collapsed { --bs-app-sidebar-width: 74px; }
body.fb-nav-collapsed #kt_app_sidebar,
body.fb-nav-collapsed .app-sidebar { width: 74px !important; min-width: 74px !important; max-width: 74px !important; }
/* Reflow the content into the space freed by the collapsed rail. Metronic's fixed-sidebar layout
   pins the content's inline-start offset at the EXPANDED rail width (265px) and enforces it so hard
   that neither margin (physical or logical, even inline !important) can move it — but `transform`,
   `position: relative` + `left`, and `width` are not locked. So we slide #kt_app_main left by the
   reclaimed width (265-74 = 191px) and widen it to match, so the content genuinely fills the space
   instead of leaving a blank gap. The fixed header pushes with the rail via its own `left`.
   (Verified live 2026-07-14.)
   David (2026-09-25): "Save/cancel is still not anchored to the bottom of the product module
   despite many requests. Probably the same for many other modules. I am sure this is a
   regression." Root cause found by re-reading this exact rule: `transform` establishes a new
   containing block for any `position: fixed` DESCENDANT (CSS spec) — so DetailForm's own
   `.fb-detail-footer-fixed` (§V662, added 2026-09-13, two months after this rule), being a fixed-
   position child of #kt_app_main, stopped anchoring to the actual browser viewport and started
   anchoring to #kt_app_main's OWN box instead, whenever the sidebar rail is collapsed — on a page
   whose content is taller than the viewport (any detail page with several tabs' worth of fields,
   Product included), that box is taller than the screen, so the "fixed" footer lands off past the
   visible fold instead of pinned to the bottom — reads exactly like plain in-flow/sticky
   positioning, not fixed, matching every symptom of this report. This explains why it survived
   "many requests": every previous fix (§V736, §V741, §V674) addressed a real but DIFFERENT bug
   (the footer being genuinely covered/hidden), never this transform/containing-block interaction,
   and it only reproduces with the sidebar collapsed — easy to miss if a fix is verified with the
   sidebar in its default expanded state. `position: relative; left: -191px` is visually identical
   to `transform: translateX(-191px)` (same box, offset the same way, no layout reflow either way)
   but does NOT establish a new containing block for fixed descendants, so it stops capturing
   DetailForm's fixed footer (or any other `position: fixed` element that ends up inside
   #kt_app_main) while still reflowing content into the reclaimed rail width exactly as before. */
body.fb-nav-collapsed #kt_app_header { left: 74px !important; }
body.fb-nav-collapsed #kt_app_main {
  position: relative;
  left: -191px;
  width: calc(100% + 191px) !important;
  max-width: calc(100% + 191px) !important;
}
body.fb-nav-collapsed #kt_app_sidebar .menu-title,
body.fb-nav-collapsed #kt_app_sidebar .menu-arrow,
body.fb-nav-collapsed #kt_app_sidebar .menu-bullet,
body.fb-nav-collapsed #kt_app_sidebar .menu-sub { display: none !important; }
/* §V829 #c - the collapsed rail shows a compact logo (the tenant's, else the favicon) above the expand button, as real's minimised sidebar does; it used to hide the logo link entirely. */
.fb-nav-logo-min { display: none; }
body.fb-nav-collapsed .fb-nav-logo-full { display: none !important; }
body.fb-nav-collapsed .fb-nav-logo-min { display: block !important; }
body.fb-nav-collapsed .app-sidebar-logo { flex-direction: column !important; gap: .25rem; padding-top: .5rem; padding-bottom: .5rem; }
/* Sidebar branding footer (see IconRail.razor's own comment) — same default/minimize swap real's
   app-overrides.css does for its Metronic minimize state, ported to this app's hand-rolled
   fb-nav-collapsed class instead of Metronic's [data-kt-app-sidebar-minimize=on] attribute. */
.app-sidebar-branding-minimize { display: none; }
body.fb-nav-collapsed .app-sidebar-branding-default { display: none !important; }
body.fb-nav-collapsed .app-sidebar-branding-minimize { display: inline-block !important; }
body.fb-nav-collapsed .app-sidebar-logo { justify-content: center !important; padding-left: 0 !important; padding-right: 0 !important; }
body.fb-nav-collapsed #kt_app_sidebar .menu-link { justify-content: center; padding-left: .4rem !important; padding-right: .4rem !important; }
body.fb-nav-collapsed #kt_app_sidebar .menu-icon { margin: 0 !important; }
body.fb-nav-collapsed .fb-nav-toggle i { transform: rotate(180deg); }

/* PARITY.md §V60/CLAUDE.md's own noted gap: real TSMPlus's minimized rail temporarily EXPANDS on
   hover (Metronic's own [data-kt-app-sidebar-minimize=on][data-kt-app-sidebar-hoverable=true]
   mechanism) rather than staying permanently narrow. This app's collapse is hand-rolled (not
   Metronic's KTApp minimize engine — see the comment above on why), so hover-expand is added the
   same way: a pure CSS :hover override, layered on top of the existing click-toggle collapse
   without touching its persisted state. #kt_app_sidebar is already `position: fixed`
   ([data-kt-app-sidebar-fixed=true], set in index.html), so widening it on hover floats it over
   the content as an overlay — no reflow of #kt_app_main/#kt_app_header needed, matching the real
   app's own overlay behavior instead of live-resizing the page underneath the cursor. */
body.fb-nav-collapsed #kt_app_sidebar:hover {
  width: 265px !important; min-width: 265px !important; max-width: 265px !important;
  z-index: 105; box-shadow: 6px 0 16px rgba(0, 0, 0, .18);
}
body.fb-nav-collapsed #kt_app_sidebar:hover .menu-title,
body.fb-nav-collapsed #kt_app_sidebar:hover .menu-bullet,
body.fb-nav-collapsed #kt_app_sidebar:hover .fb-nav-logo { display: revert !important; }
/* hover-expanded rail: back to the full logo beside the toggle */
body.fb-nav-collapsed #kt_app_sidebar:hover .fb-nav-logo-full { display: revert !important; }
body.fb-nav-collapsed #kt_app_sidebar:hover .fb-nav-logo-min { display: none !important; }
body.fb-nav-collapsed #kt_app_sidebar:hover .app-sidebar-logo { flex-direction: row !important; }
/* David: "the hover over menu mode is missing the little down arrows to indicate a sub menu."
   `.menu-arrow` was in the same blanket `display: revert !important` list as menu-title/bullet
   above — wrong fix for this one specifically. Metronic's own `.menu-item .menu-link .menu-arrow`
   rule (style.bundle.css) is `display: flex; align-items: stretch` — the arrow glyph is drawn by
   an `::after` pseudo-element with NO explicit height of its own; it only gets its visible 9px
   height by being a flex child that STRETCHES to fill its flex-container parent. `revert` doesn't
   mean "restore Metronic's own rule" — it means "fall back to the browser's user-agent default
   stylesheet", which for a bare `<span>` is `display: inline`. An inline `.menu-arrow` isn't a
   flex container at all, so `align-items: stretch` never applies and the height-less `::after`
   pseudo collapses to 0 — genuinely present in the DOM (`display: revert` did make the outer
   `<span>` visible) but rendering at zero height, i.e. invisible. Confirmed via computed style:
   `::after` height was `9px` in normal/pinned mode vs `0px` here. Fixed by setting `display: flex`
   explicitly (Metronic's own value) instead of `revert`, restoring the same stretch context. */
body.fb-nav-collapsed #kt_app_sidebar:hover .menu-arrow { display: flex !important; }
/* David: "when its in hover over mode it is not collapsible" — this rule used to also blanket-
   revert `.menu-sub` (every accordion's sub-list) unconditionally, which `!important`-forced EVERY
   section's children permanently visible while hovering, regardless of KTMenu's own expand/collapse
   state (`.menu-sub-accordion` needs its own `.show` class to actually be open — that's the entire
   accordion mechanism). So clicking a section header to collapse it while hovering had zero visible
   effect: this CSS always won over KTMenu's own inline `display`/`height`. Scoped to `.menu-sub.show`
   only — an accordion that's actually collapsed stays collapsed during hover-preview too, same as
   permanent/pinned mode, and clicking a header now visibly toggles it either way. */
body.fb-nav-collapsed #kt_app_sidebar:hover .menu-sub.show { display: revert !important; }
body.fb-nav-collapsed #kt_app_sidebar:hover .app-sidebar-logo { justify-content: revert !important; padding-left: revert !important; padding-right: revert !important; }
body.fb-nav-collapsed #kt_app_sidebar:hover .menu-link { justify-content: revert !important; padding-left: revert !important; padding-right: revert !important; }
body.fb-nav-collapsed #kt_app_sidebar:hover .menu-icon { margin: revert !important; }
body.fb-nav-collapsed #kt_app_sidebar:hover .app-sidebar-branding-default { display: revert !important; }
body.fb-nav-collapsed #kt_app_sidebar:hover .app-sidebar-branding-minimize { display: none !important; }
}

/* Blazor WASM boot indicator. Without this the default index.html <svg class="loading-progress">
   <circle> elements render as a big SOLID BLACK blob (SVG default fill is black) on every reload —
   this styles them into a thin TSM-blue progress ring, centred, on the app background. */
.loading-progress {
  position: relative; display: block; width: 6rem; height: 6rem; margin: 30vh auto 1rem auto;
}
.loading-progress circle {
  fill: none; stroke: #e6eaee; stroke-width: 0.5rem;
  transform-origin: 50% 50%; transform: rotate(-90deg);
}
.loading-progress circle:last-child {
  stroke: var(--fb-primary, #369ac1);
  stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
  transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
  position: absolute; text-align: center; font-weight: 600; inset: calc(30vh + 2.6rem) 0 auto 0;
  color: var(--fb-text-muted);
}
.loading-progress-text:after { content: "Loading TSMPlus…"; }

/* Grid density toggle (TrialGrid "Compress Grid Spacing" action) — tightens Telerik cell padding. */
.fb-grid-compact .k-table-td, .fb-grid-compact .k-table-th { padding-top: 3px; padding-bottom: 3px; }

/* Refresh + Actions row above each grid (see TrialGrid — kept out of the Telerik toolbar to dodge
   its adaptive-overflow mis-measurement under Metronic). Right-aligned compact buttons. */
.fb-grid-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin-bottom: 8px; }
.fb-grid-actions .k-button { width: auto; flex: 0 0 auto; }
/* F10 — Scheduling's drag-a-job-onto-a-technician affordance.
   §V274 — David: "drag and drop not happening to scheduler." Matches real's own `.draggable` class
   exactly (Pages/Modules/Scheduling/TimelineScheduler.razor's <style> block): without
   user-select:none, a mousedown-then-move gesture on an element containing text/icon content can
   get interpreted by the browser as a text-selection attempt instead of a native HTML5 drag
   initiation — draggable="true" alone doesn't prevent this. A likely second real cause (on top of
   the already-fixed grid-row pointer-event conflict, §V273) of drags never actually starting for a
   genuine mouse gesture, despite working via synthetic DragEvent dispatch (which bypasses the
   browser's own gesture-detection phase entirely). */
.fb-drag-row, .fb-scheduler-timeline-appt {
    cursor: grab;
    -webkit-user-select: none; -ms-user-select: none; user-select: none;
}
.fb-drag-row:active, .fb-scheduler-timeline-appt:active { cursor: grabbing; }
.fb-scheduler-drop-target { transition: background-color .15s; }
.fb-scheduler-drop-target:hover { background-color: var(--fb-light, #f3f6f9); }
/* §V269 remainder — Scheduling's rebuilt day-cell timeline table: pin the leftmost
   Technician/Day header column while the table scrolls horizontally, matching real's own
   `sticky-column` class on the same table. */
.sticky-column { position: sticky; left: 0; z-index: 1; background: inherit; }
/* §V270 — the day-cell timeline table's own cells need a real visible min-height/border
   independent of Bootstrap's `table-bordered` (Metronic's own `.table tr, .table th, .table td`
   reset sets `min-height:inherit`/`border-width:0` with HIGHER specificity than a bare class
   selector, since it's class+element vs. class-only — confirmed via `getComputedStyle` showing
   the rule losing outright, not just a cascade-order issue. `!important` is the pragmatic fix
   since this is presentational-only and scoped to a class this app invented, not a shared one). */
/* David: double-clicking an empty cell to open the Create Appointment modal left a blue
   selection/focus outline on the cell underneath it — same root cause as §V274 above (native
   browser text-selection triggered by a two-click gesture on cell content), just dblclick instead
   of mousedown-drag. Same fix: user-select:none on the cell itself. */
.fb-scheduler-day-cell {
    min-width: 150px !important; min-height: 64px !important;
    border: 1px solid var(--bs-border-color, #e4e6ef) !important;
    vertical-align: top !important; padding: 4px !important;
    -webkit-user-select: none; -ms-user-select: none; user-select: none;
}
.fb-scheduler-day-cell.fb-scheduler-weekend { background-color: var(--fb-light, #f5f8fa) !important; }
/* The grid's own toolbar now holds only the search box — keep it a sane width, not full-bleed. */
.k-grid-toolbar .k-searchbox { max-width: 440px; }
/* The real TSMPlus grid has no toolbar column-chooser button — column management lives in the
   "Manage Columns" modal (Actions ▸ Select Columns). We keep Telerik's chooser tool mounted but
   off-screen: it's the only source of hidden columns' titles, which fbReadColumns scrapes by opening
   this button's popup off-screen. Never remove it — the modal depends on it. Per-column ⋮ menus stay. */
.k-grid-toolbar .k-grid-column-menu { position: absolute !important; left: -10000px !important; top: 0 !important; }

/* Manage Columns modal (TrialGrid) — mirrors firebird-dapr ShowOrHideGridColumns. */
.fb-mc { padding: 4px 4px 0; height: 100%; display: flex; flex-direction: column; }
/* §V1392 — real's dialog fills the window: lists stretch to the footer and scroll inside themselves. */
.fb-mc-toolbar, .fb-mc-footer { flex-shrink: 0; }
.fb-mc-cols { flex: 1 1 auto; min-height: 0; }
.fb-mc-col { display: flex; flex-direction: column; min-height: 0; }
@media (min-width: 768px) { .fb-mc-cols { flex-wrap: nowrap; } }
.fb-mc-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.fb-mc-search { flex: 1 1 auto; }
.fb-mc-hint { font-size: .8rem; margin-bottom: 8px; }
.fb-mc-pane { background: var(--bs-gray-100, #f4f4f4); border-radius: 8px; padding: 10px; flex: 1 1 auto; min-height: 160px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.fb-mc-item { display: flex; align-items: center; gap: 10px; background: var(--fb-surface); border-radius: 6px; padding: 8px 12px; }
.fb-mc-item[draggable="true"] { cursor: grab; }
.fb-mc-grip { color: var(--bs-gray-500, #99a1b7); }
.fb-mc-label { font-weight: 600; font-size: .9rem; }
.fb-mc-empty { font-size: .85rem; padding: 8px; }
.fb-mc-footer { display: flex; align-items: center; gap: 8px; margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--bs-gray-200, #e9e9e9); }

/* Job Details (and other detail pages) — the Customer / Location / Contact / Billing cards that sit
   at the TOP of the Details card, matching the real JobDetailsCard's FbCol tiles. */
.fb-jd-card { background: var(--bs-gray-100, #f4f4f4); border: 1px solid var(--bs-gray-200, #e9e9e9); border-radius: 8px; padding: 12px 14px; height: 100%; }

/* Card ⋮ menu (PickerCard) — a lightweight dropdown anchored to the ellipsis button. */
.fb-card-menu-wrap { position: relative; }
.fb-card-menu { position: absolute; right: 0; top: 100%; z-index: 100; min-width: 160px; background: var(--fb-surface);
    border: 1px solid var(--bs-gray-300, #dbdbdb); border-radius: 8px; box-shadow: 0 6px 24px rgba(0,0,0,.12);
    padding: 6px; display: flex; flex-direction: column; }
.fb-card-menu-item { display: block; width: 100%; text-align: left; background: none; border: 0; border-radius: 6px;
    padding: 7px 12px; font-size: .9rem; font-weight: 600; color: var(--bs-gray-800, #252525); text-decoration: none; }
.fb-card-menu-item:hover { background: var(--bs-gray-100, #f4f4f4); }

/* PickerModal — searchable candidate list. */
.fb-picker-list { max-height: 420px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; }
.fb-picker-row { text-align: left; background: none; border: 1px solid transparent; border-radius: 8px; padding: 10px 12px; width: 100%; }
.fb-picker-row:hover { background: var(--bs-gray-100, #f4f4f4); border-color: var(--bs-gray-200, #e9e9e9); }

/* Reports launcher tiles (one per report type). */
.fb-report-tile { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px; border: 1px solid var(--bs-gray-200, #e9e9e9);
    border-radius: 8px; background: var(--fb-surface); text-decoration: none; color: var(--bs-gray-900, #181c32); height: 100%; }
.fb-report-tile:hover { border-color: var(--bs-primary, #1b84ff); background: var(--bs-gray-100, #f9f9f9); }

.fb-app { display: flex; min-height: 100vh; }

/* Icon rail (left nav) — collapsed to icons, expands on hover as an overlay */
.fb-icon-rail-wrap { position: relative; width: 56px; flex-shrink: 0; z-index: 40; }
.fb-icon-rail {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 56px;
  background: #fff;
  border-right: 1px solid var(--fb-border);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: width 0.15s ease;
}
.fb-icon-rail:hover { width: 230px; box-shadow: 2px 0 16px rgba(0,0,0,0.1); }
.fb-rail-item {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 11px 18px;
  color: var(--fb-text-muted);
  white-space: nowrap;
  border-left: 3px solid transparent;
  cursor: pointer;
  text-decoration: none;
}
.fb-rail-item:hover { background: var(--fb-primary-light); color: var(--fb-primary-hover); }
.fb-rail-item.active { color: var(--fb-primary-hover); border-left-color: var(--fb-primary); background: var(--fb-primary-light); }
.fb-rail-item.disabled { cursor: default; color: var(--white-200); }
.fb-rail-item.disabled:hover { background: none; color: var(--white-200); }
.fb-rail-icon { width: 20px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.fb-rail-label { opacity: 0; transition: opacity 0.08s; font-size: 13.5px; }
.fb-icon-rail:hover .fb-rail-label { opacity: 1; }
.fb-rail-badge {
  margin-left: auto;
  font-size: 9px;
  text-transform: uppercase;
  background: var(--fb-secondary);
  padding: 1px 6px;
  border-radius: 10px;
  color: var(--fb-text-muted);
  opacity: 0;
  transition: opacity 0.08s;
}
.fb-icon-rail:hover .fb-rail-badge { opacity: 1; }

.fb-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }

.fb-topbar {
  height: 56px;
  background: #fff;
  border-bottom: 1px solid var(--fb-border);
  display: flex;
  align-items: center;
  gap: 28px;
  padding: 0 20px;
  flex-shrink: 0;
}
.fb-topbar-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 16px;
  color: var(--fb-text);
  cursor: pointer;
  flex-shrink: 0;
  text-decoration: none;
}
.fb-topbar-brand .fb-logo-dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--fb-primary);
  box-shadow: 0 0 0 4px var(--fb-primary-light);
}
.fb-topbar-nav { display: flex; align-items: center; gap: 22px; flex: 1; }
.fb-topbar-nav-item { font-size: 13.5px; font-weight: 600; color: var(--fb-text-muted); cursor: pointer; text-decoration: none; }
.fb-topbar-nav-item:hover, .fb-topbar-nav-item.active { color: var(--fb-primary-hover); }
.fb-topbar-nav-item.disabled { color: var(--white-200); cursor: default; }
.fb-topbar-icons { display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
.fb-avatar {
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--fb-primary-light);
  color: var(--fb-primary-hover);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 11px;
}

.fb-proto-bar {
  background: var(--fb-warning-light);
  border-bottom: 1px solid var(--fb-warning);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 20px;
  flex-shrink: 0;
  font-size: 12px;
}
.fb-topbar-proto-badge {
  color: var(--yellow-warning-600);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 11px;
}
.fb-topbar-user { display: flex; align-items: center; gap: 10px; font-size: 12.5px; color: var(--fb-text-muted); }

.fb-content { flex: 1; padding: 22px 26px; overflow-y: auto; }

/* Bottom footer — mirrors the real TSMPlus _Footer.razor: version bottom-left, product +
   "Powered by" branding bottom-right. */
.fb-footer {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 9px 26px; border-top: 1px solid #eceef1; background: var(--white, #fff);
  flex-shrink: 0; font-size: 12px; color: var(--fb-text-muted);
}
.fb-footer-version { font-weight: 600; color: var(--fb-text); }
.fb-footer-brand { display: flex; align-items: center; gap: 18px; }
.fb-footer-logo { display: flex; align-items: center; gap: 7px; font-weight: 700; color: var(--fb-text); }
.fb-footer-logo .fb-logo-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--fb-primary); box-shadow: 0 0 0 3px var(--fb-primary-light); }
.fb-footer-powered { color: var(--fb-text-muted); }
.fb-footer-powered strong { color: var(--fb-text); font-weight: 700; }

/* Settings area content wrapper (SettingsLayout) — the sub-nav itself now lives only in
   IconRail.razor's InSettings swap (the main app-sidebar), not duplicated here. */
.settings-content { min-width: 0; }

/* App sidebar internal scrolling — Metronic's own `.hover-scroll-overlay-y` class expects its
   KTScroll JS component (data-kt-scroll-* attributes + height recalculation), which isn't wired
   here, so the class alone did nothing and a long menu (e.g. the Settings sub-nav swap) just
   pushed past the viewport with no way to reach the bottom items. Plain-CSS fix instead of wiring
   the JS component. Two flex issues, not just one:
   (1) `min-height: 0` alone was NOT enough — Metronic's own `.flex-column-fluid` utility (shared
       by several unrelated layout containers, so only overridden here for the sidebar menu
       specifically) is `flex: 1 0 auto` — flex-shrink is 0, so the item literally cannot shrink
       below its content's height no matter what min-height says. Needs flex-shrink: 1 (or a full
       `flex: 1 1 auto` override) to actually participate in the parent's fixed-height layout.
   (2) `overflow-y: auto` on the wrapper then genuinely scrolls once the menu above it can shrink. */
.app-sidebar-menu.flex-column-fluid { flex: 1 1 auto; min-height: 0; }
/* The wrapper itself also needs an explicit bounded height for overflow-y:auto to create a real
   scroll region — without it, the wrapper just grows to its content's full height inside the now-
   correctly-sized (but overflow:hidden) parent above, silently clipping everything past that
   height instead of scrolling. 100% resolves against .app-sidebar-menu's flex-computed height,
   which is definite once flex-shrink can actually apply (the fix directly above). */
.app-sidebar-wrapper { height: 100%; overflow-y: auto; overflow-x: hidden; }

/* Settings hub — grouped link cards standing in for the real Settings left-sidebar sub-nav. */
.fb-settings-groups { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; align-items: start; }
.fb-settings-item { display: flex; align-items: center; gap: 10px; padding: 8px 6px; border-radius: 6px; text-decoration: none; color: var(--fb-text); font-size: 13.5px; }
.fb-settings-item:hover { background: var(--fb-primary-light); }
.fb-settings-item.disabled { color: var(--fb-text-muted); cursor: default; }
.fb-settings-item.disabled:hover { background: transparent; }
.fb-settings-item-icon { width: 22px; text-align: center; }
.fb-settings-item-label { flex: 1; font-weight: 600; }
.fb-settings-item-go { color: var(--fb-text-muted); font-size: 18px; }
.fb-settings-item-badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--fb-text-muted); background: #f1f3f5; border-radius: 10px; padding: 2px 8px; }

/* NEW-8 / SH-29 - real's grid header is a rounded card (p-2 p-lg-3), title left / actions right. */
.fb-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; flex-wrap: wrap; gap: 10px; background: var(--fb-surface); border: 1px solid var(--fb-border); border-radius: 8px; padding: 0.5rem; }
@media (min-width: 992px) { .fb-page-header { padding: 0.75rem; } }
.fb-page-title { font-size: 22px; font-weight: 700; margin: 0; }

.fb-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid transparent;
  border-radius: var(--fb-radius);
  padding: 7px 14px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  background: var(--fb-surface);
}
.fb-btn-primary { background: var(--fb-primary); border-color: var(--fb-primary); color: #fff; }
.fb-btn-primary:hover { background: var(--fb-primary-hover); }
.fb-btn-outline { background: var(--fb-surface); border-color: var(--fb-border); color: var(--fb-text); }
.fb-btn-outline:hover { background: #f2f4f6; }
.fb-btn:disabled { opacity: 0.5; cursor: not-allowed; }

.fb-card { background: var(--fb-surface); border-radius: var(--fb-radius); border: 1px solid var(--fb-border); box-shadow: var(--fb-shadow); margin-bottom: 18px; }
.fb-card-header { padding: 14px 18px; border-bottom: 1px solid var(--fb-border); display: flex; align-items: center; justify-content: space-between; }
.fb-card-title { font-size: 14px; font-weight: 700; margin: 0; }
.fb-card-body { padding: 16px 18px; }

.fb-empty { padding: 40px 0; text-align: center; color: var(--fb-text-muted); font-size: 13px; }

/* Rebuilt 2026-08-15 to match the real login screen (app.tsmplus.com.au's Azure B2C sign-in
   page) - full-bleed background photo plus dark overlay, brand block pinned left, sign-in card
   pinned right. Two earlier deploy attempts this same day broke this exact rule by accident -
   a Razor-style comment delimiter the first time, then this very comment quoting that same
   delimiter literally the second time, which closed itself early. Kept this note deliberately
   free of any comment-delimiter characters so it cannot happen a third time. */
.fb-login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 28px;
  padding: 48px 64px;
  background-image: linear-gradient(rgba(10,14,22,0.55), rgba(10,14,22,0.55)), url('../assets/media/auth/TSM-BG-Homepage.jpg');
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  flex-wrap: wrap;
}
.fb-login-brand-block { color: #fff; flex: 1 1 320px; max-width: 480px; }
.fb-login-wordmark { font-size: 48px; font-weight: 800; line-height: 1; color: var(--pacific-blue-400); }
.fb-login-wordmark-accent { background: linear-gradient(90deg, var(--pacific-blue-400), var(--purple-500)); -webkit-background-clip: text; background-clip: text; color: transparent; }
/* ONB-15's sales headline, restored inside the real-layout brand block (2026-08-15 follow-up —
   David: "the sales messaging above login screen is lost"). */
.fb-login-headline-title { font-size: 26px; font-weight: 700; line-height: 1.3; margin-top: 20px; margin-bottom: 10px; }
.fb-login-headline-sub { font-size: 14px; color: rgba(255,255,255,0.8); line-height: 1.5; }
.fb-login-powered-by { margin-top: 24px; font-size: 13px; color: rgba(255,255,255,0.7); line-height: 1.5; }
.fb-login-card { background: var(--fb-surface); border-radius: 10px; padding: 32px 36px; width: 380px; flex: 0 0 380px; box-shadow: 0 10px 40px rgba(0,0,0,0.35); }
/* iPhone fix (2026-08-15): 380px fixed card + wrapper padding forced the login page's layout
   viewport to 444px on a 375px phone, so iOS zoomed the whole screen out. Fill the viewport
   instead of exceeding it. */
@media (max-width: 600px) {
  .fb-login-card { width: 100%; flex: 0 0 auto; max-width: 100%; padding: 24px 18px; }
}
.fb-login-sub { color: var(--fb-text); font-size: 20px; font-weight: 700; margin-bottom: 20px; text-align: center; }
.fb-input-group { margin-bottom: 14px; }
.fb-input-label { display: block; font-size: 12.5px; font-weight: 600; margin-bottom: 5px; }
.fb-input { width: 100%; border: 1px solid var(--fb-border); border-radius: var(--fb-radius); padding: 9px 12px; font-size: 13.5px; }
.fb-input:focus { outline: none; border-color: var(--fb-primary); box-shadow: 0 0 0 3px var(--fb-primary-light); }
.fb-login-note { margin-top: 18px; font-size: 11.5px; color: var(--fb-text-muted); background: #f8f9fb; border-radius: var(--fb-radius); padding: 10px 12px; }

.fb-breadcrumb { font-size: 12.5px; color: var(--fb-text-muted); margin-bottom: 4px; }
.fb-code-link { font-weight: 700; color: var(--fb-primary-hover); }

.fb-grid-2 { display: grid; grid-template-columns: 2fr 1fr; gap: 18px; align-items: start; }
.fb-grid-3-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 20px; }
.fb-grid-3col { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 16px; }
@media (max-width: 900px) {
  .fb-grid-2 { grid-template-columns: 1fr; }
  .fb-grid-3-stats { grid-template-columns: repeat(2, 1fr); }
  .fb-grid-3col { grid-template-columns: 1fr; }
}
/* Phone (field tech): single-column stats, and let the app-main shrink so a wide Telerik grid
   scrolls WITHIN its own container instead of pushing the page body sideways. */
@media (max-width: 600px) {
  .fb-grid-3-stats { grid-template-columns: 1fr; }
  .fb-page-title { font-size: 19px; }
}
.app-main, #kt_app_main, #kt_app_content, #kt_app_content_container { min-width: 0; max-width: 100%; }
body { overflow-x: hidden; }

/* Mobile card list (TrialGrid on phones): a stacked, tap-able card per record — the field tech's
   list view, replacing the desktop grid below md (767px). Each card leads with the record's title
   row (code + primary badge), then a few key fields. Generous tap targets. */
.fb-mobile-cards { display: flex; flex-direction: column; gap: 10px; }
.fb-mobile-card {
  background: var(--fb-surface); border: 1px solid var(--fb-border); border-radius: var(--fb-radius);
  box-shadow: var(--fb-shadow); padding: 14px 16px; cursor: pointer;
  -webkit-tap-highlight-color: transparent; transition: background 0.1s ease;
}
.fb-mobile-card:active { background: #f1f5f9; }

/* §V276 — a genuine TelerikGrid rendering MobileCard content through a single wide Template column
   (TrialGrid's AlwaysCard+RowDraggable branch — see its own comment for why this has to stay a real
   TelerikGrid instead of the plain .fb-mobile-cards div swap). Strips Telerik's own header/border/
   stripe chrome so each row still reads as a plain card, same look as .fb-mobile-card above. */
.fb-card-grid .k-grid-header { display: none; }
.fb-card-grid .k-grid-table td { border: none; padding: 6px 8px; }
.fb-card-grid .k-grid-table tr { background: transparent; }
.fb-card-grid .k-grid-table tr:hover { background: transparent; }
/* The .fb-mc-* card-content classes are shared between the grid mobile-card template
   (.fb-mobile-card) and the dedicated Mobile Appointments cards (.fb-appt-card). */
.fb-mobile-card .fb-mc-head, .fb-appt-card .fb-mc-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; margin-bottom: 6px; }
.fb-mobile-card .fb-mc-code, .fb-appt-card .fb-mc-code { font-weight: 700; color: var(--fb-primary-hover); font-size: 14px; }
.fb-mobile-card .fb-mc-title, .fb-appt-card .fb-mc-title { font-weight: 600; font-size: 14px; line-height: 1.3; }
.fb-mobile-card .fb-mc-row, .fb-appt-card .fb-mc-row { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; padding: 3px 0; }
.fb-mobile-card .fb-mc-row .fb-mc-label, .fb-appt-card .fb-mc-row .fb-mc-label { color: var(--fb-text-muted); flex: 0 0 auto; }
.fb-mobile-card .fb-mc-row .fb-mc-val, .fb-appt-card .fb-mc-row .fb-mc-val { font-weight: 500; text-align: right; }
.fb-mobile-card .fb-mc-badges, .fb-appt-card .fb-mc-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.fb-appt-card { -webkit-tap-highlight-color: transparent; transition: background 0.1s ease; }
.fb-appt-card:active { background: #f1f5f9; }
.fb-mobile-empty { text-align: center; color: var(--fb-text-muted); padding: 40px 0; font-size: 13px; }

/* Reported: on a browser window shorter than a modal's content, the Save/Cancel (or Wizard's
   Cancel/Back/Next/Save) footer scrolled off the bottom with no way to reach it — the TelerikWindow
   had no height cap, so it just grew past the viewport. Fix, applied to every TelerikWindow modal
   (EntityFormModal, EntityWizardModal, LineItemsEditor, PickerModal, ProductPickerModal, RulesEngine)
   via the shared `.k-window`/`.k-window-content` classes rather than per-component: cap the window's
   own height to the viewport, then let ONLY the scrollable body area (`.fb-modal-body` directly, or
   the wizard's step-content column) scroll internally — the footer sits outside that scrolling area
   as a non-growing flex item, so it's always visible regardless of window height or content length.
   This matches real TSMPlus's own modal behavior (a scrolling body with Next/Save/Cancel anchored to
   the bottom), not a phone-only fix — the old mobile-only `.k-window-content` override below is kept
   only for its width/position rules; its own overflow/max-height are now redundant with this. */
/* David, live on his iPhone 17, 2nd round: v0.625.0's -webkit-overflow-scrolling fix did NOT
   resolve "still not scrolling... still cant get to save button" -- that property is a legacy,
   largely no-op hint on modern iOS anyway. Found the REAL cause via computed-style ancestor-chain
   inspection: Telerik's own CDN theme sets touch-action: none on .k-window itself (not just the
   titlebar/resize-handles, which separately carry their own inline touch-action:none for
   drag/resize -- confirmed both still present after this override). Per the CSS touch-action spec,
   an ancestor's "none" intersects with (and wins over) whatever a descendant like .fb-modal-body
   declares -- Chromium-based test tooling never caught this because it drives scroll via
   programmatic/mouse events, not real touch gestures, so this bug was invisible to every check
   this session could run short of a real device. Overriding it back to auto restores normal touch
   scrolling in the content area; the titlebar/resize-handles keep their own inline none untouched,
   so drag-to-move/resize is unaffected. NOT verified against a real iPhone by this session --
   needs David's own on-device confirmation. */
.k-window { touch-action: auto !important; }
.k-window { max-height: 92vh; max-height: 92dvh; }
.k-window-content { display: flex; flex-direction: column; max-height: 92vh; max-height: 92dvh; overflow: hidden; }
.k-window-content > .fb-modal-body { flex: 1 1 auto; overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
.k-window-content > .fb-modal-actions { flex: 0 0 auto; }
.k-window-content > .fb-wizard { flex: 1 1 auto; min-height: 0; overflow: hidden; }
.fb-wizard > .flex-grow-1.d-flex.flex-column { min-height: 0; }
.fb-wizard .fb-modal-body { overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
.fb-wizard-rail { overflow-y: auto; }

/* David, live on his iPhone 17: "create po just a little too wide" + "vertical scrolling not
   working" after picking a Part in the Line Item wizard's own nested "Create New Job Item"
   modal. Confirmed via computed-style inspection (not just the bounding rect): that step's
   `.row.g-4` Bootstrap grid carries a -6px negative gutter margin that this 2px horizontal
   padding never absorbs, bleeding ~4px past the modal's own right edge on ANY narrow viewport
   (not a fixed-width assumption -- Bootstrap's row/gutter math is relative, so this bleed scales
   with whatever gutter size is active, not one specific phone). overflow-x: hidden clips that
   bleed everywhere .fb-modal-body is used, rather than patching one wizard step. Separately,
   .fb-modal-body's content (1218px tall in that same repro) was already using the correct
   overflow-y: auto, and scrolled fine in this session's own (Chromium) browser -- but David's
   real iPhone Safari session couldn't reach the Save button at all. This matches a well-known
   iOS Safari gap: a nested overflow-y: auto flex child doesn't reliably accept touch-scroll
   gestures without an explicit -webkit-overflow-scrolling: touch (added above on both the
   plain-modal and wizard-step scroll rules) -- Chromium doesn't need this hint, which is why it
   only reproduced on a real device, not in this session's own testing tools. NOT verified against
   a real iPhone by this session (no such device available here) -- needs David's own confirmation
   on-device before considering this closed. */
.fb-modal-body { padding: 4px 2px 2px; overflow-x: hidden; }

/* F90 — David reported the Asset Work History modal "gets too short" on some tabs, with the
   History tab's job drilldown link unreachable. Root-caused via direct DOM measurement (not a
   visual guess): switching to a shorter tab correctly shrinks .modal-body's own clientHeight, but
   .modal-dialog's outer box stays pinned at its tallest-ever size (measured identical before/after
   a tab switch that removes most of the content) — short tab content then renders far below the
   visible viewport with no scroll affordance suggesting it's there. Same class of stale-layout bug
   the .k-window/.fb-modal-body rules above already fix for Telerik modals; this is the equivalent
   fix for this app's plain-Bootstrap `.modal-dialog-scrollable` modals (AssetWorkHistoryModal.razor,
   ComplianceTab.razor's answer modal — both swap tab/question content in place via Blazor's
   @if/else-if, not a fresh navigation, which is exactly the case Bootstrap's own default sizing
   doesn't reliably re-measure). Forces the height cap onto an explicit flex chain instead of
   relying on Bootstrap's own recalculation. */
.modal-dialog-scrollable .modal-content { display: flex; flex-direction: column; max-height: 92vh; max-height: 92dvh; overflow: hidden; }
.modal-dialog-scrollable .modal-content > .modal-body { flex: 1 1 auto; overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
.fb-modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--fb-border); }
.fb-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
.fb-form-field { display: flex; flex-direction: column; gap: 4px; font-size: 12.5px; }
.fb-form-field > span { color: var(--fb-text-muted); font-weight: 600; }
.fb-form-field em { color: var(--carnation-500); font-style: normal; }
.fb-form-full { grid-column: 1 / -1; }
.fb-btn-danger { background: var(--carnation-500); color: #fff; border: 1px solid var(--carnation-500); }
.fb-btn-danger:hover { filter: brightness(0.95); }

/* David: "highlight the guided tours button in a bright colour - purple to match the logo." Layers
   onto Bootstrap's own .btn/.btn-sm (unchanged shape/padding, matching this topbar's other icon
   buttons) rather than the separate .fb-btn family, which has its own sizing. */
.fb-btn-guided-tours { background: var(--purple-500); border-color: var(--purple-500); color: #fff; }
.fb-btn-guided-tours:hover, .fb-btn-guided-tours:focus { background: var(--purple-600); border-color: var(--purple-600); color: #fff; }
.fb-btn-guided-tours i { color: #fff; }
/* David: "My Notes button should be much more prominent... perhaps coloured to stand out (and on
   the tour as well)." Same solid-fill treatment as .fb-btn-guided-tours above, but the app's
   existing --yellow-warning-* tokens instead of purple — a deliberately different colour so the
   two "always-visible entry point" buttons (Tours vs Notes) read as distinct features, not one
   family. Dark text on the lighter default fill (readable contrast on --yellow-warning-400);
   white on the darker hover/focus fill (--yellow-warning-600). */
.fb-btn-notes { background: var(--yellow-warning-400); border-color: var(--yellow-warning-400); color: #1e1e2d; }
.fb-btn-notes:hover, .fb-btn-notes:focus { background: var(--yellow-warning-600); border-color: var(--yellow-warning-600); color: #fff; }
.fb-btn-notes i { color: inherit; }
@media (max-width: 600px) { .fb-form-grid { grid-template-columns: 1fr; } }

/* ── On-screen guided tour (TourOverlay.razor/TourService.cs) ─────────────────────────────────
   David: wizards should "point to things on the screen and lead the user through" instead of just
   describing them in a modal.
   §V2 fix — David: "the creation screens are popup modals, the new wizard highlighting is in the
   background and can't interact." A z-index of ~2000 loses to Telerik's TelerikWindow/k-overlay,
   which assign their own z-index dynamically at runtime (an incrementing per-popup counter, not a
   fixed value in any stylesheet — confirmed by finding no static `.k-window`/`.k-overlay` z-index
   rule at all) rather than a stable number this app could simply out-rank once. Using a
   near-max value here (comfortably above anything Kendo's counter will ever reach) instead of
   guessing at a "high enough" fixed number.
   §V3 fix — David: "they grey out the rest of the screen so I cannot see it." The original
   `.tour-spotlight` used an oversized box-shadow (0 0 0 9999px) to fill the ENTIRE rest of the
   viewport with a dark overlay as its "cutout" trick — combined with `.tour-backdrop`, too much of
   the page became unreadable. Dropped the 9999px shadow entirely: `.tour-spotlight` is now just a
   glowing border/ring around the target, no darkening at all, so the rest of the screen (and any
   open modal behind/around it) stays fully visible. `.tour-backdrop` itself is now effectively a
   no-op (kept as an empty hook, not deleted, in case a future pass wants a very light vignette). */
.tour-backdrop { position: fixed; inset: 0; z-index: 2147483000; pointer-events: none; }
.tour-spotlight {
  position: fixed; z-index: 2147483001; pointer-events: none; border-radius: 10px;
  box-shadow: 0 0 0 3px var(--fb-primary, #009ef7), 0 0 0 6px rgba(0,158,247,.35), 0 0 16px 2px rgba(0,158,247,.5);
  transition: top .2s ease, left .2s ease, width .2s ease, height .2s ease;
}
.tour-callout {
  position: fixed; z-index: 2147483002; width: 320px; max-width: calc(100vw - 32px);
  background: var(--fb-surface); border-radius: var(--fb-radius, 10px); box-shadow: 0 12px 32px rgba(0,0,0,.28);
  padding: 16px 18px; transition: top .2s ease, left .2s ease;
}
.tour-callout-center {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
}
.tour-callout-bottom { transform: translate(-50%, 0); }
.tour-callout-top { transform: translate(-50%, -100%); }
.tour-callout-left { transform: translate(-100%, -50%); }
.tour-callout-right { transform: translate(0, -50%); }
.tour-callout-eyebrow { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--fb-primary, #009ef7); margin-bottom: 4px; }
.tour-callout-title { font-size: 15px; font-weight: 700; margin-bottom: 6px; }
.tour-callout-text { font-size: 12.5px; color: var(--fb-text-muted); line-height: 1.5; margin-bottom: 14px; }
.tour-callout-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
@media (max-width: 600px) {
  /* David, live on a phone, reported the tour callout covering Save/Cancel a THIRD time, in this
     exact wizard, even after two earlier fixes (TourOverlay.razor's own footer-aware clamp) that
     had only ever been verified at desktop width. Root cause: this rule's own `bottom: 16px` was a
     bare constant, unconditionally pinning the callout 16px off the viewport bottom regardless of
     where a `.fb-wizard-footer`/`.fb-detail-footer` actually sat underneath it — completely
     bypassing TourOverlay's clamp, which only ever computed `top`/`left` (both overridden here
     anyway). Now consults `--tour-callout-safe-bottom`, the same footer measurement TourOverlay's
     desktop clamp uses, set inline by CalloutStyle() on every render — floors to the original 16px
     when there's no footer to clear (see that variable's own doc comment for the math). */
  .tour-callout, .tour-callout-bottom, .tour-callout-top, .tour-callout-left, .tour-callout-right {
    left: 50% !important; top: auto !important; bottom: var(--tour-callout-safe-bottom, 16px) !important; transform: translate(-50%, 0) !important; width: calc(100vw - 32px);
  }
}

/* §V(tour-notes) — the per-step note icon + its popover panel. `.tour-callout` is itself
   `position: fixed`, so an absolutely-positioned child anchors to IT, not the viewport — no
   extra positioning context needed. Sits above `.tour-spotlight` (z-index 2147483001) so it's
   never obscured by the highlight ring. */
.tour-note-btn { position: relative; flex-shrink: 0; font-size: 11.5px; font-weight: 600; padding: 3px 10px; }
.tour-note-badge { position: absolute; top: -6px; right: -6px; min-width: 16px; height: 16px; font-size: 9px; }
.tour-note-panel {
  position: absolute; top: calc(100% + 8px); right: 0; width: 300px; z-index: 2147483003;
}


/* ── Narrow-desktop header fit (≤1199px) ──────────────────────────────────────────────────────
   Reported: on a narrower-than-full browser the fixed header overflowed and the right-hand tooling
   (incl. the Settings gear) was pushed off-screen, unreachable. Fix: below Metronic's xl breakpoint
   let the header size to content and WRAP its two clusters instead of overflowing — the tooling
   drops to a second row rather than off the edge. Header goes static so the taller wrapped header
   doesn't clip against a fixed height. Phones (≤767px) layer additional rules below. */
@media (max-width: 1199px) {
  #kt_app_header { position: static !important; height: auto !important; }
  /* #kt_app_header_wrapper and .app-navbar both need an explicit `width: 100%` here, not just
     flex-wrap. Reasoning (confirmed by live measurement, not guessed): (1) .app-navbar carries
     Bootstrap's `flex-shrink-0` in the markup (Topbar.razor) — that utility is itself !important,
     so `flex-wrap: wrap` alone doesn't stop it sizing to its unwrapped max-content width; overriding
     flex-shrink back to 1 is necessary. (2) Even with flex-shrink fixed, a `flex-wrap: wrap`
     container's own preferred width (used when it's sized as a flex item in ITS parent) is still
     computed from its content's natural/max-content size in some browsers, NOT its wrapped size —
     so the wrapper and navbar kept rendering wider than the viewport regardless. Forcing
     `width: 100%` sidesteps that ambiguity entirely: each is told its exact box size directly
     instead of leaving it to intrinsic-sizing + flex-wrap interaction. */
  #kt_app_header_wrapper { flex-wrap: wrap; row-gap: 6px; width: 100%; }
  .app-navbar { width: 100%; flex-shrink: 1 !important; flex-wrap: wrap; justify-content: flex-end; gap: 0.4rem !important; }
  .app-navbar .form-select { width: auto !important; min-width: 0; max-width: 150px; }
}

/* §V191 — detail-page tab strip (FbTabs.razor) wraps to a second row instead of horizontal-
   scrolling/clipping once its tabs exceed the available width (e.g. Jobs' 14 tabs at ~1355px).
   `.nav-line-tabs` already switched to `flex-wrap` in the markup; this just gives wrapped rows
   breathing room and keeps the bottom line-tab border looking continuous across rows. Only
   applies ≥768px — below that FbTabs renders its own dropdown+arrows instead of this <ul>. */
.fb-tabs .nav-line-tabs.flex-wrap { row-gap: 4px; }

/* David: "tabs on forms contrast is too light and hard to read." Root cause: Metronic's own
   .nav-line-tabs rule (style.bundle.css) colors every tab label var(--kt-gray-500) regardless of
   active state (#A1A5B7 in light mode) — ~2.5:1 against a white card, below WCAG AA's 4.5:1
   minimum for normal text, and the active tab only gets an underline, no color change, so it reads
   almost as faint as the inactive ones. Scoped to .fb-tabs (this file's own convention for
   overriding Metronic tab styling, see the rule above) rather than touching --kt-gray-500 globally,
   which other unrelated Metronic components also read. */
.fb-tabs .nav-line-tabs .nav-item .nav-link { color: var(--kt-gray-700); }
.fb-tabs .nav-line-tabs .nav-item .nav-link.active { color: var(--kt-primary); font-weight: 700; }

/* ── Mobile shell fixes (phones ≤767px) ───────────────────────────────────────────────────────
   Two reported iPhone bugs: (1) the top header was wider than the viewport and pushed the page
   sideways; (2) modal popups used a fixed 560–780px TelerikWindow width and rendered off-screen. */
@media (max-width: 767px) {
  /* (1) Header sizes to the phone. Don't let it stick (avoids fixed-offset math once the tooling
     navbar wraps), and allow the right-hand tooling to wrap to a second row instead of overflowing.
     CORRECTED (reported live on an iPhone, reproduced + root-caused via a same-origin test iframe
     at true 375px width — see ≤1199px block above for the full explanation of why `width: 100%`,
     not just flex-wrap, was needed): the master-data menu previously became a horizontal scroll
     strip (overflow-x: auto + flex-wrap: nowrap) that could render scrolled away from its own
     start; now it wraps instead, and every header cluster gets an explicit 100% width so none of
     them can ever measure wider than the phone's own viewport. */
  #kt_app_header { position: static !important; height: auto !important; }
  #kt_app_header_container { padding-left: 10px !important; padding-right: 10px !important; max-width: 100vw; flex-wrap: wrap; }
  #kt_app_header_wrapper { flex-wrap: wrap; row-gap: 6px; width: 100%; }
  .app-navbar { width: 100%; flex-shrink: 1 !important; flex-wrap: wrap; justify-content: flex-end; gap: 0.35rem !important; }
  .app-navbar .form-select { width: auto !important; min-width: 0; max-width: 140px; }
  /* SUPERSEDED — David: "Whole system does not work as expected on an iphone... menus dont work
     as expected." The wrap-instead-of-scroll fix above (still true and useful in principle) still
     left this row rendering as a full-width band of plain, unstyled-looking links pushed above the
     page content on a real phone — confirmed live via a 375px screenshot. Real TSMPlus's own mobile
     header doesn't show this row at all. Now hidden below 992px (`lg`) entirely; the same 5 links
     (Contacts/Products/Assets/Resources/Locations) are reachable via the topbar's existing "More"
     (⋮) dropdown instead — see Topbar.razor's own comment on that change. */
  .app-header-menu { display: none !important; }

  /* (2) All modals are TelerikWindow (.k-window). Force them to the phone width; height-capping
     and body-only scrolling (footer always anchored/reachable) now come from the shared,
     all-viewport-sizes rule above this media query — this block only needs the phone-specific
     width/position override. !important overrides Telerik's inline width + JS centering.
     F85 (TSMTrial_Fixes.md) follow-up, self-caught 2026-08-15: this rule pins top/left to a
     small (2vw, 2vh) offset from the corner — deliberately NOT centered — but never neutralized
     Telerik's own "k-centered" positioning, which independently applies a
     `transform: translate(-halfWidth, -halfHeight)` (in pixels, computed from the window's own
     rendered size) meant to pair with a `top:50%;left:50%` centering scheme. Combining our
     small positive top/left with Telerik's own centering transform on top produced a large
     negative offset (confirmed live: a Create Job wizard measured x:-179, more than half
     off-screen, on every build from v0.616.0 through v0.622.0 — several increasingly complex
     attempts at fixing the modal's declared Width never touched this, because the actual bug was
     never the width, it was this leftover transform). Neutralizing it lets the intended top/left
     positioning apply cleanly. */
  .k-window {
    width: 96vw !important; max-width: 96vw !important;
    left: 2vw !important; right: auto !important;
    top: 2vh !important; max-height: 94vh !important;
    /* F93 — iOS Safari's vh is the LARGE viewport (toolbars retracted), so with the address/bottom bars
       showing in portrait the modal's footer (Save) sat below the visible area. dvh tracks the visible one. */
    top: 2dvh !important; max-height: 94dvh !important;
    transform: none !important;
  }
  .k-window-content { max-height: 94vh !important; max-height: 94dvh !important; }
  .k-window-content > .fb-modal-actions { padding-bottom: env(safe-area-inset-bottom, 0px); }
  /* Modal footer buttons wrap rather than overflow the narrow window. */
  .k-window .fb-modal-body + div { flex-wrap: wrap; row-gap: 6px; }
  .stepper-nav { gap: 0.75rem !important; }

  /* (3) Detail pages: cards/forms stack to a single column, the tab strip scrolls, and the
     sticky Save/Cancel/Delete footer wraps instead of overflowing. Touch targets get taller. */
  .fb-detail-header { gap: 8px; }
  .fb-detail-footer { flex-wrap: wrap; row-gap: 8px; }
  .fb-detail-footer .btn { flex: 1 1 auto; min-height: 42px; }
  .fb-tabs .nav-line-tabs { -webkit-overflow-scrolling: touch; }
  .fb-tabs .nav-line-tabs .nav-link { padding-top: 10px; padding-bottom: 10px; }
  /* Bootstrap already stacks .row>.col-md-* below 768; make sure edit inputs are finger-sized. */
  .form-control, .form-select { min-height: 40px; }
  .btn-sm { min-height: 36px; }

  /* (3b) Grids that stay in table mode on a phone (detail-tab grids that supply no MobileCard
     template — see TrialGrid.razor) keep their toolbar usable: the search box and its operator
     dropdown stack full-width instead of the 12em select squeezing the input to nothing, and the
     Refresh/Actions row sits above them without overflowing. */
  .fb-grid-search { flex-wrap: wrap; }
  .fb-grid-search .form-control, .fb-grid-search .form-select { width: 100% !important; flex: 1 0 100%; }
  .fb-grid-actions { flex-wrap: wrap; }

  /* (3c) Documents tab (DocumentsTab.razor): the file tree and the preview pane stack instead of
     sitting side by side, so the divider between them has to move from the right edge to the
     bottom, and the tree drops its desktop 520px scroll cap (two stacked scroll areas on a phone
     is worse than one long page). */
  .fb-doc-tree { border-right: 0 !important; border-bottom: 1px solid var(--fb-border); max-height: none !important; }
  .fb-doc-preview { padding-left: 1rem !important; padding-right: 1rem !important; }

  /* (3d) Create/Edit wizards (EntityWizardModal — every Create screen in the app: Order, Job,
     Quote, Invoice, Bill, Contact, Product, Asset, Location, Supplier, Project, Appointment,
     Timesheet, …, 22 modals on the one shell). David: "Create PO screen does not work on a phone."
     Cause: the wizard is a two-column flex row whose left step rail is a hard `width:240px` plus
     `pe-6 me-6` (≈2.5rem each) — ~320px of a window that phone CSS caps at 96vw (≈360px on a 375px
     screen). The content column carries `min-width:0`, so it collapsed to a few pixels — rail
     visible, no fields. FIX REVISED (2026-08-15): the first attempt at this replaced the rail with
     an invented horizontal strip of numbered circles. PARITY.md §I/§J records that the real app's
     create wizard IS `FbTabs Mode="Stepper"` — the same component behind every detail page's tab
     strip — and that component's own mobile mode (already built here, in `FbTabs.razor`, and
     confirmed matching real) is a `‹ prev | <select> | next ›` bar, not a rail-turned-strip. So no
     CSS override is needed at all: `EntityWizardModal.razor` now renders that exact bar (same
     markup/classes as FbTabs) when its own `<TelerikMediaQuery>` reports mobile, replacing the rail
     outright rather than reshaping it — same pattern the rest of the app already uses, not a
     wizard-specific one. */

  /* EntityFormModal's two-column form grid (TimerFormModal) goes single-column — two form fields
     side by side inside a 96vw window is unusable on a phone. */
  .fb-form-grid { grid-template-columns: 1fr; }

  /* (4) Mobile Appointments field screen: filter button rows are full-width touch targets. */
  .fb-mobile-appts .btn-group .btn { min-height: 40px; flex: 1 1 0; }
  .fb-mobile-appts .btn-group.flex-wrap { flex-wrap: wrap; gap: 4px; }
  .fb-mobile-appts .btn-group.flex-wrap .btn { flex: 1 1 30%; border-radius: var(--fb-radius) !important; }

  /* (5) iPhone fixes (2026-08-15, David live): the drawer is the phone's menu, so the desktop
     collapse chevron is hidden (collapse is desktop-only now — see the min-width:768px block). */
  .fb-nav-toggle { display: none !important; }

  /* (6) Footer is hidden outright on phones (David, 2026-08-15: "remove the trial footer when
     running on a phone, the screen is not big enough"). 0.613.0 had compacted it to two rows
     instead; those rules are gone with it. This takes the version/Release Notes link, the TSMPlus
     + TSM logos, and TrialTools (TRIAL badge, tenant name, "View as" role picker, Reset data) off
     the phone entirely — all of which are either desktop-demo tooling or branding, and none of
     which the real TSMPlus phone experience shows either. Kept at ≥768px, unchanged. */
  #kt_app_footer { display: none !important; }

  /* (7) David, live: "I can no longer see version number on phone... very subtle... certainly
     not on its own row." Fixed-position so it overlays content instead of consuming a layout row;
     small + low-opacity so it reads as a faint watermark, not a UI element competing for
     attention. Only rendered below this same 768px breakpoint (Footer.razor's own d-md-none). */
  #fb-mobile-version-badge {
    position: fixed;
    bottom: 2px;
    right: 6px;
    font-size: 9px;
    line-height: 1;
    color: var(--fb-text-muted);
    opacity: 0.55;
    z-index: 10;
    text-decoration: none;
    pointer-events: auto;
  }
}

/* Editable-detail form footer (DetailForm) — sticky at the bottom of the content, Delete left /
   Save-Cancel right (matches TSMPlus FbForm). */
.fb-detail-footer { background: rgba(255,255,255,0.92); backdrop-filter: blur(2px); border-top: 1px solid var(--fb-border); margin-top: 8px; z-index: 5; }
[data-theme="dark"] .fb-detail-footer { background: rgba(30,30,45,0.92); }

.fb-stat-tile { background: var(--fb-surface); border: 1px solid var(--fb-border); border-radius: var(--fb-radius); padding: 16px 18px; box-shadow: var(--fb-shadow); }
/* §V(admin-dashboard-drilldown) — hover affordance for the /admin Dashboard's clickable KPI tiles,
   so it's visually obvious the card is a control before the click, not just after. */
.fb-stat-tile-clickable { transition: box-shadow 0.15s ease, border-color 0.15s ease; }
.fb-stat-tile-clickable:hover { box-shadow: 0 0 0 1px var(--fb-primary), var(--fb-shadow); border-color: var(--fb-primary); }
.fb-stat-value { font-size: 24px; font-weight: 700; margin: 4px 0 2px; }
.fb-stat-label { font-size: 12px; color: var(--fb-text-muted); text-transform: uppercase; letter-spacing: 0.03em; }
.fb-stat-sub { font-size: 12px; color: var(--fb-text-muted); }

.fb-field-row { display: flex; padding: 6px 0; font-size: 13px; border-bottom: 1px solid #f1f3f5; }
.fb-field-row:last-child { border-bottom: none; }
.fb-field-label { width: 42%; color: var(--fb-text-muted); }
.fb-field-value { width: 58%; font-weight: 500; }

/* Two-part pill badge: circular code chip fused to a label pill, same hue */
.fb-pill { display: inline-flex; align-items: center; border-radius: 999px; font-size: 11px; font-weight: 700; color: #fff; white-space: nowrap; }
/* F52 — real two-tone capsule: a WHITE coin (not a dark rgba overlay) holding the code in the pill's
   own colour (set via inline `color` on the span, consumed here through currentColor for both text
   and ring), sitting beside the solid-colour label segment. */
.fb-pill-code { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: #fff; border: 1.5px solid currentColor; font-size: 9.5px; margin: 2px; flex-shrink: 0; }
.fb-pill-label { padding: 0 10px 0 5px; }

/* F53 — initials avatar for entity-reference cards (PickerCard: Customer/Location/Contact/Supplier
   etc, app-wide). Same white-fill + currentColor-ring/text treatment as the Pill coin (F52), for a
   consistent look, with the per-name stable colour from BadgeService.ColourFor. */
.fb-avatar { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px;
  border-radius: 50%; background: #fff; border: 1.5px solid currentColor; font-weight: 700; font-size: 12px;
  flex-shrink: 0; margin-top: 1px; }
/* §V210 — compact variant of the same avatar for tight inline placements (Project Planning tab's
   Gantt stage panel, beside the stage code) where the full 36px size doesn't fit a narrow column. */
.fb-avatar-xs { width: 18px; height: 18px; font-size: 8.5px; border-width: 1.25px; margin-top: 0; }
.fb-pill-neutral { background: var(--white-600); }
.fb-pill-primary { background: var(--pacific-blue-500); }
.fb-pill-info { background: var(--purple-500); }
.fb-pill-warning { background: var(--yellow-warning-400); }
.fb-pill-success { background: var(--green-success-400); }
.fb-pill-danger { background: var(--carnation-500); }

/* NEW-8 / SH-31 - real's detail header is sticky under the fixed app header. */
.fb-detail-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 2px; position: sticky; top: calc(var(--kt-app-header-height, 0px) + var(--fb-banner-offset, 0px)); z-index: 95; background: var(--kt-app-bg-color, var(--fb-surface)); padding: 6px 0; }
.fb-detail-title-row { display: flex; align-items: center; gap: 10px; }
.fb-back-btn { width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 1px solid var(--fb-border); background: var(--fb-surface); color: var(--fb-primary-hover); cursor: pointer; flex-shrink: 0; }
.fb-back-btn:hover { background: var(--fb-primary-light); }
.fb-detail-title { font-size: 18px; font-weight: 700; margin: 0; }
.fb-detail-price-line { font-size: 12.5px; color: var(--fb-text-muted); margin: 2px 0 14px; }
.fb-detail-price-line strong { color: var(--fb-text); font-weight: 700; }
.fb-detail-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.fb-stage-action-btn { border: none; border-radius: var(--fb-radius); background: var(--green-success-400); color: #fff; font-size: 12.5px; font-weight: 700; padding: 7px 14px; cursor: pointer; }
.fb-stage-action-btn:hover { filter: brightness(0.95); }
.fb-detail-footer { display: flex; align-items: center; gap: 10px; padding-top: 16px; margin-top: 8px; border-top: 1px solid var(--fb-border); }

/* §V662 — the default ("Fixed to the bottom of the screen") variant: pinned to the actual browser
   viewport bottom, spanning the content column only (left offset tracks the sidebar's own current
   width via the same --bs-app-sidebar-width Metronic var #kt_app_header already keys off, so it
   automatically follows the collapsed/hover-expanded sidebar state for free). Unlike plain
   position:sticky, this stays visible even when the page's own content is shorter than the
   viewport. padding-bottom below compensates so the fixed bar never overlaps the last field. */
.fb-detail-footer-fixed { position: fixed; left: var(--bs-app-sidebar-width, 265px); right: 0; bottom: 0; z-index: 90; padding-left: 24px; padding-right: 24px; }
/* #kt_app_content_container is the one shared wrapper every routed page's @Body renders into
   (MainLayout.razor) — scoping the compensating padding here (rather than a per-page class, which
   doesn't consistently exist across all 60 DetailForm call sites) reaches every page uniformly. */
#kt_app_content_container:has(.fb-detail-footer-fixed) { padding-bottom: 64px; }
/* Below Metronic's own sidebar-to-drawer breakpoint the sidebar no longer occupies persistent
   horizontal space (it becomes an overlay), so the fixed bar should span the full width — same
   991.98px threshold this file already uses elsewhere for sidebar-driven layout switches. */
@media (max-width: 991.98px) {
  .fb-detail-footer-fixed { left: 0; }
}

.fb-text-muted { color: var(--fb-text-muted); }
.fb-currency { font-variant-numeric: tabular-nums; }

/* Blazor's built-in error banner — hidden unless the framework explicitly shows it via JS. */
#blazor-error-ui {
  background: lightyellow;
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  display: none;
  left: 0;
  padding: 0.6rem 1.25rem 0.7rem 1.25rem;
  position: fixed;
  width: 100%;
  z-index: 1000;
}
#blazor-error-ui .dismiss {
  cursor: pointer;
  position: absolute;
  right: 0.75rem;
  top: 0.5rem;
}

/* David: live-reported "I don't see [the version-update banner]". Root cause: MainLayout.razor's
   version-update/save-error banners are plain flow divs at the very top of the page — fine below
   Metronic's 992px breakpoint, where .app-header stays in normal flow below them, but at desktop
   widths .app-header switches to position:fixed;top:0 (style.bundle.css) and paints directly on
   top of them, hiding them completely. window.fbLayout.setBannerOffset (index.html) measures the
   banners' real rendered height after every MainLayout render and writes it here; loaded last, so
   this file's own rule wins over the vendor one without needing !important (same convention as
   every other fb-* override in this file). Only the two things that actually need to move — the
   fixed header's own top, and the wrapper's top margin that clears it for the sidebar/main content
   — not every other header-height-derived position in the vendor sheet, keeping this a narrow,
   low-risk fix rather than touching a wide surface of interdependent Metronic layout variables. */
:root { --fb-banner-offset: 0px; }
@media (min-width: 992px) {
  [data-kt-app-header-fixed=true] .app-header { top: var(--fb-banner-offset); }
  [data-kt-app-header-fixed=true] .app-wrapper { margin-top: calc(var(--kt-app-header-height) + var(--fb-banner-offset)); }
}

/* David: "it also needs to be much more prominent, visible from everywhere" — a persistent
   floating action button (ReadyToTalkFab.razor), reachable on every page instead of buried on one
   wizard page. Sits above the sticky footer (z-index 90) but well below the tour overlay's
   near-max z-index, so an active guided tour still draws on top of it if the two ever overlap. */
.fb-ready-to-talk-fab {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 200;
  border-radius: 999px;
  padding: 12px 20px;
  font-weight: 700;
  font-size: 13.5px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  animation: fb-ready-to-talk-pulse 2.5s ease-in-out infinite;
}
.fb-ready-to-talk-fab:hover { animation-play-state: paused; }
@keyframes fb-ready-to-talk-pulse {
  0%, 100% { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), 0 0 0 0 rgba(27, 132, 255, 0.4); }
  50% { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), 0 0 0 10px rgba(27, 132, 255, 0); }
}
@media (max-width: 767px) {
  .fb-ready-to-talk-fab { right: 14px; bottom: 14px; padding: 10px 16px; font-size: 12.5px; }
}
/* FAB hidden while any modal is open — body.fb-modal-open is toggled by index.html's fbWatchModals
   (MutationObserver over .modal/.k-window visibility). David's chosen fix (2026-08-16): a tap on the
   FAB during an open modal was silently swallowed, so hide it until the modal closes. */
body.fb-modal-open .fb-ready-to-talk-fab { display: none !important; }

/* §V674 — same "floating element eats a real click" bug as the modal case immediately above, just
   never extended to the fixed detail-footer (DetailForm.razor's Save/Cancel bar in its default
   IsSaveBarFixedOnBottom mode, §V662): .fb-detail-footer-fixed sits position:fixed at the exact
   same bottom-right corner as this FAB, but at z-index 90 versus the FAB's 200 — so on ANY detail
   page with unsaved changes, while the FAB is still showing (true for most trial visitors, who
   haven't yet clicked it), a tap aimed at Save silently hit the FAB instead, with no error and no
   visible sign anything was wrong. Reproduced live on both a Quote's own detail-footer Save button
   and Settings > Quote Defaults' identical fixed footer. Same fix shape as the modal case: hide the
   FAB whenever the fixed footer is actually on screen, rather than trying to reconcile the two
   fixed-position elements' stacking order. */
body:has(.fb-detail-footer-fixed) .fb-ready-to-talk-fab { display: none !important; }

/* §V736 — David, live: "Urgent Save/cancel and delete buttons are missing (eg product
   maintenance, jobs etc)". Same fixed-bottom collision class as §V674 above, one layer further
   down the stack: #kt_app_footer (the TrialTools/"TRIAL" badge bar, `position: sticky; bottom: 0;
   z-index: 90;` — see this file's own comment near its rule) pins to the exact same screen rect
   as `.fb-detail-footer-fixed` (also bottom: 0, also z-index: 90). Tied z-index falls back to DOM
   order, and #kt_app_footer paints after the routed page content, so it wins the tie and fully
   covers Save/Cancel/Delete — not just eating clicks like the FAB did, but hiding the whole bar
   with no visible sign anything is missing. Reproduced live: Product detail (Acme Spares tenant),
   any edited/dirty record. #kt_app_footer is trial-only tooling with no real-TSMPlus counterpart
   (see Footer.razor's own header comment), so hiding it while a real save action is on screen is
   the correct trade — same shape as the FAB fix immediately above. */
body:has(.fb-detail-footer-fixed) #kt_app_footer { display: none !important; }

/* AI assistant launcher (AssistantDrawer.razor) — circular avatar pinned bottom-LEFT, per David's
   supplied reference. Bottom-right is already taken by .fb-ready-to-talk-fab, so the two never
   collide. Sits above the sidebar (z-index 200, same band as the other FAB) and hides behind any
   open modal for the same reason ReadyToTalkFab does. */
.fb-ai-fab {
  position: fixed;
  left: 20px;
  bottom: 20px;
  z-index: 200;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: 3px solid #fff;
  background: var(--fb-primary, #1b84ff);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease;
}
.fb-ai-fab i { font-size: 24px; line-height: 1; }
.fb-ai-fab:hover { transform: scale(1.06); box-shadow: 0 10px 28px rgba(0, 0, 0, 0.32); }
/* Presence dot — purely a visual cue that the assistant is available, not a live status. */
.fb-ai-fab-dot {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--green-success-500, #17c653);
  border: 2px solid #fff;
}
body.fb-modal-open .fb-ai-fab { display: none !important; }
@media (max-width: 767px) {
  /* Tucked in slightly on phones so it clears the sidebar toggle and thumb reach. */
  .fb-ai-fab { left: 12px; bottom: 12px; width: 50px; height: 50px; }
  .fb-ai-fab i { font-size: 21px; }
}

/* F88 — global Timers quick-access widget. Rebuilt 2026-08-16 after David checked the real app
   live and rereading _Navbar.razor/TimerDrawer.razor: the trigger is an inline topbar icon (see
   Topbar.razor), not a floating corner FAB (the earlier build's structural mistake, not just a
   wrong position) — .fb-timer-fab no longer exists. The panel itself is a real full-height
   right-edge slide-in drawer (real's own FbDrawer: 350px desktop / 100vw mobile), not a floating
   rounded card, with a backdrop to close on outside-click like every other drawer in this app.
   Same z-index tier as the ready-to-talk FAB and hidden under open modals for the same swallow
   reason. */
.fb-timer-drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
  z-index: 200;
}
.fb-timer-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 201;
  width: 350px;
  display: flex;
  flex-direction: column;
  background: var(--fb-surface);
  border-left: 1px solid var(--fb-border);
  padding: 20px;
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.15);
}
.fb-timer-drawer-body { overflow-y: auto; flex: 1 1 auto; }
body.fb-modal-open .fb-timer-drawer-backdrop, body.fb-modal-open .fb-timer-drawer { display: none !important; }
@media (max-width: 767px) {
  .fb-timer-drawer { width: 100vw; }
}

/* Knowledge Base drawer (KnowledgeBaseDrawer.razor) — same backdrop/panel mechanics as the
   Timer drawer above, but wider (real's own FbDrawer for this one is 550px) since it hosts an
   iframe of the real doc site rather than a list of cards. */
.fb-kb-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 201;
  width: 550px;
  display: flex;
  flex-direction: column;
  background: var(--fb-surface);
  border-left: 1px solid var(--fb-border);
  padding: 20px;
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.15);
}
.fb-kb-drawer-body { flex: 1 1 auto; min-height: 0; }
.fb-kb-frame { width: 100%; height: 100%; border: 0; }
body.fb-modal-open .fb-kb-drawer { display: none !important; }
@media (max-width: 767px) {
  .fb-kb-drawer { width: 100vw; }
}

/* §V731 — per-record Notes drawer (RecordNotesDrawer.razor), same backdrop/panel mechanics as the
   Timer/Knowledge Base drawers above (real's own NotesDrawer.razor is a slide-out TelerikDrawer,
   not a modal — this reuses the app's own equivalent mechanism rather than a bootstrap modal).
   Sized like the Timer drawer (a list of cards + a compose box, not a reading surface). */
.fb-notes-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 201;
  width: 420px;
  display: flex;
  flex-direction: column;
  background: var(--fb-surface);
  border-left: 1px solid var(--fb-border);
  padding: 20px;
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.15);
}
.fb-notes-drawer-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
body.fb-modal-open .fb-notes-drawer { display: none !important; }
@media (max-width: 767px) {
  .fb-notes-drawer { width: 100vw; }
}

/* AI assistant drawer (AssistantDrawer.razor) — same backdrop/panel mechanics as the Timer and
   Knowledge Base drawers above. Sized like the KB drawer (both are reading surfaces), but its body
   is a scrolling conversation with a pinned composer at the bottom rather than a single iframe. */
.fb-ai-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 201;
  width: 550px;
  display: flex;
  flex-direction: column;
  background: var(--fb-surface);
  border-left: 1px solid var(--fb-border);
  padding: 20px;
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.15);
}
/* min-height:0 is what actually lets this scroll — without it the flex item grows to fit its
   content and pushes the composer off-screen instead of scrolling. */
.fb-ai-thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 4px; }
.fb-ai-composer { flex: 0 0 auto; padding-top: 12px; border-top: 1px solid var(--fb-border); }

.fb-ai-turn { padding-bottom: 18px; margin-bottom: 18px; border-bottom: 1px solid var(--fb-border); }
.fb-ai-turn:last-child { border-bottom: 0; }

.fb-ai-question {
  font-weight: 600;
  color: var(--fb-text, #181c32);
  background: var(--bs-gray-100, #f9f9f9);
  border-radius: 6px;
  padding: 8px 10px;
  margin-bottom: 10px;
}
/* pre-wrap so the model's paragraph breaks survive without needing markdown rendering. */
.fb-ai-answer { white-space: pre-wrap; line-height: 1.55; font-size: 0.95rem; }

/* The interpreted-query line (AIA-004): visually distinct from the answer, because its whole job is
   to be checked rather than read past. */
.fb-ai-interpreted {
  font-size: 0.8rem;
  color: var(--bs-gray-700, #5e6278);
  background: var(--bs-light-primary, #f1faff);
  border-left: 3px solid var(--bs-primary, #009ef7);
  border-radius: 4px;
  padding: 6px 10px;
  margin-bottom: 8px;
}
/* Result tables are frequently wider than 550px — scroll the table, never the drawer body sideways. */
.fb-ai-result-scroll { overflow-x: auto; max-width: 100%; margin-bottom: 4px; }
.fb-ai-citations { margin-top: 8px; padding-top: 6px; border-top: 1px dashed var(--fb-border); }
.fb-ai-handover {
  margin-top: 10px;
  padding: 10px 12px;
  border-radius: 6px;
  background: var(--bs-light-warning, #fff8dd);
}
body.fb-modal-open .fb-ai-drawer { display: none !important; }
@media (max-width: 767px) {
  .fb-ai-drawer { width: 100vw; }
}

/* Assistant "Videos" button — a hand-rolled dropup, matching this drawer's own pattern of not
   depending on Metronic's JS-driven dropdown/menu components (see AssistantDrawer.razor's header
   comment). Positioned relative to the composer so the menu floats just above the button. */
.fb-ai-videos { position: relative; }
.fb-ai-videos-backdrop { position: fixed; inset: 0; z-index: 202; }
.fb-ai-videos-menu {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% + 4px);
  z-index: 203;
  background: var(--fb-surface);
  border: 1px solid var(--fb-border);
  border-radius: 8px;
  box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.12);
  padding: 6px;
}
.fb-ai-videos-item {
  display: flex;
  align-items: center;
  padding: 8px 10px;
  border-radius: 6px;
  color: var(--fb-text, #181c32);
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: none;
}
.fb-ai-videos-item:hover { background: var(--bs-gray-100, #f9f9f9); color: var(--fb-text, #181c32); }
.fb-ai-videos-item + .fb-ai-videos-item { margin-top: 2px; }

/* Scheduling toolbar "Excluded Statuses" wrapper (Scheduling.razor) — see that file's own comment.
   Desktop keeps the real toolbar's 220px field; phones give the multiselect its own full row so the
   icon row beside it can wrap instead of expanding the page past 375px (iOS auto-zoom-out). */
@media (min-width: 768px) { .fb-sched-excluded { min-width: 220px; } }
@media (max-width: 767px) {
  .fb-sched-excluded { min-width: 0; flex: 1 1 100%; }
}

/* GlobalSearchBox.razor (§V global-search, F80) — dropdown result rows. */
.fb-search-result { color: var(--bs-gray-800, #252525); }
.fb-search-result:hover { background: var(--bs-gray-100, #f4f4f4); }

/* §Vnnn — clean single-document printing for Job/Quote/Invoice/PO "Print" actions. Job/Quote/
   Invoice/Order detail pages all already assemble a real printable document (CompletionDocument.razor
   for Quote/Invoice/Order, JobDetail's own Job Report + Asset Work cards for Job) marked with
   .fb-print-area — without this, window.print() (RecordActionsMenu's Print action, JobDetail's
   PrintWork) would print the whole app chrome (sidebar/topbar/tabs/buttons) around it. Standard
   "print only this element" pattern: everything is hidden, then the print-area subtree (and only
   it) is made visible again and repositioned to the page origin. */
@media print {
  body * { visibility: hidden; }
  .fb-print-area, .fb-print-area * { visibility: visible; }
  .fb-print-area {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    margin: 0;
    box-shadow: none;
    border: none;
  }
  .fb-print-area .btn,
  .fb-print-area .card-toolbar { display: none !important; }
}

/* §V669 — real's PR 11202 ("Redesign Completion tab document preview", ADO 13156) wraps the
   Completion tab's printable document in a bounded, shadowed .fb-document-preview container so it
   reads as a page, not a full-bleed form (exact values reused from real's own app-overrides.css —
   colors/dimensions are facts, not copyrightable expression, same convention as this file's other
   --fb-*/--pacific-blue-* tokens). Values are a plain custom class, not a Bootstrap .container-*
   class, deliberately: those grow at xl/xxl breakpoints (960->1140->1320px) instead of staying
   capped at an A4-ish width. The @media print block above already zeroes box-shadow/border/position
   for the print output regardless, so this on-screen framing doesn't affect what actually prints. */
.fb-document-preview {
  max-width: 960px;
  margin: 0 auto;
  padding: 40px 48px;
  background-color: var(--bs-body-bg, #fff);
  box-shadow: 0 2px 12px rgba(0, 0, 0, .12);
}
@media (max-width: 767.98px) {
  .fb-document-preview {
    max-width: 100%;
    padding: 16px;
    box-shadow: none;
  }
}

/* Phones: a detail page's fixed Save/Cancel/Delete bar wraps taller than the 64px padding reserved for it and the
   bottom-left assistant bubble sat on top of its first button. Reserve more room and lift the bubble above the bar. */
@media (max-width: 767.98px) {
  #kt_app_content_container:has(.fb-detail-footer-fixed) { padding-bottom: 120px; }
  body:has(.fb-detail-footer-fixed) .fb-ai-fab { bottom: 96px; }
}

/* Phone-only layout fixes found in an iPhone walk-through (every rule is inside the ≤767px query, so desktop is untouched). */
@media (max-width: 767.98px) {
  /* Scheduling: the Splitter put the Unscheduled Jobs panel beside the board as a ~100px sliver (text wrapped one
     character per line, scheduler cut off). Stack the panes instead: scheduler first, job list below at full width. */
  .fb-sched-splitter { flex-direction: column !important; height: auto !important; }
  .fb-sched-splitter > .k-pane { flex: 0 0 auto !important; width: 100% !important; max-width: 100% !important; height: auto !important; }
  .fb-sched-splitter > .k-splitbar { display: none !important; }
  /* Fixed-width inputs in card headers (Files upload row etc.) pushed the page 90px wider than the screen. */
  .card-header .w-250px, .card-header .w-200px, .card-header .w-150px { width: 100% !important; }
  /* Calendar/Scheduling week & fortnight headers ("Sun, 9/27") overlapped at ~55px per day column. */
  .k-scheduler-head .k-scheduler-cell, .k-scheduler-head th { font-size: 10.5px; white-space: normal; word-break: break-word; text-align: center; padding-left: 2px; padding-right: 2px; }
}

/* §V966 - real's app.css defines .bg-warning-light; the trial had no such rule, so an On Hold picker row (PickerModal) was never shaded. --kt-warning-light follows the light / dark theme. */
.bg-warning-light { background-color: var(--kt-warning-light, var(--fb-warning-light)) !important; }

/* §V1093 - scheduler / calendar cells: real's app.css shades working and non-working time per the person's schedule */
.working-slot { background-color: var(--bs-gray-100, #f9f9f9) !important; }
.non-working-slot { background-color: var(--bs-gray-200, #f1f1f4) !important; }

/* SH2: avatar menu rows match live's larger plain-text rows (§V1288 addendum). */
.fb-account-menu .menu-link { font-size: .95rem; padding-top: .6rem; padding-bottom: .6rem; color: var(--bs-gray-800); }
.fb-account-menu .menu-link.active { background: var(--bs-gray-100); color: var(--bs-primary); }
/* NEW-1 / DEF-2 - on a Defaults / Settings page real shows plain h6 headings, not cards: flatten the cards inside the shell. */
.fb-settings-shell .card { border: 0; box-shadow: none; background: transparent; margin-bottom: 1rem !important; }
.fb-settings-shell .card > .card-header { border: 0; padding: 0; min-height: 0; margin-bottom: .5rem; }
.fb-settings-shell .card > .card-header .card-title { font-size: 1rem; font-weight: 600; margin: 0; }
.fb-settings-shell .card > .card-body { padding: 0; }

/* G3 (§V1213#1) - Preview Mode split: grid left, quick-view pane right. .fb-grid-wrap is a no-op wrapper so other grids' layout is unchanged. */
.fb-grid-wrap { display: contents; }
.fb-preview-split { display: flex; gap: .75rem; align-items: flex-start; }
.fb-preview-split .fb-preview-list { flex: 1 1 60%; min-width: 0; }
.fb-preview-split .fb-preview-pane { flex: 0 0 40%; min-width: 280px; max-width: 50%; position: sticky; top: .5rem; }
.fb-preview-selected td { background: var(--bs-primary-light, rgba(0,110,255,.08)) !important; }

/* SH5 (§V1291 / §V1209#3) - phone / tablet layout slice. Not seen in a browser. */
.fb-phone-bar { display: none; }
.fb-mobile-card .fb-mc-sub { font-size: 12.5px; color: var(--fb-text-muted); margin-top: 2px; }
.fb-mobile-card .fb-mc-head:has(.ms-auto) { justify-content: flex-start; align-items: center; }
.fb-mobile-card .fb-mc-more { margin-top: 4px; }
.fb-mobile-card .fb-mc-more-toggle { list-style: none; cursor: pointer; text-align: center; color: var(--fb-text-muted); padding: 2px 0; }
.fb-mobile-card .fb-mc-more-toggle::-webkit-details-marker { display: none; }
.fb-mobile-card .fb-mc-more[open] > .fb-mc-more-toggle i { transform: rotate(180deg); }
@media (max-width: 767.98px) {
  /* live's bottom bar: timer icon left, round "+" right. Sits under drawers (z 200) and modals, above page content. */
  .fb-phone-bar {
    display: flex; align-items: center; justify-content: space-between;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 95; height: 56px; padding: 0 16px;
    background: var(--fb-surface); border-top: 1px solid var(--fb-border); box-shadow: 0 -2px 10px rgba(0,0,0,.08);
  }
  .fb-phone-bar .btn { position: relative; }
  .fb-phone-bar-plus { width: 44px; height: 44px; border-radius: 50% !important; margin-top: -16px; box-shadow: 0 4px 12px rgba(0,0,0,.25); }
  /* No create permission / no create entry here => no "+". */
  body:not(:has(#global-create-btn)) .fb-phone-bar-plus { display: none; }
  /* A detail page's fixed Save/Cancel bar owns the bottom edge; modals cover everything. */
  body:has(.fb-detail-footer-fixed) .fb-phone-bar, body.fb-modal-open .fb-phone-bar { display: none; }
  /* Reserve room for the bar and lift the floating bubbles above it. */
  #kt_app_content_container { padding-bottom: 72px; }
  .fb-ai-fab, .fb-ready-to-talk-fab { bottom: 68px; }
  #fb-mobile-version-badge { bottom: 58px; }
  /* Top-bar Create becomes the compact "+" (label hidden); the bottom bar carries the labelled-less "+" too, like live. */
  #global-create-btn span { display: none; }
  #global-create-btn { padding-left: .6rem; padding-right: .6rem; }
}

/* P12-F (SH-128 / SH-129) - one grid toolbar row: the search box sits in the Refresh / Actions row; the Telerik toolbar keeps only the off-screen column chooser. */
.fb-grid-actions { flex-wrap: wrap; }
.fb-grid-actions .fb-grid-search { flex: 1 1 260px; min-width: 200px; max-width: 640px; margin-right: auto; }
.fb-grid-wrap .k-grid-toolbar:has(.k-grid-column-menu) { padding: 0 !important; border: 0 !important; min-height: 0 !important; height: 0; overflow: visible; }
.fb-mobile-compact .fb-mc-row, .fb-mobile-compact .fb-mc-badges { display: none; }
